From c8d99bac8ef96ac94bccdf3c754e7d43b3f1703c Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Sun, 8 Jul 2018 14:41:12 -0400 Subject: [PATCH 0001/1451] Fix regression in translating anything at all In commit 840ee20 (Rename translation resources from aur to aurweb, 2018-07-07) the translations file was renamed but we never actually switched to using the renamed translations. As a result, every single push to the AUR contains the following traceback: remote: Traceback (most recent call last): remote: File "/usr/bin/aurweb-notify", line 11, in remote: load_entry_point('aurweb==4.7.0', 'console_scripts', 'aurweb-notify')() remote: File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 541, in main remote: File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 69, in send remote: File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 56, in get_body_fmt remote: File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 192, in get_body remote: File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/l10n.py", line 14, in translate remote: File "/usr/lib/python3.6/gettext.py", line 514, in translation remote: raise OSError(ENOENT, 'No translation file found for domain', domain) remote: FileNotFoundError: [Errno 2] No translation file found for domain: 'aur' Signed-off-by: Eli Schwartz Signed-off-by: Lukas Fleischer --- aurweb/l10n.py | 2 +- web/lib/translator.inc.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index e58e3fe2..66e0f1c0 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -9,7 +9,7 @@ class Translator: if lang == 'en': return s if lang not in self._translator: - self._translator[lang] = gettext.translation("aur", + self._translator[lang] = gettext.translation("aurweb", "../../web/locale", languages=[lang]) self._translator[lang].install() diff --git a/web/lib/translator.inc.php b/web/lib/translator.inc.php index d10f8e90..cd944c56 100644 --- a/web/lib/translator.inc.php +++ b/web/lib/translator.inc.php @@ -131,9 +131,8 @@ function set_lang() { } $streamer = new FileReader('../locale/' . $LANG . - '/LC_MESSAGES/aur.mo'); + '/LC_MESSAGES/aurweb.mo'); $l10n = new gettext_reader($streamer, true); return; } - From a7865ef5aa0309976b5dd2642210632babe106d9 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Sun, 22 Jul 2018 10:41:57 +0200 Subject: [PATCH 0002/1451] Make the locale directory configurable Add a new configuration option to specify the locale directory to use. This allows the Python scripts to find the translations, even when not being run from the source code checkout. At the same time, multiple parallel aurweb setups can still use different sets of translations. Fixes FS#59278. Signed-off-by: Lukas Fleischer --- aurweb/l10n.py | 5 ++++- conf/config.defaults | 1 + web/lib/translator.inc.php | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index 66e0f1c0..a7c0103e 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -1,8 +1,11 @@ import gettext +import aurweb.config + class Translator: def __init__(self): + self._localedir = aurweb.config.get('options', 'localedir') self._translator = {} def translate(self, s, lang): @@ -10,7 +13,7 @@ class Translator: return s if lang not in self._translator: self._translator[lang] = gettext.translation("aurweb", - "../../web/locale", + self._localedir, languages=[lang]) self._translator[lang].install() return _(s) diff --git a/conf/config.defaults b/conf/config.defaults index be37f430..c8bc3a7e 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -35,6 +35,7 @@ snapshot_uri = /cgit/aur.git/snapshot/%s.tar.gz enable-maintenance = 1 maintenance-exceptions = 127.0.0.1 render-comment-cmd = /usr/local/bin/aurweb-rendercomment +localedir = /srv/http/aurweb/aur.git/web/locale/ # memcache or apc cache = none memcache_servers = 127.0.0.1:11211 diff --git a/web/lib/translator.inc.php b/web/lib/translator.inc.php index cd944c56..334d0e76 100644 --- a/web/lib/translator.inc.php +++ b/web/lib/translator.inc.php @@ -130,7 +130,8 @@ function set_lang() { setcookie("AURLANG", $LANG, $cookie_time, "/"); } - $streamer = new FileReader('../locale/' . $LANG . + $localedir = config_get('options', 'localedir'); + $streamer = new FileReader($localedir . '/' . $LANG . '/LC_MESSAGES/aurweb.mo'); $l10n = new gettext_reader($streamer, true); From 3578e77ad4e9258495eed7e786b7dc3aebcf1b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Mon, 6 Aug 2018 02:02:57 +0200 Subject: [PATCH 0003/1451] Allow listing all comments from a user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg Signed-off-by: Lukas Fleischer --- web/html/account.php | 20 +++++- web/html/css/aurweb.css | 42 +++++++++++++ web/html/index.php | 2 + web/html/pkgbase.php | 10 ++- web/lib/acctfuncs.inc.php | 42 +++++++++++++ web/lib/aur.inc.php | 53 ++++++++++++++++ web/lib/credentials.inc.php | 2 + web/lib/pkgbasefuncs.inc.php | 10 ++- web/lib/pkgfuncs.inc.php | 4 ++ web/template/account_details.php | 3 + web/template/account_edit_form.php | 1 + web/template/pkg_comments.php | 99 ++++++++++++++++++++++-------- 12 files changed, 258 insertions(+), 30 deletions(-) diff --git a/web/html/account.php b/web/html/account.php index c30a89aa..9695c9b7 100644 --- a/web/html/account.php +++ b/web/html/account.php @@ -8,7 +8,7 @@ include_once('acctfuncs.inc.php'); # access Account specific functions $action = in_request("Action"); $need_userinfo = array( - "DisplayAccount", "DeleteAccount", "AccountInfo", "UpdateAccount" + "DisplayAccount", "DeleteAccount", "AccountInfo", "UpdateAccount", "ListComments" ); if (in_array($action, $need_userinfo)) { @@ -166,6 +166,24 @@ if (isset($_COOKIE["AURSID"])) { $row["Username"]); } + } elseif ($action == "ListComments") { + if (has_credential(CRED_ACCOUNT_LIST_COMMENTS)) { + # display the comment list if they're a TU/dev + + $total_comment_count = account_comments_count($row["ID"]); + list($pagination_templs, $per_page, $offset) = calculate_pagination($total_comment_count); + + $username = $row["Username"]; + $uid = $row["ID"]; + $comments = account_comments($uid, $per_page, $offset); + + $comment_section = "account"; + include('pkg_comments.php'); + + } else { + print __("You are not allowed to access this area."); + } + } else { if (has_credential(CRED_ACCOUNT_SEARCH)) { # display the search page if they're a TU/dev diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index f5e10371..593c9ae8 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -148,3 +148,45 @@ label.confirmation, color: red; font-weight: bold; } + +.package-comments { + margin-top: 1.5em; +} + +.comments-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +/* arrowed headings */ +.comments-header h3 span.text { + display: block; + background: #1794D1; + font-size: 15px; + padding: 2px 10px; + color: white; +} + +.comments-header .comments-header-nav { + align-self: flex-end; +} + +.comment-header { + clear: both; + font-size: 1em; + margin-top: 1.5em; + border-bottom: 1px dotted #bbb; +} + +.comments div { + margin-bottom: 1em; +} + +.comments div p { + margin-bottom: 0.5em; +} + +.comments .more { + font-weight: normal; +} diff --git a/web/html/index.php b/web/html/index.php index 2c53cddd..b2cd840e 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -142,6 +142,8 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { $_REQUEST['Action'] = "UpdateAccount"; } elseif ($tokens[3] == 'delete') { $_REQUEST['Action'] = "DeleteAccount"; + } elseif ($tokens[3] == 'comments') { + $_REQUEST['Action'] = "ListComments"; } else { header("HTTP/1.0 404 Not Found"); include "./404.php"; diff --git a/web/html/pkgbase.php b/web/html/pkgbase.php index cf9a6c60..46ad77e6 100644 --- a/web/html/pkgbase.php +++ b/web/html/pkgbase.php @@ -43,6 +43,7 @@ if (isset($_POST['IDs'])) { /* Perform package base actions. */ $via = isset($_POST['via']) ? $_POST['via'] : NULL; +$return_to = isset($_POST['return_to']) ? $_POST['return_to'] : NULL; $ret = false; $output = ""; $fragment = ""; @@ -133,7 +134,14 @@ if (check_token()) { /* Redirect back to package request page on success. */ header('Location: ' . get_pkgreq_route()); exit(); - } if (isset($base_id)) { + } elseif ((current_action("do_DeleteComment") || + current_action("do_UndeleteComment")) && $return_to) { + header('Location: ' . $return_to); + exit(); + } elseif (current_action("do_PinComment") && $return_to) { + header('Location: ' . $return_to); + exit(); + } elseif (isset($base_id)) { /* Redirect back to package base page on success. */ header('Location: ' . get_pkgbase_uri($pkgbase_name) . $fragment); exit(); diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index df573755..dc444842 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -1403,3 +1403,45 @@ function accept_terms($uid, $termrev) { $dbh->exec($q); } } + +function account_comments($uid, $limit, $offset=0) { + $dbh = DB::connect(); + $q = "SELECT PackageComments.ID, Comments, UsersID, "; + $q.= "PackageBaseId, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, "; + $q.= "PinnedTS, "; + $q.= "C.UserName as DelUserName, RenderedComment, "; + $q.= "PB.ID as PackageBaseID, PB.Name as PackageBaseName "; + $q.= "FROM PackageComments "; + $q.= "LEFT JOIN PackageBases PB ON PackageComments.PackageBaseID = PB.ID "; + $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; + $q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID "; + $q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID "; + $q.= "WHERE A.ID = " . $dbh->quote($uid) . " "; + $q.= "ORDER BY CommentTS DESC"; + + if ($limit > 0) { + $q.=" LIMIT " . intval($limit); + } + + if ($offset > 0) { + $q.=" OFFSET " . intval($offset); + } + + $result = $dbh->query($q); + if (!$result) { + return null; + } + + return $result->fetchAll(); +} + +function account_comments_count($uid) { + $dbh = DB::connect(); + $q = "SELECT COUNT(*) "; + $q.= "FROM PackageComments "; + $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; + $q.= "WHERE A.ID = " . $dbh->quote($uid); + + $result = $dbh->query($q); + return $result->fetchColumn(); +} diff --git a/web/lib/aur.inc.php b/web/lib/aur.inc.php index feb4006b..e9530fc0 100644 --- a/web/lib/aur.inc.php +++ b/web/lib/aur.inc.php @@ -705,3 +705,56 @@ function aur_location() { } return $location; } + +/** + * Calculate pagination templates + * + * @return array The array of pagination templates, per page, and offset values + */ +function calculate_pagination($total_comment_count) { + /* Sanitize paging variables. */ + if (isset($_GET["O"])) { + $_GET["O"] = max(intval($_GET["O"]), 0); + } else { + $_GET["O"] = 0; + } + $offset = $_GET["O"]; + + if (isset($_GET["PP"])) { + $_GET["PP"] = bound(intval($_GET["PP"]), 1, 250); + } else { + $_GET["PP"] = 10; + } + $per_page = $_GET["PP"]; + + // Page offsets start at zero, so page 2 has offset 1, which means that we + // need to add 1 to the offset to get the current page. + $current_page = ceil($offset / $per_page) + 1; + $num_pages = ceil($total_comment_count / $per_page); + $pagination_templs = array(); + + if ($current_page > 1) { + $previous_page = $current_page - 1; + $previous_offset = ($previous_page - 1) * $per_page; + $pagination_templs['« ' . __('First')] = 0; + $pagination_templs['‹ ' . __('Previous')] = $previous_offset; + } + + if ($current_page - 5 > 1) { + $pagination_templs["..."] = false; + } + + for ($i = max($current_page - 5, 1); $i <= min($num_pages, $current_page + 5); $i++) { + $pagination_templs[$i] = ($i - 1) * $per_page; + } + + if ($current_page + 5 < $num_pages) + $pagination_templs["... "] = false; + + if ($current_page < $num_pages) { + $pagination_templs[__('Next') . ' ›'] = $current_page * $per_page; + $pagination_templs[__('Last') . ' »'] = ($num_pages - 1) * $per_page; + } + + return array($pagination_templs, $per_page, $offset); +} diff --git a/web/lib/credentials.inc.php b/web/lib/credentials.inc.php index d8698a87..c1251197 100644 --- a/web/lib/credentials.inc.php +++ b/web/lib/credentials.inc.php @@ -5,6 +5,7 @@ define("CRED_ACCOUNT_EDIT", 2); define("CRED_ACCOUNT_EDIT_DEV", 3); define("CRED_ACCOUNT_LAST_LOGIN", 4); define("CRED_ACCOUNT_SEARCH", 5); +define("CRED_ACCOUNT_LIST_COMMENTS", 28); define("CRED_COMMENT_DELETE", 6); define("CRED_COMMENT_UNDELETE", 27); define("CRED_COMMENT_VIEW_DELETED", 22); @@ -48,6 +49,7 @@ function has_credential($credential, $approved_users=array()) { $atype = account_from_sid($_COOKIE['AURSID']); switch ($credential) { + case CRED_ACCOUNT_LIST_COMMENTS: case CRED_PKGBASE_FLAG: case CRED_PKGBASE_NOTIFY: case CRED_PKGBASE_VOTE: diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php index 72c33b6d..953a5817 100644 --- a/web/lib/pkgbasefuncs.inc.php +++ b/web/lib/pkgbasefuncs.inc.php @@ -44,7 +44,7 @@ function pkgbase_comments_count($base_id, $include_deleted, $only_pinned=false) * * @return array All package comment information for a specific package base */ -function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false) { +function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false, $offset=0) { $base_id = intval($base_id); $limit = intval($limit); if (!$base_id) { @@ -71,6 +71,9 @@ function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false if ($limit > 0) { $q.=" LIMIT " . $limit; } + if ($offset > 0) { + $q.=" OFFSET " . $offset; + } $result = $dbh->query($q); if (!$result) { return null; @@ -273,6 +276,7 @@ function pkgbase_display_details($base_id, $row, $SID="") { include('pkgbase_details.php'); if ($SID) { + $comment_section = "package"; include('pkg_comment_box.php'); } @@ -281,13 +285,17 @@ function pkgbase_display_details($base_id, $row, $SID="") { $limit_pinned = isset($_GET['pinned']) ? 0 : 5; $pinned = pkgbase_comments($base_id, $limit_pinned, false, true); if (!empty($pinned)) { + $comment_section = "package"; include('pkg_comments.php'); } unset($pinned); + $limit = isset($_GET['comments']) ? 0 : 10; $comments = pkgbase_comments($base_id, $limit, $include_deleted); + if (!empty($comments)) { + $comment_section = "package"; include('pkg_comments.php'); } } diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index ad254746..140b8fc2 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -624,13 +624,17 @@ function pkg_display_details($id=0, $row, $SID="") { $limit_pinned = isset($_GET['pinned']) ? 0 : 5; $pinned = pkgbase_comments($base_id, $limit_pinned, false, true); if (!empty($pinned)) { + $comment_section = "package"; include('pkg_comments.php'); } unset($pinned); + $limit = isset($_GET['comments']) ? 0 : 10; $comments = pkgbase_comments($base_id, $limit, $include_deleted); + if (!empty($comments)) { + $comment_section = "package"; include('pkg_comments.php'); } } diff --git a/web/template/account_details.php b/web/template/account_details.php index 024bd9c3..fa6b528c 100644 --- a/web/template/account_details.php +++ b/web/template/account_details.php @@ -82,6 +82,9 @@
  • + +
  • + diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index 6eff81bd..38d5274c 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -2,6 +2,7 @@

    ', '') ?> ', '') ?> + ', '') ?>

    diff --git a/web/template/pkg_comments.php b/web/template/pkg_comments.php index 3e5e5cc5..3001a342 100644 --- a/web/template/pkg_comments.php +++ b/web/template/pkg_comments.php @@ -1,28 +1,69 @@ -
    -

    - - - - - - - + + + +
    + +
    + +
    +

    + + + + + + + + + + + + +

    + + 1): ?> +

    + $pagestart): ?> + + + + + + + + + + + + +

    -

    +
    $row): ?> ' . htmlspecialchars($row['PackageBaseName']) . ''; + $heading = __('Commented on package %s on %s', $pkg_uri, $date_fmtd); } $is_deleted = $row['DelTS']; @@ -50,8 +91,13 @@ if (!isset($count)) { } $heading .= ')'; } + + $comment_classes = "comment-header"; + if ($is_deleted) { + $comment_classes .= " comment-deleted"; + } ?> -

    " class="comment-deleted"> +

    " class=""> @@ -59,6 +105,7 @@ if (!isset($count)) { + " /> @@ -70,6 +117,7 @@ if (!isset($count)) { + " /> @@ -79,13 +127,14 @@ if (!isset($count)) { <?= __('Edit comment') ?> - = 5)): ?> + = 5)): ?>
    - + " /> + " />
    @@ -97,6 +146,7 @@ if (!isset($count)) { + " /> @@ -114,13 +164,8 @@ if (!isset($count)) { - - 10 && !isset($_GET['comments']) && !isset($pinned)): ?> -

    - -

    - + diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php index b2ce8cbe..a6857c4e 100644 --- a/web/template/pkgbase_details.php +++ b/web/template/pkgbase_details.php @@ -49,9 +49,9 @@ $base_uri = get_pkgbase_uri($row['Name']); - () + (, ) -
    +
    () @@ -135,3 +135,16 @@ endif; + + From eeaa1c3a3220e3735445d094dc7d2cd9ac07b621 Mon Sep 17 00:00:00 2001 From: Stephan Springer Date: Sat, 4 Jan 2020 14:00:28 +0100 Subject: [PATCH 0044/1451] Separate text from footer in notification emails Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 591b5ca4..6c3be222 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -151,7 +151,7 @@ class CommentNotification(Notification): body = self._l10n.translate( '{user} [1] added the following comment to {pkgbase} [2]:', lang).format(user=self._user, pkgbase=self._pkgbase) - body += '\n\n' + self._text + '\n\n' + body += '\n\n' + self._text + '\n\n-- \n' dnlabel = self._l10n.translate('Disable notifications', lang) body += self._l10n.translate( 'If you no longer wish to receive notifications about this ' @@ -196,7 +196,7 @@ class UpdateNotification(Notification): '{pkgbase} [2].', lang).format( user=self._user, pkgbase=self._pkgbase) - body += '\n\n' + body += '\n\n-- \n' dnlabel = self._l10n.translate('Disable notifications', lang) body += self._l10n.translate( 'If you no longer wish to receive notifications about this ' @@ -362,6 +362,7 @@ class DeleteNotification(Notification): dnlabel = self._l10n.translate('Disable notifications', lang) return self._l10n.translate( '{user} [1] merged {old} [2] into {new} [3].\n\n' + '-- \n' 'If you no longer wish receive notifications about the ' 'new package, please go to [3] and click "{label}".', lang).format(user=self._user, old=self._old_pkgbase, From daee20c694000e1e85a98760773bcbbdc0709527 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 10:23:50 +0100 Subject: [PATCH 0045/1451] Require current password when setting a new one Prevent from easily taking over an account by changing the password with a stolen session ID. Fixes FS#65325. Signed-off-by: Lukas Fleischer --- web/html/account.php | 1 + web/html/register.php | 2 ++ web/lib/acctfuncs.inc.php | 15 ++++++++++++-- web/template/account_edit_form.php | 32 +++++++++++++++++++----------- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/web/html/account.php b/web/html/account.php index 1d59e9c9..7c6c424a 100644 --- a/web/html/account.php +++ b/web/html/account.php @@ -34,6 +34,7 @@ if ($action == "UpdateAccount") { in_request("S"), in_request("E"), in_request("H"), + in_request("PO"), in_request("P"), in_request("C"), in_request("R"), diff --git a/web/html/register.php b/web/html/register.php index a4264829..8174e342 100644 --- a/web/html/register.php +++ b/web/html/register.php @@ -26,6 +26,7 @@ if (in_request("Action") == "NewAccount") { in_request("H"), '', '', + '', in_request("R"), in_request("L"), in_request("TZ"), @@ -54,6 +55,7 @@ if (in_request("Action") == "NewAccount") { in_request("H"), '', '', + '', in_request("R"), in_request("L"), in_request("TZ"), diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index e754989a..1de49b01 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -96,6 +96,7 @@ function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="" * @param string $S Whether or not the account is suspended * @param string $E The e-mail address for the user * @param string $H Whether or not the e-mail address should be hidden + * @param string $PO The old password of the user * @param string $P The password for the user * @param string $C The confirmed password for the user * @param string $R The real name of the user @@ -116,7 +117,7 @@ function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="" * * @return array Boolean indicating success and message to be printed */ -function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="", +function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$PO="",$P="",$C="", $R="",$L="",$TZ="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="",$captcha_salt="",$captcha="") { global $SUPPORTED_LANGS; @@ -134,6 +135,7 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="" if(isset($_COOKIE['AURSID'])) { $editor_user = uid_from_sid($_COOKIE['AURSID']); + $row = account_details(in_request("ID"), in_request("U")); } else { $editor_user = null; @@ -159,9 +161,18 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="" . "\n"; } - if (!$error && $P && $C && ($P != $C)) { + if (!$error && $P && !$C) { + $error = __("Please confirm your new password."); + } + if (!$error && $P && !$PO) { + $error = __("Please enter your old password in order to set a new one."); + } + if (!$error && $P && $P != $C) { $error = __("Password fields do not match."); } + if (!$error && $P && check_passwd($UID, $PO) != 1) { + $error = __("The old password is invalid."); + } if (!$error && $P != '' && !good_passwd($P)) { $length_min = config_get_int('options', 'passwd_min_len'); $error = __("Your password must be at least %s characters.", diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index 5e84aa71..25e91853 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -86,18 +86,6 @@ />

    - -

    - - -

    - -

    - - -

    - -

    @@ -150,6 +138,26 @@

    + +
    + +

    + + +

    + +

    + + +

    + +

    + + +

    +
    + +

    From 4ececd6041133ea9745261b7a2ac0da1e8976e21 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 13:19:16 +0100 Subject: [PATCH 0046/1451] Keep signature delimiters intact in notifications Since commit eeaa1c3 (Separate text from footer in notification emails, 2020-01-04), information about unsubscribing from notifications is added in a signature block. However, the code to format the email body trimmed the RFC 3676 signature delimiter, replacing "-- " by "--". Fix this by adding a special case for signature delimiters. Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 6c3be222..f2767fd8 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -54,6 +54,9 @@ class Notification: def get_body_fmt(self, lang): body = '' for line in self.get_body(lang).splitlines(): + if line == '-- ': + body += '-- \n' + continue body += textwrap.fill(line, break_long_words=False) + '\n' for i, ref in enumerate(self.get_refs()): body += '\n' + '[%d] %s' % (i + 1, ref) From d0e5c3db693296ed7e909c3a0a4457d1328d87de Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 13:23:51 +0100 Subject: [PATCH 0047/1451] t2500: fix test cases Since commit eeaa1c3 (Separate text from footer in notification emails, 2020-01-04), information about unsubscribing from notifications is added in a signature block. Fix the test cases accordingly. Signed-off-by: Lukas Fleischer --- test/t2500-notify.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/t2500-notify.sh b/test/t2500-notify.sh index 3080dc24..380e65b8 100755 --- a/test/t2500-notify.sh +++ b/test/t2500-notify.sh @@ -101,6 +101,7 @@ test_expect_success 'Test subject and body of comment notifications.' ' This is a test comment. + -- If you no longer wish to receive notifications about this package, please go to the package page [2] and select "Disable notifications". @@ -126,6 +127,7 @@ test_expect_success 'Test subject and body of update notifications.' ' cat <<-EOD >expected && user [1] pushed a new commit to foobar [2]. + -- If you no longer wish to receive notifications about this package, please go to the package page [2] and select "Disable notifications". @@ -264,6 +266,7 @@ test_expect_success 'Test subject and body of merge notifications.' ' cat <<-EOD >expected && user [1] merged foobar [2] into foobar2 [3]. + -- If you no longer wish receive notifications about the new package, please go to [3] and click "Disable notifications". From f090896fa1e9570715cfcdec7b23ecf95d25e936 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 11:58:16 +0100 Subject: [PATCH 0048/1451] Undo accidental code addition Rollback an accidental change that sneaked into commit daee20c (Require current password when setting a new one, 2020-01-30). Signed-off-by: Lukas Fleischer --- web/lib/acctfuncs.inc.php | 1 - 1 file changed, 1 deletion(-) diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index 1de49b01..601d4ce0 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -135,7 +135,6 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$PO="",$P=" if(isset($_COOKIE['AURSID'])) { $editor_user = uid_from_sid($_COOKIE['AURSID']); - $row = account_details(in_request("ID"), in_request("U")); } else { $editor_user = null; From 7aa420d24da7e8c2c214ab421d44b4684d42e73e Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 12:39:52 +0100 Subject: [PATCH 0049/1451] Verify current password against logged in user When changing the password of an account, instead of asking for the old password of the account, ask for the password of the currently logged in user. This allows privileged users to edit other accounts without knowing their passwords. Signed-off-by: Lukas Fleischer --- web/lib/acctfuncs.inc.php | 9 ++++----- web/template/account_edit_form.php | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index 601d4ce0..d2144c2a 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -134,10 +134,9 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$PO="",$P=" $dbh = DB::connect(); if(isset($_COOKIE['AURSID'])) { - $editor_user = uid_from_sid($_COOKIE['AURSID']); - } - else { - $editor_user = null; + $uid_session = uid_from_sid($_COOKIE['AURSID']); + } else { + $uid_session = null; } if (empty($E) || empty($U)) { @@ -169,7 +168,7 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$PO="",$P=" if (!$error && $P && $P != $C) { $error = __("Password fields do not match."); } - if (!$error && $P && check_passwd($UID, $PO) != 1) { + if (!$error && $P && check_passwd($uid_session, $PO) != 1) { $error = __("The old password is invalid."); } if (!$error && $P != '' && !good_passwd($P)) { diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index 25e91853..7bd233a8 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -140,9 +140,9 @@

    - +

    - +

    From 8fc8898fef39af20a24c9928464fd8420481d819 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 11:52:32 +0100 Subject: [PATCH 0050/1451] Require password when deleting an account Further reduce the attack surface in case of a stolen session ID. Signed-off-by: Lukas Fleischer --- web/html/account.php | 17 +++++++++++++---- web/template/account_delete.php | 11 +++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/web/html/account.php b/web/html/account.php index 7c6c424a..03af8d43 100644 --- a/web/html/account.php +++ b/web/html/account.php @@ -120,12 +120,21 @@ if (isset($_COOKIE["AURSID"])) { } elseif ($action == "DeleteAccount") { /* Details for account being deleted. */ if (can_edit_account($row)) { - $UID = $row['ID']; + $uid_removal = $row['ID']; + $uid_session = uid_from_sid($_COOKIE['AURSID']); + $username = $row['Username']; + if (in_request('confirm') && check_token()) { - user_delete($UID); - header('Location: /'); + if (check_passwd($uid_session, $_REQUEST['passwd']) == 1) { + user_delete($uid_removal); + header('Location: /'); + } else { + echo "
    • "; + echo __("Invalid password."); + echo "
    "; + include("account_delete.php"); + } } else { - $username = $row['Username']; include("account_delete.php"); } } else { diff --git a/web/template/account_delete.php b/web/template/account_delete.php index 718b172f..d0c6e74d 100644 --- a/web/template/account_delete.php +++ b/web/template/account_delete.php @@ -12,8 +12,15 @@
    -

    +

    + + +

    + +

    + +

    " /> From def2787b45275de2b8dfab0ece87f35ea280567b Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 14:00:07 +0100 Subject: [PATCH 0051/1451] Require password when changing account information Since commits daee20c (Require current password when setting a new one, 2020-01-30) and 8fc8898 (Require password when deleting an account, 2020-01-30), changing a password and deleting an account require the current password. Extend this to all other profile changes. Signed-off-by: Lukas Fleischer --- web/html/account.php | 5 +++-- web/html/register.php | 4 ++-- web/lib/acctfuncs.inc.php | 19 +++++++------------ web/template/account_edit_form.php | 17 +++++++++-------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/web/html/account.php b/web/html/account.php index 03af8d43..ff9aba5b 100644 --- a/web/html/account.php +++ b/web/html/account.php @@ -34,7 +34,6 @@ if ($action == "UpdateAccount") { in_request("S"), in_request("E"), in_request("H"), - in_request("PO"), in_request("P"), in_request("C"), in_request("R"), @@ -49,7 +48,9 @@ if ($action == "UpdateAccount") { in_request("UN"), in_request("ON"), in_request("ID"), - $row["Username"]); + $row["Username"], + in_request("passwd") + ); } } diff --git a/web/html/register.php b/web/html/register.php index 8174e342..610befc4 100644 --- a/web/html/register.php +++ b/web/html/register.php @@ -26,7 +26,6 @@ if (in_request("Action") == "NewAccount") { in_request("H"), '', '', - '', in_request("R"), in_request("L"), in_request("TZ"), @@ -40,6 +39,7 @@ if (in_request("Action") == "NewAccount") { in_request("ON"), 0, "", + '', in_request("captcha_salt"), in_request("captcha"), ); @@ -55,7 +55,6 @@ if (in_request("Action") == "NewAccount") { in_request("H"), '', '', - '', in_request("R"), in_request("L"), in_request("TZ"), @@ -69,6 +68,7 @@ if (in_request("Action") == "NewAccount") { in_request("ON"), 0, "", + '', in_request("captcha_salt"), in_request("captcha") ); diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index d2144c2a..345d27af 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -96,7 +96,6 @@ function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="" * @param string $S Whether or not the account is suspended * @param string $E The e-mail address for the user * @param string $H Whether or not the e-mail address should be hidden - * @param string $PO The old password of the user * @param string $P The password for the user * @param string $C The confirmed password for the user * @param string $R The real name of the user @@ -112,13 +111,14 @@ function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="" * @param string $ON Whether to notify of ownership changes * @param string $UID The user ID of the modified account * @param string $N The username as present in the database + * @param string $passwd The password of the logged in user. * @param string $captcha_salt The salt used for the CAPTCHA. * @param string $captcha The CAPTCHA answer. * * @return array Boolean indicating success and message to be printed */ -function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$PO="",$P="",$C="", - $R="",$L="",$TZ="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="",$captcha_salt="",$captcha="") { +function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="", + $R="",$L="",$TZ="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="",$passwd="",$captcha_salt="",$captcha="") { global $SUPPORTED_LANGS; $error = ''; @@ -133,10 +133,11 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$PO="",$P=" $dbh = DB::connect(); - if(isset($_COOKIE['AURSID'])) { + if (isset($_COOKIE['AURSID'])) { $uid_session = uid_from_sid($_COOKIE['AURSID']); - } else { - $uid_session = null; + if (!$error && check_passwd($uid_session, $passwd) != 1) { + $error = __("Invalid password."); + } } if (empty($E) || empty($U)) { @@ -162,15 +163,9 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$PO="",$P=" if (!$error && $P && !$C) { $error = __("Please confirm your new password."); } - if (!$error && $P && !$PO) { - $error = __("Please enter your old password in order to set a new one."); - } if (!$error && $P && $P != $C) { $error = __("Password fields do not match."); } - if (!$error && $P && check_passwd($uid_session, $PO) != 1) { - $error = __("The old password is invalid."); - } if (!$error && $P != '' && !good_passwd($P)) { $length_min = config_get_int('options', 'passwd_min_len'); $error = __("Your password must be at least %s characters.", diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index 7bd233a8..09d65c0f 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -140,12 +140,7 @@

    - -

    - - -

    - +

    @@ -182,16 +177,22 @@

    -
    + + +

    + + +

    +

    ()

    -
    +

    From 23c0c9c372a7443e96115441571ea57bb24881c7 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 13:09:05 +0100 Subject: [PATCH 0052/1451] Update copyright range in the cgit footer --- web/template/cgit/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/template/cgit/footer.html b/web/template/cgit/footer.html index 1c0bf6f6..14c358f1 100644 --- a/web/template/cgit/footer.html +++ b/web/template/cgit/footer.html @@ -1,6 +1,6 @@

    From e5a839bf0b9884e2a015b3f0b3fdbf23d1a1654c Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 16:57:22 +0100 Subject: [PATCH 0053/1451] Add option to send reset key for a given user name In addition to supporting email addresses in the reset key form, also support user names. The reset key is then sent to the email address in the user's profile. Signed-off-by: Lukas Fleischer --- web/html/passreset.php | 25 ++++++++++++------------- web/lib/acctfuncs.inc.php | 13 +++++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/web/html/passreset.php b/web/html/passreset.php index 9e7cee88..b3c8bd29 100644 --- a/web/html/passreset.php +++ b/web/html/passreset.php @@ -11,14 +11,14 @@ if (isset($_COOKIE["AURSID"])) { $error = ''; -if (isset($_GET['resetkey'], $_POST['email'], $_POST['password'], $_POST['confirm'])) { +if (isset($_GET['resetkey'], $_POST['user'], $_POST['password'], $_POST['confirm'])) { $resetkey = $_GET['resetkey']; - $email = $_POST['email']; + $user = $_POST['user']; $password = $_POST['password']; $confirm = $_POST['confirm']; - $uid = uid_from_email($email); + $uid = uid_from_loginname($user); - if (empty($email) || empty($password)) { + if (empty($user) || empty($password)) { $error = __('Missing a required field.'); } elseif ($password != $confirm) { $error = __('Password fields do not match.'); @@ -31,16 +31,15 @@ if (isset($_GET['resetkey'], $_POST['email'], $_POST['password'], $_POST['confir } if (empty($error)) { - $error = password_reset($password, $resetkey, $email); + $error = password_reset($password, $resetkey, $user); } -} elseif (isset($_POST['email'])) { - $email = $_POST['email']; - $username = username_from_id(uid_from_email($email)); +} elseif (isset($_POST['user'])) { + $user = $_POST['user']; - if (empty($email)) { + if (empty($user)) { $error = __('Missing a required field.'); } else { - send_resetkey($email); + send_resetkey($user); header('Location: ' . get_uri('/passreset/') . '?step=confirm'); exit(); } @@ -67,7 +66,7 @@ html_header(__("Password Reset")); - + @@ -89,8 +88,8 @@ html_header(__("Password Reset"));
    -

    -

    +

    +

    diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index 345d27af..f6cda69c 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -755,13 +755,13 @@ function create_resetkey($resetkey, $uid) { /** * Send a reset key to a specific e-mail address * - * @param string $email E-mail address of the user resetting their password + * @param string $user User name or email address of the user * @param bool $welcome Whether to use the welcome message * * @return void */ -function send_resetkey($email, $welcome=false) { - $uid = uid_from_email($email); +function send_resetkey($user, $welcome=false) { + $uid = uid_from_loginname($user); if ($uid == null) { return; } @@ -779,11 +779,11 @@ function send_resetkey($email, $welcome=false) { * * @param string $password The new password * @param string $resetkey Code e-mailed to a user to reset a password - * @param string $email E-mail address of the user resetting their password + * @param string $user User name or email address of the user * * @return string|void Redirect page if successful, otherwise return error message */ -function password_reset($password, $resetkey, $email) { +function password_reset($password, $resetkey, $user) { $hash = password_hash($password, PASSWORD_DEFAULT); $dbh = DB::connect(); @@ -792,7 +792,8 @@ function password_reset($password, $resetkey, $email) { $q.= "ResetKey = '' "; $q.= "WHERE ResetKey != '' "; $q.= "AND ResetKey = " . $dbh->quote($resetkey) . " "; - $q.= "AND Email = " . $dbh->quote($email); + $q.= "AND (Email = " . $dbh->quote($user) . " OR "; + $q.= "UserName = " . $dbh->quote($user) . ")"; $result = $dbh->exec($q); if (!$result) { From ee2aa9755fa3c94e8c8a697c3f7a9627027994d5 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 30 Jan 2020 17:15:33 +0100 Subject: [PATCH 0054/1451] Add support for backup email addresses Support secondary email addresses that can be used to recover an account in case access to the primary email address is lost. Reset keys for an account are always sent to both the primary and the backup email address. Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 12 ++++++++---- schema/aur-schema.sql | 1 + upgrading/4.9.0.txt | 6 ++++++ web/html/account.php | 3 +++ web/html/login.php | 2 +- web/html/passreset.php | 6 +++--- web/html/register.php | 4 +++- web/lib/acctfuncs.inc.php | 15 +++++++++++---- web/template/account_edit_form.php | 12 +++++++++++- 9 files changed, 47 insertions(+), 14 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index f2767fd8..b0f218b5 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -90,13 +90,17 @@ class Notification: class ResetKeyNotification(Notification): def __init__(self, conn, uid): - cur = conn.execute('SELECT UserName, Email, LangPreference, ' + - 'ResetKey FROM Users WHERE ID = ?', [uid]) - self._username, self._to, self._lang, self._resetkey = cur.fetchone() + cur = conn.execute('SELECT UserName, Email, BackupEmail, ' + + 'LangPreference, ResetKey ' + + 'FROM Users WHERE ID = ?', [uid]) + self._username, self._to, self._backup, self._lang, self._resetkey = cur.fetchone() super().__init__() def get_recipients(self): - return [(self._to, self._lang)] + if self._backup: + return [(self._to, self._lang), (self._backup, self._lang)] + else: + return [(self._to, self._lang)] def get_subject(self, lang): return self._l10n.translate('AUR Password Reset', lang) diff --git a/schema/aur-schema.sql b/schema/aur-schema.sql index fa991ba6..1f86df20 100644 --- a/schema/aur-schema.sql +++ b/schema/aur-schema.sql @@ -23,6 +23,7 @@ CREATE TABLE Users ( Suspended TINYINT UNSIGNED NOT NULL DEFAULT 0, Username VARCHAR(32) NOT NULL, Email VARCHAR(254) NOT NULL, + BackupEmail VARCHAR(254) NULL DEFAULT NULL, HideEmail TINYINT UNSIGNED NOT NULL DEFAULT 0, Passwd VARCHAR(255) NOT NULL, Salt CHAR(32) NOT NULL DEFAULT '', diff --git a/upgrading/4.9.0.txt b/upgrading/4.9.0.txt index 4c79283e..241f24af 100644 --- a/upgrading/4.9.0.txt +++ b/upgrading/4.9.0.txt @@ -4,3 +4,9 @@ ALTER TABLE PackageRequests ADD COLUMN ClosedTS BIGINT UNSIGNED NULL DEFAULT NULL; ALTER TABLE PackageRequests ADD COLUMN ClosedUID INTEGER UNSIGNED NULL DEFAULT NULL; ---- + +2. Add a new column to store backup email addresses: + +---- +ALTER TABLE Users ADD COLUMN BackupEmail VARCHAR(254) NULL DEFAULT NULL; +---- diff --git a/web/html/account.php b/web/html/account.php index ff9aba5b..c05d136d 100644 --- a/web/html/account.php +++ b/web/html/account.php @@ -33,6 +33,7 @@ if ($action == "UpdateAccount") { in_request("T"), in_request("S"), in_request("E"), + in_request("BE"), in_request("H"), in_request("P"), in_request("C"), @@ -97,6 +98,7 @@ if (isset($_COOKIE["AURSID"])) { $row["AccountTypeID"], $row["Suspended"], $row["Email"], + $row["BackupEmail"], $row["HideEmail"], "", "", @@ -159,6 +161,7 @@ if (isset($_COOKIE["AURSID"])) { in_request("T"), in_request("S"), in_request("E"), + in_request("BE"), in_request("H"), in_request("P"), in_request("C"), diff --git a/web/html/login.php b/web/html/login.php index df517055..01454414 100644 --- a/web/html/login.php +++ b/web/html/login.php @@ -26,7 +26,7 @@ html_header('AUR ' . __("Login"));

    - +

    diff --git a/web/html/passreset.php b/web/html/passreset.php index b3c8bd29..26b9bbbb 100644 --- a/web/html/passreset.php +++ b/web/html/passreset.php @@ -65,7 +65,7 @@ html_header(__("Password Reset"));

    - + @@ -81,14 +81,14 @@ html_header(__("Password Reset")); -

    ', ''); ?>

    -

    +

    diff --git a/web/html/register.php b/web/html/register.php index 610befc4..fee0a68f 100644 --- a/web/html/register.php +++ b/web/html/register.php @@ -23,6 +23,7 @@ if (in_request("Action") == "NewAccount") { 1, 0, in_request("E"), + in_request("BE"), in_request("H"), '', '', @@ -52,6 +53,7 @@ if (in_request("Action") == "NewAccount") { 1, 0, in_request("E"), + in_request("BE"), in_request("H"), '', '', @@ -75,7 +77,7 @@ if (in_request("Action") == "NewAccount") { } } else { print '

    ' . __("Use this form to create an account.") . '

    '; - display_account_form("NewAccount", "", "", "", "", "", "", "", "", $LANG); + display_account_form("NewAccount", "", "", "", "", "", "", "", "", "", $LANG); } echo ''; diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index f6cda69c..443fb4b1 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -46,6 +46,7 @@ function html_format_pgp_fingerprint($fingerprint) { * @param string $T The account type of the displayed user * @param string $S Whether the displayed user has a suspended account * @param string $E The e-mail address of the displayed user + * @param string $BE The backup e-mail address of the displayed user * @param string $H Whether the e-mail address of the displayed user is hidden * @param string $P The password value of the displayed user * @param string $C The confirmed password value of the displayed user @@ -67,7 +68,7 @@ function html_format_pgp_fingerprint($fingerprint) { * * @return void */ -function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="", +function display_account_form($A,$U="",$T="",$S="",$E="",$BE="",$H="",$P="",$C="",$R="", $L="",$TZ="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="",$captcha_salt="",$captcha="") { global $SUPPORTED_LANGS; @@ -95,6 +96,7 @@ function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="" * @param string $T The account type for the user * @param string $S Whether or not the account is suspended * @param string $E The e-mail address for the user + * @param string $BE The backup e-mail address for the user * @param string $H Whether or not the e-mail address should be hidden * @param string $P The password for the user * @param string $C The confirmed password for the user @@ -117,7 +119,7 @@ function display_account_form($A,$U="",$T="",$S="",$E="",$H="",$P="",$C="",$R="" * * @return array Boolean indicating success and message to be printed */ -function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="", +function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$BE="",$H="",$P="",$C="", $R="",$L="",$TZ="",$HP="",$I="",$K="",$PK="",$J="",$CN="",$UN="",$ON="",$UID=0,$N="",$passwd="",$captcha_salt="",$captcha="") { global $SUPPORTED_LANGS; @@ -175,6 +177,9 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="" if (!$error && !valid_email($E)) { $error = __("The email address is invalid."); } + if (!$error && $BE && !valid_email($BE)) { + $error = __("The backup email address is invalid."); + } if (!$error && !empty($HP) && !valid_homepage($HP)) { $error = __("The home page is invalid, please specify the full HTTP(s) URL."); @@ -311,6 +316,7 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="" } $U = $dbh->quote($U); $E = $dbh->quote($E); + $BE = $dbh->quote($BE); $P = $dbh->quote($P); $R = $dbh->quote($R); $L = $dbh->quote($L); @@ -319,9 +325,9 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="" $I = $dbh->quote($I); $K = $dbh->quote(str_replace(" ", "", $K)); $q = "INSERT INTO Users (AccountTypeID, Suspended, "; - $q.= "InactivityTS, Username, Email, Passwd , "; + $q.= "InactivityTS, Username, Email, BackupEmail, Passwd , "; $q.= "RealName, LangPreference, Timezone, Homepage, IRCNick, PGPKey) "; - $q.= "VALUES (1, 0, 0, $U, $E, $P, $R, $L, $TZ, "; + $q.= "VALUES (1, 0, 0, $U, $E, $BE, $P, $R, $L, $TZ, "; $q.= "$HP, $I, $K)"; $result = $dbh->exec($q); if (!$result) { @@ -374,6 +380,7 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$H="",$P="",$C="" $q.= ", Suspended = 0"; } $q.= ", Email = " . $dbh->quote($E); + $q.= ", BackupEmail = " . $dbh->quote($BE); if ($H) { $q.= ", HideEmail = 1"; } else { diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index 09d65c0f..edacbbf3 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -76,11 +76,21 @@ ()

    -

    +

    + + +

    +

    + + + + +

    +

    /> From e5f8fe5528960f5033df0e2a0bad14aea9154741 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 31 Jan 2020 09:16:23 +0100 Subject: [PATCH 0055/1451] Explain the hide email address setting Signed-off-by: Lukas Fleischer --- web/template/account_edit_form.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index edacbbf3..a4ea9949 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -80,6 +80,14 @@

    +

    + + /> +

    +

    + +

    +

    @@ -88,14 +96,10 @@ + " . __("Hide Email Address") . "") ?>

    -

    - - /> -

    -

    From aa555f9ae5a68f7567c862d017e27c78d1b6ec95 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 31 Jan 2020 09:25:57 +0100 Subject: [PATCH 0056/1451] Explain syntax/features in the comments section Addresses FS#64983. Signed-off-by: Lukas Fleischer --- web/template/pkg_comment_form.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/template/pkg_comment_form.php b/web/template/pkg_comment_form.php index 3feee8fb..e8a516e3 100644 --- a/web/template/pkg_comment_form.php +++ b/web/template/pkg_comment_form.php @@ -8,6 +8,10 @@ +

    + + ', "") ?> +

    From 8ff21fd39c537ef930e2bd32f76cd28d40fa3b67 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 31 Jan 2020 08:44:23 +0100 Subject: [PATCH 0057/1451] Update message catalog Signed-off-by: Lukas Fleischer --- po/aurweb.pot | 146 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 21 deletions(-) diff --git a/po/aurweb.pot b/po/aurweb.pot index e49dc132..aeed9f02 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -1,14 +1,14 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: AUR v4.6.0\n" +"Project-Id-Version: AURWEB v4.8.0\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -73,6 +73,10 @@ msgstr "" msgid "You do not have permission to edit this account." msgstr "" +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "" @@ -367,10 +371,10 @@ msgid "Enter login credentials" msgstr "" #: html/login.php -msgid "User name or email address" +msgid "User name or primary email address" msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "" @@ -431,7 +435,7 @@ msgid "Your password has been reset successfully." msgstr "" #: html/passreset.php -msgid "Confirm your e-mail address:" +msgid "Confirm your user name or primary e-mail address:" msgstr "" #: html/passreset.php @@ -449,12 +453,12 @@ msgstr "" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a " -"message to the %saur-general%s mailing list." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" +msgid "Enter your user name or your primary e-mail address:" msgstr "" #: html/pkgbase.php @@ -646,19 +650,19 @@ msgstr "" msgid "Close Request" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "" @@ -759,10 +763,18 @@ msgstr "" msgid "Can contain only one period, underscore or hyphen." msgstr "" +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "" +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -802,6 +814,18 @@ msgstr "" msgid "The SSH public key, %s%s%s, is already in use." msgstr "" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1210,6 +1234,10 @@ msgstr "" msgid "Edit this user's account" msgstr "" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1220,6 +1248,11 @@ msgstr "" msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "" @@ -1257,7 +1290,33 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." msgstr "" #: template/account_edit_form.php @@ -1268,6 +1327,16 @@ msgstr "" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new " +"password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to " @@ -1294,6 +1363,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1422,7 +1509,7 @@ msgstr "" msgid "Disable notifications" msgstr "" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1453,6 +1540,10 @@ msgstr "" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1499,8 +1590,15 @@ msgstr "" msgid "Add Comment" msgstr "" -#: template/pkg_comments.php -msgid "View all comments" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and " +"URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." msgstr "" #: template/pkg_comments.php @@ -1511,6 +1609,10 @@ msgstr "" msgid "Latest Comments" msgstr "" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1521,6 +1623,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1557,10 +1664,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "" - #: template/pkg_details.php msgid "Package Details" msgstr "" @@ -2106,6 +2209,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go " "to [3] and click \"{label}\"." msgstr "" From c277a3de8f3255be31a2c3f08cd49fe2f6c36aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Thu, 30 Jan 2020 03:35:12 +0100 Subject: [PATCH 0058/1451] rendercomment: respectful linkification of Git commits Turn the git-commits markdown processor into an inline processor, which is smart enough not to convert Git hashes contained in code blocks or links. Signed-off-by: Lukas Fleischer --- aurweb/scripts/rendercomment.py | 36 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 5e18fd59..5c597481 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -40,19 +40,26 @@ class FlysprayLinksExtension(markdown.extensions.Extension): md.preprocessors.add('flyspray-links', preprocessor, '_end') -class GitCommitsPreprocessor(markdown.preprocessors.Preprocessor): - _oidre = re.compile(r'(\b)([0-9a-f]{7,40})(\b)') +class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): + """ + Turn Git hashes like f7f5152be5ab into links to AUR's cgit. + + Only commit references that do exist are linkified. Hashes are shortened to + shorter non-ambiguous prefixes. Only hashes with at least 7 digits are + considered. + """ + _repo = pygit2.Repository(repo_path) - _head = None def __init__(self, md, head): self._head = head - super(markdown.preprocessors.Preprocessor, self).__init__(md) + super().__init__(r'\b([0-9a-f]{7,40})\b', md) - def handleMatch(self, m): - oid = m.group(2) + def handleMatch(self, m, data): + oid = m.group(1) if oid not in self._repo: - return oid + # Unkwown OID; preserve the orginal text. + return None, None, None prefixlen = 12 while prefixlen < 40: @@ -60,13 +67,10 @@ class GitCommitsPreprocessor(markdown.preprocessors.Preprocessor): break prefixlen += 1 - html = '[`' + oid[:prefixlen] + '`]' - html += '(' + commit_uri % (self._head, oid[:prefixlen]) + ')' - - return html - - def run(self, lines): - return [self._oidre.sub(self.handleMatch, line) for line in lines] + el = markdown.util.etree.Element('a') + el.set('href', commit_uri % (self._head, oid[:prefixlen])) + el.text = markdown.util.AtomicString(oid[:prefixlen]) + return el, m.start(0), m.end(0) class GitCommitsExtension(markdown.extensions.Extension): @@ -77,8 +81,8 @@ class GitCommitsExtension(markdown.extensions.Extension): super(markdown.extensions.Extension, self).__init__() def extendMarkdown(self, md, md_globals): - preprocessor = GitCommitsPreprocessor(md, self._head) - md.preprocessors.add('git-commits', preprocessor, '_end') + processor = GitCommitsInlineProcessor(md, self._head) + md.inlinePatterns.add('git-commits', processor, '_end') class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): From 0fc69e96bdd1197ca2b91bcd16a3064366954c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Fri, 31 Jan 2020 03:20:26 +0100 Subject: [PATCH 0059/1451] rendercomment: add a test for Git commit links Signed-off-by: Lukas Fleischer --- test/t2600-rendercomment.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/t2600-rendercomment.sh b/test/t2600-rendercomment.sh index edf290cd..7b3a4a8d 100755 --- a/test/t2600-rendercomment.sh +++ b/test/t2600-rendercomment.sh @@ -63,4 +63,33 @@ test_expect_success 'Test link conversion.' ' test_cmp actual expected ' +test_expect_success 'Test Git commit linkification.' ' + local oid=`git -C aur.git rev-parse --verify HEAD` + cat <<-EOD | sqlite3 aur.db && + INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (5, 1, " + $oid + ${oid:0:7} + x.$oid.x + ${oid}x + 0123456789abcdef + \`$oid\` + http://example.com/$oid + ", ""); + EOD + "$RENDERCOMMENT" 5 && + cat <<-EOD >expected && +

    ${oid:0:12} + ${oid:0:7} + x.${oid:0:12}.x + ${oid}x + 0123456789abcdef + $oid + http://example.com/$oid

    + EOD + cat <<-EOD | sqlite3 aur.db >actual && + SELECT RenderedComment FROM PackageComments WHERE ID = 5; + EOD + test_cmp actual expected +' + test_done From 199f34e42e78ca97f154a4880b77b3f1d01fa9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sat, 1 Feb 2020 19:07:41 +0100 Subject: [PATCH 0060/1451] rendercomment: safer auto-linkification of URLs Fixes a few edge cases: - URLs within code blocks used to get redundant <> added, breaking bash code snippets like `curl https://...` into `curl `. - Links written with markdown's syntax also used to get an extra pair of brackets. Signed-off-by: Lukas Fleischer --- aurweb/scripts/rendercomment.py | 21 ++++++++++++--------- test/t2600-rendercomment.sh | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 5c597481..346ccff1 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -13,17 +13,20 @@ repo_path = aurweb.config.get('serve', 'repo-path') commit_uri = aurweb.config.get('options', 'commit_uri') -class LinkifyPreprocessor(markdown.preprocessors.Preprocessor): - _urlre = re.compile(r'(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' - r'(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))') - - def run(self, lines): - return [self._urlre.sub(r'<\1>', line) for line in lines] - - class LinkifyExtension(markdown.extensions.Extension): + """ + Turn URLs into links, even without explicit markdown. + Do not linkify URLs in code blocks. + """ + + # Captures http(s) and ftp URLs until the first non URL-ish character. + # Excludes trailing punctuation. + _urlre = (r'(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' + r'(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))') + def extendMarkdown(self, md, md_globals): - md.preprocessors.add('linkify', LinkifyPreprocessor(md), '_end') + processor = markdown.inlinepatterns.AutolinkInlineProcessor(self._urlre, md) + md.inlinePatterns.add('linkify', processor, '_end') class FlysprayLinksPreprocessor(markdown.preprocessors.Preprocessor): diff --git a/test/t2600-rendercomment.sh b/test/t2600-rendercomment.sh index 7b3a4a8d..b0209eb5 100755 --- a/test/t2600-rendercomment.sh +++ b/test/t2600-rendercomment.sh @@ -51,11 +51,22 @@ test_expect_success 'Test HTML sanitizing.' ' test_expect_success 'Test link conversion.' ' cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (4, 1, "Visit https://www.archlinux.org/.", ""); + INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (4, 1, " + Visit https://www.archlinux.org/. + Visit . + Visit \`https://www.archlinux.org/\`. + Visit [Arch Linux](https://www.archlinux.org/). + Visit [Arch Linux][arch]. + [arch]: https://www.archlinux.org/ + ", ""); EOD "$RENDERCOMMENT" 4 && cat <<-EOD >expected && -

    Visit https://www.archlinux.org/.

    +

    Visit https://www.archlinux.org/. + Visit https://www.archlinux.org/. + Visit https://www.archlinux.org/. + Visit Arch Linux. + Visit Arch Linux.

    EOD cat <<-EOD | sqlite3 aur.db >actual && SELECT RenderedComment FROM PackageComments WHERE ID = 4; From 127bb4c84cf8584dd4ee9f544333782d386224ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 2 Feb 2020 20:25:08 +0100 Subject: [PATCH 0061/1451] rendercomment: safer Flyspray task linkification When an FS#123 is part of a code block, it must not be converted into a link. FS#123 may also appear inside an URL, in which case regular linkifaction of URLs must take precedence. Signed-off-by: Lukas Fleischer --- aurweb/scripts/rendercomment.py | 21 ++++++++++++++------- test/t2600-rendercomment.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 346ccff1..22ed7b93 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -29,18 +29,25 @@ class LinkifyExtension(markdown.extensions.Extension): md.inlinePatterns.add('linkify', processor, '_end') -class FlysprayLinksPreprocessor(markdown.preprocessors.Preprocessor): - _fsre = re.compile(r'\b(FS#(\d+))\b') - _sub = r'[\1](https://bugs.archlinux.org/task/\2)' +class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): + """ + Turn Flyspray task references like FS#1234 into links to bugs.archlinux.org. - def run(self, lines): - return [self._fsre.sub(self._sub, line) for line in lines] + The pattern's capture group 0 is the text of the link and group 1 is the + Flyspray task ID. + """ + + def handleMatch(self, m, data): + el = markdown.util.etree.Element('a') + el.set('href', f'https://bugs.archlinux.org/task/{m.group(1)}') + el.text = markdown.util.AtomicString(m.group(0)) + return el, m.start(0), m.end(0) class FlysprayLinksExtension(markdown.extensions.Extension): def extendMarkdown(self, md, md_globals): - preprocessor = FlysprayLinksPreprocessor(md) - md.preprocessors.add('flyspray-links', preprocessor, '_end') + processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b',md) + md.inlinePatterns.add('flyspray-links', processor, '_end') class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): diff --git a/test/t2600-rendercomment.sh b/test/t2600-rendercomment.sh index b0209eb5..1da422d3 100755 --- a/test/t2600-rendercomment.sh +++ b/test/t2600-rendercomment.sh @@ -103,4 +103,30 @@ test_expect_success 'Test Git commit linkification.' ' test_cmp actual expected ' +test_expect_success 'Test Flyspray issue linkification.' ' + sqlite3 aur.db <<-EOD && + INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (6, 1, " + FS#1234567. + *FS#1234* + FS# + XFS#1 + \`FS#1234\` + https://archlinux.org/?test=FS#1234 + ", ""); + EOD + "$RENDERCOMMENT" 6 && + cat <<-EOD >expected && +

    FS#1234567. + FS#1234 + FS# + XFS#1 + FS#1234 + https://archlinux.org/?test=FS#1234

    + EOD + sqlite3 aur.db <<-EOD >actual && + SELECT RenderedComment FROM PackageComments WHERE ID = 6; + EOD + test_cmp actual expected +' + test_done From 81faab9978a80e1608b99b5d45456548eaf01a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 2 Feb 2020 20:25:33 +0100 Subject: [PATCH 0062/1451] rendercomment: test headings lowering Signed-off-by: Lukas Fleischer --- test/t2600-rendercomment.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/t2600-rendercomment.sh b/test/t2600-rendercomment.sh index 1da422d3..1ba560af 100755 --- a/test/t2600-rendercomment.sh +++ b/test/t2600-rendercomment.sh @@ -129,4 +129,30 @@ test_expect_success 'Test Flyspray issue linkification.' ' test_cmp actual expected ' +test_expect_success 'Test headings lowering.' ' + sqlite3 aur.db <<-EOD && + INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (7, 1, " + # One + ## Two + ### Three + #### Four + ##### Five + ###### Six + ", ""); + EOD + "$RENDERCOMMENT" 7 && + cat <<-EOD >expected && +
    One
    +
    Two
    +
    Three
    +
    Four
    +
    Five
    +
    Six
    + EOD + sqlite3 aur.db <<-EOD >actual && + SELECT RenderedComment FROM PackageComments WHERE ID = 7; + EOD + test_cmp actual expected +' + test_done From e15d5c8180fab81d3e533cc521c1a98ff6f5b0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 2 Feb 2020 20:26:13 +0100 Subject: [PATCH 0063/1451] rendercomment: use python-markdown's new registration API First, this gets rid of the deprecation warnings Python displayed. Second, this fixes the case where a link contained a pair of underscores, which used to be interpreted as an emphasis because the linkify processor ran after the emphasis processor. Signed-off-by: Lukas Fleischer --- aurweb/scripts/rendercomment.py | 10 ++++++---- test/t2600-rendercomment.sh | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 22ed7b93..76865d27 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -26,7 +26,8 @@ class LinkifyExtension(markdown.extensions.Extension): def extendMarkdown(self, md, md_globals): processor = markdown.inlinepatterns.AutolinkInlineProcessor(self._urlre, md) - md.inlinePatterns.add('linkify', processor, '_end') + # Register it right after the default <>-link processor (priority 120). + md.inlinePatterns.register(processor, 'linkify', 119) class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): @@ -47,7 +48,7 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): class FlysprayLinksExtension(markdown.extensions.Extension): def extendMarkdown(self, md, md_globals): processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b',md) - md.inlinePatterns.add('flyspray-links', processor, '_end') + md.inlinePatterns.register(processor, 'flyspray-links', 118) class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): @@ -92,7 +93,7 @@ class GitCommitsExtension(markdown.extensions.Extension): def extendMarkdown(self, md, md_globals): processor = GitCommitsInlineProcessor(md, self._head) - md.inlinePatterns.add('git-commits', processor, '_end') + md.inlinePatterns.register(processor, 'git-commits', 117) class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): @@ -106,7 +107,8 @@ class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): class HeadingExtension(markdown.extensions.Extension): def extendMarkdown(self, md, md_globals): - md.treeprocessors.add('heading', HeadingTreeprocessor(md), '_end') + # Priority doesn't matter since we don't conflict with other processors. + md.treeprocessors.register(HeadingTreeprocessor(md), 'heading', 30) def get_comment(conn, commentid): diff --git a/test/t2600-rendercomment.sh b/test/t2600-rendercomment.sh index 1ba560af..be408b80 100755 --- a/test/t2600-rendercomment.sh +++ b/test/t2600-rendercomment.sh @@ -52,7 +52,8 @@ test_expect_success 'Test HTML sanitizing.' ' test_expect_success 'Test link conversion.' ' cat <<-EOD | sqlite3 aur.db && INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (4, 1, " - Visit https://www.archlinux.org/. + Visit https://www.archlinux.org/#_test_. + Visit *https://www.archlinux.org/*. Visit . Visit \`https://www.archlinux.org/\`. Visit [Arch Linux](https://www.archlinux.org/). @@ -62,7 +63,8 @@ test_expect_success 'Test link conversion.' ' EOD "$RENDERCOMMENT" 4 && cat <<-EOD >expected && -

    Visit https://www.archlinux.org/. +

    Visit https://www.archlinux.org/#_test_. + Visit https://www.archlinux.org/. Visit https://www.archlinux.org/. Visit https://www.archlinux.org/. Visit Arch Linux. From d4632aaffa062de7cdc03c3e33e13fa0d9e77317 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Mon, 10 Feb 2020 11:05:27 +0100 Subject: [PATCH 0064/1451] Translation updates from Transifex Signed-off-by: Lukas Fleischer --- po/ar.po | 160 ++++++++++++++++++++++++++------ po/ast.po | 162 ++++++++++++++++++++++++++------ po/ca.po | 156 +++++++++++++++++++++++++------ po/cs.po | 164 ++++++++++++++++++++++++++------ po/da.po | 225 ++++++++++++++++++++++++++++++++------------ po/de.po | 168 ++++++++++++++++++++++++++------- po/el.po | 156 +++++++++++++++++++++++++------ po/es.po | 160 ++++++++++++++++++++++++++------ po/es_419.po | 160 ++++++++++++++++++++++++++------ po/fi.po | 172 +++++++++++++++++++++++++++------- po/fr.po | 162 ++++++++++++++++++++++++++------ po/he.po | 258 ++++++++++++++++++++++++++++++++++++--------------- po/hr.po | 150 +++++++++++++++++++++++++----- po/hu.po | 160 ++++++++++++++++++++++++++------ po/it.po | 160 ++++++++++++++++++++++++++------ po/ja.po | 206 ++++++++++++++++++++++++++++++---------- po/nb.po | 162 ++++++++++++++++++++++++++------ po/nl.po | 158 +++++++++++++++++++++++++------ po/pl.po | 173 +++++++++++++++++++++++++++------- po/pt_BR.po | 162 ++++++++++++++++++++++++++------ po/pt_PT.po | 162 ++++++++++++++++++++++++++------ po/ro.po | 158 +++++++++++++++++++++++++------ po/ru.po | 160 ++++++++++++++++++++++++++------ po/sk.po | 167 ++++++++++++++++++++++++++------- po/sr.po | 160 ++++++++++++++++++++++++++------ po/tr.po | 162 ++++++++++++++++++++++++++------ po/uk.po | 207 +++++++++++++++++++++++++++++++---------- po/zh_CN.po | 162 ++++++++++++++++++++++++++------ po/zh_TW.po | 210 ++++++++++++++++++++++++++++++----------- 29 files changed, 4002 insertions(+), 980 deletions(-) diff --git a/po/ar.po b/po/ar.po index 64a1b870..7664c478 100644 --- a/po/ar.po +++ b/po/ar.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # safa1996alfulaij , 2015 @@ -9,9 +9,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Arabic (http://www.transifex.com/lfleischer/aurweb/language/ar/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -74,6 +74,10 @@ msgstr "تعذّر جلب معلومات المستخدم المحدّد." msgid "You do not have permission to edit this account." msgstr "لا صلاحيّات لديك لتحرير هذا الحساب." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "استخدم هذه الاستمارة للبحث عن حسابات موجودة." @@ -368,10 +372,10 @@ msgid "Enter login credentials" msgstr "أدخل بيانات الولوج" #: html/login.php -msgid "User name or email address" -msgstr "اسم المستخدم أو عنوان البريد الإلكترونيّ" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "كلمة المرور" @@ -432,8 +436,8 @@ msgid "Your password has been reset successfully." msgstr "صُفّرت كلمة مرورك بنجاح." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "أكّد عنوان بريدك الإلكترونيّ:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -450,13 +454,13 @@ msgstr "تابع" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "إن نسيت عنوان البريد الإلكترونيّ الذي استخدمته للتّسجيل، فضلًا أرسل رسالة إلى قائمة %saur-general%s البريديّة." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "أدخل عنوان بريدك الإلكترونيّ:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -648,19 +652,19 @@ msgstr "" msgid "Close Request" msgstr "أغلق الطّلب" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "الأولى" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "السّابقة" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "التّالية" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "الأخيرة" @@ -761,10 +765,18 @@ msgstr "يجب أن يبدأ وينتهي بحرف أو رقم." msgid "Can contain only one period, underscore or hyphen." msgstr "يمكنه احتواء نقطة واحدة، أو شرطة سفليّة واحدة أو شرطة واحدة." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "عنوان البريد الإلكترونيّ غير صالح." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -804,6 +816,18 @@ msgstr "العنوان %s%s%s مستخدم بالفعل." msgid "The SSH public key, %s%s%s, is already in use." msgstr "مفتاح SSH العموميّ %s%s%s مستخدم بالفعل." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1213,6 +1237,10 @@ msgstr "اعرض ملفّ هذا المستخدم الشخصيّ" msgid "Edit this user's account" msgstr "حرّر حساب هذا المستخدم" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1223,6 +1251,11 @@ msgstr "انقر %sهنا%s إن أردت حذف هذا الحساب نهائي msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "مطلوب" @@ -1260,8 +1293,34 @@ msgid "Hide Email Address" msgstr "أخفِ عنوان البريد الإلكترونيّ" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "أعد كتابة كلمة المرور" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1271,6 +1330,16 @@ msgstr "اللغة" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "أعد كتابة كلمة المرور" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1297,6 +1366,24 @@ msgstr "أخطرني بتحديثات الحزم" msgid "Notify of ownership changes" msgstr "أخطرني بتغيير المُلّاك" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1425,7 +1512,7 @@ msgstr "صوّت لهذه الحزمة" msgid "Disable notifications" msgstr "عطّل الإخطارات" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "فعّل الإخطارات" @@ -1460,6 +1547,10 @@ msgstr "عنوان غِت للاستنساخ" msgid "read-only" msgstr "للقراءة فقط" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1506,9 +1597,16 @@ msgstr "حرّر تعليق: %s" msgid "Add Comment" msgstr "أضف تعليقًا" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "اعرض كلّ التّعليقات" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1518,6 +1616,10 @@ msgstr "التّعليقات المثبّتة" msgid "Latest Comments" msgstr "آخر التّعليقات" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1528,6 +1630,11 @@ msgstr "علّق %s على %s" msgid "Anonymous comment on %s" msgstr "تعليق مجهول على %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1564,10 +1671,6 @@ msgstr "ثبّت التّعليق" msgid "Unpin comment" msgstr "فكّ تثبيت التّعليق" -#: template/pkg_comments.php -msgid "All comments" -msgstr "كلّ التّعليقات" - #: template/pkg_details.php msgid "Package Details" msgstr "تفاصيل الحزمة" @@ -2130,6 +2233,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/ast.po b/po/ast.po index df9db049..5e08e86b 100644 --- a/po/ast.po +++ b/po/ast.po @@ -1,18 +1,18 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # enolp , 2014-2015,2017 # Ḷḷumex03 , 2014 -# Pablo Lezaeta Reyes [pˈaβ̞lo lˌe̞θaˈeta rˈejɛ] , 2014-2015 +# prflr88 , 2014-2015 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Asturian (http://www.transifex.com/lfleischer/aurweb/language/ast/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -75,6 +75,10 @@ msgstr "Nun pudo recibise la información pal usuariu especificáu." msgid "You do not have permission to edit this account." msgstr "Nun tienes permisu pa editar esta cuenta." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Usa esti formulariu pa guetar cuentes esistentes." @@ -369,10 +373,10 @@ msgid "Enter login credentials" msgstr "Introduz les tos credenciales d'aniciu sesión" #: html/login.php -msgid "User name or email address" -msgstr "Nome d'usuariu o direición de corréu" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Contraseña" @@ -433,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "La to contraseña reanicióse con ésitu." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Confirma'l to corréu:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -451,13 +455,13 @@ msgstr "Siguir" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Si escaecisti la dirección de corréu electrónicu utilizasti pa rexistrar, complacer unviar un mensaxe a la llista de orréu %saur-xeneral%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Introduz la to direción de corréu:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -649,19 +653,19 @@ msgstr "" msgid "Close Request" msgstr "Zarar solicitú" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Siguiente" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "" @@ -762,10 +766,18 @@ msgstr "" msgid "Can contain only one period, underscore or hyphen." msgstr "" +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "La direición de corréu nun ye válida." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -805,6 +817,18 @@ msgstr "La direición, %s%s%s, yá ta n'usu." msgid "The SSH public key, %s%s%s, is already in use." msgstr "La llave pública SSH, %s%s%s, ye yá n'usu." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1214,6 +1238,10 @@ msgstr "" msgid "Edit this user's account" msgstr "" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1224,6 +1252,11 @@ msgstr "Primi %sequí%s si quies desaniciar esta cuenta dafechu." msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "riquíu" @@ -1261,8 +1294,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Teclexa de nueves la contraseña" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1272,6 +1331,16 @@ msgstr "Llingua" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Teclexa de nueves la contraseña" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1298,6 +1367,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1426,7 +1513,7 @@ msgstr "Votar pol paquete" msgid "Disable notifications" msgstr "" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1457,6 +1544,10 @@ msgstr "URL pa clonar con Git" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1503,9 +1594,16 @@ msgstr "" msgid "Add Comment" msgstr "Amestar comentariu" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Ver tolos comentarios" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1515,6 +1613,10 @@ msgstr "" msgid "Latest Comments" msgstr "Comentarios caberos" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1525,6 +1627,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1561,10 +1668,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Tolos comentarios" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalles del paquete" @@ -2111,6 +2214,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/ca.po b/po/ca.po index 4ce79e69..c30fb1d8 100644 --- a/po/ca.po +++ b/po/ca.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Adolfo Jayme-Barrientos, 2014 @@ -10,9 +10,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Catalan (http://www.transifex.com/lfleischer/aurweb/language/ca/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -75,6 +75,10 @@ msgstr "No s'ha pogut obtenir la informació de l'usuari especificat." msgid "You do not have permission to edit this account." msgstr "No teniu permís per a editar aquest compte." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Utilitzeu aquest formulari per a cercar comptes existents." @@ -369,10 +373,10 @@ msgid "Enter login credentials" msgstr "Introduïu les credencials d'inici de sessió" #: html/login.php -msgid "User name or email address" +msgid "User name or primary email address" msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Contrasenya" @@ -433,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "La seva contrasenya s'ha restablert correctament." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Tots" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -451,13 +455,13 @@ msgstr "Continuar" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Si ha oblidat l'adreça de correu electrònic utilitzada al registrar-se, si us plau envïi un missatge a la llista de correu %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Introduiu la vostra adreça de correu." +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -649,19 +653,19 @@ msgstr "" msgid "Close Request" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Primer" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Anterior" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Següent" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Darrer" @@ -762,10 +766,18 @@ msgstr "Comença i finalitza amb una lletra o número" msgid "Can contain only one period, underscore or hyphen." msgstr "Només pot contenir un punt, guió o guió baix." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "L'adreça del correu-e no és vàlida." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -805,6 +817,18 @@ msgstr "L'adressa, %s%s%s, ja s'està fent servir." msgid "The SSH public key, %s%s%s, is already in use." msgstr "" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1214,6 +1238,10 @@ msgstr "Visualitza els paquets d'aquest usuari" msgid "Edit this user's account" msgstr "" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1224,6 +1252,11 @@ msgstr "" msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "requerit" @@ -1261,8 +1294,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Escriu altre cop la contrasenya" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1272,6 +1331,16 @@ msgstr "Idioma" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Escriu altre cop la contrasenya" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1298,6 +1367,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1426,7 +1513,7 @@ msgstr "Vota per aquest paquet" msgid "Disable notifications" msgstr "Deshabilitar notificacions" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1457,6 +1544,10 @@ msgstr "" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1503,8 +1594,15 @@ msgstr "" msgid "Add Comment" msgstr "Afegir un comentari" -#: template/pkg_comments.php -msgid "View all comments" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." msgstr "" #: template/pkg_comments.php @@ -1515,6 +1613,10 @@ msgstr "" msgid "Latest Comments" msgstr "Darrers Comentaris" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1525,6 +1627,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1561,10 +1668,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Tots el comentaris" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalls del paquet" @@ -2111,6 +2214,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/cs.po b/po/cs.po index a69a21dc..ef165358 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Daniel Milde , 2017 -# Jaroslav Lichtblau , 2015-2016 -# Jaroslav Lichtblau , 2014 +# Jaroslav Lichtblau , 2015-2016 +# Jaroslav Lichtblau , 2014 # Jiří Vírava , 2017-2018 # Lukas Fleischer , 2011 # Pavel Ševeček , 2014 @@ -13,9 +13,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-31 10:10+0000\n" -"Last-Translator: Jiří Vírava \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Czech (http://www.transifex.com/lfleischer/aurweb/language/cs/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -78,6 +78,10 @@ msgstr "Nelze obdržet informace pro vybraného uživatele." msgid "You do not have permission to edit this account." msgstr "Nemáte oprávnění pro úpravu tohoto účtu." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Pro vyhledání existujících účtů použíte tento formulář." @@ -372,10 +376,10 @@ msgid "Enter login credentials" msgstr "Vložit přihlašovací údaje" #: html/login.php -msgid "User name or email address" -msgstr "Uživatelské jméno nebo e-mailová adresa" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Heslo" @@ -436,8 +440,8 @@ msgid "Your password has been reset successfully." msgstr "Heslo bylo úspěšně resetováno." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Potvrďte svou e-mailovou adresu:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -454,13 +458,13 @@ msgstr "Pokračovat" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Pokud jste zapomněli emailovou adresu použitou při registraci, pošlete zprávu na %saur-general%s mailing list." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Zadejte emailovou adresu:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -652,19 +656,19 @@ msgstr "Odeslat žádost" msgid "Close Request" msgstr "Uzavřít žádost" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "První" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Předchozí" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Další" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Poslední" @@ -765,10 +769,18 @@ msgstr "Začíná a končí písmenem nebo číslicí" msgid "Can contain only one period, underscore or hyphen." msgstr "Může obsahovat pouze jednu tečku, podtržítko nebo spojovník." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Vadná emailová adresa." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "Domovská stránka je neplatná, zadejte úplnou adresu URL HTTP(s)." @@ -808,6 +820,18 @@ msgstr "Adresa, %s%s%s, je již použita." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Veřejný SSH klíč, %s%s%s, je již použit." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1217,6 +1241,10 @@ msgstr "Zobrazit balíčky tohoto uživatele" msgid "Edit this user's account" msgstr "Upravit tento uživatelský účet" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1227,6 +1255,11 @@ msgstr "" msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "vyžadováno" @@ -1264,8 +1297,34 @@ msgid "Hide Email Address" msgstr "Skrýt email" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Heslo znovu" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1275,6 +1334,16 @@ msgstr "Jazyk" msgid "Timezone" msgstr "Časové pásmo" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Heslo znovu" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1301,6 +1370,24 @@ msgstr "Oznámení o aktualizacích balíčku" msgid "Notify of ownership changes" msgstr "Oznámit změnu vlastnictví" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1429,7 +1516,7 @@ msgstr "Hlasovat pro tento balíček" msgid "Disable notifications" msgstr "Vypnout oznámení" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Zapnout oznámení" @@ -1462,6 +1549,10 @@ msgstr "" msgid "read-only" msgstr "jen pro čtení" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1508,9 +1599,16 @@ msgstr "" msgid "Add Comment" msgstr "Přidat komentář" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Zobrazit všechny komentáře" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1520,6 +1618,10 @@ msgstr "Připnuté komentáře" msgid "Latest Comments" msgstr "Nejnovější komentáře" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1530,6 +1632,11 @@ msgstr "%s přidal komentář %s" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1566,10 +1673,6 @@ msgstr "Připnout komentář" msgid "Unpin comment" msgstr "Odepnout komentář" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Všechny komentáře" - #: template/pkg_details.php msgid "Package Details" msgstr "Detaily balíčku" @@ -2124,6 +2227,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/da.po b/po/da.po index f4e2c2e2..b78fc785 100644 --- a/po/da.po +++ b/po/da.po @@ -1,17 +1,18 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: +# Linuxbruger , 2018 # Louis Tim Larsen , 2015 # Lukas Fleischer , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Danish (http://www.transifex.com/lfleischer/aurweb/language/da/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -21,29 +22,29 @@ msgstr "" #: html/404.php msgid "Page Not Found" -msgstr "Siden blev ikke fundet" +msgstr "Side ikke fundet" #: html/404.php msgid "Sorry, the page you've requested does not exist." -msgstr "Beklager, den forspurgte side findes ikke." +msgstr "Beklager, den forespurgte side eksisterer ikke." #: html/404.php template/pkgreq_close_form.php msgid "Note" -msgstr "" +msgstr "Note" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "" +msgstr "Git klone URL'er, er ikke ment til at blive åbnet i en browser." #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "" +msgstr "For at klone Git lageret af %s, kør %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "" +msgstr "Klik %sher%s for at vende tilbage til %s detalje siden." #: html/503.php msgid "Service Unavailable" @@ -52,7 +53,7 @@ msgstr "Service utilgængelig" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "" +msgstr "Lad vær med at gå i panik! Denne side er nede på grund af vedligeholdelse. Vi vil være tilbage snart." #: html/account.php msgid "Account" @@ -74,6 +75,10 @@ msgstr "Kunne ikke hente information om den specifikke bruger." msgid "You do not have permission to edit this account." msgstr "Du har ikke tilladelse til at redigere denne konto." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Brug denne formular til at søge i eksisterende konti." @@ -84,11 +89,11 @@ msgstr "Du skal være logget ind for at se brugerinformation." #: html/addvote.php template/tu_list.php msgid "Add Proposal" -msgstr "" +msgstr "Tilføj Forslag" #: html/addvote.php msgid "Invalid token for user action." -msgstr "" +msgstr "Ugyldigt tegn for bruger handling." #: html/addvote.php msgid "Username does not exist." @@ -101,7 +106,7 @@ msgstr "%s har allerede et forslag kørende for dem." #: html/addvote.php msgid "Invalid type." -msgstr "" +msgstr "Ugyldig type." #: html/addvote.php msgid "Proposal cannot be empty." @@ -117,7 +122,7 @@ msgstr "Fremsæt et forslag til afstemning." #: html/addvote.php msgid "Applicant/TU" -msgstr "" +msgstr "Ansøger/TU" #: html/addvote.php msgid "(empty if not applicable)" @@ -130,15 +135,15 @@ msgstr "Type" #: html/addvote.php msgid "Addition of a TU" -msgstr "" +msgstr "Tilføjelse af en TU" #: html/addvote.php msgid "Removal of a TU" -msgstr "" +msgstr "Bortskaffelse af en TU" #: html/addvote.php msgid "Removal of a TU (undeclared inactivity)" -msgstr "" +msgstr "Bortskaffelse af en TU (ikke erklæret inaktivitet)" #: html/addvote.php msgid "Amendment of Bylaws" @@ -154,11 +159,11 @@ msgstr "Tilføj" #: html/comaintainers.php template/comaintainers_form.php msgid "Manage Co-maintainers" -msgstr "" +msgstr "Håndter Co-vedligeholdere" #: html/commentedit.php template/pkg_comments.php msgid "Edit comment" -msgstr "" +msgstr "Rediger kommentar" #: html/home.php template/header.php msgid "Dashboard" @@ -170,78 +175,78 @@ msgstr "Hjem" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "Mine Markerede Pakker" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Mine forespørgelser" #: html/home.php msgid "My Packages" -msgstr "Mine pakker" +msgstr "Mine Pakker" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Søg efter pakker jeg vedligeholder" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Co-Vedligeholdt Pakker" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Søg efter pakker jeg co-vedligeholder" #: html/home.php #, php-format msgid "" "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " "Guidelines%s for more information." -msgstr "" +msgstr "Velkommen til AUR! Venligst læs %sAUR Bruger Retningslinier %s og %sAUR TU Retningslinier%s for mere information." #: html/home.php #, php-format msgid "" "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " "otherwise they will be deleted!" -msgstr "" +msgstr "Bidragede PKGBUILDs %sskal%s være i overensstemmelse med %sArch Pakke Standarder%s ellers vil de blive slettet!" #: html/home.php msgid "Remember to vote for your favourite packages!" -msgstr "" +msgstr "Husk at stemme for dine favorit pakker!" #: html/home.php msgid "Some packages may be provided as binaries in [community]." -msgstr "" +msgstr "Nogle pakker kan være stillet til rådighed som binær i (fællesskab)." #: html/home.php msgid "DISCLAIMER" -msgstr "" +msgstr "ANSVARSFRASKRIVELSE" #: html/home.php template/footer.php msgid "" "AUR packages are user produced content. Any use of the provided files is at " "your own risk." -msgstr "" +msgstr "AUR pakker er bruger produceret materiale. Hvilken som helst brug af de filer der er stillet til rådighed er på din egen risiko." #: html/home.php msgid "Learn more..." -msgstr "" +msgstr "Lær mere..." #: html/home.php msgid "Support" -msgstr "" +msgstr "Support" #: html/home.php msgid "Package Requests" -msgstr "" +msgstr "Pakke Forespørgelser" #: html/home.php #, php-format msgid "" "There are three types of requests that can be filed in the %sPackage " "Actions%s box on the package details page:" -msgstr "" +msgstr "Der er tre typer af forespørgelser der kan udfyldes i%sPakke Handlinger%s boxen på pakke detaljer siden:" #: html/home.php msgid "Orphan Request" @@ -251,11 +256,11 @@ msgstr "" msgid "" "Request a package to be disowned, e.g. when the maintainer is inactive and " "the package has been flagged out-of-date for a long time." -msgstr "" +msgstr "Forespørg en pakke til at blive forstødt, f.eks når vedligeholderen er inaktiv og pakkerne er blevet markeret som dato-udløbet i lang tid." #: html/home.php msgid "Deletion Request" -msgstr "" +msgstr "Sletning Forespørgsel" #: html/home.php msgid "" @@ -368,10 +373,10 @@ msgid "Enter login credentials" msgstr "" #: html/login.php -msgid "User name or email address" +msgid "User name or primary email address" msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Adgangskode" @@ -432,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "Din adgangskode blev nulstillet." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Bekræft din nye mail-adresse:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -450,13 +455,13 @@ msgstr "Forsæt" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Indtast din mail-adresse:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -648,19 +653,19 @@ msgstr "" msgid "Close Request" msgstr "Luk forspørgsel" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Første" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Tidligere" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Næste" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Sidste" @@ -761,10 +766,18 @@ msgstr "Start og slut med et bogstav eller tal" msgid "Can contain only one period, underscore or hyphen." msgstr "Kan kun indeholde ét punktum, én bundstreg eller én bindestreg." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "E-mail adressen er ugyldig." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -804,6 +817,18 @@ msgstr "" msgid "The SSH public key, %s%s%s, is already in use." msgstr "" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1213,6 +1238,10 @@ msgstr "Vis pakker fra denne bruger" msgid "Edit this user's account" msgstr "" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1223,6 +1252,11 @@ msgstr "" msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "påkrævet" @@ -1260,8 +1294,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Bekræft adgangskode" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1271,6 +1331,16 @@ msgstr "Sprog" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Bekræft adgangskode" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1297,6 +1367,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1425,7 +1513,7 @@ msgstr "Stem på denne pakke" msgid "Disable notifications" msgstr "Deaktiver notifikationer" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1456,6 +1544,10 @@ msgstr "" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1502,9 +1594,16 @@ msgstr "" msgid "Add Comment" msgstr "Tilføj kommentar" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Vis alle kommentarer" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1514,6 +1613,10 @@ msgstr "" msgid "Latest Comments" msgstr "Seneste kommentarer" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1524,6 +1627,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1560,10 +1668,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Alle kommentarer" - #: template/pkg_details.php msgid "Package Details" msgstr "Detaljer om pakken" @@ -2110,6 +2214,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/de.po b/po/de.po index 00ab95c2..8b2ea808 100644 --- a/po/de.po +++ b/po/de.po @@ -1,10 +1,10 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: -# Alexander Griesbaum , 2013 -# Alexander Griesbaum , 2013-2014 +# 9d91e189c22376bb4ee81489bc27fc28, 2013 +# 9d91e189c22376bb4ee81489bc27fc28, 2013-2014 # bjo , 2013 # FabianS_ , 2012 # go2sh , 2015-2016 @@ -17,17 +17,17 @@ # Matthias Gorissen , 2012 # Nuc1eoN , 2014 # Nuc1eoN , 2014 -# simon04 , 2018 +# Simon Legner , 2018 # Simon Schneider , 2011 -# Stefan Auditor , 2017-2018 +# Stefan Auditor , 2017-2018,2020 # Thomas_Do , 2013-2014 # Thomas_Do , 2012-2013 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 15:47+0000\n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 11:12+0000\n" "Last-Translator: Stefan Auditor \n" "Language-Team: German (http://www.transifex.com/lfleischer/aurweb/language/de/)\n" "MIME-Version: 1.0\n" @@ -91,6 +91,10 @@ msgstr "Es konnten keine Informationen für den angegebenen Benutzer geladen wer msgid "You do not have permission to edit this account." msgstr "Du hast keine Berechtigung, dieses Konto zu ändern." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "Passwort ungültig." + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Benutze dieses Formular, um vorhandene Konten zu suchen." @@ -385,10 +389,10 @@ msgid "Enter login credentials" msgstr "Zugangsdaten eingeben" #: html/login.php -msgid "User name or email address" -msgstr "Benutzername oder E-Mail-Adresse" +msgid "User name or primary email address" +msgstr "Benutzername oder primäre E-Mail-Adresse" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Passwort" @@ -449,8 +453,8 @@ msgid "Your password has been reset successfully." msgstr "Dein Passwort wurde erfolgreich zurückgesetzt." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Bestätige Deine E-Mail-Adresse:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "Benutzername oder primäre E-Mail-Adresse bestätigen:" #: html/passreset.php msgid "Enter your new password:" @@ -467,13 +471,13 @@ msgstr "Weiter" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Wenn Du die E-Mail-Adresse vergessen hast, die Du für Deine Registrierung benutzt hast, sende bitte eine Nachricht an die %saur-general%s Mailing-Liste." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Gib Deine E-Mail-Adresse ein:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "Benutzername oder primäre E-Mail-Adresse eingeben:" #: html/pkgbase.php msgid "Package Bases" @@ -665,19 +669,19 @@ msgstr "Anfrage absenden" msgid "Close Request" msgstr "Anfrage schließen" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Erste" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Zurück" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Weiter" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Letzte" @@ -778,10 +782,18 @@ msgstr "Muss mit einem Buchstaben oder einer Zahl beginnen und enden" msgid "Can contain only one period, underscore or hyphen." msgstr "Kann nur einen Punkt, Unter- oder Bindestrich enthalten." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "Bitte das neue Passwort bestätigen." + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Die E-Mail-Adresse ist ungültig." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "Die sicherungs E-Mail-Adresse ist ungültig." + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "Diese Adresse ist ungültig, bitte eine vollständige HTTP(S) URL angeben." @@ -821,6 +833,18 @@ msgstr "Die Adresse %s%s%s wird bereits verwendet." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Der öffentliche SSH Schlüssel, %s%s%s, ist bereits in Benutzung." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "Das CPATCHA fehlt." + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "Das CAPTCHA ist abgelaufen. Bitte versuche es noch einmal." + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "Die eingegebene CAPTCHA Antwort ist ungültig." + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1230,6 +1254,10 @@ msgstr "Alle Pakete dieses Benutzers anzeigen" msgid "Edit this user's account" msgstr "Konto dieses Benutzers bearbeiten" +#: template/account_details.php +msgid "List this user's comments" +msgstr "Die Kommentare des Benutzers anzeigen" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1240,6 +1268,11 @@ msgstr "Klicke %shier%s, wenn du diesen Account unwiderruflich entfernen möchte msgid "Click %shere%s for user details." msgstr "Klicke %shere%s für Benutzerdetails." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "Notwendig" @@ -1277,8 +1310,34 @@ msgid "Hide Email Address" msgstr "Verstecke E-Mail-Adresse" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Bestätige das Passwort" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "Sicherungs E-Mail-Adresse" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1288,6 +1347,16 @@ msgstr "Sprache" msgid "Timezone" msgstr "Zeitzone" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Bestätige das Passwort" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1314,6 +1383,24 @@ msgstr "Benachrichtige Paketaktualisierungen" msgid "Notify of ownership changes" msgstr "Benachrichtige Besitzeränderungen" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "Um die Änderungen an deinem Profil zu bestätigen, gib bitte Dein aktuelles Paswort ein:" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "Dein aktuelles Passwort" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "Antwort" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1442,7 +1529,7 @@ msgstr "Für dieses Paket stimmen" msgid "Disable notifications" msgstr "Benachrichtigungen deaktivieren" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Benachrichtigungen aktivieren" @@ -1473,6 +1560,10 @@ msgstr "Git Clone URL" msgid "read-only" msgstr "nur lesen" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "kopieren" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1519,9 +1610,16 @@ msgstr "Bearbeite Kommentar für: %s" msgid "Add Comment" msgstr "Kommentar hinzufügen" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Zeige alle Kommentare" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "%sMarkdown syntax%s wird teilweise unterstützt." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1531,6 +1629,10 @@ msgstr "Angeheftete Kommentare" msgid "Latest Comments" msgstr "Neueste Kommentare" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "Kommentare für" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1541,6 +1643,11 @@ msgstr "%s kommentierte %s" msgid "Anonymous comment on %s" msgstr "Anonym kommentierte %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1577,10 +1684,6 @@ msgstr "Kommentar anheften" msgid "Unpin comment" msgstr "Kommentar losheften" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Alle Kommentare" - #: template/pkg_details.php msgid "Package Details" msgstr "Paket-Details" @@ -2127,8 +2230,9 @@ msgstr "AUR Paket gelöscht: {pkgbase}" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] hat {old} [2] in {new} [3] gemerged.\n\nWenn Du keine weiteren Benachrichtigungen über das neue Paket erhalten möchtest, gehe bitte zu [3] und klicke \"{label}\"." +msgstr "" #: scripts/notify.py #, python-brace-format diff --git a/po/el.po b/po/el.po index 8eaa5f0f..7af57544 100644 --- a/po/el.po +++ b/po/el.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Achilleas Pipinellis, 2014 @@ -14,9 +14,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Greek (http://www.transifex.com/lfleischer/aurweb/language/el/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -79,6 +79,10 @@ msgstr "Δεν ήταν δυνατή η λήψη πληροφοριών για msgid "You do not have permission to edit this account." msgstr "Δεν έχετε την άδεια να επεξεργαστείτε αυτόν τον λογαριασμό." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Χρησιμοποιήστε αυτή τη φόρμα για να αναζητήσετε υπάρχοντες λογαριασμούς." @@ -373,10 +377,10 @@ msgid "Enter login credentials" msgstr "Εισάγετε πιστοποιητικά εισόδου" #: html/login.php -msgid "User name or email address" +msgid "User name or primary email address" msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Κωδικός" @@ -437,8 +441,8 @@ msgid "Your password has been reset successfully." msgstr "Το συνθηματικό σας έχει επαναφερθεί με επιτυχία." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Παρακαλώ επιβεβαιώστε την διεύθυνση e-mail σας:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -455,13 +459,13 @@ msgstr "Συνεχίστε" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Εάν έχετε ξεχάσει την ηλεκτρονική διεύθυνση που χρησιμοποιήσατε για να εγγραφείτε, παρακαλώ στείλτε ένα μήνυμα στη λίστα ταχυδρομείου %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Εισάγετε την διεύθυνση e-mail σας:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -653,19 +657,19 @@ msgstr "" msgid "Close Request" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Πρώτο" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Προηγούμενο" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Επόμενο" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Τελευταίο" @@ -766,10 +770,18 @@ msgstr "Ξεκινήστε και τελειώστε με γράμμα ή αρι msgid "Can contain only one period, underscore or hyphen." msgstr "Μπορεί να περιλαμβάνει μόνο μία τελεία, κάτω παύλα ή παύλα." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Αυτή η διεύθυνση email δεν είναι έγκυρη." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -809,6 +821,18 @@ msgstr "Η διεύθυνση, %s%s%s, χρησιμοποιείται ήδη." msgid "The SSH public key, %s%s%s, is already in use." msgstr "" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1218,6 +1242,10 @@ msgstr "Δείτε τα πακέτα αυτού του χρήστη" msgid "Edit this user's account" msgstr "Τροποποιήστε το λογαριασμό αυτού του χρήστη" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1228,6 +1256,11 @@ msgstr "" msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "απαιτούμενο" @@ -1265,8 +1298,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Πληκτρολογήστε ξανά τον κωδικό σας." +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1276,6 +1335,16 @@ msgstr "Γλώσσα" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Πληκτρολογήστε ξανά τον κωδικό σας." + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1302,6 +1371,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1430,7 +1517,7 @@ msgstr "Ψηφίστε για αυτό το πακέτο" msgid "Disable notifications" msgstr "Απενεργοποιήστε τις ειδοποιήσεις" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1461,6 +1548,10 @@ msgstr "" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1507,8 +1598,15 @@ msgstr "" msgid "Add Comment" msgstr "Προσθέστε σχόλιο" -#: template/pkg_comments.php -msgid "View all comments" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." msgstr "" #: template/pkg_comments.php @@ -1519,6 +1617,10 @@ msgstr "" msgid "Latest Comments" msgstr "Τελευταία σχόλια" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1529,6 +1631,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1565,10 +1672,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Όλα τα σχόλια" - #: template/pkg_details.php msgid "Package Details" msgstr "Πληροφορίες Πακέτου" @@ -2115,6 +2218,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/es.po b/po/es.po index b4f62494..d60f45f1 100644 --- a/po/es.po +++ b/po/es.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Adolfo Jayme-Barrientos, 2015 @@ -19,9 +19,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2019-07-16 01:55+0000\n" -"Last-Translator: prflr88 \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Spanish (http://www.transifex.com/lfleischer/aurweb/language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -84,6 +84,10 @@ msgstr "No se pudo obtener la información del usuario especificado." msgid "You do not have permission to edit this account." msgstr "No tienes los permisos para editar esta cuenta." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Usa este formulario para buscar cuentas existentes." @@ -378,10 +382,10 @@ msgid "Enter login credentials" msgstr "Proporciona tus datos de acceso" #: html/login.php -msgid "User name or email address" -msgstr "Nombre de usuario y dirección de correo" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Contraseña" @@ -442,8 +446,8 @@ msgid "Your password has been reset successfully." msgstr "Se ha restablecido la contraseña correctamente." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Confirma tu dirección de correo:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -460,13 +464,13 @@ msgstr "Continuar" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Si olvidaste la dirección de correo que usaste para registrarte, envía un mensaje a la %slista de correo general del AUR%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Introduce tu dirección de correo:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -658,19 +662,19 @@ msgstr "Enviar solicitud" msgid "Close Request" msgstr "Cerrar solicitud" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Primero" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Anterior" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Siguiente" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Último" @@ -771,10 +775,18 @@ msgstr "Comenzar y acabar con una letra o número" msgid "Can contain only one period, underscore or hyphen." msgstr "Solamente puede contener un punto, guion bajo o guion." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "La dirección de correo no es válida." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "La página de inicio no es válida. Especifica la dirección HTTP(S) completa." @@ -814,6 +826,18 @@ msgstr "La dirección, %s%s%s, ya está en uso." msgid "The SSH public key, %s%s%s, is already in use." msgstr "La clave pública SSH %s%s%s ya está en uso." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1223,6 +1247,10 @@ msgstr "Ver los paquetes de este usuario" msgid "Edit this user's account" msgstr "Editar la cuenta de este usuario" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1233,6 +1261,11 @@ msgstr "Haz clic %saquí%s si deseas eliminar permanentemente esta cuenta." msgid "Click %shere%s for user details." msgstr "Haz clic %saquí%s para ver los detalles del usuario." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "obligatorio" @@ -1270,8 +1303,34 @@ msgid "Hide Email Address" msgstr "Ocultar dirreción de correo" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Reescribe la contraseña" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1281,6 +1340,16 @@ msgstr "Idioma" msgid "Timezone" msgstr "Huso horario" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Reescribe la contraseña" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1307,6 +1376,24 @@ msgstr "Notificar de actualizaciones de un paquete" msgid "Notify of ownership changes" msgstr "Notificar de cambios de propietario" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1435,7 +1522,7 @@ msgstr "Votar por este paquete" msgid "Disable notifications" msgstr "Deshabilitar notificaciones" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Habilitar notificaciones" @@ -1466,6 +1553,10 @@ msgstr "Dirección URL de clonado con Git" msgid "read-only" msgstr "Solamente lectura" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1512,9 +1603,16 @@ msgstr "Editar comentario para: %s" msgid "Add Comment" msgstr "Añadir un comentario" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Ver todos los comentarios" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1524,6 +1622,10 @@ msgstr "Comentarios fijados" msgid "Latest Comments" msgstr "Últimos comentarios" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1534,6 +1636,11 @@ msgstr "%s comentó en %s" msgid "Anonymous comment on %s" msgstr "Comentario anónimo en %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1570,10 +1677,6 @@ msgstr "Comentario fijado" msgid "Unpin comment" msgstr "Comentario desfijado" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Todos los comentarios" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalles del paquete" @@ -2120,6 +2223,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/es_419.po b/po/es_419.po index 0c0e4602..444eccb7 100644 --- a/po/es_419.po +++ b/po/es_419.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Angel Velasquez , 2011 @@ -17,9 +17,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2019-07-16 01:55+0000\n" -"Last-Translator: prflr88 \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Spanish (Latin America) (http://www.transifex.com/lfleischer/aurweb/language/es_419/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -82,6 +82,10 @@ msgstr "No se pudo obtener la información del usuario especificado." msgid "You do not have permission to edit this account." msgstr "No tiene permisos para editar esta cuenta." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Use este formulario para buscar cuentas existentes." @@ -376,10 +380,10 @@ msgid "Enter login credentials" msgstr "Introduce las credenciales de autentificación" #: html/login.php -msgid "User name or email address" -msgstr "Nombre de usuario y dirección de correo" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Contraseña" @@ -440,8 +444,8 @@ msgid "Your password has been reset successfully." msgstr "Su contraseña fue reiniciada con éxito." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Confirme su dirección de correo:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -458,13 +462,13 @@ msgstr "Continuar" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Si olvidó la dirección de correo que usó para registrarse, envíe un mensaje a la %slista de correo aur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Introduzca su dirección de correo:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -656,19 +660,19 @@ msgstr "Enviar petición" msgid "Close Request" msgstr "Cerrar Petición" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Primero" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Anterior" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Siguiente" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Último" @@ -769,10 +773,18 @@ msgstr "Comenzar y acabar con una letra o número" msgid "Can contain only one period, underscore or hyphen." msgstr "Solo puede contener un punto, guion bajo o guion." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "La dirección de correo no es válida." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "La página de inicio no es válida. Especifique la URL en HTTP(S) completa." @@ -812,6 +824,18 @@ msgstr "La dirección, %s%s%s, ya está en uso." msgid "The SSH public key, %s%s%s, is already in use." msgstr "La clave pública SSH %s%s%s ya está en uso." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1221,6 +1245,10 @@ msgstr "Ver los paquetes de este usuario" msgid "Edit this user's account" msgstr "Editar la cuenta de este usuario" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1231,6 +1259,11 @@ msgstr "Haga clic %saquí%s si desea borrar permanentemente esa cuenta." msgid "Click %shere%s for user details." msgstr "Haga clic %saquí%s para ver los detalles del usuario." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "obligatorio" @@ -1268,8 +1301,34 @@ msgid "Hide Email Address" msgstr "Ocultar dirreción de correo" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Reescriba la contraseña" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1279,6 +1338,16 @@ msgstr "Idioma" msgid "Timezone" msgstr "Zona horaria" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Reescriba la contraseña" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1305,6 +1374,24 @@ msgstr "Notificar sobre actualizaciones de un paquete" msgid "Notify of ownership changes" msgstr "Notificarme de cambios de propietario" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1433,7 +1520,7 @@ msgstr "Votar por este paquete" msgid "Disable notifications" msgstr "Deshabilitar notificaciones" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Habilitar notificaciones" @@ -1464,6 +1551,10 @@ msgstr "URL de clonado con Git" msgid "read-only" msgstr "Solo lectura" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1510,9 +1601,16 @@ msgstr "Editar commentario para: %s" msgid "Add Comment" msgstr "Agregar un comentario" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Ver todos los comentarios" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1522,6 +1620,10 @@ msgstr "Comentarios anclados" msgid "Latest Comments" msgstr "Últimos comentarios" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1532,6 +1634,11 @@ msgstr "%s comentó en %s" msgid "Anonymous comment on %s" msgstr "Comentario anónimo en %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1568,10 +1675,6 @@ msgstr "Comentario anclado" msgid "Unpin comment" msgstr "Comentario desanclado" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Todos los comentarios" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalles del paquete" @@ -2118,6 +2221,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/fi.po b/po/fi.po index 166b3104..d2daad80 100644 --- a/po/fi.po +++ b/po/fi.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Elias Autio, 2016 @@ -11,9 +11,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2019-02-18 20:40+0000\n" -"Last-Translator: Nikolay Korotkiy \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Finnish (http://www.transifex.com/lfleischer/aurweb/language/fi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -76,6 +76,10 @@ msgstr "Valitun käyttäjän tietoja ei voitu noutaa." msgid "You do not have permission to edit this account." msgstr "Sinulla ei ole oikeuksia tämän käyttäjätlin muokkaamiseen." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Etsi käyttäjätilejä." @@ -370,10 +374,10 @@ msgid "Enter login credentials" msgstr "Kirjautumistiedot" #: html/login.php -msgid "User name or email address" -msgstr "Käyttäjänimi tai sähköpostiosoite" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Salasana" @@ -434,8 +438,8 @@ msgid "Your password has been reset successfully." msgstr "Salasanasi on palautettu onnistuneesti." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Vahvista sähköpostiositteesi:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -452,13 +456,13 @@ msgstr "Jatka" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Jos olet unohtanut rekisteröityessäsi käyttämäsi sähköpostiosoitteen lähetä viesti %saur-general%s postituslistalle." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Sähköpostiosoitteesi:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -650,19 +654,19 @@ msgstr "Lähetä pyyntö" msgid "Close Request" msgstr "Sulje pyyntö" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Ensimmäinen" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Edellinen" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Seuraava" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Viimeinen" @@ -763,10 +767,18 @@ msgstr "Alkaa ja loppua kirjaimeen tai numeroon" msgid "Can contain only one period, underscore or hyphen." msgstr "Voi sisältää vain yhden väliviivan, alaviivan tai pisteen." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Sähköpostiosoite ei ole kelvollinen." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "Kotisivun osoite on virheellinen, määrittele koko http(s) URL." @@ -806,6 +818,18 @@ msgstr "Osoite %s%s%s on jo käytössä." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Julkinen SSH-avain %s%s%s on jo käytössä." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1215,6 +1239,10 @@ msgstr "Näytä käyttäjän paketit" msgid "Edit this user's account" msgstr "" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1225,6 +1253,11 @@ msgstr "Klikkaa %stästä%s, jos haluat peruuttamattomasti poistaa tämän tilin msgid "Click %shere%s for user details." msgstr "Klikkaa %stästä%s saadaksesi käyttäjän tiedot." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "vaaditaan" @@ -1262,8 +1295,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Salasana uudelleen:" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1273,6 +1332,16 @@ msgstr "Kieli" msgid "Timezone" msgstr "Aikavyöhyke" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Salasana uudelleen:" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1299,6 +1368,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1348,7 +1435,7 @@ msgstr "" #: template/comaintainers_form.php msgid "Users" -msgstr "" +msgstr "Käyttäjät" #: template/comaintainers_form.php template/pkg_comment_form.php msgid "Save" @@ -1427,7 +1514,7 @@ msgstr "Äänestä pakettia" msgid "Disable notifications" msgstr "En halua enää ilmoituksia" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1458,6 +1545,10 @@ msgstr "Git-kloonausosoite" msgid "read-only" msgstr "vain luku" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1504,9 +1595,16 @@ msgstr "" msgid "Add Comment" msgstr "Lisää kommentti" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Näytä kaikki kommentit" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1516,6 +1614,10 @@ msgstr "" msgid "Latest Comments" msgstr "Uusimmat kommentit" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1526,6 +1628,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1562,10 +1669,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Kaikki kommentit" - #: template/pkg_details.php msgid "Package Details" msgstr "Paketin tiedot" @@ -1715,7 +1818,7 @@ msgstr "" #: template/pkgreq_results.php msgid "Date" -msgstr "" +msgstr "Päivämäärä" #: template/pkgreq_results.php #, php-format @@ -1753,7 +1856,7 @@ msgstr "" #: template/pkgreq_results.php msgid "Closed" -msgstr "" +msgstr "Suljettu" #: template/pkg_search_form.php msgid "Name, Description" @@ -1765,7 +1868,7 @@ msgstr "Pelkkä nimi" #: template/pkg_search_form.php msgid "Exact Name" -msgstr "" +msgstr "Nimi täsmälleen" #: template/pkg_search_form.php msgid "Exact Package Base" @@ -1974,7 +2077,7 @@ msgstr "Loppu" #: template/tu_details.php msgid "Result" -msgstr "" +msgstr "Tulos" #: template/tu_details.php template/tu_list.php msgid "No" @@ -2112,8 +2215,9 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] yhdisti paketin {old} [2] pakettiin {new} [3].\n\nJos et enää halua saada ilmoituksia uudesta paketista, mene osoitteeseen [3] ja klikkaa \"{label}\"." +msgstr "" #: scripts/notify.py #, python-brace-format diff --git a/po/fr.po b/po/fr.po index 8b650e25..e73a8c99 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Alexandre Macabies , 2018 @@ -16,9 +16,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2019-02-11 18:21+0000\n" -"Last-Translator: Xorg\n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: French (http://www.transifex.com/lfleischer/aurweb/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -81,6 +81,10 @@ msgstr "Impossible de trouver l’information pour l’utilisateur spécifié." msgid "You do not have permission to edit this account." msgstr "Vous n’avez pas la permission d’éditer ce compte." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Utilisez ce formulaire pour rechercher des comptes existants." @@ -375,10 +379,10 @@ msgid "Enter login credentials" msgstr "Entrez vos identifiants" #: html/login.php -msgid "User name or email address" -msgstr "Nom d'utilisateur ou adresse e-mail :" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Mot de passe" @@ -439,8 +443,8 @@ msgid "Your password has been reset successfully." msgstr "Votre mot de passe a été réinitialisé avec succès." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Confirmez votre adresse e-mail :" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -457,13 +461,13 @@ msgstr "Continuer" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Si vous avez oublié avec quelle adresse e-mail vous vous êtes inscrit, veuillez envoyer un message sur la mailing-list %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Entrez votre adresse e-mail :" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -655,19 +659,19 @@ msgstr "Soumettre une demande" msgid "Close Request" msgstr "Fermer la requête" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Première" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Précédente" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Suivant" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Dernière" @@ -768,10 +772,18 @@ msgstr "doit débuter et se terminer par une lettre ou un chiffre." msgid "Can contain only one period, underscore or hyphen." msgstr "ne peut contenir qu'un seul point, tiret bas ou virgule," +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "L'adresse email n'est pas valide." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "La page d'accueil est invalide, spécifiez l'URL HTTP(s) complet." @@ -811,6 +823,18 @@ msgstr "L’adresse %s%s%s est déjà utilisée." msgid "The SSH public key, %s%s%s, is already in use." msgstr "La clé SSH publique, %s%s%s, est déjà utilisée." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1220,6 +1244,10 @@ msgstr "Visualiser les paquets de cet utilisateur." msgid "Edit this user's account" msgstr "Éditer le compte de cet utilisateur." +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1230,6 +1258,11 @@ msgstr "Cliquez %sici%s si vous voulez effacer ce compte de façon définitive." msgid "Click %shere%s for user details." msgstr "Cliquez %sici%s pour obtenir les détails de l'utilisateur." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "requis" @@ -1267,8 +1300,34 @@ msgid "Hide Email Address" msgstr "Cacher l'adresse e-mail" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Retapez le mot de passe" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1278,6 +1337,16 @@ msgstr "Langue" msgid "Timezone" msgstr "Fuseau horaire" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Retapez le mot de passe" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1304,6 +1373,24 @@ msgstr "Notifications de mises à jour de paquets" msgid "Notify of ownership changes" msgstr "Notifier des changements de propriétaire" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1432,7 +1519,7 @@ msgstr "Voter pour ce paquet" msgid "Disable notifications" msgstr "Désactiver les notifications" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Activer les notifications" @@ -1463,6 +1550,10 @@ msgstr "URL de clone (Git)" msgid "read-only" msgstr "lecture seule" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1509,9 +1600,16 @@ msgstr "Commentaire édité le : %s" msgid "Add Comment" msgstr "Ajouter un commentaire" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Voir tous les commentaires" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1521,6 +1619,10 @@ msgstr "Commentaires épinglés" msgid "Latest Comments" msgstr "Derniers commentaires" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1531,6 +1633,11 @@ msgstr "%s a commenté le %s" msgid "Anonymous comment on %s" msgstr "Commentaire anonyme le %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1567,10 +1674,6 @@ msgstr "Épingler le commentaire" msgid "Unpin comment" msgstr "Désépingler le commentaire" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Tous les commentaires" - #: template/pkg_details.php msgid "Package Details" msgstr "Détails du paquet" @@ -2117,8 +2220,9 @@ msgstr "Paquet AUR supprimé : {pkgbase}" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] a fusionné {old} [2] dans {new} [3].\n\nSi vous ne souhaitez plus recevoir de notifications à propos du nouveau paquet, rendez-vous sur la page du paquet [3] et sélectionnez \"{label}\". " +msgstr "" #: scripts/notify.py #, python-brace-format diff --git a/po/he.po b/po/he.po index 16d1196d..21ca1c8b 100644 --- a/po/he.po +++ b/po/he.po @@ -1,18 +1,18 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # GenghisKhan , 2016 # Lukas Fleischer , 2011 -# Yaron Shahrabani , 2016-2018 +# Yaron Shahrabani , 2016-2020 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-02-01 14:37+0000\n" +"Last-Translator: Yaron Shahrabani \n" "Language-Team: Hebrew (http://www.transifex.com/lfleischer/aurweb/language/he/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -75,6 +75,10 @@ msgstr "לא ניתן לקבל נתונים עבור המשתמש שנבחר." msgid "You do not have permission to edit this account." msgstr "אין לך הרשאה לערוך חשבון זה." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "הססמה שגויה." + #: html/account.php msgid "Use this form to search existing accounts." msgstr "נא להשתמש בטופס על מנת לחפש אחר חשבונות קיימים." @@ -341,11 +345,11 @@ msgstr "ביטול הצבעה" #: html/index.php template/pkg_search_form.php template/pkg_search_results.php msgid "Notify" -msgstr "התרעה" +msgstr "התראה" #: html/index.php template/pkg_search_results.php msgid "UnNotify" -msgstr "ביטול התרעה" +msgstr "ביטול התראה" #: html/index.php msgid "UnFlag" @@ -362,17 +366,17 @@ msgstr "נכנסת בשם: %s" #: html/login.php template/header.php msgid "Logout" -msgstr "ניתוק" +msgstr "יציאה" #: html/login.php msgid "Enter login credentials" msgstr "נא להזין פרטי גישה" #: html/login.php -msgid "User name or email address" -msgstr "שם משתמש או כתובת דוא״ל" +msgid "User name or primary email address" +msgstr "שם משתמש או כתובת דוא״ל עיקרית" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "ססמה" @@ -433,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "הססמה שלך התאפסה בהצלחה." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "אישור כתובת הדוא״ל שלך" +msgid "Confirm your user name or primary e-mail address:" +msgstr "אימות של שם המשתמש או כתובת הדוא״ל העיקרית:" #: html/passreset.php msgid "Enter your new password:" @@ -451,13 +455,13 @@ msgstr "המשך" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "אם שכחת את כתובת הדוא״ל בה השתמש כדי להירשם, נא לשלוח הודעה לקבוצת הדיוור %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "אם שכחת את שם המשתמש ואת כתובת הדוא״ל העיקרית בה השתמשת כדי להירשם, נא לשלוח הודעה לקבוצת הדיוור %saur-general%s." #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "נא להזין את כתובת הדוא״ל שלך:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "נא להקליד את שם המשתמש או את כתובת הדוא״ל העיקרית שלך:" #: html/pkgbase.php msgid "Package Bases" @@ -533,7 +537,7 @@ msgstr "ניתן להשתמש בטופס זה כדי לשלול את הבעלו msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "סימון התיבה הזאת מהווה את אישורך להסיר את שותפותך בתחזוקת חבילה זו." #: html/pkgdisown.php #, php-format @@ -565,14 +569,14 @@ msgstr "סימון התגובה" #: html/pkgflag.php msgid "Flag Package Out-Of-Date" -msgstr "סימון תגובה כבלתי עדכנית" +msgstr "סימון תגובה כלא עדכנית" #: html/pkgflag.php #, php-format msgid "" "Use this form to flag the package base %s%s%s and the following packages " "out-of-date: " -msgstr "יש להשתמש בטופס זה כדי לסמן את בסיס החבילה %s%s%s ואת החבילות הבאות לבלתי עדכניות:" +msgstr "יש להשתמש בטופס זה כדי לסמן את בסיס החבילה %s%s%s ואת החבילות הבאות כלא עדכניות:" #: html/pkgflag.php #, php-format @@ -585,7 +589,7 @@ msgstr "נא %sלא%s להשתמש בטופס הזה כדי לדווח על תק msgid "" "Enter details on why the package is out-of-date below, preferably including " "links to the release announcement or the new release tarball." -msgstr "נא להזין את הפרטים על מדוע החבילה אינה בתוקף, מומלץ להוסיף קישורים להכרזות המתאימות או לקובצי הגרסה העדכנית." +msgstr "נא להזין את הפרטים על מדוע החבילה לא עדכנית, מומלץ להוסיף קישורים להכרזות המתאימות או לקובצי הגרסה העדכנית." #: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php #: template/pkgreq_results.php @@ -598,7 +602,7 @@ msgstr "סימון" #: html/pkgflag.php msgid "Only registered users can flag packages out-of-date." -msgstr "רק משתמשים רשומים יכולים לסמן חבילות כפגות תוקף." +msgstr "רק משתמשים רשומים יכולים לסמן חבילות כלא עדכניות." #: html/pkgmerge.php msgid "Package Merging" @@ -649,19 +653,19 @@ msgstr "שליחת בקשה" msgid "Close Request" msgstr "סגירת בקשה" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "ראשון" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "הקודם" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "הבא" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "אחרון" @@ -762,10 +766,18 @@ msgstr "יש להתחיל ולסיים עם תו או מספר" msgid "Can contain only one period, underscore or hyphen." msgstr "יכול הכיל רק נקודה אחת, קו תחתון או מקף." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "נא לאשר את הססמה החדשה שלך." + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "כתובת הדוא״ל שהוזנה אינה תקינה." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "כתובת הדוא״ל לגיבוי שגויה." + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "דף הבית שגוי, נא לציין את כתובת ה־HTTP(s) המלאה." @@ -805,6 +817,18 @@ msgstr "הכתובת, %s%s%s, כבר נמצאת בשימוש." msgid "The SSH public key, %s%s%s, is already in use." msgstr "מפתח ה־SSH הציבורי, %s%s%s, כבר נמצא בשימוש." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "הקאפצ׳ה חסרה." + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "תוקף הקאפצ׳ה פג. נא לנסות שוב." + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "התשובה שמילאת בקאפצ׳ה שגויה." + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1014,17 +1038,17 @@ msgstr "ההצבעות שלך נקלטו עבור החבילות המסומנו #: lib/pkgbasefuncs.inc.php msgid "Couldn't add to notification list." -msgstr "לא ניתן לצרף את רשימת ההתרעות." +msgstr "לא ניתן לצרף את רשימת ההתראות." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been added to the comment notification list for %s." -msgstr "צורפת אל רשימת ההתרעות עבור %s." +msgstr "צורפת אל רשימת ההתראות עבור %s." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been removed from the comment notification list for %s." -msgstr "הוסרת מרשימה ההתרעות עבור ההערות של %s." +msgstr "הוסרת מרשימת ההתראות להערות של %s." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to undelete this comment." @@ -1214,6 +1238,10 @@ msgstr "צפייה בחבילות המשתמש" msgid "Edit this user's account" msgstr "עריכת החשבון של משתמש זה" +#: template/account_details.php +msgid "List this user's comments" +msgstr "הצגת תגובות המשתמש הזה" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1224,6 +1252,11 @@ msgstr "נא ללחוץ %sכאן%s אם רצונך הוא למחוק את החש msgid "Click %shere%s for user details." msgstr "יש ללחוץ %sכאן%s לפרטים על המשתמש." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "נא ללחוץ %sכאן%s כדי להציג את התגובות שהוגשו דרך החשבון הזה." + #: template/account_edit_form.php msgid "required" msgstr "נדרש" @@ -1261,8 +1294,34 @@ msgid "Hide Email Address" msgstr "הסתרת כתובת דוא״ל" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "הקלדת הססמה מחדש" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "אם בחירתך תהיה שלא להסתיר את כתובת הדוא״ל שלך, היא תהיה גלויה לכלל המשתמשים ב־AUR. בחירה הפוכה תגרום לכך שהכתובת תהיה גלויה בפני חברי הסגל של Arch לינוקס בלבד." + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "כתובת דוא״ל לגיבוי" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "באפשרותך לציין כתובת דוא״ל משנית בה ניתן להשתמש לשחזור החשבון שלך אם אבדה גישתך לכתובת הדוא״ל העיקרית שלך." + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "קישורים לאיפוס ססמה תמיד נשלחים לכתובת הדוא״ל העיקרית שלך ואל כתובת הדוא״ל שלך כגיבוי." + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "כתובת הדוא״ל שלך כגיבוי תמיד זמינה אך ורק לחברי הסגל של Arch לינוקס ללא תלות בהגדרה %s." #: template/account_edit_form.php msgid "Language" @@ -1272,6 +1331,16 @@ msgstr "שפה" msgid "Timezone" msgstr "אזור זמן" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "אם ברצונך לשנות את הססמה, נא להקליד ססמה חדשה ולאשר את הססמה על ידי הקלדתה פעם נוספת." + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "הקלדת הססמה מחדש" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1284,7 +1353,7 @@ msgstr "מפתח SSH ציבורי" #: template/account_edit_form.php msgid "Notification settings" -msgstr "הגדרות התרעה" +msgstr "הגדרות התראה" #: template/account_edit_form.php msgid "Notify of new comments" @@ -1298,6 +1367,24 @@ msgstr "להודיע לי ל עדכונים בחבילה" msgid "Notify of ownership changes" msgstr "להודיע לי על שינויים בבעלות" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "כדי לאשר את השינויים בפרופיל נא להקליד את הססמה הנוכחית שלך:" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "הססמה הנוכחית שלך" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "כדי להגן על AUR מפני יצירה אוטומטית של חשבונות, אנו מבקשים ממך לציין מה הפלט של הפקודה הבאה:" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "תשובה" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1361,12 +1448,12 @@ msgstr "הערת סימון כלא עדכנית: %s" #: template/flag_comment.php #, php-format msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" -msgstr "%s%s%s סומנה בדגל %s%s%s כבלתי עדכנית %s%s%s מהסיבה הבאה:" +msgstr "%s%s%s סומנה בדגל %s%s%s כלא עדכנית %s%s%s מהסיבה הבאה:" #: template/flag_comment.php #, php-format msgid "%s%s%s is not flagged out-of-date." -msgstr "%s%s%s אינה מסומנת כפגת תוקף." +msgstr "%s%s%s אינה מסומנת כלא עדכנית." #: template/flag_comment.php msgid "Return to Details" @@ -1408,7 +1495,7 @@ msgstr "סימון כלא עדכנית (%s)" #: template/pkgbase_actions.php msgid "Flag package out-of-date" -msgstr "סימון החבילה כבלתי עדכנית" +msgstr "סימון החבילה כלא עדכנית" #: template/pkgbase_actions.php msgid "Unflag package" @@ -1424,11 +1511,11 @@ msgstr "להצביע לחבילה זו" #: template/pkgbase_actions.php scripts/notify.py msgid "Disable notifications" -msgstr "נטרול התרעות" +msgstr "השבתת התראות" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" -msgstr "הפעלת התרעות" +msgstr "הפעלת התראות" #: template/pkgbase_actions.php msgid "Manage Co-Maintainers" @@ -1459,6 +1546,10 @@ msgstr "כתובת השכפול מ־Git" msgid "read-only" msgstr "לקריאה בלבד" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "ללחוץ להעתקה" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1490,7 +1581,7 @@ msgstr "פופולריות" #: template/pkgbase_details.php template/pkg_details.php msgid "First Submitted" -msgstr "נשלחה לראשונה" +msgstr "הוגשה לראשונה" #: template/pkgbase_details.php template/pkg_details.php msgid "Last Updated" @@ -1505,9 +1596,16 @@ msgstr "עריכת תגובה עבור: %s" msgid "Add Comment" msgstr "הוספת תגובה" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "הצגת כל התגובות" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "מזהי הגשות של Git שמפנים להגשות במאגר החבילות של AUR וכתובות מומרים אוטומטית לקישורים." + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "יש תמיכה חלקית ב%sתחביר Markdown%s" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1517,6 +1615,10 @@ msgstr "תגובות נעוצות" msgid "Latest Comments" msgstr "התגובות האחרונות" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "תגובות על" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1527,6 +1629,11 @@ msgstr "נכתבה תגובה ע״י %s ב־%s" msgid "Anonymous comment on %s" msgstr "תגובה אלמונית ב־%s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "הוגשה תגובה על החבילה %s ב־%s" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1563,10 +1670,6 @@ msgstr "הצמדת התגובה" msgid "Unpin comment" msgstr "ביטול הצמדת התגובה" -#: template/pkg_comments.php -msgid "All comments" -msgstr "כל התגובות" - #: template/pkg_details.php msgid "Package Details" msgstr "נתוני חבילה" @@ -1733,10 +1836,10 @@ msgstr[3] "נותרו ~%d ימים" #, php-format msgid "~%d hour left" msgid_plural "~%d hours left" -msgstr[0] "נותרה שעה ~%d" -msgstr[1] "~%d שעות נותרו" -msgstr[2] "~%d שעות נותרו" -msgstr[3] "~%d שעות נותרו" +msgstr[0] "נותרה בערך שעה (%d)" +msgstr[1] "בערך שעתיים (%d) נותרו" +msgstr[2] "בערך %d שעות נותרו" +msgstr[3] "בערך %d שעות נותרו" #: template/pkgreq_results.php msgid "<1 hour left" @@ -1894,7 +1997,7 @@ msgstr "פעולות" #: template/pkg_search_results.php msgid "Unflag Out-of-date" -msgstr "ביטול סימון כלא מעודכן" +msgstr "ביטול סימון כלא עדכנית" #: template/pkg_search_results.php msgid "Adopt Packages" @@ -1922,7 +2025,7 @@ msgstr "חיפוש" #: template/stats/general_stats_table.php msgid "Statistics" -msgstr "סטטיסטיקות" +msgstr "סטטיסטיקה" #: template/stats/general_stats_table.php msgid "Orphan Packages" @@ -1970,12 +2073,12 @@ msgstr "פרטי הצעה" #: template/tu_details.php msgid "This vote is still running." -msgstr "ההצבעה עדיין קיימת." +msgstr "ההצבעה עדיין מתרחשת." #: template/tu_details.php #, php-format msgid "Submitted: %s by %s" -msgstr "נשלח: %s על ידי %s" +msgstr "הוגש: %s על ידי %s" #: template/tu_details.php template/tu_list.php msgid "End" @@ -2023,7 +2126,7 @@ msgstr "חזרה" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "" +msgstr "איפוס ססמה ב־AUR" #: scripts/notify.py #, python-brace-format @@ -2031,98 +2134,99 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "" +msgstr "הוגשה בקשה לאיפוס ססמה לחשבון {user} שמקושר לכתובת הדוא״ל שלך. אם ברצונך לאפס את הססמה שלך עליך להיכנס לקישור [1] שלהלן, אם לא ביקשת מוטב להתעלם מההודעה הזאת ולא יתבצע אף שינוי." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "ברוך בואך למאגר בתחזוקת המשתמשים של Arch" #: scripts/notify.py msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "" +msgstr "ברוך הבא למאגר חבילות המשתמשים של Arch! כדי להגדיר ססמה ראשונית לחשבון החדש שלך נא ללחוץ על הקישור [1] שלהלן. אם הקישור לא עובד, נא לנסות להעתיק ולהדביק אותו בדפדפן." #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "הערה ב־AUR על {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "התגובה הבאה נוספה על ידי {user} [1] לחבילה {pkgbase} [2]:" #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "אם לא מעניין אותך לקבל יותר הודעות על החבילה הזאת נא לגשת לעמוד החבילה [2] ולבחור ב„{label}”." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "עדכון בחבילת AUR:‏ {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "נדחפה הגשה חדשה מאת {user} [1] אל {pkgbase} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "התראת חבילה לא עדכנית ב־AUR עבור {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "החבילה שלך {pkgbase} [1] סומנה כלא עדכנית על ידי {user} [2]:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "הודעת בעלות ב־AUR על {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "החבילה {pkgbase} [1] אומצה על ידי {user} [2]." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "החבילה {pkgbase} [1] נושלה על ידי {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "התראה למתחזקי משנה ב־AUR של {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "נוספת לרשימת מתחזקי המשנה של {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "הוסרת מרשימת מתחזקי המשנה עבור {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "חבילה נמחקה ב־AUR‏: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{old} [2] מוזג לתוך {new} [3] על ידי {user} [1].\n\n-- \nכדי לא לקבל עוד הודעות על החבילה החדשה, עליך לגשת אל [3] וללחוץ על „{label}”." #: scripts/notify.py #, python-brace-format @@ -2130,16 +2234,16 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr " {pkgbase} [2] נמחקה על ידי{user} [1].\n\nלא תישלחנה אליך התראות נוספות על חבילה זו." #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "תזכורת הצבעה למשתמש מהימן: הצעה {id}" #: scripts/notify.py #, python-brace-format msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." -msgstr "" +msgstr "נא לזכור להצביע בהצעה {id} [1]. ההצבעה תסתיים בעוד פחות מ־48 שעות." diff --git a/po/hr.po b/po/hr.po index 56a101d6..0a522fb8 100644 --- a/po/hr.po +++ b/po/hr.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Lukas Fleischer , 2011 @@ -8,9 +8,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Croatian (http://www.transifex.com/lfleischer/aurweb/language/hr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -73,6 +73,10 @@ msgstr "Nije moguće naći informacije o zadanom korisniku." msgid "You do not have permission to edit this account." msgstr "Nemate ovlasti da bi mjenjali ovaj račun." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Koristite ovaj formular za pretraživanj postoječih računa." @@ -367,10 +371,10 @@ msgid "Enter login credentials" msgstr "" #: html/login.php -msgid "User name or email address" +msgid "User name or primary email address" msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Lozinka" @@ -431,7 +435,7 @@ msgid "Your password has been reset successfully." msgstr "" #: html/passreset.php -msgid "Confirm your e-mail address:" +msgid "Confirm your user name or primary e-mail address:" msgstr "" #: html/passreset.php @@ -449,12 +453,12 @@ msgstr "" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" +msgid "Enter your user name or your primary e-mail address:" msgstr "" #: html/pkgbase.php @@ -647,19 +651,19 @@ msgstr "" msgid "Close Request" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Sljedeći" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "" @@ -760,10 +764,18 @@ msgstr "Zapični i završi sa slovom ili brojkom" msgid "Can contain only one period, underscore or hyphen." msgstr "Može sadržavati samo jednu točku, donju crticu ili povlaku." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Email adresa je neispravna." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -803,6 +815,18 @@ msgstr "" msgid "The SSH public key, %s%s%s, is already in use." msgstr "" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1212,6 +1236,10 @@ msgstr "Pregledaj pakete ovog korisnika" msgid "Edit this user's account" msgstr "" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1222,6 +1250,11 @@ msgstr "" msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "obvezno" @@ -1259,8 +1292,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Ponovno upišite lozinku" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1270,6 +1329,16 @@ msgstr "Jezik" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Ponovno upišite lozinku" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1296,6 +1365,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1424,7 +1511,7 @@ msgstr "" msgid "Disable notifications" msgstr "" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1456,6 +1543,10 @@ msgstr "" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1502,8 +1593,15 @@ msgstr "" msgid "Add Comment" msgstr "" -#: template/pkg_comments.php -msgid "View all comments" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." msgstr "" #: template/pkg_comments.php @@ -1514,6 +1612,10 @@ msgstr "" msgid "Latest Comments" msgstr "" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1524,6 +1626,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1560,10 +1667,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalji o paketu" @@ -2114,6 +2217,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/hu.po b/po/hu.po index 8dd39982..1d4cf856 100644 --- a/po/hu.po +++ b/po/hu.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Balló György , 2013 @@ -11,9 +11,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Hungarian (http://www.transifex.com/lfleischer/aurweb/language/hu/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -76,6 +76,10 @@ msgstr "Nem sikerült letölteni a megadott felhasználó információit." msgid "You do not have permission to edit this account." msgstr "Nincs engedélyed ennek a fióknak a szerkesztéséhez." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Már meglévő felhasználói fiókok kereséséhez használd ezt az űrlapot." @@ -370,10 +374,10 @@ msgid "Enter login credentials" msgstr "Bejelentkezési adatok megadása" #: html/login.php -msgid "User name or email address" -msgstr "Felhasználónév vagy e-mail cím" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Jelszó" @@ -434,8 +438,8 @@ msgid "Your password has been reset successfully." msgstr "Jelszavad sikeresen visszaállításra került." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Erősíts meg az e-mail címedet:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -452,13 +456,13 @@ msgstr "Folytatás" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Ha elfelejtetted az e-mail címet, amit a regisztrációhoz használtál, akkor küldj egy üzenetet az %saur-general%s levelezőlistára." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Add meg az e-mail címedet:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -650,19 +654,19 @@ msgstr "Kérelem beküldése" msgid "Close Request" msgstr "Kérelem lezárása" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Első" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Előző" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Következő" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Utolsó" @@ -763,10 +767,18 @@ msgstr "Betűvel vagy számjeggyel kezdődjön és végződjön" msgid "Can contain only one period, underscore or hyphen." msgstr "Csak egyetlen pontot, aláhúzást vagy kötőjelet tartalmazhat." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Érvénytelen e-mail cím." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -806,6 +818,18 @@ msgstr "A(z) %s%s%s cím már használatban van." msgid "The SSH public key, %s%s%s, is already in use." msgstr "A(z) %s%s%s nyilvános SSH kulcs már használatban van." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1215,6 +1239,10 @@ msgstr "A felhasználó csomagjainak megtekintése" msgid "Edit this user's account" msgstr "Ezen felhasználó fiókjának szerkesztése" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1225,6 +1253,11 @@ msgstr "Kattints %side%s, ha véglegesen törölni szeretnéd ezt a fiókot." msgid "Click %shere%s for user details." msgstr "Kattints %side%s a felhasználó részleteihez." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "kötelező" @@ -1262,8 +1295,34 @@ msgid "Hide Email Address" msgstr "E-mail cím elrejtése" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Megismételt jelszó" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1273,6 +1332,16 @@ msgstr "Nyelv" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Megismételt jelszó" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1299,6 +1368,24 @@ msgstr "Értesítés csomagfrissítésekről." msgid "Notify of ownership changes" msgstr "Értesítés tulajdonváltozásokról" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1427,7 +1514,7 @@ msgstr "Szavazás erre a csomagra" msgid "Disable notifications" msgstr "Értesítések kikapcsolása" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Értesítések engedélyezése" @@ -1458,6 +1545,10 @@ msgstr "Git klónozási URL" msgid "read-only" msgstr "csak olvasható" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1504,9 +1595,16 @@ msgstr "Hozzászólás szerkesztése ehhez: %s" msgid "Add Comment" msgstr "Hosszászólás" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Összes megjegyzés megjelenítése" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1516,6 +1614,10 @@ msgstr "Rögzített hozzászólások" msgid "Latest Comments" msgstr "Legújabb hozzászólások" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1526,6 +1628,11 @@ msgstr "%s hozzászólt ekkor: %s" msgid "Anonymous comment on %s" msgstr "Névtelen hozzászólás ekkor: %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1562,10 +1669,6 @@ msgstr "Hozzászólás rögzítése" msgid "Unpin comment" msgstr "Hozzászólás feloldása" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Összes hozzászólás" - #: template/pkg_details.php msgid "Package Details" msgstr "Részletes csomaginformáció" @@ -2112,6 +2215,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/it.po b/po/it.po index 59da571b..5dfdd9fc 100644 --- a/po/it.po +++ b/po/it.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Giovanni Scafora , 2011-2015 @@ -11,9 +11,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2019-07-18 16:40+0000\n" -"Last-Translator: mattia_b89 \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Italian (http://www.transifex.com/lfleischer/aurweb/language/it/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -76,6 +76,10 @@ msgstr "Impossibile recuperare le informazioni dell'utente specificato." msgid "You do not have permission to edit this account." msgstr "Non hai i permessi necessari per modificare questo account." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Usa questo modulo per cercare gli account esistenti." @@ -370,10 +374,10 @@ msgid "Enter login credentials" msgstr "Inserisci le credenziali di accesso" #: html/login.php -msgid "User name or email address" -msgstr "Il nome utente o l'indirizzo email" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Password" @@ -434,8 +438,8 @@ msgid "Your password has been reset successfully." msgstr "La tua password è stata ripristinata con successo." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Conferma il tuo indirizzo e-mail:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -452,13 +456,13 @@ msgstr "Continua" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Se hai dimenticato l'indirizzo e-mail utilizzato per registrarti, invia un messaggio nella mailing list %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Inserisci il tuo indirizzo email:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -650,19 +654,19 @@ msgstr "Invia la richiesta" msgid "Close Request" msgstr "Chiudi la richiesta" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Primo" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Precedente" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Successivo" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Ultimo" @@ -763,10 +767,18 @@ msgstr "Inizia e termina con una lettera o un numero" msgid "Can contain only one period, underscore or hyphen." msgstr "Può contenere solo un punto, un trattino basso o un trattino." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "L'indirizzo email non risulta valido." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "La homepage non è valida, per favore specificare l'URL HTTP(s) completo." @@ -806,6 +818,18 @@ msgstr "L'indirizzo %s%s%s è già in uso." msgid "The SSH public key, %s%s%s, is already in use." msgstr "La chiave pubblica SSH %s%s%s, è già in uso." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1215,6 +1239,10 @@ msgstr "Mostra i pacchetti di quest'utente" msgid "Edit this user's account" msgstr "Modifica l'account di quest'utente" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1225,6 +1253,11 @@ msgstr "Clicca %squi%s se vuoi eliminare definitivamente questo account." msgid "Click %shere%s for user details." msgstr "Click %squì%s per il dettagli dell'utente." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "obbligatorio" @@ -1262,8 +1295,34 @@ msgid "Hide Email Address" msgstr "Nascondi l'indirizzo email" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Riscrivi la password" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1273,6 +1332,16 @@ msgstr "Lingua" msgid "Timezone" msgstr "Fuso orario" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Riscrivi la password" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1299,6 +1368,24 @@ msgstr "Notifica degli aggiornamenti dei pacchetti" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1427,7 +1514,7 @@ msgstr "Vota per questo pacchetto" msgid "Disable notifications" msgstr "Disabilita le notifiche" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Abilita le notifiche" @@ -1458,6 +1545,10 @@ msgstr "Git Clone URL" msgid "read-only" msgstr "sola lettura" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1504,9 +1595,16 @@ msgstr "Modifica il commento di: %s" msgid "Add Comment" msgstr "Aggiungi un commento" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Mostra tutti i commenti" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1516,6 +1614,10 @@ msgstr "Elimina i commenti" msgid "Latest Comments" msgstr "Ultimi commenti" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1526,6 +1628,11 @@ msgstr "%s ha commentato su %s" msgid "Anonymous comment on %s" msgstr "Commento anonimo su %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1562,10 +1669,6 @@ msgstr "Inserisci il commento" msgid "Unpin comment" msgstr "Elimina il commento" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Tutti i commenti" - #: template/pkg_details.php msgid "Package Details" msgstr "Dettagli del pacchetto" @@ -2112,6 +2215,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/ja.po b/po/ja.po index 4c8034b9..337e7ee8 100644 --- a/po/ja.po +++ b/po/ja.po @@ -1,19 +1,19 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # kusakata, 2013 # kusakata, 2013 -# kusakata, 2013-2017 +# kusakata, 2013-2018 # 尾ノ上卓朗 , 2017 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Japanese (http://www.transifex.com/lfleischer/aurweb/language/ja/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -76,6 +76,10 @@ msgstr "指定のユーザーの情報が取得できませんでした。" msgid "You do not have permission to edit this account." msgstr "あなたはこのアカウントを編集する権利を持っていません。" +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "アカウントの検索はこのフォームを使って下さい。" @@ -370,10 +374,10 @@ msgid "Enter login credentials" msgstr "ログイン情報を入力してください" #: html/login.php -msgid "User name or email address" -msgstr "ユーザー名またはメールアドレス" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "パスワード" @@ -434,8 +438,8 @@ msgid "Your password has been reset successfully." msgstr "パスワードのリセットが成功しました。" #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "メールアドレスの確認:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -452,13 +456,13 @@ msgstr "続行" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "登録したメールアドレスを忘れてしまった場合は、%saur-general%s メーリングリストにメッセージを送って下さい。" +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "メールアドレスを入力:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -534,7 +538,7 @@ msgstr "このフォームを使って以下のパッケージを含むパッケ msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "チェックボックスを選択して、パッケージの共同メンテナから降りることを確定してください。" #: html/pkgdisown.php #, php-format @@ -650,19 +654,19 @@ msgstr "リクエストを送信" msgid "Close Request" msgstr "リクエストをクローズ" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "最初" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "前へ" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "次へ" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "最後" @@ -763,10 +767,18 @@ msgstr "最初と最後の文字は英数字にしてください" msgid "Can contain only one period, underscore or hyphen." msgstr "ピリオド、アンダーライン、ハイフンはひとつだけ含めることができます。" +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "メールアドレスが不正です。" +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "ホームページが不正です。完全な HTTP(s) URL を入力してください。" @@ -806,6 +818,18 @@ msgstr "%s%s%s というメールアドレスは既に使われています。" msgid "The SSH public key, %s%s%s, is already in use." msgstr "SSH 公開鍵、%s%s%s は既に使われています。" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1215,6 +1239,10 @@ msgstr "ユーザーのパッケージを見る" msgid "Edit this user's account" msgstr "このユーザーのアカウントを編集" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1225,6 +1253,11 @@ msgstr "このアカウントを恒久的に削除したい場合は%sこちら% msgid "Click %shere%s for user details." msgstr "ユーザーの詳細は%sこちら%sをクリック。" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "必須" @@ -1262,8 +1295,34 @@ msgid "Hide Email Address" msgstr "メールアドレスを非公開にする" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "パスワードの再入力" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1273,6 +1332,16 @@ msgstr "言語" msgid "Timezone" msgstr "タイムゾーン" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "パスワードの再入力" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1299,6 +1368,24 @@ msgstr "パッケージアップデートの通知" msgid "Notify of ownership changes" msgstr "所有者の変更の通知" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1427,7 +1514,7 @@ msgstr "このパッケージに投票する" msgid "Disable notifications" msgstr "通知を止める" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "通知を有効にする" @@ -1457,6 +1544,10 @@ msgstr "Git クローン URL" msgid "read-only" msgstr "リードオンリー" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1503,9 +1594,16 @@ msgstr "コメントを編集: %s" msgid "Add Comment" msgstr "コメントを投稿する" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "全てのコメントを表示" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1515,6 +1613,10 @@ msgstr "ピン留めされたコメント" msgid "Latest Comments" msgstr "最新のコメント" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1525,6 +1627,11 @@ msgstr "%s が %s にコメントを投稿しました" msgid "Anonymous comment on %s" msgstr "匿名ユーザーが %s にコメントを投稿しました" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1561,10 +1668,6 @@ msgstr "コメントをピン留めする" msgid "Unpin comment" msgstr "コメントのピン留めを解除" -#: template/pkg_comments.php -msgid "All comments" -msgstr "全てのコメント" - #: template/pkg_details.php msgid "Package Details" msgstr "パッケージの詳細" @@ -2009,7 +2112,7 @@ msgstr "前へ" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "" +msgstr "AUR パスワードのリセット" #: scripts/notify.py #, python-brace-format @@ -2017,96 +2120,97 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "" +msgstr "あなたのメールアドレスと関連付けられたアカウント {user} のパスワードのリセットリクエストが送信されました。パスワードをリセットしたいときは下のリンク [1] を開いて下さい、そうでない場合はこのメッセージは無視して下さい。" #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "Arch User Repository にようこそ" #: scripts/notify.py msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "" +msgstr "Arch User Repository にようこそ!アカウントのパスワードを設定するために、下のリンク [1] をクリックしてください。リンクを押せないときは、一旦ブラウザにURLをコピーしてください。" #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "{pkgbase} の AUR コメント" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "{user} [1] は以下のコメントを {pkgbase} [2] に追加しました:" #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "このパッケージの通知を受け取りたくない場合は、パッケージのページ [2] を開いて \"{label}\" を選択してください。" #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "AUR パッケージアップデート: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} [1] は {pkgbase} [2] に新しいコミットを投稿しました。" #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} の AUR Out-of-date 通知" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "あなたのパッケージ {pkgbase} [1] は {user} [2] によって out-of-date フラグが立てられました:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} の AUR 所有者通知" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "パッケージ {pkgbase} [1] は {user} [2] によって継承されました。" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "パッケージ {pkgbase} [1] は {user} [2] から放棄されました。" #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} の AUR 共同メンテナ通知" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "あなたは {pkgbase} [1] の共同メンテナリストに追加されました。" #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "あなたは {pkgbase} [1] の共同メンテナリストから削除されました。" #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "AUR パッケージ削除: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" @@ -2116,16 +2220,16 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] は {pkgbase} [2] を削除しました。\n\nこのパッケージの通知が送信されることはありません。" #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "TU 投票リマインダー: 提案 {id}" #: scripts/notify.py #, python-brace-format msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." -msgstr "" +msgstr "{id} [1] の提案について投票してください。投票期限は48時間以内です。" diff --git a/po/nb.po b/po/nb.po index 602d4163..200b6060 100644 --- a/po/nb.po +++ b/po/nb.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Alexander F Rødseth , 2015,2017-2019 @@ -13,9 +13,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2019-09-02 11:29+0000\n" -"Last-Translator: Alexander F Rødseth \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Norwegian Bokmål (http://www.transifex.com/lfleischer/aurweb/language/nb/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -78,6 +78,10 @@ msgstr "Kunne ikke hente informasjon om den valgte brukeren." msgid "You do not have permission to edit this account." msgstr "Du har ikke tilgang til å endre denne kontoen." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Bruk skjemaet for å søke etter eksisterende kontoer." @@ -372,10 +376,10 @@ msgid "Enter login credentials" msgstr "Fyll ut innloggingsinformasjon" #: html/login.php -msgid "User name or email address" -msgstr "Brukernavn eller e-postadresse" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Passord" @@ -436,8 +440,8 @@ msgid "Your password has been reset successfully." msgstr "Passordet ditt er nullstilt." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Bekreft e-postadressen din:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -454,13 +458,13 @@ msgstr "Fortsett" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Hvis du har glemt e-postadressen du brukte for å registrere deg, så kan du sende en e-post til listen %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Angi din e-postadresse:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -652,19 +656,19 @@ msgstr "Send inn forespørsel" msgid "Close Request" msgstr "Lukk forespørsel" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Første" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Forrige" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Neste" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Siste" @@ -765,10 +769,18 @@ msgstr "Start og slutt med en bokstav eller et siffer" msgid "Can contain only one period, underscore or hyphen." msgstr "Kan kun innehold ett punktum, en understrek, eller en bindestrek." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "E-postadressen er ugyldig." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "Ugyldig hjemmeside, vennligst spesifiser hele HTTP(s) adressen." @@ -808,6 +820,18 @@ msgstr "Adressen, %s%s%s, er allerede i bruk." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Den offentlige SSH-nøkkelen, %s%s%s, er allerede i bruk." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1217,6 +1241,10 @@ msgstr "Vis pakkene til denne brukereren" msgid "Edit this user's account" msgstr "Endre brukerkonto" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1227,6 +1255,11 @@ msgstr "Klikk %sher%s hvis du vil slette denne kontoen for alltid." msgid "Click %shere%s for user details." msgstr "Klikk %sher%s for brukerdetaljer." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "trengs" @@ -1264,8 +1297,34 @@ msgid "Hide Email Address" msgstr "Gjem e-postadresse" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Skriv inn passordet på nytt" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1275,6 +1334,16 @@ msgstr "Språk" msgid "Timezone" msgstr "Tidssone" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Skriv inn passordet på nytt" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1301,6 +1370,24 @@ msgstr "Gi beskjed om pakkeoppdateringer" msgid "Notify of ownership changes" msgstr "Gi beskjed om endring av eierskap" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1429,7 +1516,7 @@ msgstr "Stem på denne pakken" msgid "Disable notifications" msgstr "Slå av beskjeder" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Gi beskjed" @@ -1460,6 +1547,10 @@ msgstr "Git-arkiv" msgid "read-only" msgstr "skrivebeskyttet" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1506,9 +1597,16 @@ msgstr "Rediger kommentar for: %s" msgid "Add Comment" msgstr "Legg til kommentar" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Vis alle kommentarer" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1518,6 +1616,10 @@ msgstr "La stå" msgid "Latest Comments" msgstr "Siste kommentarer" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1528,6 +1630,11 @@ msgstr "%s kommenterte %s" msgid "Anonymous comment on %s" msgstr "Anonym kommenterte %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1564,10 +1671,6 @@ msgstr "Fest kommentar" msgid "Unpin comment" msgstr "Løsne kommentar" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Alle kommentarer" - #: template/pkg_details.php msgid "Package Details" msgstr "Om pakken" @@ -2114,8 +2217,9 @@ msgstr "AUR pakken ble slettet: {pkgbase}" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] tok bort og slo sammen {old} [2] med {new} [3].\n\nDersom du ikke lenger vil ha beskjeder om denne pakken, vennligst gå til [3] og klikk på \"{label}\"." +msgstr "" #: scripts/notify.py #, python-brace-format diff --git a/po/nl.po b/po/nl.po index e062fe53..e5dc26d0 100644 --- a/po/nl.po +++ b/po/nl.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Heimen Stoffels , 2015 @@ -12,9 +12,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Dutch (http://www.transifex.com/lfleischer/aurweb/language/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -77,6 +77,10 @@ msgstr "Kon geen informatie ophalen voor de opgegeven gebruiker." msgid "You do not have permission to edit this account." msgstr "U heeft geen toestemming om dit account te bewerken." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Gebruik dit formulier om bestaande accounts te zoeken." @@ -371,10 +375,10 @@ msgid "Enter login credentials" msgstr "Vul uw inloggegevens in" #: html/login.php -msgid "User name or email address" +msgid "User name or primary email address" msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Wachtwoord" @@ -435,8 +439,8 @@ msgid "Your password has been reset successfully." msgstr "Uw wachtwoord is met succes teruggezet." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Bevestig uw e-mailadres:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -453,13 +457,13 @@ msgstr "Doorgaan" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Als je het e-mail adres waarmee je geregistreerd hebt vergeten bent, stuur dan een bericht naar de %saur-general%s mailing list." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Vul uw e-mail adres in:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -651,19 +655,19 @@ msgstr "" msgid "Close Request" msgstr "Aanvraag sluiten" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Eerste" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Vorige" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Volgende" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Laatste" @@ -764,10 +768,18 @@ msgstr "Beginnen en eindigen met een letter of nummer" msgid "Can contain only one period, underscore or hyphen." msgstr "Kan maar één punt, komma of koppelteken bevatten." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Het e-mailadres is ongeldig." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -807,6 +819,18 @@ msgstr "Het adres %s%s%s is al in gebruik." msgid "The SSH public key, %s%s%s, is already in use." msgstr "De SSH publieke sleutel, %s%s%s, is al ingebruik." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1216,6 +1240,10 @@ msgstr "Bekijk gebruiker zijn pakketten" msgid "Edit this user's account" msgstr "Bewerk account van deze gebruiker" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1226,6 +1254,11 @@ msgstr "Klik %shier%s als u dit account permanent wilt verwijderen." msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "verplicht" @@ -1263,8 +1296,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Voer wachtwoord opnieuw in" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1274,6 +1333,16 @@ msgstr "Taal" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Voer wachtwoord opnieuw in" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1300,6 +1369,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1428,7 +1515,7 @@ msgstr "Stem voor dit pakket" msgid "Disable notifications" msgstr "Schakel notificaties uit" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1459,6 +1546,10 @@ msgstr "Git Clone URL" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1505,9 +1596,16 @@ msgstr "" msgid "Add Comment" msgstr "Voeg Comment Toe" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Bekijk alle commentaar" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1517,6 +1615,10 @@ msgstr "" msgid "Latest Comments" msgstr "Nieuwste Comments" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1527,6 +1629,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1563,10 +1670,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Alle comments" - #: template/pkg_details.php msgid "Package Details" msgstr "Pakketdetails" @@ -2113,6 +2216,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/pl.po b/po/pl.po index 909d2ccb..56e9f6bb 100644 --- a/po/pl.po +++ b/po/pl.po @@ -1,15 +1,16 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Bartłomiej Piotrowski , 2011 # Bartłomiej Piotrowski , 2014 +# hawkeye116477 , 2019 # Chris Warrick, 2013 # Chris Warrick, 2012 # Kwpolska , 2011 # Lukas Fleischer , 2011 -# Marcin Mikołajczak , 2017 +# Marcin Mikołajczak , 2017 # Michal T , 2016 # Nuc1eoN , 2014 # Piotr Strębski , 2017-2018 @@ -18,9 +19,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-06-11 15:26+0000\n" -"Last-Translator: Piotr Strębski \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Polish (http://www.transifex.com/lfleischer/aurweb/language/pl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -83,6 +84,10 @@ msgstr "Uzyskanie informacji o podanym użytkowniku nie powiodło się." msgid "You do not have permission to edit this account." msgstr "Nie masz uprawnień do edycji tego konta." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Przy użyciu tego formularza możesz przeszukać istniejące konta." @@ -195,11 +200,11 @@ msgstr "Wyszukaj pakiety, którymi się opiekuję" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Współ-utrzymywane pakiety" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Wyszukaj pakiety, które współ-utrzymuję" #: html/home.php #, php-format @@ -377,10 +382,10 @@ msgid "Enter login credentials" msgstr "Podaj poświadczenia logowania" #: html/login.php -msgid "User name or email address" -msgstr "Nazwa użytkownika lub adres e-mail" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Hasło" @@ -441,8 +446,8 @@ msgid "Your password has been reset successfully." msgstr "Twoje hasło zostało pomyślnie zresetowane." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Potwierdź swój adres e-mail:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -459,13 +464,13 @@ msgstr "Kontynuuj" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Jeśli zapomniałeś adresu e-mail, którego użyłeś do rejestracji, prosimy o wysłanie wiadomości na naszą listę dyskusyjną %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Wpisz swój adres e-mail:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -657,19 +662,19 @@ msgstr "Prześlij prośbę" msgid "Close Request" msgstr "Zamknij prośbę" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Pierwsza" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Poprzednia" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Następna" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Ostatnia" @@ -701,7 +706,7 @@ msgstr "wydanie %d" #: html/tos.php msgid "I accept the terms and conditions above." -msgstr "" +msgstr "Akceptuję powyższe warunki i zasady." #: html/tu.php template/account_details.php template/header.php msgid "Trusted User" @@ -770,10 +775,18 @@ msgstr "Zacznij i zakończ literą lub cyfrą" msgid "Can contain only one period, underscore or hyphen." msgstr "Może zawierać tylko jedną kropkę, podkreślnik lub myślnik." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Adres e-mail jest nieprawidłowy." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -813,6 +826,18 @@ msgstr "Adres, %s%s%s, jest już używany." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Publiczny klucz SSH, %s%s%s, jest już używany." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1222,6 +1247,10 @@ msgstr "Wyświetl pakiety tego użytkownika" msgid "Edit this user's account" msgstr "Edycja tego konta użytkownika" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1232,6 +1261,11 @@ msgstr "Kliknij %stutaj%s, jeśli chcesz nieodwracalnie usunąć to konto." msgid "Click %shere%s for user details." msgstr "Kliknij %stutaj%s by wyświetlić szczegóły użytkownika." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "wymagane" @@ -1269,8 +1303,34 @@ msgid "Hide Email Address" msgstr "Ukryj adres e-mail" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Hasło (ponownie)" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1280,6 +1340,16 @@ msgstr "Język" msgid "Timezone" msgstr "Strefa czasowa" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Hasło (ponownie)" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1306,6 +1376,24 @@ msgstr "Powiadom o aktualizacjach pakietu" msgid "Notify of ownership changes" msgstr "Powiadom o zmianie właściciela" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1434,7 +1522,7 @@ msgstr "Zagłosuj na ten pakiet" msgid "Disable notifications" msgstr "Wyłącz powiadomienia" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Włącz powiadomienia" @@ -1467,6 +1555,10 @@ msgstr "URL klonu Git" msgid "read-only" msgstr "tylko do odczytu" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1513,9 +1605,16 @@ msgstr "Edycja komentarza do: %s" msgid "Add Comment" msgstr "Dodaj komentarz" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Pokaż wszystkie komentarze" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1525,6 +1624,10 @@ msgstr "Przypięte komentarze" msgid "Latest Comments" msgstr "Ostatnie komentarze" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1535,6 +1638,11 @@ msgstr "%s skomentował dnia %s" msgid "Anonymous comment on %s" msgstr "Anonimowy komentarz do %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1571,10 +1679,6 @@ msgstr "Przypnij komentarz" msgid "Unpin comment" msgstr "Odepnij komentarz" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Wszystkie komentarze" - #: template/pkg_details.php msgid "Package Details" msgstr "Informacje o pakiecie" @@ -1700,7 +1804,7 @@ msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." -msgstr "" +msgstr "Brak żądań spełniających podane kryteria wyszukiwania." #: template/pkgreq_results.php #, php-format @@ -2082,7 +2186,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "Powiadomienie AUR o nieaktualnym pakiecie {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2129,6 +2233,7 @@ msgstr "Usunięty pakiet AUR: {pkgbase}" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/pt_BR.po b/po/pt_BR.po index 8c05a312..1a54b01a 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1,12 +1,12 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Albino Biasutti Neto Bino , 2011 # Fábio Nogueira , 2016 # Rafael Fontenelle , 2012-2015 -# Rafael Fontenelle , 2011,2015-2018 +# Rafael Fontenelle , 2011,2015-2018,2020 # Rafael Fontenelle , 2011 # Sandro , 2011 # Sandro , 2011 @@ -14,8 +14,8 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-21 18:28+0000\n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 12:57+0000\n" "Last-Translator: Rafael Fontenelle \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/lfleischer/aurweb/language/pt_BR/)\n" "MIME-Version: 1.0\n" @@ -79,6 +79,10 @@ msgstr "Não foi possível obter informações para o usuário especificado." msgid "You do not have permission to edit this account." msgstr "Você não tem permissão para editar esta conta." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "Senha inválida." + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Utilize este formulário para procurar contas existentes." @@ -373,10 +377,10 @@ msgid "Enter login credentials" msgstr "Digite as credenciais de login" #: html/login.php -msgid "User name or email address" -msgstr "Nome de usuário ou endereço de e-mail" +msgid "User name or primary email address" +msgstr "Nome de usuário ou endereço de e-mail primário" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Senha" @@ -437,8 +441,8 @@ msgid "Your password has been reset successfully." msgstr "Sua senha foi redefinida com sucesso." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Confirme seu endereço de e-mail:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "Confirme seu nome de usuário ou endereço de e-mail primário:" #: html/passreset.php msgid "Enter your new password:" @@ -455,13 +459,13 @@ msgstr "Continuar" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Se você esqueceu o endereço de e-mail que você usou para registrar, por favor envie uma mensagem para a lista de e-mail do %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "Se você esqueceu o nome de usuário e o endereço de e-mail primário que você usou para se registrar, envie uma mensagem para a lista de discussão %saur-general%s." #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Digite o seu endereço de e-mail:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "Digite seu nome de usuário ou seu endereço de e-mail primário:" #: html/pkgbase.php msgid "Package Bases" @@ -653,19 +657,19 @@ msgstr "Enviar requisição" msgid "Close Request" msgstr "Fechar requisição" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Primeiro" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Anterior" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Próxima" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Última" @@ -766,10 +770,18 @@ msgstr "Começo e fim com uma letra ou um número" msgid "Can contain only one period, underscore or hyphen." msgstr "Pode conter somente um ponto, traço inferior ou hífen." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "Por favor, confirme sua nova senha." + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "O endereço de e-mail é inválido." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "O endereço de e-mail reserva é inválido" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "A página inicial é inválida. Por favor especificar a URL HTTP(s) completa." @@ -809,6 +821,18 @@ msgstr "O endereço, %s%s%s, já está sendo usado." msgid "The SSH public key, %s%s%s, is already in use." msgstr "A chave pública de SSH, %s%s%s, já está em uso." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "O CAPTCHA está faltando." + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "Este CAPTCHA expirou. Por favor, tente novamente." + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "A resposta de CAPTCHA inserida é inválida." + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1218,6 +1242,10 @@ msgstr "Visualizar pacotes deste usuário" msgid "Edit this user's account" msgstr "Edite a conta desse usuário" +#: template/account_details.php +msgid "List this user's comments" +msgstr "Listar os comentários deste usuário" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1228,6 +1256,11 @@ msgstr "Clique %saqui%s se você deseja excluir permanentemente esta conta." msgid "Click %shere%s for user details." msgstr "Clique %saqui%s para os detalhes do usuário." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "Clique %saqui%s para listar os comentários feitos por esta conta." + #: template/account_edit_form.php msgid "required" msgstr "obrigatório" @@ -1265,8 +1298,34 @@ msgid "Hide Email Address" msgstr "Ocultar endereço de e-mail" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Re-digite a senha" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "Se você não ocultar seu endereço de e-mail, ele ficará visível para todos os usuários registrados do AUR. Se você ocultar seu endereço de e-mail, ele estará visível apenas para membros da equipe do Arch Linux." + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "Endereço de e-mail reserva" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "Opcionalmente, forneça um endereço de e-mail secundário que possa ser usado para restaurar sua conta, caso você perca o acesso ao seu endereço de e-mail primário." + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "Os links de redefinição de senha são sempre enviados ao seu endereço de e-mail primário e reserva." + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "Seu endereço de e-mail reserva sempre é visível apenas para membros da equipe do Arch Linux, independentemente da configuração %s." #: template/account_edit_form.php msgid "Language" @@ -1276,6 +1335,16 @@ msgstr "Idioma" msgid "Timezone" msgstr "Fuso horário" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "Se você deseja alterar a senha, insira uma nova senha e confirme a nova senha digitando-a novamente." + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Re-digite a senha" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1302,6 +1371,24 @@ msgstr "Notificar sobre atualizações de pacotes" msgid "Notify of ownership changes" msgstr "Notificar sobre mudanças de mantenedor" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "Para confirmar as alterações no perfil, digite sua senha atual:" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "Sua senha atual" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "Para proteger o AUR contra a criação automatizada de contas, solicitamos que você forneça o resultado do seguinte comando:" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "Resposta" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1430,7 +1517,7 @@ msgstr "Votar neste pacote" msgid "Disable notifications" msgstr "Desabilitar notificações" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Habilitar notificações" @@ -1461,6 +1548,10 @@ msgstr "Git Clone URL" msgid "read-only" msgstr "somente leitura" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "clique para copiar" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1507,9 +1598,16 @@ msgstr "Editar comentário para: %s" msgid "Add Comment" msgstr "Adicionar comentário" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Ver todos os comentários" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "Os identificadores de commit Git que fazem referência a commits no repositório de pacote AUR e os URLs são convertidos em links automaticamente." + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "Há suporte parcial à %ssintaxe Markdown%s." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1519,6 +1617,10 @@ msgstr "Comentários afixados" msgid "Latest Comments" msgstr "Últimos comentários" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "Comentários para" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1529,6 +1631,11 @@ msgstr "%s comentou em %s" msgid "Anonymous comment on %s" msgstr "Comentário anônimo em %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "Comentou no pacote %s em %s" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1565,10 +1672,6 @@ msgstr "Afixar comentário" msgid "Unpin comment" msgstr "Desafixar comentário" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Todos os comentários" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalhes do pacote" @@ -2115,8 +2218,9 @@ msgstr "Pacote do AUR excluído: {pkgbase}" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] mesclou {old} [2] ao {new} [3].\n\nSe você não deseja mais receber notificações sobre o novo pacote, por favor acesse a [3] e clique em \"{label}\"." +msgstr "{user} [1] mesclou {old} [2] ao {new} [3].\n\n-- \nSe você não deseja mais receber notificações sobre o novo pacote, por favor acesse a [3] e clique em \"{label}\"." #: scripts/notify.py #, python-brace-format diff --git a/po/pt_PT.po b/po/pt_PT.po index 30a75434..0303993a 100644 --- a/po/pt_PT.po +++ b/po/pt_PT.po @@ -1,21 +1,21 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Christophe Silva , 2018 # Gaspar Santos , 2011 # R00KIE , 2013,2016 # R00KIE , 2011 -# DarkVenger, 2013-2015 -# DarkVenger, 2012 +# c0d75bae60e6967ec54315cff4da5848, 2013-2015 +# c0d75bae60e6967ec54315cff4da5848, 2012 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Portuguese (Portugal) (http://www.transifex.com/lfleischer/aurweb/language/pt_PT/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -78,6 +78,10 @@ msgstr "Não foi possível obter informações para o utilizador especificado." msgid "You do not have permission to edit this account." msgstr "Não tem autorização para editar esta conta." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Utilize este formulário para procurar contas existentes." @@ -372,10 +376,10 @@ msgid "Enter login credentials" msgstr "Introduza as credenciais para login" #: html/login.php -msgid "User name or email address" -msgstr "Usuário ou endereço de email" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Palavra-passe" @@ -436,8 +440,8 @@ msgid "Your password has been reset successfully." msgstr "A palavra-passe foi reiniciada com êxito." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Confirme o endereço de e-mail:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -454,13 +458,13 @@ msgstr "Continue" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Se se esqueceu do endereço de e-mail que utilizou para efectuar o registo, por favor envie uma mensagem para a lista de discussão %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Introduza o endereço de e-mail:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -652,19 +656,19 @@ msgstr "Submeter Pedido" msgid "Close Request" msgstr "Fechar Pedido" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Primeiro" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Anterior" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Seguinte" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Ultimo." @@ -765,10 +769,18 @@ msgstr "Começar e acabar com uma letra ou um número" msgid "Can contain only one period, underscore or hyphen." msgstr "Apenas pode conter um ponto, underscore ou hífen." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "O endereço de e-mail é inválido." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "A página inicial é inválida, especifique o URL de HTTP (s) completo (s)." @@ -808,6 +820,18 @@ msgstr "O endereço, %s%s%s, já está a ser utilizado." msgid "The SSH public key, %s%s%s, is already in use." msgstr "A chave pública SSH, %s %s %s, já está em uso." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1217,6 +1241,10 @@ msgstr "Ver os pacotes deste utilizador" msgid "Edit this user's account" msgstr "Editar a conta deste utilizador" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1227,6 +1255,11 @@ msgstr "" msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "necessário" @@ -1264,8 +1297,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Reintroduza a palavra-passe" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1275,6 +1334,16 @@ msgstr "Língua" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Reintroduza a palavra-passe" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1301,6 +1370,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1429,7 +1516,7 @@ msgstr "Votar neste pacote" msgid "Disable notifications" msgstr "Desativar notificações" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1460,6 +1547,10 @@ msgstr "" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1506,8 +1597,15 @@ msgstr "" msgid "Add Comment" msgstr "Adicionar comentário" -#: template/pkg_comments.php -msgid "View all comments" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." msgstr "" #: template/pkg_comments.php @@ -1518,6 +1616,10 @@ msgstr "" msgid "Latest Comments" msgstr "Últimos Comentários" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1528,6 +1630,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1564,10 +1671,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Todos os comentários" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalhes do Pacote" @@ -2114,6 +2217,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/ro.po b/po/ro.po index 0859fa42..7394ed83 100644 --- a/po/ro.po +++ b/po/ro.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Arthur Țițeică , 2013-2015 @@ -10,9 +10,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Romanian (http://www.transifex.com/lfleischer/aurweb/language/ro/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -75,6 +75,10 @@ msgstr "Nu s-au putut prelua informații despre utilizatorul specificat." msgid "You do not have permission to edit this account." msgstr "Nu ai permisiune pentru a modifica acest cont." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Folosește acest formular pentru a căuta conturi existente." @@ -369,10 +373,10 @@ msgid "Enter login credentials" msgstr "Introdu datele de autentificare" #: html/login.php -msgid "User name or email address" +msgid "User name or primary email address" msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Parolă" @@ -433,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "Parola ta a fost restabilită cu succes." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Confirmă adresa de e-mail:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -451,13 +455,13 @@ msgstr "Continuă" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Dacă ai uitat adresa de email folosită la înregistrare, trimite un mesaj la lista de discuții %saur-general%s" +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Introdu adresa ta de e-mail:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -649,19 +653,19 @@ msgstr "" msgid "Close Request" msgstr "Închide cererea" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Prim" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Precedent" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Înainte" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Ultim" @@ -762,10 +766,18 @@ msgstr "Începe și sfârșește cu o literă sau un număr." msgid "Can contain only one period, underscore or hyphen." msgstr "Poate conține doar o virgulă, linie joasă sau cratimă." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Adresa de email nu este validă." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -805,6 +817,18 @@ msgstr "Adresa %s%s%s este deja folosită." msgid "The SSH public key, %s%s%s, is already in use." msgstr "" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1214,6 +1238,10 @@ msgstr "Vezi pachetele acestui utilizator" msgid "Edit this user's account" msgstr "Modifică contul acestui utilizator" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1224,6 +1252,11 @@ msgstr "Clic %saici%s dacă dorești să ștergi definitiv acest cont." msgid "Click %shere%s for user details." msgstr "" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "cerut" @@ -1261,8 +1294,34 @@ msgid "Hide Email Address" msgstr "" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Rescrie parola" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1272,6 +1331,16 @@ msgstr "Limbă" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Rescrie parola" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1298,6 +1367,24 @@ msgstr "" msgid "Notify of ownership changes" msgstr "" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1426,7 +1513,7 @@ msgstr "Votează acest pachet" msgid "Disable notifications" msgstr "Dezactivează notificări" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "" @@ -1458,6 +1545,10 @@ msgstr "" msgid "read-only" msgstr "" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1504,9 +1595,16 @@ msgstr "" msgid "Add Comment" msgstr "Adaugă comentariu" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Vizualizează toate comentariile" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1516,6 +1614,10 @@ msgstr "" msgid "Latest Comments" msgstr "Ultimele comentarii" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1526,6 +1628,11 @@ msgstr "" msgid "Anonymous comment on %s" msgstr "" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1562,10 +1669,6 @@ msgstr "" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Toate comentariile" - #: template/pkg_details.php msgid "Package Details" msgstr "Detalii pachet" @@ -2116,6 +2219,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/ru.po b/po/ru.po index abe4a5c9..86a01cc6 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Evgeniy Alekseev , 2014-2015 @@ -17,9 +17,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Russian (http://www.transifex.com/lfleischer/aurweb/language/ru/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -82,6 +82,10 @@ msgstr "Не удалось получить информацию об указ msgid "You do not have permission to edit this account." msgstr "Вы не имеете права редактировать эту учетную запись." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Используйте эту форму для поиска существующих учетных записей." @@ -376,10 +380,10 @@ msgid "Enter login credentials" msgstr "Введите учётные данные" #: html/login.php -msgid "User name or email address" -msgstr "Имя пользователя или email" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Пароль" @@ -440,8 +444,8 @@ msgid "Your password has been reset successfully." msgstr "Ваш пароль успешно сброшен." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Подтвердите адрес своей электронной почты:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -458,13 +462,13 @@ msgstr "Продолжить" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Если вы забыли электронный адрес, который вы использовали для регистрации, пожалуйста, отошлите сообщение в список рассылки %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Введите свой адрес электронной почты:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -656,19 +660,19 @@ msgstr "Отправить запрос" msgid "Close Request" msgstr "Закрыть запрос" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Первый" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Назад" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Далее" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Последний" @@ -769,10 +773,18 @@ msgstr "Начинаются и заканчиваются цифрой или msgid "Can contain only one period, underscore or hyphen." msgstr "Может содержать только одну точку, подчёркивание или тире." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Неправильный адрес электронной почты." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "Неверная домашняя страница, укажите полный адрес с указанием протокола HTTP(s)." @@ -812,6 +824,18 @@ msgstr "Адрес %s%s%s уже используется." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Публичный SSH ключ %s%s%s уже используется." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1221,6 +1245,10 @@ msgstr "Посмотреть пакеты этого пользователя" msgid "Edit this user's account" msgstr "Отредактировать этот аккаунт" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1231,6 +1259,11 @@ msgstr "Нажмите %sздесь%s, если Вы хотите удалить msgid "Click %shere%s for user details." msgstr "Нажмите %sздесь%s, чтобы просмотреть данные о пользователе." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "необходимо" @@ -1268,8 +1301,34 @@ msgid "Hide Email Address" msgstr "Скрыть email." #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Введите пароль еще раз" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1279,6 +1338,16 @@ msgstr "Язык" msgid "Timezone" msgstr "Часовой пояс" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Введите пароль еще раз" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1305,6 +1374,24 @@ msgstr "Уведомлять об обновлении пакета" msgid "Notify of ownership changes" msgstr "Уведомлять об измененях собственности" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1433,7 +1520,7 @@ msgstr "Проголосовать за пакет" msgid "Disable notifications" msgstr "Выключить уведомления" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Включить уведомления" @@ -1466,6 +1553,10 @@ msgstr "URL для git clone" msgid "read-only" msgstr "только чтение" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1512,9 +1603,16 @@ msgstr "Редактировать комментарий для: %s" msgid "Add Comment" msgstr "Добавить комментарий" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Посмотреть все комментарии" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1524,6 +1622,10 @@ msgstr "Закрепленные комментарии" msgid "Latest Comments" msgstr "Последние комментарии" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1534,6 +1636,11 @@ msgstr "%s прокомментировал %s" msgid "Anonymous comment on %s" msgstr "Анонимный комментарий для %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1570,10 +1677,6 @@ msgstr "Закрепить комментарий" msgid "Unpin comment" msgstr "Открепить комментарий" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Все комментарии" - #: template/pkg_details.php msgid "Package Details" msgstr "Информация о пакете" @@ -2128,6 +2231,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/sk.po b/po/sk.po index ffbd86c7..d54a9dab 100644 --- a/po/sk.po +++ b/po/sk.po @@ -1,17 +1,18 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # archetyp , 2013-2016 +# Jose Riha , 2018 # Matej Ľach , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Slovak (http://www.transifex.com/lfleischer/aurweb/language/sk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -74,6 +75,10 @@ msgstr "Nemožno získať informácie pre špecifikovaného užívateľa. " msgid "You do not have permission to edit this account." msgstr "Nemáte potrebné oprávnenia, pre úpravu tohoto účtu. " +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Použite tento formulár pre vyhľadávanie v existujúcich účtoch." @@ -368,10 +373,10 @@ msgid "Enter login credentials" msgstr "Zadajte prihlasovacie údaje" #: html/login.php -msgid "User name or email address" -msgstr "Užívateľské meno alebo emailová adresa" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Heslo" @@ -432,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "Heslo bolo úspešne obnovené." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Potvrďte svoju e-mailovú adresu:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -450,13 +455,13 @@ msgstr "Pokračuj" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Ak ste zabudli e-mailovú adresu, ktorú ste použili pri registrácii, pošlite prosím správu na %saur-general%s mailing list." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Zadajte svoju e-mailovú adresu:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -648,19 +653,19 @@ msgstr "Odoslať žiadosť" msgid "Close Request" msgstr "Zatvoriť žiadosť" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Prvý" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Predchádzajúci" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" -msgstr "Ďaľej " +msgstr "Ďalej " -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Posledný" @@ -746,7 +751,7 @@ msgstr "Chýba ID užívateľa" #: lib/acctfuncs.inc.php msgid "The username is invalid." -msgstr "Užívateľké meno je neplatné." +msgstr "Užívateľské meno je neplatné." #: lib/acctfuncs.inc.php #, php-format @@ -761,10 +766,18 @@ msgstr "Začínať a končiť s písmenom alebo číslom" msgid "Can contain only one period, underscore or hyphen." msgstr "Môže obsahovať len jednu bodku, podčiarkovník alebo pomlčku." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "E-mailová adresa nie je platná." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -804,6 +817,18 @@ msgstr "Adresa %s%s%s sa už používa." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Verejný SSH kľúč %s%s%s sa už používa." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1213,6 +1238,10 @@ msgstr "Pozrieť balíčky tohto užívateľa" msgid "Edit this user's account" msgstr "Editovať účet tohto užívateľa" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1223,6 +1252,11 @@ msgstr "Kliknite %ssem%s ak chcete natrvalo vymazať tento účet." msgid "Click %shere%s for user details." msgstr "Kliknite %ssem%s pre informácie o užívateľovi." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "povinný" @@ -1260,8 +1294,34 @@ msgid "Hide Email Address" msgstr "Skryť emailovú adresu" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Potvrďte heslo" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1271,6 +1331,16 @@ msgstr "Jazyk" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Potvrďte heslo" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1297,6 +1367,24 @@ msgstr "Upozorni na aktualizácie balíčkov" msgid "Notify of ownership changes" msgstr "Upozorni na zmeny vo vlastníctve balíčka" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1342,7 +1430,7 @@ msgstr "Nie sú ďalšie výsledky na zobrazenie." #, php-format msgid "" "Use this form to add co-maintainers for %s%s%s (one user name per line):" -msgstr "Použite tento formulár pre pridanie spolupracovníkov pre %s%s%s (jedno užívateľké meno na riadok):" +msgstr "Použite tento formulár pre pridanie spolupracovníkov pre %s%s%s (jedno užívateľské meno na riadok):" #: template/comaintainers_form.php msgid "Users" @@ -1425,7 +1513,7 @@ msgstr "Hlasuj za tento balíček" msgid "Disable notifications" msgstr "Vypni upozornenia" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Povoliť upozornenia" @@ -1458,6 +1546,10 @@ msgstr "Git Clone URL" msgid "read-only" msgstr "len na čítanie" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1504,9 +1596,16 @@ msgstr "Editovať komentár k: %s" msgid "Add Comment" msgstr "Pridať komentár" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Pozrieť všetky komentáre" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1516,6 +1615,10 @@ msgstr "Pripnuté komentáre" msgid "Latest Comments" msgstr "Posledné komentáre" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1526,6 +1629,11 @@ msgstr "%s dal komentár k %s" msgid "Anonymous comment on %s" msgstr "Anonymný komentár k %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1562,10 +1670,6 @@ msgstr "Pripnúť komentár" msgid "Unpin comment" msgstr "Odopnúť komentár" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Všetky komentáre" - #: template/pkg_details.php msgid "Package Details" msgstr "Detaily balíčka" @@ -2120,6 +2224,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/sr.po b/po/sr.po index 0a49ba72..24a52ab7 100644 --- a/po/sr.po +++ b/po/sr.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Lukas Fleischer , 2011 @@ -10,9 +10,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Serbian (http://www.transifex.com/lfleischer/aurweb/language/sr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -75,6 +75,10 @@ msgstr "Ne mogu da dobavim podatke o izabranom korisniku." msgid "You do not have permission to edit this account." msgstr "Nemate dozvole za uređivanje ovog naloga." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Ovim obrascem pretražujete postojeće naloge." @@ -369,10 +373,10 @@ msgid "Enter login credentials" msgstr "Unesite podatke za prijavu" #: html/login.php -msgid "User name or email address" -msgstr "Korisničko ime ili adresa e-pošte" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Lozinka" @@ -433,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "Vaša lozinka je uspešno resetovana." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Potvrdite adresu e-pošte:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -451,13 +455,13 @@ msgstr "Nastavi" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Ukoliko ste zaboravili registracionu adresu e-pošte, molimo pošaljite poruku na dopisnu listu %saur-general%s." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Unesite adresu e-pošte:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -649,19 +653,19 @@ msgstr "Podnesi zahtev" msgid "Close Request" msgstr "Zatvori zahtev" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Prva" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Prethodna" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Sledeća" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Zadnja" @@ -762,10 +766,18 @@ msgstr "Počnje i završava slovom ili brojem" msgid "Can contain only one period, underscore or hyphen." msgstr "Može sadržati samo jedan razmak, podvlaku ili crticu." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Neispravna adresa e-pošte." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "Domaća stranica je neispravna, molimo navedite puni HTTP(s) URL." @@ -805,6 +817,18 @@ msgstr "Adresa %s%s%s je već u upotrebi." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Javni SSH ključ %s%s%s je već u upotrebi." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1214,6 +1238,10 @@ msgstr "Pregledaj korisnikove pakete" msgid "Edit this user's account" msgstr "Uređivanje naloga ovog korisnika" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1224,6 +1252,11 @@ msgstr "Kliknite %sovde%s ako želite trajno brisanje ovog naloga." msgid "Click %shere%s for user details." msgstr "Kliknite %sovde%s za podatke o korisniku." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "neophodno" @@ -1261,8 +1294,34 @@ msgid "Hide Email Address" msgstr "Skrij adresu e-pošte" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Ponovo unesite lozinku" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1272,6 +1331,16 @@ msgstr "Jezik" msgid "Timezone" msgstr "Vremenska zona" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Ponovo unesite lozinku" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1298,6 +1367,24 @@ msgstr "Obavesti o nadogradnjama paketa" msgid "Notify of ownership changes" msgstr "Obavesti o promenama vlasništva" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1426,7 +1513,7 @@ msgstr "Glasajte za paket" msgid "Disable notifications" msgstr "Ugasi obaveštenja" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Uključi obaveštenja" @@ -1458,6 +1545,10 @@ msgstr "URL za git kloniranje" msgid "read-only" msgstr "samo za čitanje" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1504,9 +1595,16 @@ msgstr "Uređivanje komentara za: %s" msgid "Add Comment" msgstr "Dodaj komentar" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Prikaži sve komentare" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1516,6 +1614,10 @@ msgstr "Izdvojeni komentari" msgid "Latest Comments" msgstr "Poslednji komentari" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1526,6 +1628,11 @@ msgstr "%s postavi komentar %s u" msgid "Anonymous comment on %s" msgstr "Anoniman komentar za %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1562,10 +1669,6 @@ msgstr "Izdvoji komentar" msgid "Unpin comment" msgstr "Odvoji komentar" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Svi komentari" - #: template/pkg_details.php msgid "Package Details" msgstr "Podaci o paketu" @@ -2116,6 +2219,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/tr.po b/po/tr.po index 95cb4707..04161700 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # tarakbumba , 2011,2013-2015 # tarakbumba , 2012,2014 -# Demiray “tulliana” Muhterem , 2015 +# Demiray Muhterem , 2015 # Lukas Fleischer , 2011 # Samed Beyribey , 2012 # Samed Beyribey , 2012 @@ -15,9 +15,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Turkish (http://www.transifex.com/lfleischer/aurweb/language/tr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -80,6 +80,10 @@ msgstr "Belirtilen kullanıcı verileri alınamadı." msgid "You do not have permission to edit this account." msgstr "Bu hesap üzerinde değişiklik yapma izniniz yok." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Mevcut hesaplar içinde arama yapmak için bu formu kullanın." @@ -374,10 +378,10 @@ msgid "Enter login credentials" msgstr "Giriş bilgilerinizi doldurun" #: html/login.php -msgid "User name or email address" -msgstr "Kullanıcı adı veya e-posta adresi" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Parola" @@ -438,8 +442,8 @@ msgid "Your password has been reset successfully." msgstr "Parolanız başarıyla sıfırlandı." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "E-posta adresinizi onaylayın:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -456,13 +460,13 @@ msgstr "Devam" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Kayıt olurken kullandığınız e-posta adresini hatırlamıyorsanız lütfen %saur-general%s posta listesine mesaj gönderin." +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "E-posta adresinizi girin:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -654,19 +658,19 @@ msgstr "" msgid "Close Request" msgstr "Kapama Talebi" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "İlk" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Önceki" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "İleri" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Son" @@ -767,10 +771,18 @@ msgstr "Bir rakam veya harf ile başlatıp bitirmelisiniz" msgid "Can contain only one period, underscore or hyphen." msgstr "Sadece bir nokta, alt çizgi veya tire barındırabilir." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "E-posta adresi geçerli değil." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "" @@ -810,6 +822,18 @@ msgstr "Adres, %s%s%s, zaten kullanılıyor." msgid "The SSH public key, %s%s%s, is already in use." msgstr "SSH kamu anahtarı, %s%s%s, zaten kullanımda." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1219,6 +1243,10 @@ msgstr "Bu kullanıcı tarafından hazırlanan paketleri göster" msgid "Edit this user's account" msgstr "Bu kullanıcının hesabını düzenleyin" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1229,6 +1257,11 @@ msgstr "Bu hesabı temelli olarak silmek istiyorsanız %sburaya%s tıklayın." msgid "Click %shere%s for user details." msgstr "Kullanıcı detayları için %sburaya%s tıklayın." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "gerekli" @@ -1266,8 +1299,34 @@ msgid "Hide Email Address" msgstr "E-posta Adresini Gizle" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Parolayı tekrar girin" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1277,6 +1336,16 @@ msgstr "Dil" msgid "Timezone" msgstr "" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Parolayı tekrar girin" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1303,6 +1372,24 @@ msgstr "Paket güncellemelerini bildir" msgid "Notify of ownership changes" msgstr "Sahiplik değişikliklerini bildir." +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1431,7 +1518,7 @@ msgstr "Pakete oy ver" msgid "Disable notifications" msgstr "Bildirimleri kapat" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Bildirimleri etkinleştir" @@ -1462,6 +1549,10 @@ msgstr "Git Clone URL" msgid "read-only" msgstr "salt okunur" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1508,9 +1599,16 @@ msgstr "Yorumu düzenle: %s" msgid "Add Comment" msgstr "Yorum Ekle" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Tüm yorumları görünüle" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1520,6 +1618,10 @@ msgstr "İğnelenmiş Yorumlar" msgid "Latest Comments" msgstr "Son Yorumlar" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1530,6 +1632,11 @@ msgstr "%s, %s'e yorum yaptı." msgid "Anonymous comment on %s" msgstr "%s'e isimsiz yorum" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1566,10 +1673,6 @@ msgstr "Yorumu iğnele" msgid "Unpin comment" msgstr "" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Tüm yorumlar" - #: template/pkg_details.php msgid "Package Details" msgstr "Paket Ayrıntıları" @@ -2116,6 +2219,7 @@ msgstr "" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." msgstr "" diff --git a/po/uk.po b/po/uk.po index b4ec36d0..7dd65d90 100644 --- a/po/uk.po +++ b/po/uk.po @@ -1,20 +1,21 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # Lukas Fleischer , 2011 # Rax Garfield , 2012 # Rax Garfield , 2012 +# Vladislav Glinsky , 2019 # Yarema aka Knedlyk , 2011-2018 # Данило Коростіль , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-25 20:15+0000\n" -"Last-Translator: Yarema aka Knedlyk \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Ukrainian (http://www.transifex.com/lfleischer/aurweb/language/uk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -46,7 +47,7 @@ msgstr "Щоб клонувати сховище Git з %s, виконайте % #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "Клацніть %sтут%s для повернення на сторінку інформації %s." +msgstr "Клацніть %sтут%s для повернення на сторінку подробиць %s." #: html/503.php msgid "Service Unavailable" @@ -55,7 +56,7 @@ msgstr "Сервіс недоступний" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "Не панікуйте! Сторінка закрита на технічне обслуговування. Ми швидко повернемося." +msgstr "Не панікуйте! Сторінка закрита на технічне обслуговування. Ми скоро повернемося." #: html/account.php msgid "Account" @@ -77,6 +78,10 @@ msgstr "Не вдалося отримати інформацію про вка msgid "You do not have permission to edit this account." msgstr "У вас недостатньо прав для редагування цього облікового запису." +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "Шукайте облікові записи за допомогою цієї форми." @@ -91,7 +96,7 @@ msgstr "Додати пропозицію" #: html/addvote.php msgid "Invalid token for user action." -msgstr "Невірний маркер для дій користувача." +msgstr "Невірний маркер дії користувача." #: html/addvote.php msgid "Username does not exist." @@ -185,7 +190,7 @@ msgstr "Мої пакунки" #: html/home.php msgid "Search for packages I maintain" -msgstr "Пошук пакунків які я підтримую" +msgstr "Пошук пакунків, які я підтримую" #: html/home.php msgid "Co-Maintained Packages" @@ -248,7 +253,7 @@ msgstr "Існують три типи запитів, які можна ств #: html/home.php msgid "Orphan Request" -msgstr "Запит щодо покинення пакунку" +msgstr "Запит покинути пакунок" #: html/home.php msgid "" @@ -265,7 +270,7 @@ msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " "the package maintainer and file orphan request if necessary." -msgstr "Запит щодо вилучення пакунку зі Сховища Користувацьких Пакунків AUR. Будь ласка, не використовуйте його, якщо пакунок містить ваду, яку можна легко виправити. Для цього зв’яжіться з супровідником пакунку і заповніть форму на покинення пакунку в разі необхідності." +msgstr "Запит щодо вилучення пакунку зі сховища користувацьких пакунків AUR. Будь ласка, не використовуйте його, якщо пакунок містить проблеми, які можна легко виправити. Натомість зв’яжіться з супровідником пакунку та в разі необхідності зробіть запит покинути пакунок." #: html/home.php msgid "Merge Request" @@ -371,10 +376,10 @@ msgid "Enter login credentials" msgstr "Увійдіть, ввівши облікові дані." #: html/login.php -msgid "User name or email address" -msgstr "Назва користувача або адреса електронної пошти" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "Пароль" @@ -435,8 +440,8 @@ msgid "Your password has been reset successfully." msgstr "Ваш пароль успішно скинуто." #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "Підтвердьте адресу електронної пошти:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -453,13 +458,13 @@ msgstr "Продовжити" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "Якщо Ви забули електронну адресу, використану при реєстрації, зверніться до списку розсилання %saur-general%s" +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "Введіть адресу електронної пошти:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -651,19 +656,19 @@ msgstr "Надіслати запит" msgid "Close Request" msgstr "Закриття запиту" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "Перший" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "Попередній" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "Далі" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "Останній" @@ -741,7 +746,7 @@ msgstr "Проголосували" msgid "" "Account registration has been disabled for your IP address, probably due to " "sustained spam attacks. Sorry for the inconvenience." -msgstr "Реєстрація рахунку закрита для Вашої адреси IP, можливо із-за інтенсивної спам-атаки. Вибачте за незручності." +msgstr "Реєстрація облікових записів закрита для вашої IP-адреси, можливо із-за інтенсивних спам-атак. Вибачте за незручності." #: lib/acctfuncs.inc.php msgid "Missing User ID" @@ -764,10 +769,18 @@ msgstr "Початок та кінець з літери або цифри" msgid "Can contain only one period, underscore or hyphen." msgstr "Може містити тільки один період, підкреслення або дефіс." +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "Адреса електронної пошти неправильна." +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "Неправильна домашня сторінка, вкажіть повну адресу HTTP(s)." @@ -782,7 +795,7 @@ msgstr "Неправильний публічний ключ SSH." #: lib/acctfuncs.inc.php msgid "Cannot increase account permissions." -msgstr "Неможливо збільшити дозволи рахунку." +msgstr "Неможливо збільшити дозволи облікового запису." #: lib/acctfuncs.inc.php msgid "Language is not currently supported." @@ -807,15 +820,27 @@ msgstr "Адрес %s%s%s вже використовується." msgid "The SSH public key, %s%s%s, is already in use." msgstr "Публічний ключ SSH, %s%s%s, вже використовується." +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." -msgstr "Помилка при спробі створити рахунок %s%s%s." +msgstr "Помилка при спробі створити обліковий запис %s%s%s." #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully created." -msgstr "Рахунок %s%s%s успішно створено." +msgstr "Обліковий запис %s%s%s успішно створено." #: lib/acctfuncs.inc.php msgid "A password reset key has been sent to your e-mail address." @@ -828,12 +853,12 @@ msgstr "Використовуйте обліковий запис, увійшо #: lib/acctfuncs.inc.php #, php-format msgid "No changes were made to the account, %s%s%s." -msgstr "Ніяких змін не внесено до рахунку, %s%s%s." +msgstr "Жодних змін до облікового запису %s%s%s не внесено." #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully modified." -msgstr "Рахунок %s%s%s успішно змінено." +msgstr "Обліковий запис %s%s%s успішно змінено." #: lib/acctfuncs.inc.php msgid "" @@ -843,7 +868,7 @@ msgstr "Форма логування зараз заборонена для В #: lib/acctfuncs.inc.php msgid "Account suspended" -msgstr "Рахунок вилучено" +msgstr "Обліковий запис вилучено" #: lib/acctfuncs.inc.php #, php-format @@ -851,7 +876,7 @@ msgid "" "Your password has been reset. If you just created a new account, please use " "the link from the confirmation email to set an initial password. Otherwise, " "please request a reset key on the %sPassword Reset%s page." -msgstr "Ваш пароль скинуто. Якщо Ви щойно створили новий рахунок, тоді використайте посилання електронного листа-підтвердження для встановлення початкового пароля. В протилежному випадку використайте запит щодо скидання паролю на сторінці %sPassword Reset%s." +msgstr "Ваш пароль скинуто. Якщо Ви щойно створили новий обліковий запис, використайте посилання з електронного листа-підтвердження для встановлення початкового пароля. В протилежному випадку використовайте запит щодо скидання пароля на сторінці %sPassword Reset%s." #: lib/acctfuncs.inc.php msgid "Bad username or password." @@ -872,7 +897,7 @@ msgstr "Немає" #: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php #, php-format msgid "View account information for %s" -msgstr "Показати інформацію про рахунок для %s" +msgstr "Переглянути інформацію облікового запису %s" #: lib/aurjson.class.php msgid "Package base ID or package base name missing." @@ -1113,7 +1138,7 @@ msgstr "Запит успішно закритий." #: template/account_delete.php #, php-format msgid "You can use this form to permanently delete the AUR account %s." -msgstr "Ви можете використати цю форму для вилучення рахунку в AUR %s." +msgstr "Для безповоротного видалення облікового запису %s з AUR ви можете використати цю форму." #: template/account_delete.php #, php-format @@ -1214,7 +1239,11 @@ msgstr "Переглянути пакунки цього користувача" #: template/account_details.php msgid "Edit this user's account" -msgstr "Редагування рахунку цього користувача" +msgstr "Редагувати обліковий запис цього користувача" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" #: template/account_edit_form.php #, php-format @@ -1226,6 +1255,11 @@ msgstr "Натисніть %sтут%s, якщо Ви бажаєте безпов msgid "Click %shere%s for user details." msgstr "Клацніть %sтут%s, щоб дізнатися більше про користувача." +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "обов'язково" @@ -1263,8 +1297,34 @@ msgid "Hide Email Address" msgstr "Приховати адресу електронної пошти" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "Введіть пароль ще раз" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1274,6 +1334,16 @@ msgstr "Мова" msgid "Timezone" msgstr "Часова зона" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Введіть пароль ще раз" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1300,6 +1370,24 @@ msgstr "Повідомлення про оновлення пакунків" msgid "Notify of ownership changes" msgstr "Сповіщення про зміну власника" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1381,7 +1469,7 @@ msgstr "Всі права застережено %s 2004-%d Команда Ро #: template/header.php msgid " My Account" -msgstr "Мій рахунок" +msgstr "Мій обліковий запис" #: template/pkgbase_actions.php msgid "Package Actions" @@ -1428,7 +1516,7 @@ msgstr "Голосувати за цей пакунок" msgid "Disable notifications" msgstr "Відключити сповіщення" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "Включити сповіщення" @@ -1461,6 +1549,10 @@ msgstr "Адреса URL для клонування Git" msgid "read-only" msgstr "тільки для читання" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1507,9 +1599,16 @@ msgstr "Редагувати коментар для: %s" msgid "Add Comment" msgstr "Додати коментар" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "Переглянути всі коментарі" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1519,6 +1618,10 @@ msgstr "Прикріплені коментарі" msgid "Latest Comments" msgstr "Останні коментарі" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1529,6 +1632,11 @@ msgstr "%s коментував про %s" msgid "Anonymous comment on %s" msgstr "Анонімний коментар про %s" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1565,10 +1673,6 @@ msgstr "Прикріпити коментар" msgid "Unpin comment" msgstr "Відкріпити коментар" -#: template/pkg_comments.php -msgid "All comments" -msgstr "Всі коментарі" - #: template/pkg_details.php msgid "Package Details" msgstr "Подробиці пакунку" @@ -1663,7 +1767,7 @@ msgstr "Вилучення" #: template/pkgreq_form.php msgid "Orphan" -msgstr "Позначити застарілим" +msgstr "Покинути" #: template/pkgreq_form.php template/pkg_search_results.php msgid "Merge into" @@ -1690,7 +1794,7 @@ msgid "" "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Надсилаючи запит на зречення, Ви просите Довіреного Користувача позбавити базу пакунків власника. Робіть це, якщо пакунок потребує якоїсь дії від супровідника, супровідник не робить жодних дій і Ви вже попередньо намагалися зв'язатися з ним." +msgstr "Надсилаючи запит покинути пакунок, Ви просите Довіреного Користувача позбавити базу пакунків власника. Робіть це, лише коли пакунок потребує якоїсь дії від супровідника, супровідник не робить жодних дій і ви вже попередньо намагалися зв'язатися з ним." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2033,7 +2137,7 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "Запит на скидання паролю для облікового запису {user} надісланий на пов’язану з ним адресу електронної пошти. Щоб скинути пароль, натисніть на посилання [1] нижче; щоб залишити все як є, проігноруйте це повідомлення." +msgstr "Запит скидання пароля для облікового запису {user} надіслано на пов’язану з ним адресу електронної пошти. Щоб скинути пароль, перейдіть за посиланням [1] нижче; щоб залишити все як є, проігноруйте це повідомлення." #: scripts/notify.py msgid "Welcome to the Arch User Repository" @@ -2044,7 +2148,7 @@ msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "Вітаємо в Сховищі Користувацьких Пакунків! Щоб встановити початковий пароль для Вашого рахунку натисніть на посилання [1] зверху. Якщо посилання не спрацьовує, скопіюйте його і вставте у Ваш переглядач інтернету." +msgstr "Вітаємо в AUR – сховищі користувацьких пакунків! Щоб встановити початковий пароль для вашого облікового запису натисніть на посилання [1] нижче. Якщо посилання не спрацьовує, скопіюйте його і вставте у ваш браузер." #: scripts/notify.py #, python-brace-format @@ -2123,8 +2227,9 @@ msgstr "Пакунок з AUR вилучено: {pkgbase}" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] об'єднав {old} [2] в {new} [3].\n\nЯкщо Ви не бажаєте більше отримувати повідомлень про новий пакунок, перейдіть до [3] і клацніть \"{label}\"." +msgstr "" #: scripts/notify.py #, python-brace-format diff --git a/po/zh_CN.po b/po/zh_CN.po index 15b4c74d..175ca3a2 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1,6 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: # leonfeng , 2015-2016 @@ -15,9 +15,9 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-23 12:28+0000\n" -"Last-Translator: pingplug \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-01-31 08:29+0000\n" +"Last-Translator: Lukas Fleischer\n" "Language-Team: Chinese (China) (http://www.transifex.com/lfleischer/aurweb/language/zh_CN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -80,6 +80,10 @@ msgstr "无法获取指定用户的信息。" msgid "You do not have permission to edit this account." msgstr "您没有权限编辑此帐户。" +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "使用此表单查找存在的帐户。" @@ -374,10 +378,10 @@ msgid "Enter login credentials" msgstr "输入账户信息" #: html/login.php -msgid "User name or email address" -msgstr "用户名或 E-mail" +msgid "User name or primary email address" +msgstr "" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "密码" @@ -438,8 +442,8 @@ msgid "Your password has been reset successfully." msgstr "密码已经成功重置。" #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "确认邮箱地址:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "" #: html/passreset.php msgid "Enter your new password:" @@ -456,13 +460,13 @@ msgstr "继续" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "如果您忘记了注册账号时使用的邮件地址,请向 %saur-general%s 邮件列表寻求帮助。" +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "输入您的邮箱地址:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "" #: html/pkgbase.php msgid "Package Bases" @@ -654,19 +658,19 @@ msgstr "提交请求" msgid "Close Request" msgstr "关闭请求" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "第一页" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "上一页" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "下一页" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "末页" @@ -767,10 +771,18 @@ msgstr "开头和结尾是数字/英文字母" msgid "Can contain only one period, underscore or hyphen." msgstr "最多包含一个“.”,“_”,或“-”" +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "错误的 Email 地址。" +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "首页无效,请指定完整的 HTTP(s) URL。" @@ -810,6 +822,18 @@ msgstr "该地址 %s%s%s 已被使用。" msgid "The SSH public key, %s%s%s, is already in use." msgstr "SSH 公钥 %s%s%s 已被使用。" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1219,6 +1243,10 @@ msgstr "查看这个用户提交的软件包" msgid "Edit this user's account" msgstr "编辑此用户的帐号" +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1229,6 +1257,11 @@ msgstr "如果你想要彻底删除这个帐号,请点击%s这里%s。" msgid "Click %shere%s for user details." msgstr "点击 %s这里%s 访问用户详情页面" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + #: template/account_edit_form.php msgid "required" msgstr "必填" @@ -1266,8 +1299,34 @@ msgid "Hide Email Address" msgstr "隐藏 E-mail" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "确认密码" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "" #: template/account_edit_form.php msgid "Language" @@ -1277,6 +1336,16 @@ msgstr "语言" msgid "Timezone" msgstr "时区" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "确认密码" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1303,6 +1372,24 @@ msgstr "软件包更新提醒" msgid "Notify of ownership changes" msgstr "所有权更改提醒" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1431,7 +1518,7 @@ msgstr "为这个软件包投票" msgid "Disable notifications" msgstr "禁用通知" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "启用通知" @@ -1461,6 +1548,10 @@ msgstr "Git 克隆地址" msgid "read-only" msgstr "只读" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1507,9 +1598,16 @@ msgstr "编辑 %s 的评论" msgid "Add Comment" msgstr "添加评论" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "查看所有评论" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1519,6 +1617,10 @@ msgstr "已锁定评论" msgid "Latest Comments" msgstr "最新的评论" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1529,6 +1631,11 @@ msgstr "%s 在 %s 发表了评论" msgid "Anonymous comment on %s" msgstr "在 %s 发表的匿名评论" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1565,10 +1672,6 @@ msgstr "锁定评论" msgid "Unpin comment" msgstr "解锁评论" -#: template/pkg_comments.php -msgid "All comments" -msgstr "全部评论" - #: template/pkg_details.php msgid "Package Details" msgstr "软件包详情" @@ -2111,8 +2214,9 @@ msgstr "AUR 软件包删除:{pkgbase}" msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "用户 {user} [1] 将原软件包 {old} [2] 并入新软件包 {new} [3]。\n\n若您不愿再收到关于新软件包的通知,请到此软件包页面 [3] 并点击 \"{label}\"。" +msgstr "" #: scripts/notify.py #, python-brace-format diff --git a/po/zh_TW.po b/po/zh_TW.po index 43292889..5226940a 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -1,16 +1,18 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the AUR package. +# This file is distributed under the same license as the AURWEB package. # # Translators: -# Jeff Huang , 2014-2017 +# byStarTW (pan93412) , 2018 +# 黃柏諺 , 2014-2017 +# 黃柏諺 , 2020 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" -"POT-Creation-Date: 2018-05-17 22:58+0200\n" -"PO-Revision-Date: 2018-05-18 04:38+0000\n" -"Last-Translator: Lukas Fleischer \n" +"POT-Creation-Date: 2020-01-31 09:29+0100\n" +"PO-Revision-Date: 2020-02-01 03:21+0000\n" +"Last-Translator: 黃柏諺 \n" "Language-Team: Chinese (Taiwan) (http://www.transifex.com/lfleischer/aurweb/language/zh_TW/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -73,6 +75,10 @@ msgstr "無法擷取指定使用者的資訊。" msgid "You do not have permission to edit this account." msgstr "您沒有權限編輯此帳號。" +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "無效的密碼。" + #: html/account.php msgid "Use this form to search existing accounts." msgstr "使用此表單來搜尋已有的帳號。" @@ -367,10 +373,10 @@ msgid "Enter login credentials" msgstr "輸入登入資訊" #: html/login.php -msgid "User name or email address" -msgstr "使用者名稱或電子郵件地址" +msgid "User name or primary email address" +msgstr "使用者名稱或主要電子郵件地址" -#: html/login.php template/account_edit_form.php +#: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" msgstr "密碼" @@ -431,8 +437,8 @@ msgid "Your password has been reset successfully." msgstr "您的密碼已成功重置。" #: html/passreset.php -msgid "Confirm your e-mail address:" -msgstr "確認您的電子郵件位置:" +msgid "Confirm your user name or primary e-mail address:" +msgstr "確認您的使用者名稱或主要電子郵件地址:" #: html/passreset.php msgid "Enter your new password:" @@ -449,13 +455,13 @@ msgstr "繼續" #: html/passreset.php #, php-format msgid "" -"If you have forgotten the e-mail address you used to register, please send a" -" message to the %saur-general%s mailing list." -msgstr "如果您忘了您用來註冊的電子郵件地址,請寄一封訊息到 %saur-general%s 郵件列表中。" +"If you have forgotten the user name and the primary e-mail address you used " +"to register, please send a message to the %saur-general%s mailing list." +msgstr "如果您忘記了使用者名稱與您用於註冊的主要電子郵件地址,請傳送訊息到 %saur-general%s 郵件清單。" #: html/passreset.php -msgid "Enter your e-mail address:" -msgstr "輸入您的電子郵件地址:" +msgid "Enter your user name or your primary e-mail address:" +msgstr "輸入您的使用者名稱或您的主要電子郵件地址:" #: html/pkgbase.php msgid "Package Bases" @@ -531,7 +537,7 @@ msgstr "使用這個表單以棄置一個包含以下套件的套件基礎 %s%s% msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "一核取此核取框,意味著你不想要再成為軟體包的共同維護者。" #: html/pkgdisown.php #, php-format @@ -647,19 +653,19 @@ msgstr "遞交請求" msgid "Close Request" msgstr "關閉請求" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" msgstr "第一頁" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Previous" msgstr "上一頁" -#: html/pkgreq.php lib/pkgfuncs.inc.php template/tu_list.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" msgstr "下一頁" -#: html/pkgreq.php lib/pkgfuncs.inc.php +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" msgstr "最後一頁" @@ -760,10 +766,18 @@ msgstr "以字母或數字當做開頭或結尾" msgid "Can contain only one period, underscore or hyphen." msgstr "最多包含一個「.」、「_」或「-」。" +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "請確認您的新密碼。" + #: lib/acctfuncs.inc.php msgid "The email address is invalid." msgstr "電子郵件地址無效。" +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "備份電子郵件地址無效。" + #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." msgstr "首頁無效,請指定完整的 HTTP(s) URL。" @@ -803,6 +817,18 @@ msgstr "該位址 %s%s%s 已被使用。" msgid "The SSH public key, %s%s%s, is already in use." msgstr "SSH 公開金鑰,%s%s%s,已在使用中。" +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "驗證碼遺失。" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "驗證碼已過期。請再試一次。" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "輸入的驗證碼答案無效。" + #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." @@ -1212,6 +1238,10 @@ msgstr "檢視此使用者的套件" msgid "Edit this user's account" msgstr "編輯此使用者的帳號" +#: template/account_details.php +msgid "List this user's comments" +msgstr "列出此使用者的留言" + #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." @@ -1222,6 +1252,11 @@ msgstr "如果您想要永久刪除此帳號,請點擊 %s這裡%s 。" msgid "Click %shere%s for user details." msgstr "點選 %s此處%s 來取得使用者的詳細資訊。" +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "點擊%s此處%s以列出此帳號的留言。" + #: template/account_edit_form.php msgid "required" msgstr "必填" @@ -1259,8 +1294,34 @@ msgid "Hide Email Address" msgstr "隱藏電子郵件地址" #: template/account_edit_form.php -msgid "Re-type password" -msgstr "重新輸入密碼" +msgid "" +"If you do not hide your email address, it is visible to all registered AUR " +"users. If you hide your email address, it is visible to members of the Arch " +"Linux staff only." +msgstr "如果您不隱藏您的電子郵件地址,它就是對所有已註冊的 AUR 使用者可見。如果您隱藏您的電子郵件地址,它就只對 Arch Linux 工作人員可見。" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "備援電子郵件地址" + +#: template/account_edit_form.php +msgid "" +"Optionally provide a secondary email address that can be used to restore " +"your account in case you lose access to your primary email address." +msgstr "選擇性提供次要的電子郵件地址,讓您在您失去對主要電子郵件地址的存取權時,仍可復原您的帳號。" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +msgstr "密碼重設連結一直都會同時寄往您的主要與備援電子郵件地址。" + +#: template/account_edit_form.php +#, php-format +msgid "" +"Your backup email address is always only visible to members of the Arch " +"Linux staff, independent of the %s setting." +msgstr "您的備援電子郵件地址只對 Arch Linux 工作人員可見,與 %s 設定無關。" #: template/account_edit_form.php msgid "Language" @@ -1270,6 +1331,16 @@ msgstr "語言" msgid "Timezone" msgstr "時區" +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "如果您想要變更密碼,輸入新密碼並再輸入一次以確認新密碼。" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "重新輸入密碼" + #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" @@ -1296,6 +1367,24 @@ msgstr "通知套件更新" msgid "Notify of ownership changes" msgstr "擁有者變更通知" +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "要確認個人檔案變更,請輸入您目前的密碼:" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "您目前的密碼" + +#: template/account_edit_form.php +msgid "" +"To protect the AUR against automated account creation, we kindly ask you to " +"provide the output of the following command:" +msgstr "為了保護 AUR 不受自動建立帳號的侵擾,我們請您提供以下指令的輸出:" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "回答" + #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" @@ -1424,7 +1513,7 @@ msgstr "為此套件投票" msgid "Disable notifications" msgstr "停用通知" -#: template/pkgbase_actions.php +#: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" msgstr "啟用通知" @@ -1454,6 +1543,10 @@ msgstr "Git Clone URL" msgid "read-only" msgstr "唯讀" +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "點擊以複製" + #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" @@ -1500,9 +1593,16 @@ msgstr "編輯評論:%s" msgid "Add Comment" msgstr "新增評論" -#: template/pkg_comments.php -msgid "View all comments" -msgstr "檢視所有評論" +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "引用 AUR 軟體庫的 Git 遞交識別符,URL 會自動轉換為連結。" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "部份支援 %sMarkdown 語法%s" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1512,6 +1612,10 @@ msgstr "已釘選評論" msgid "Latest Comments" msgstr "最新的評論" +#: template/pkg_comments.php +msgid "Comments for" +msgstr "評論" + #: template/pkg_comments.php #, php-format msgid "%s commented on %s" @@ -1522,6 +1626,11 @@ msgstr "%s 在 %s 上評論" msgid "Anonymous comment on %s" msgstr "在 %s 上的匿名評論" +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "在軟體包 %s 的 %s 評論" + #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" @@ -1558,10 +1667,6 @@ msgstr "釘選評論" msgid "Unpin comment" msgstr "解除釘選評論" -#: template/pkg_comments.php -msgid "All comments" -msgstr "所有評論" - #: template/pkg_details.php msgid "Package Details" msgstr "套件詳細資訊" @@ -2006,7 +2111,7 @@ msgstr "返回" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "" +msgstr "AUR 密碼重設" #: scripts/notify.py #, python-brace-format @@ -2014,98 +2119,99 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "" +msgstr "有一個關於您帳號 {user} 對應到電子信箱的密碼重設請求。若您想要重設您的密碼,請前往以下的網址 [1],否則忽略這訊息就沒事了。" #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "歡迎來到 Arch 使用者軟體庫" #: scripts/notify.py msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "" +msgstr "歡迎來到 Arch 使用者軟體庫!為了要設定您新帳號的初始密碼,請點擊以下的連結 [1]。若連結沒辦法點擊,請嘗試複製並貼上連結到您的瀏覽器中。 " #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "{pkgbase} 的 AUR 評論" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "{user} [1] 加入以下留言到 {pkgbase} [2]:" #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "若您不再想收到此軟體包的通知,請前往軟體包頁面 [2] 並選擇「{label}」。" #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "AUR 軟體包更新:{pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} [1] 推送了新的提交到 {pkgbase} [2]。" #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} 的 AUR 過期通知" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "您的軟體包 {pkgbase} [1] 已經被 {user} [2] 使用者標記為過期:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} 的 AUR 所有權通知" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "軟體包 {pkgbase} [1] 已經被 {user} [2] 使用者採用。" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "軟體包 {pkgbase} [1] 已經被 {user} [2] 使用者拋棄。" #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} 的 AUR 共同維護者通知" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "您已經加入到 {pkgbase} [1] 的共同維護者列表中。" #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "您已經從 {pkgbase} [1] 的共同維護者列表中移除了。" #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "已刪除 AUR 軟體包:{pkgbase}" #: scripts/notify.py #, python-brace-format msgid "" "{user} [1] merged {old} [2] into {new} [3].\n" "\n" +"-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] 已合併 {old} [2] 到 {new} [3].\n\n-- \n如果您不想要再收到關於新軟體包的通知,請到 [3] 然後點擊「{label}」。" #: scripts/notify.py #, python-brace-format @@ -2113,16 +2219,16 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] 已經刪除 {pkgbase} [2]。\n\n您將再也不會收到此軟體包的通知。" #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "TU 投票提醒:編號為 {id} 的建議" #: scripts/notify.py #, python-brace-format msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." -msgstr "" +msgstr "請記得在第 {id} [1] 號建議投下你的投票,投票期限即將在 48 個小時內結束。" From 3f2654e79e70caa828f3464386b0a3367cbac758 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Mon, 10 Feb 2020 15:25:03 +0100 Subject: [PATCH 0065/1451] Update README and convert to Markdown syntax Signed-off-by: Lukas Fleischer --- README => README.md | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) rename README => README.md (73%) diff --git a/README b/README.md similarity index 73% rename from README rename to README.md index e633ec3f..a4ab584e 100644 --- a/README +++ b/README.md @@ -17,32 +17,14 @@ The aurweb project includes Directory Layout ---------------- -aurweb:: - aurweb Python modules. - -conf:: - Configuration and configuration templates. - -doc:: - Project documentation. - -po:: - Translation files for strings in the aurweb interface. - -schema:: - Schema for the SQL database. Script for dummy data generation. - -scripts:: - Scripts for AUR maintenance. - -test:: - Test suite and test cases. - -upgrading:: - Instructions for upgrading setups from one release to another. - -web:: - Web interface for the AUR. +* `aurweb`: aurweb Python modules, Git interface and maintenance scripts +* `conf`: configuration and configuration templates +* `doc`: project documentation +* `po`: translation files for strings in the aurweb interface +* `schema`: schema for the SQL database +* `test`: test suite and test cases +* `upgrading`: instructions for upgrading setups from one release to another +* `web`: web interface for the AUR Links ----- From de549fb2d5063394f91e06f366bc5d426f5f0891 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 7 Feb 2020 13:22:29 +0100 Subject: [PATCH 0066/1451] Support smtplib for sending emails Support mail delivery without a local MTA. Instead, an SMTP server can now be configured using the smtp-server option in the [notifications] section. In order to use this option, the value of the sendmail option must be empty. Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 22 ++++++++++++++++++---- conf/config.defaults | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index b0f218b5..6c5c709e 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import email.mime.text +import email.utils +import smtplib import subprocess import sys import textwrap @@ -63,7 +65,6 @@ class Notification: return body.rstrip() def send(self): - sendmail = aurweb.config.get('notifications', 'sendmail') sender = aurweb.config.get('notifications', 'sender') reply_to = aurweb.config.get('notifications', 'reply-to') reason = self.__class__.__name__ @@ -79,13 +80,26 @@ class Notification: msg['Reply-to'] = reply_to msg['To'] = to msg['X-AUR-Reason'] = reason + msg['Date'] = email.utils.formatdate(localtime=True) for key, value in self.get_headers().items(): msg[key] = value - p = subprocess.Popen([sendmail, '-t', '-oi'], - stdin=subprocess.PIPE) - p.communicate(msg.as_bytes()) + sendmail = aurweb.config.get('notifications', 'sendmail') + if sendmail: + # send email using the sendmail binary specified in the + # configuration file + p = subprocess.Popen([sendmail, '-t', '-oi'], + stdin=subprocess.PIPE) + p.communicate(msg.as_bytes()) + else: + # send email using smtplib; no local MTA required + server_addr = aurweb.config.get('notifications', 'smtp-server') + + server = smtplib.SMTP(server_addr) + server.set_debuglevel(0) + server.sendmail(sender, recipient, msg.as_bytes()) + server.quit() class ResetKeyNotification(Notification): diff --git a/conf/config.defaults b/conf/config.defaults index c519eae6..23d46b06 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -47,7 +47,8 @@ window_length = 86400 [notifications] notify-cmd = /usr/local/bin/aurweb-notify -sendmail = /usr/bin/sendmail +sendmail = +smtp-server = localhost sender = notify@aur.archlinux.org reply-to = noreply@aur.archlinux.org From b855ce9452ed7b83d0f6f538a17755918cc4132f Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 7 Feb 2020 13:44:29 +0100 Subject: [PATCH 0067/1451] Make SMTP port and authentication configurable Add more options to configure the smtplib implementation for sending notification emails. The port can be changed using the new smtp-port option. Encryption can be configured using smtp-use-ssl and smtp-use-starttls. Keep in mind that you usually also need to change the port when enabling either of these options. Authentication can be configured using smtp-user and smtp-password. Authentication is disabled if either of these values is empty. Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 20 +++++++++++++++++++- conf/config.defaults | 5 +++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 6c5c709e..5b18a476 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -65,6 +65,7 @@ class Notification: return body.rstrip() def send(self): + sendmail = aurweb.config.get('notifications', 'sendmail') sender = aurweb.config.get('notifications', 'sender') reply_to = aurweb.config.get('notifications', 'reply-to') reason = self.__class__.__name__ @@ -95,8 +96,25 @@ class Notification: else: # send email using smtplib; no local MTA required server_addr = aurweb.config.get('notifications', 'smtp-server') + server_port = aurweb.config.getint('notifications', 'smtp-port') + use_ssl = aurweb.config.getboolean('notifications', 'smtp-use-ssl') + use_starttls = aurweb.config.getboolean('notifications', 'smtp-use-starttls') + user = aurweb.config.get('notifications', 'smtp-user') + passwd = aurweb.config.get('notifications', 'smtp-password') + + if use_ssl: + server = smtplib.SMTP_SSL(server_addr, server_port) + else: + server = smtplib.SMTP(server_addr, server_port) + + if use_starttls: + server.ehlo() + server.starttls() + server.ehlo() + + if user and passwd: + server.login(user, passwd) - server = smtplib.SMTP(server_addr) server.set_debuglevel(0) server.sendmail(sender, recipient, msg.as_bytes()) server.quit() diff --git a/conf/config.defaults b/conf/config.defaults index 23d46b06..b69d0312 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -49,6 +49,11 @@ window_length = 86400 notify-cmd = /usr/local/bin/aurweb-notify sendmail = smtp-server = localhost +smtp-port = 25 +smtp-use-ssl = 0 +smtp-use-starttls = 0 +smtp-user = +smtp-password = sender = notify@aur.archlinux.org reply-to = noreply@aur.archlinux.org From 65c98d12161906ab680fc2c9572f7c78b16efd82 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Tue, 11 Feb 2020 13:21:26 +0100 Subject: [PATCH 0068/1451] Use relative URIs for {source_file,log,commit}_uri Signed-off-by: Lukas Fleischer --- conf/config.defaults | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/config.defaults b/conf/config.defaults index b69d0312..447dacac 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -28,9 +28,9 @@ request_idle_time = 1209600 request_archive_time = 15552000 auto_orphan_age = 15552000 auto_delete_age = 86400 -source_file_uri = https://aur.archlinux.org/cgit/aur.git/tree/%s?h=%s -log_uri = https://aur.archlinux.org/cgit/aur.git/log/?h=%s -commit_uri = https://aur.archlinux.org/cgit/aur.git/commit/?h=%s&id=%s +source_file_uri = /cgit/aur.git/tree/%s?h=%s +log_uri = /cgit/aur.git/log/?h=%s +commit_uri = /cgit/aur.git/commit/?h=%s&id=%s snapshot_uri = /cgit/aur.git/snapshot/%s.tar.gz enable-maintenance = 1 maintenance-exceptions = 127.0.0.1 From 5ca1e271f9023b41b613313745bc700dc15d802f Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Wed, 12 Feb 2020 15:16:37 -0500 Subject: [PATCH 0069/1451] Fix PHP 7.4 warnings If a db query returned NULL instead of an array, then accessing $row[0] now throws a warning. The undocumented behavior of evaluating to NULL is maintained, and we want to return NULL anyway, so add a check for the value and fall back on the default function return type. Signed-off-by: Eli Schwartz Signed-off-by: Lukas Fleischer --- web/lib/aur.inc.php | 28 +++++++++++++++++++++------- web/lib/pkgfuncs.inc.php | 4 +++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/web/lib/aur.inc.php b/web/lib/aur.inc.php index e9530fc0..dbcc23a4 100644 --- a/web/lib/aur.inc.php +++ b/web/lib/aur.inc.php @@ -197,7 +197,9 @@ function username_from_id($id) { } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** @@ -222,7 +224,9 @@ function username_from_sid($sid="") { } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** @@ -339,7 +343,9 @@ function email_from_sid($sid="") { } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** @@ -365,7 +371,9 @@ function account_from_sid($sid="") { } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** @@ -390,7 +398,9 @@ function uid_from_sid($sid="") { } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** @@ -512,7 +522,9 @@ function uid_from_username($username) { } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** @@ -546,7 +558,9 @@ function uid_from_email($email) { } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index a4cd17ac..8c915711 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -147,7 +147,9 @@ function pkg_from_name($name="") { return; } $row = $result->fetch(PDO::FETCH_NUM); - return $row[0]; + if ($row) { + return $row[0]; + } } /** From 050b08081a48887bcf03e9d943a1370aac949518 Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Wed, 12 Feb 2020 15:16:38 -0500 Subject: [PATCH 0070/1451] Fix more PHP 7.4 warnings The try_login() function documents it returns an array containing an 'error' key, and our only caller *only* consults the 'error' key. Then the function returns null instead of an array, if the login succeeded! I question why we bother returning the new SID if we never use it, surely we could either return the error or return default null. But, for now, I'm just going to fix it to return what it's actually supposed to, without changing the API. Signed-off-by: Eli Schwartz Signed-off-by: Lukas Fleischer --- web/lib/acctfuncs.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index 443fb4b1..d238c0e0 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -659,6 +659,7 @@ function try_login() { } header("Location: " . get_uri($referer)); $login_error = ""; + return array('SID' => $new_sid, 'error' => null); } /** From 33d8fe035eab204f09363ace55a002c739050fa0 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Fri, 21 Feb 2020 09:25:12 +0200 Subject: [PATCH 0071/1451] README.md: fix a small typo Signed-off-by: Lukas Fleischer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4ab584e..b2095b3f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ and installed using the Arch Linux package manager `pacman`. The aurweb project includes * A web interface to search for packaging scripts and display package details. -* A SSH/Git interface to submit and update packages and package meta data. +* An SSH/Git interface to submit and update packages and package meta data. * Community features such as comments, votes, package flagging and requests. * Editing/deletion of packages and accounts by Trusted Users and Developers. * Area for Trusted Users to post AUR-related proposals and vote on them. From afe3f5d0e562104fb930b650ca799ba433a68c2e Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 21 Feb 2020 10:41:29 +0100 Subject: [PATCH 0072/1451] README.md: add references to Transifex Signed-off-by: Lukas Fleischer --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index b2095b3f..f7285a51 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,11 @@ Links * Questions, comments, and patches related to aurweb can be sent to the AUR development mailing list: aur-dev@archlinux.org -- mailing list archives: https://mailman.archlinux.org/mailman/listinfo/aur-dev + +Translations +------------ + +Translations are welcome via our Transifex project at +https://www.transifex.com/lfleischer/aurweb; see `doc/i18n.txt` for details. + +![Transifex](http://www.transifex.net/projects/p/aurweb/chart/image_png) From cbab9870c1388f2d60a8d6cfbb936c384c1b58b6 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Sat, 22 Feb 2020 12:06:17 +0100 Subject: [PATCH 0073/1451] Fix HTML code in the account search results table Do not add an opening

    tag for every row. Instead, wrap all rows in . While at it, also simplify the code used to color the rows. Signed-off-by: Lukas Fleischer --- web/template/account_search_results.php | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/web/template/account_search_results.php b/web/template/account_search_results.php index 81cd8185..0f7eb7a4 100644 --- a/web/template/account_search_results.php +++ b/web/template/account_search_results.php @@ -16,17 +16,9 @@ else: - $row): - if ($i % 2): - $c = "even"; - else: - $c = "odd"; - endif; - ?> - - + + $row): ?> + - + +
    "> @@ -49,10 +41,8 @@ else:
    From 4b2102ceb26b77bc8ee3e9b9d8929a915f1e65a9 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 27 Feb 2020 16:44:04 +0100 Subject: [PATCH 0074/1451] Properly escape passwords in the account edit form Addresses FS#65639. Signed-off-by: Lukas Fleischer --- web/template/account_edit_form.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php index a4ea9949..4ce6b875 100644 --- a/web/template/account_edit_form.php +++ b/web/template/account_edit_form.php @@ -157,12 +157,12 @@

    - +

    - +

    From 7188743fc3b1a9c1f5f65e323a6502d018bd95d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 16 Feb 2020 21:56:10 +0100 Subject: [PATCH 0075/1451] Migrate the database schema to SQLAlchemy The new schema was generated with sqlacodegen and then manually adjusted to fit schema/aur-schema.sql faithfully, both in the organisation of the code and in the SQL generated by SQLAlchemy. Initializing the database now requires the new tool aurweb.initdb. References to aur-schema.sql have been updated and the old schema dropped. Signed-off-by: Lukas Fleischer --- INSTALL | 12 +- TESTING | 23 +-- aurweb/db.py | 27 +++ aurweb/initdb.py | 47 +++++ aurweb/schema.py | 387 ++++++++++++++++++++++++++++++++++++++ schema/Makefile | 12 -- schema/aur-schema.sql | 415 ----------------------------------------- schema/reloadtestdb.sh | 29 --- test/Makefile | 6 +- test/setup.sh | 5 +- 10 files changed, 481 insertions(+), 482 deletions(-) create mode 100644 aurweb/initdb.py create mode 100644 aurweb/schema.py delete mode 100644 schema/Makefile delete mode 100644 schema/aur-schema.sql delete mode 100755 schema/reloadtestdb.sh diff --git a/INSTALL b/INSTALL index 7170aea1..68fe5dcd 100644 --- a/INSTALL +++ b/INSTALL @@ -45,16 +45,16 @@ read the instructions below. if the defaults file does not exist) and adjust the configuration (pay attention to disable_http_login, enable_maintenance and aur_location). -4) Create a new MySQL database and a user and import the aurweb SQL schema: +4) Install Python modules and dependencies: - $ mysql -uaur -p AUR = 1)) + aurweb.schema.metadata.create_all(engine) + feed_initial_data(engine.connect()) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + prog='python -m aurweb.initdb', + description='Initialize the aurweb database.') + parser.add_argument('-v', '--verbose', action='count', default=0, + help='increase verbosity') + args = parser.parse_args() + run(args) diff --git a/aurweb/schema.py b/aurweb/schema.py new file mode 100644 index 00000000..b1261e86 --- /dev/null +++ b/aurweb/schema.py @@ -0,0 +1,387 @@ +from sqlalchemy import CHAR, Column, ForeignKey, Index, MetaData, String, TIMESTAMP, Table, Text, text +from sqlalchemy.dialects.mysql import BIGINT, DECIMAL, INTEGER, TINYINT +from sqlalchemy.ext.compiler import compiles + + +@compiles(TINYINT, 'sqlite') +def compile_tinyint_sqlite(type_, compiler, **kw): + """TINYINT is not supported on SQLite. Substitute it with INTEGER.""" + return 'INTEGER' + + +metadata = MetaData() + +# Define the Account Types for the AUR. +AccountTypes = Table( + 'AccountTypes', metadata, + Column('ID', TINYINT(unsigned=True), primary_key=True), + Column('AccountType', String(32), nullable=False, server_default=text("''")), + mysql_engine='InnoDB', +) + + +# User information for each user regardless of type. +Users = Table( + 'Users', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('AccountTypeID', ForeignKey('AccountTypes.ID', ondelete="NO ACTION"), nullable=False, server_default=text("1")), + Column('Suspended', TINYINT(unsigned=True), nullable=False, server_default=text("0")), + Column('Username', String(32), nullable=False, unique=True), + Column('Email', String(254), nullable=False, unique=True), + Column('BackupEmail', String(254)), + Column('HideEmail', TINYINT(unsigned=True), nullable=False, server_default=text("0")), + Column('Passwd', String(255), nullable=False), + Column('Salt', CHAR(32), nullable=False, server_default=text("''")), + Column('ResetKey', CHAR(32), nullable=False, server_default=text("''")), + Column('RealName', String(64), nullable=False, server_default=text("''")), + Column('LangPreference', String(6), nullable=False, server_default=text("'en'")), + Column('Timezone', String(32), nullable=False, server_default=text("'UTC'")), + Column('Homepage', Text), + Column('IRCNick', String(32), nullable=False, server_default=text("''")), + Column('PGPKey', String(40)), + Column('LastLogin', BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Column('LastLoginIPAddress', String(45)), + Column('LastSSHLogin', BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Column('LastSSHLoginIPAddress', String(45)), + Column('InactivityTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Column('RegistrationTS', TIMESTAMP, nullable=False, server_default=text("CURRENT_TIMESTAMP")), + Column('CommentNotify', TINYINT(1), nullable=False, server_default=text("1")), + Column('UpdateNotify', TINYINT(1), nullable=False, server_default=text("0")), + Column('OwnershipNotify', TINYINT(1), nullable=False, server_default=text("1")), + Index('UsersAccountTypeID', 'AccountTypeID'), + mysql_engine='InnoDB', +) + + +# SSH public keys used for the aurweb SSH/Git interface. +SSHPubKeys = Table( + 'SSHPubKeys', metadata, + Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + Column('Fingerprint', String(44), primary_key=True), + Column('PubKey', String(4096), nullable=False), + mysql_engine='InnoDB', +) + + +# Track Users logging in/out of AUR web site. +Sessions = Table( + 'Sessions', metadata, + Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + Column('SessionID', CHAR(32), nullable=False, unique=True), + Column('LastUpdateTS', BIGINT(unsigned=True), nullable=False), + mysql_engine='InnoDB', +) + + +# Information on package bases +PackageBases = Table( + 'PackageBases', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('Name', String(255), nullable=False, unique=True), + Column('NumVotes', INTEGER(unsigned=True), nullable=False, server_default=text("0")), + Column('Popularity', DECIMAL(10, 6, unsigned=True), nullable=False, server_default=text("0")), + Column('OutOfDateTS', BIGINT(unsigned=True)), + Column('FlaggerComment', Text, nullable=False), + Column('SubmittedTS', BIGINT(unsigned=True), nullable=False), + Column('ModifiedTS', BIGINT(unsigned=True), nullable=False), + Column('FlaggerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # who flagged the package out-of-date? + # deleting a user will cause packages to be orphaned, not deleted + Column('SubmitterUID', ForeignKey('Users.ID', ondelete='SET NULL')), # who submitted it? + Column('MaintainerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # User + Column('PackagerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # Last packager + Index('BasesMaintainerUID', 'MaintainerUID'), + Index('BasesNumVotes', 'NumVotes'), + Index('BasesPackagerUID', 'PackagerUID'), + Index('BasesSubmitterUID', 'SubmitterUID'), + mysql_engine='InnoDB', +) + + +# Keywords of package bases +PackageKeywords = Table( + 'PackageKeywords', metadata, + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + Column('Keyword', String(255), primary_key=True, nullable=False, server_default=text("''")), + mysql_engine='InnoDB', +) + + +# Information about the actual packages +Packages = Table( + 'Packages', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), + Column('Name', String(255), nullable=False, unique=True), + Column('Version', String(255), nullable=False, server_default=text("''")), + Column('Description', String(255)), + Column('URL', String(8000)), + mysql_engine='InnoDB', +) + + +# Information about licenses +Licenses = Table( + 'Licenses', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('Name', String(255), nullable=False, unique=True), + mysql_engine='InnoDB', +) + + +# Information about package-license-relations +PackageLicenses = Table( + 'PackageLicenses', metadata, + Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + Column('LicenseID', ForeignKey('Licenses.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + mysql_engine='InnoDB', +) + + +# Information about groups +Groups = Table( + 'Groups', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('Name', String(255), nullable=False, unique=True), + mysql_engine='InnoDB', +) + + +# Information about package-group-relations +PackageGroups = Table( + 'PackageGroups', metadata, + Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + Column('GroupID', ForeignKey('Groups.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + mysql_engine='InnoDB', +) + + +# Define the package dependency types +DependencyTypes = Table( + 'DependencyTypes', metadata, + Column('ID', TINYINT(unsigned=True), primary_key=True), + Column('Name', String(32), nullable=False, server_default=text("''")), + mysql_engine='InnoDB', +) + + +# Track which dependencies a package has +PackageDepends = Table( + 'PackageDepends', metadata, + Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), + Column('DepTypeID', ForeignKey('DependencyTypes.ID', ondelete="NO ACTION"), nullable=False), + Column('DepName', String(255), nullable=False), + Column('DepDesc', String(255)), + Column('DepCondition', String(255)), + Column('DepArch', String(255)), + Index('DependsDepName', 'DepName'), + Index('DependsPackageID', 'PackageID'), + mysql_engine='InnoDB', +) + + +# Define the package relation types +RelationTypes = Table( + 'RelationTypes', metadata, + Column('ID', TINYINT(unsigned=True), primary_key=True), + Column('Name', String(32), nullable=False, server_default=text("''")), + mysql_engine='InnoDB', +) + + +# Track which conflicts, provides and replaces a package has +PackageRelations = Table( + 'PackageRelations', metadata, + Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), + Column('RelTypeID', ForeignKey('RelationTypes.ID', ondelete="NO ACTION"), nullable=False), + Column('RelName', String(255), nullable=False), + Column('RelCondition', String(255)), + Column('RelArch', String(255)), + Index('RelationsPackageID', 'PackageID'), + Index('RelationsRelName', 'RelName'), + mysql_engine='InnoDB', +) + + +# Track which sources a package has +PackageSources = Table( + 'PackageSources', metadata, + Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), + Column('Source', String(8000), nullable=False, server_default=text("'/dev/null'")), + Column('SourceArch', String(255)), + Index('SourcesPackageID', 'PackageID'), + mysql_engine='InnoDB', +) + + +# Track votes for packages +PackageVotes = Table( + 'PackageVotes', metadata, + Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), + Column('VoteTS', BIGINT(unsigned=True)), + Index('VoteUsersIDPackageID', 'UsersID', 'PackageBaseID', unique=True), + Index('VotesPackageBaseID', 'PackageBaseID'), + Index('VotesUsersID', 'UsersID'), + mysql_engine='InnoDB', +) + + +# Record comments for packages +PackageComments = Table( + 'PackageComments', metadata, + Column('ID', BIGINT(unsigned=True), primary_key=True), + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), + Column('UsersID', ForeignKey('Users.ID', ondelete='SET NULL')), + Column('Comments', Text, nullable=False), + Column('RenderedComment', Text, nullable=False), + Column('CommentTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Column('EditedTS', BIGINT(unsigned=True)), + Column('EditedUsersID', ForeignKey('Users.ID', ondelete='SET NULL')), + Column('DelTS', BIGINT(unsigned=True)), + Column('DelUsersID', ForeignKey('Users.ID', ondelete='CASCADE')), + Column('PinnedTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Index('CommentsPackageBaseID', 'PackageBaseID'), + Index('CommentsUsersID', 'UsersID'), + mysql_engine='InnoDB', +) + + +# Package base co-maintainers +PackageComaintainers = Table( + 'PackageComaintainers', metadata, + Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), + Column('Priority', INTEGER(unsigned=True), nullable=False), + Index('ComaintainersPackageBaseID', 'PackageBaseID'), + Index('ComaintainersUsersID', 'UsersID'), + mysql_engine='InnoDB', +) + + +# Package base notifications +PackageNotifications = Table( + 'PackageNotifications', metadata, + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), + Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + Index('NotifyUserIDPkgID', 'UserID', 'PackageBaseID', unique=True), + mysql_engine='InnoDB', +) + + +# Package name blacklist +PackageBlacklist = Table( + 'PackageBlacklist', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('Name', String(64), nullable=False, unique=True), + mysql_engine='InnoDB', +) + + +# Providers in the official repositories +OfficialProviders = Table( + 'OfficialProviders', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('Name', String(64), nullable=False), + Column('Repo', String(64), nullable=False), + Column('Provides', String(64), nullable=False), + Index('ProviderNameProvides', 'Name', 'Provides', unique=True), + mysql_engine='InnoDB', +) + + +# Define package request types +RequestTypes = Table( + 'RequestTypes', metadata, + Column('ID', TINYINT(unsigned=True), primary_key=True), + Column('Name', String(32), nullable=False, server_default=text("''")), + mysql_engine='InnoDB', +) + + +# Package requests +PackageRequests = Table( + 'PackageRequests', metadata, + Column('ID', BIGINT(unsigned=True), primary_key=True), + Column('ReqTypeID', ForeignKey('RequestTypes.ID', ondelete="NO ACTION"), nullable=False), + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='SET NULL')), + Column('PackageBaseName', String(255), nullable=False), + Column('MergeBaseName', String(255)), + Column('UsersID', ForeignKey('Users.ID', ondelete='SET NULL')), + Column('Comments', Text, nullable=False), + Column('ClosureComment', Text, nullable=False), + Column('RequestTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Column('ClosedTS', BIGINT(unsigned=True)), + Column('ClosedUID', ForeignKey('Users.ID', ondelete='SET NULL')), + Column('Status', TINYINT(unsigned=True), nullable=False, server_default=text("0")), + Index('RequestsPackageBaseID', 'PackageBaseID'), + Index('RequestsUsersID', 'UsersID'), + mysql_engine='InnoDB', +) + + +# Vote information +TU_VoteInfo = Table( + 'TU_VoteInfo', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('Agenda', Text, nullable=False), + Column('User', String(32), nullable=False), + Column('Submitted', BIGINT(unsigned=True), nullable=False), + Column('End', BIGINT(unsigned=True), nullable=False), + Column('Quorum', DECIMAL(2, 2, unsigned=True), nullable=False), + Column('SubmitterID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + Column('Yes', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), + Column('No', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), + Column('Abstain', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), + Column('ActiveTUs', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), + mysql_engine='InnoDB', +) + + +# Individual vote records +TU_Votes = Table( + 'TU_Votes', metadata, + Column('VoteID', ForeignKey('TU_VoteInfo.ID', ondelete='CASCADE'), nullable=False), + Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + mysql_engine='InnoDB', +) + + +# Malicious user banning +Bans = Table( + 'Bans', metadata, + Column('IPAddress', String(45), primary_key=True), + Column('BanTS', TIMESTAMP, nullable=False), + mysql_engine='InnoDB', +) + + +# Terms and Conditions +Terms = Table( + 'Terms', metadata, + Column('ID', INTEGER(unsigned=True), primary_key=True), + Column('Description', String(255), nullable=False), + Column('URL', String(8000), nullable=False), + Column('Revision', INTEGER(unsigned=True), nullable=False, server_default=text("1")), + mysql_engine='InnoDB', +) + + +# Terms and Conditions accepted by users +AcceptedTerms = Table( + 'AcceptedTerms', metadata, + Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), + Column('TermsID', ForeignKey('Terms.ID', ondelete='CASCADE'), nullable=False), + Column('Revision', INTEGER(unsigned=True), nullable=False, server_default=text("0")), + mysql_engine='InnoDB', +) + + +# Rate limits for API +ApiRateLimit = Table( + 'ApiRateLimit', metadata, + Column('IP', String(45), primary_key=True), + Column('Requests', INTEGER(11), nullable=False), + Column('WindowStart', BIGINT(20), nullable=False), + Index('ApiRateLimitWindowStart', 'WindowStart'), + mysql_engine='InnoDB', +) diff --git a/schema/Makefile b/schema/Makefile deleted file mode 100644 index 62d08567..00000000 --- a/schema/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -aur-schema-sqlite.sql: aur-schema.sql - sed \ - -e 's/ ENGINE = InnoDB//' \ - -e 's/ [A-Z]* UNSIGNED NOT NULL AUTO_INCREMENT/ INTEGER NOT NULL/' \ - -e 's/([0-9, ]*) UNSIGNED / UNSIGNED /' \ - -e 's/ MySQL / SQLite /' \ - $< >$@ - -clean: - rm -rf aur-schema-sqlite.sql - -.PHONY: clean diff --git a/schema/aur-schema.sql b/schema/aur-schema.sql deleted file mode 100644 index 1f86df20..00000000 --- a/schema/aur-schema.sql +++ /dev/null @@ -1,415 +0,0 @@ --- The MySQL database layout for the AUR. Certain data --- is also included such as AccountTypes, etc. --- - --- Define the Account Types for the AUR. --- -CREATE TABLE AccountTypes ( - ID TINYINT UNSIGNED NOT NULL AUTO_INCREMENT, - AccountType VARCHAR(32) NOT NULL DEFAULT '', - PRIMARY KEY (ID) -) ENGINE = InnoDB; -INSERT INTO AccountTypes (ID, AccountType) VALUES (1, 'User'); -INSERT INTO AccountTypes (ID, AccountType) VALUES (2, 'Trusted User'); -INSERT INTO AccountTypes (ID, AccountType) VALUES (3, 'Developer'); -INSERT INTO AccountTypes (ID, AccountType) VALUES (4, 'Trusted User & Developer'); - - --- User information for each user regardless of type. --- -CREATE TABLE Users ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - AccountTypeID TINYINT UNSIGNED NOT NULL DEFAULT 1, - Suspended TINYINT UNSIGNED NOT NULL DEFAULT 0, - Username VARCHAR(32) NOT NULL, - Email VARCHAR(254) NOT NULL, - BackupEmail VARCHAR(254) NULL DEFAULT NULL, - HideEmail TINYINT UNSIGNED NOT NULL DEFAULT 0, - Passwd VARCHAR(255) NOT NULL, - Salt CHAR(32) NOT NULL DEFAULT '', - ResetKey CHAR(32) NOT NULL DEFAULT '', - RealName VARCHAR(64) NOT NULL DEFAULT '', - LangPreference VARCHAR(6) NOT NULL DEFAULT 'en', - Timezone VARCHAR(32) NOT NULL DEFAULT 'UTC', - Homepage TEXT NULL DEFAULT NULL, - IRCNick VARCHAR(32) NOT NULL DEFAULT '', - PGPKey VARCHAR(40) NULL DEFAULT NULL, - LastLogin BIGINT UNSIGNED NOT NULL DEFAULT 0, - LastLoginIPAddress VARCHAR(45) NULL DEFAULT NULL, - LastSSHLogin BIGINT UNSIGNED NOT NULL DEFAULT 0, - LastSSHLoginIPAddress VARCHAR(45) NULL DEFAULT NULL, - InactivityTS BIGINT UNSIGNED NOT NULL DEFAULT 0, - RegistrationTS TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - CommentNotify TINYINT(1) NOT NULL DEFAULT 1, - UpdateNotify TINYINT(1) NOT NULL DEFAULT 0, - OwnershipNotify TINYINT(1) NOT NULL DEFAULT 1, - PRIMARY KEY (ID), - UNIQUE (Username), - UNIQUE (Email), - FOREIGN KEY (AccountTypeID) REFERENCES AccountTypes(ID) ON DELETE NO ACTION -) ENGINE = InnoDB; -CREATE INDEX UsersAccountTypeID ON Users (AccountTypeID); - - --- SSH public keys used for the aurweb SSH/Git interface. --- -CREATE TABLE SSHPubKeys ( - UserID INTEGER UNSIGNED NOT NULL, - Fingerprint VARCHAR(44) NOT NULL, - PubKey VARCHAR(4096) NOT NULL, - PRIMARY KEY (Fingerprint), - FOREIGN KEY (UserID) REFERENCES Users(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - - --- Track Users logging in/out of AUR web site. --- -CREATE TABLE Sessions ( - UsersID INTEGER UNSIGNED NOT NULL, - SessionID CHAR(32) NOT NULL, - LastUpdateTS BIGINT UNSIGNED NOT NULL, - FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE CASCADE, - UNIQUE (SessionID) -) ENGINE = InnoDB; - - --- Information on package bases --- -CREATE TABLE PackageBases ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(255) NOT NULL, - NumVotes INTEGER UNSIGNED NOT NULL DEFAULT 0, - Popularity DECIMAL(10,6) UNSIGNED NOT NULL DEFAULT 0, - OutOfDateTS BIGINT UNSIGNED NULL DEFAULT NULL, - FlaggerComment TEXT NOT NULL, - SubmittedTS BIGINT UNSIGNED NOT NULL, - ModifiedTS BIGINT UNSIGNED NOT NULL, - FlaggerUID INTEGER UNSIGNED NULL DEFAULT NULL, -- who flagged the package out-of-date? - SubmitterUID INTEGER UNSIGNED NULL DEFAULT NULL, -- who submitted it? - MaintainerUID INTEGER UNSIGNED NULL DEFAULT NULL, -- User - PackagerUID INTEGER UNSIGNED NULL DEFAULT NULL, -- Last packager - PRIMARY KEY (ID), - UNIQUE (Name), - FOREIGN KEY (FlaggerUID) REFERENCES Users(ID) ON DELETE SET NULL, - -- deleting a user will cause packages to be orphaned, not deleted - FOREIGN KEY (SubmitterUID) REFERENCES Users(ID) ON DELETE SET NULL, - FOREIGN KEY (MaintainerUID) REFERENCES Users(ID) ON DELETE SET NULL, - FOREIGN KEY (PackagerUID) REFERENCES Users(ID) ON DELETE SET NULL -) ENGINE = InnoDB; -CREATE INDEX BasesNumVotes ON PackageBases (NumVotes); -CREATE INDEX BasesSubmitterUID ON PackageBases (SubmitterUID); -CREATE INDEX BasesMaintainerUID ON PackageBases (MaintainerUID); -CREATE INDEX BasesPackagerUID ON PackageBases (PackagerUID); - - --- Keywords of package bases --- -CREATE TABLE PackageKeywords ( - PackageBaseID INTEGER UNSIGNED NOT NULL, - Keyword VARCHAR(255) NOT NULL DEFAULT '', - PRIMARY KEY (PackageBaseID, Keyword), - FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - - --- Information about the actual packages --- -CREATE TABLE Packages ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - PackageBaseID INTEGER UNSIGNED NOT NULL, - Name VARCHAR(255) NOT NULL, - Version VARCHAR(255) NOT NULL DEFAULT '', - Description VARCHAR(255) NULL DEFAULT NULL, - URL VARCHAR(8000) NULL DEFAULT NULL, - PRIMARY KEY (ID), - UNIQUE (Name), - FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - - --- Information about licenses --- -CREATE TABLE Licenses ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(255) NOT NULL, - PRIMARY KEY (ID), - UNIQUE (Name) -) ENGINE = InnoDB; - - --- Information about package-license-relations --- -CREATE TABLE PackageLicenses ( - PackageID INTEGER UNSIGNED NOT NULL, - LicenseID INTEGER UNSIGNED NOT NULL, - PRIMARY KEY (PackageID, LicenseID), - FOREIGN KEY (PackageID) REFERENCES Packages(ID) ON DELETE CASCADE, - FOREIGN KEY (LicenseID) REFERENCES Licenses(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - - --- Information about groups --- -CREATE TABLE `Groups` ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(255) NOT NULL, - PRIMARY KEY (ID), - UNIQUE (Name) -) ENGINE = InnoDB; - - --- Information about package-group-relations --- -CREATE TABLE PackageGroups ( - PackageID INTEGER UNSIGNED NOT NULL, - GroupID INTEGER UNSIGNED NOT NULL, - PRIMARY KEY (PackageID, GroupID), - FOREIGN KEY (PackageID) REFERENCES Packages(ID) ON DELETE CASCADE, - FOREIGN KEY (GroupID) REFERENCES `Groups`(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - - --- Define the package dependency types --- -CREATE TABLE DependencyTypes ( - ID TINYINT UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(32) NOT NULL DEFAULT '', - PRIMARY KEY (ID) -) ENGINE = InnoDB; -INSERT INTO DependencyTypes VALUES (1, 'depends'); -INSERT INTO DependencyTypes VALUES (2, 'makedepends'); -INSERT INTO DependencyTypes VALUES (3, 'checkdepends'); -INSERT INTO DependencyTypes VALUES (4, 'optdepends'); - - --- Track which dependencies a package has --- -CREATE TABLE PackageDepends ( - PackageID INTEGER UNSIGNED NOT NULL, - DepTypeID TINYINT UNSIGNED NOT NULL, - DepName VARCHAR(255) NOT NULL, - DepDesc VARCHAR(255) NULL DEFAULT NULL, - DepCondition VARCHAR(255), - DepArch VARCHAR(255) NULL DEFAULT NULL, - FOREIGN KEY (PackageID) REFERENCES Packages(ID) ON DELETE CASCADE, - FOREIGN KEY (DepTypeID) REFERENCES DependencyTypes(ID) ON DELETE NO ACTION -) ENGINE = InnoDB; -CREATE INDEX DependsPackageID ON PackageDepends (PackageID); -CREATE INDEX DependsDepName ON PackageDepends (DepName); - - --- Define the package relation types --- -CREATE TABLE RelationTypes ( - ID TINYINT UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(32) NOT NULL DEFAULT '', - PRIMARY KEY (ID) -) ENGINE = InnoDB; -INSERT INTO RelationTypes VALUES (1, 'conflicts'); -INSERT INTO RelationTypes VALUES (2, 'provides'); -INSERT INTO RelationTypes VALUES (3, 'replaces'); - - --- Track which conflicts, provides and replaces a package has --- -CREATE TABLE PackageRelations ( - PackageID INTEGER UNSIGNED NOT NULL, - RelTypeID TINYINT UNSIGNED NOT NULL, - RelName VARCHAR(255) NOT NULL, - RelCondition VARCHAR(255), - RelArch VARCHAR(255) NULL DEFAULT NULL, - FOREIGN KEY (PackageID) REFERENCES Packages(ID) ON DELETE CASCADE, - FOREIGN KEY (RelTypeID) REFERENCES RelationTypes(ID) ON DELETE NO ACTION -) ENGINE = InnoDB; -CREATE INDEX RelationsPackageID ON PackageRelations (PackageID); -CREATE INDEX RelationsRelName ON PackageRelations (RelName); - - --- Track which sources a package has --- -CREATE TABLE PackageSources ( - PackageID INTEGER UNSIGNED NOT NULL, - Source VARCHAR(8000) NOT NULL DEFAULT '/dev/null', - SourceArch VARCHAR(255) NULL DEFAULT NULL, - FOREIGN KEY (PackageID) REFERENCES Packages(ID) ON DELETE CASCADE -) ENGINE = InnoDB; -CREATE INDEX SourcesPackageID ON PackageSources (PackageID); - - --- Track votes for packages --- -CREATE TABLE PackageVotes ( - UsersID INTEGER UNSIGNED NOT NULL, - PackageBaseID INTEGER UNSIGNED NOT NULL, - VoteTS BIGINT UNSIGNED NULL DEFAULT NULL, - FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE CASCADE, - FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE CASCADE -) ENGINE = InnoDB; -CREATE UNIQUE INDEX VoteUsersIDPackageID ON PackageVotes (UsersID, PackageBaseID); -CREATE INDEX VotesUsersID ON PackageVotes (UsersID); -CREATE INDEX VotesPackageBaseID ON PackageVotes (PackageBaseID); - --- Record comments for packages --- -CREATE TABLE PackageComments ( - ID BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - PackageBaseID INTEGER UNSIGNED NOT NULL, - UsersID INTEGER UNSIGNED NULL DEFAULT NULL, - Comments TEXT NOT NULL, - RenderedComment TEXT NOT NULL, - CommentTS BIGINT UNSIGNED NOT NULL DEFAULT 0, - EditedTS BIGINT UNSIGNED NULL DEFAULT NULL, - EditedUsersID INTEGER UNSIGNED NULL DEFAULT NULL, - DelTS BIGINT UNSIGNED NULL DEFAULT NULL, - DelUsersID INTEGER UNSIGNED NULL DEFAULT NULL, - PinnedTS BIGINT UNSIGNED NOT NULL DEFAULT 0, - PRIMARY KEY (ID), - FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE SET NULL, - FOREIGN KEY (EditedUsersID) REFERENCES Users(ID) ON DELETE SET NULL, - FOREIGN KEY (DelUsersID) REFERENCES Users(ID) ON DELETE CASCADE, - FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE CASCADE -) ENGINE = InnoDB; -CREATE INDEX CommentsUsersID ON PackageComments (UsersID); -CREATE INDEX CommentsPackageBaseID ON PackageComments (PackageBaseID); - --- Package base co-maintainers --- -CREATE TABLE PackageComaintainers ( - UsersID INTEGER UNSIGNED NOT NULL, - PackageBaseID INTEGER UNSIGNED NOT NULL, - Priority INTEGER UNSIGNED NOT NULL, - FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE CASCADE, - FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE CASCADE -) ENGINE = InnoDB; -CREATE INDEX ComaintainersUsersID ON PackageComaintainers (UsersID); -CREATE INDEX ComaintainersPackageBaseID ON PackageComaintainers (PackageBaseID); - --- Package base notifications --- -CREATE TABLE PackageNotifications ( - PackageBaseID INTEGER UNSIGNED NOT NULL, - UserID INTEGER UNSIGNED NOT NULL, - FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE CASCADE, - FOREIGN KEY (UserID) REFERENCES Users(ID) ON DELETE CASCADE -) ENGINE = InnoDB; -CREATE UNIQUE INDEX NotifyUserIDPkgID ON PackageNotifications (UserID, PackageBaseID); - --- Package name blacklist --- -CREATE TABLE PackageBlacklist ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(64) NOT NULL, - PRIMARY KEY (ID), - UNIQUE (Name) -) ENGINE = InnoDB; - --- Providers in the official repositories --- -CREATE TABLE OfficialProviders ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(64) NOT NULL, - Repo VARCHAR(64) NOT NULL, - Provides VARCHAR(64) NOT NULL, - PRIMARY KEY (ID) -) ENGINE = InnoDB; -CREATE UNIQUE INDEX ProviderNameProvides ON OfficialProviders (Name, Provides); - --- Define package request types --- -CREATE TABLE RequestTypes ( - ID TINYINT UNSIGNED NOT NULL AUTO_INCREMENT, - Name VARCHAR(32) NOT NULL DEFAULT '', - PRIMARY KEY (ID) -) ENGINE = InnoDB; -INSERT INTO RequestTypes VALUES (1, 'deletion'); -INSERT INTO RequestTypes VALUES (2, 'orphan'); -INSERT INTO RequestTypes VALUES (3, 'merge'); - --- Package requests --- -CREATE TABLE PackageRequests ( - ID BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - ReqTypeID TINYINT UNSIGNED NOT NULL, - PackageBaseID INTEGER UNSIGNED NULL, - PackageBaseName VARCHAR(255) NOT NULL, - MergeBaseName VARCHAR(255) NULL, - UsersID INTEGER UNSIGNED NULL DEFAULT NULL, - Comments TEXT NOT NULL, - ClosureComment TEXT NOT NULL, - RequestTS BIGINT UNSIGNED NOT NULL DEFAULT 0, - ClosedTS BIGINT UNSIGNED NULL DEFAULT NULL, - ClosedUID INTEGER UNSIGNED NULL DEFAULT NULL, - Status TINYINT UNSIGNED NOT NULL DEFAULT 0, - PRIMARY KEY (ID), - FOREIGN KEY (ReqTypeID) REFERENCES RequestTypes(ID) ON DELETE NO ACTION, - FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE SET NULL, - FOREIGN KEY (PackageBaseID) REFERENCES PackageBases(ID) ON DELETE SET NULL, - FOREIGN KEY (ClosedUID) REFERENCES Users(ID) ON DELETE SET NULL -) ENGINE = InnoDB; -CREATE INDEX RequestsUsersID ON PackageRequests (UsersID); -CREATE INDEX RequestsPackageBaseID ON PackageRequests (PackageBaseID); - --- Vote information --- -CREATE TABLE IF NOT EXISTS TU_VoteInfo ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - Agenda TEXT NOT NULL, - User VARCHAR(32) NOT NULL, - Submitted BIGINT UNSIGNED NOT NULL, - End BIGINT UNSIGNED NOT NULL, - Quorum DECIMAL(2, 2) UNSIGNED NOT NULL, - SubmitterID INTEGER UNSIGNED NOT NULL, - Yes TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', - No TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', - Abstain TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', - ActiveTUs TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', - PRIMARY KEY (ID), - FOREIGN KEY (SubmitterID) REFERENCES Users(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - --- Individual vote records --- -CREATE TABLE IF NOT EXISTS TU_Votes ( - VoteID INTEGER UNSIGNED NOT NULL, - UserID INTEGER UNSIGNED NOT NULL, - FOREIGN KEY (VoteID) REFERENCES TU_VoteInfo(ID) ON DELETE CASCADE, - FOREIGN KEY (UserID) REFERENCES Users(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - --- Malicious user banning --- -CREATE TABLE Bans ( - IPAddress VARCHAR(45) NOT NULL, - BanTS TIMESTAMP NOT NULL, - PRIMARY KEY (IPAddress) -) ENGINE = InnoDB; - --- Terms and Conditions --- -CREATE TABLE Terms ( - ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - Description VARCHAR(255) NOT NULL, - URL VARCHAR(8000) NOT NULL, - Revision INTEGER UNSIGNED NOT NULL DEFAULT 1, - PRIMARY KEY (ID) -) ENGINE = InnoDB; - --- Terms and Conditions accepted by users --- -CREATE TABLE AcceptedTerms ( - UsersID INTEGER UNSIGNED NOT NULL, - TermsID INTEGER UNSIGNED NOT NULL, - Revision INTEGER UNSIGNED NOT NULL DEFAULT 0, - FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE CASCADE, - FOREIGN KEY (TermsID) REFERENCES Terms(ID) ON DELETE CASCADE -) ENGINE = InnoDB; - --- Rate limits for API --- -CREATE TABLE `ApiRateLimit` ( - IP VARCHAR(45) NOT NULL, - Requests INT(11) NOT NULL, - WindowStart BIGINT(20) NOT NULL, - PRIMARY KEY (`ip`) -) ENGINE = InnoDB; -CREATE INDEX ApiRateLimitWindowStart ON ApiRateLimit (WindowStart); diff --git a/schema/reloadtestdb.sh b/schema/reloadtestdb.sh deleted file mode 100755 index e839dcec..00000000 --- a/schema/reloadtestdb.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -e - -DB_NAME=${DB_NAME:-AUR} -DB_USER=${DB_USER:-aur} -# Password should allow empty definition -DB_PASS=${DB_PASS-aur} -DB_HOST=${DB_HOST:-localhost} -DATA_FILE=${DATA_FILE:-dummy-data.sql} - -echo "Using database $DB_NAME, user $DB_USER, host $DB_HOST" - -mydir=$(pwd) -if [ $(basename $mydir) != "schema" ]; then - echo "you must be in the aurweb/schema directory to run this script" - exit 1 -fi - -echo "recreating database..." -mysql -h $DB_HOST -u $DB_USER -p$DB_PASS < aur-schema.sql - -if [ ! -f $DATA_FILE ]; then - echo "creating dumy-data..." - python3 gendummydata.py $DATA_FILE -fi - -echo "loading dummy-data..." -mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME < $DATA_FILE - -echo "done." diff --git a/test/Makefile b/test/Makefile index 4ce9b9be..f559e169 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,10 +1,6 @@ -FOREIGN_TARGETS = ../schema/aur-schema-sqlite.sql T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) -check: $(FOREIGN_TARGETS) $(T) - -$(FOREIGN_TARGETS): - $(MAKE) -C $(dir $@) $(notdir $@) +check: $(T) clean: $(RM) -r test-results/ diff --git a/test/setup.sh b/test/setup.sh index 5c761f22..12f6edcc 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -110,10 +110,7 @@ SSH_TTY=/dev/pts/0 export SSH_CLIENT SSH_CONNECTION SSH_TTY # Initialize the test database. -DBSCHEMA="$TOPLEVEL/schema/aur-schema-sqlite.sql" -[ -f "$DBSCHEMA" ] || error 'SQLite database schema not found' -rm -f aur.db -sqlite3 aur.db <"$DBSCHEMA" +python -m aurweb.initdb echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (1, 'user', '!', 'user@localhost', 'en', 1);" | sqlite3 aur.db echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (2, 'tu', '!', 'tu@localhost', 'en', 2);" | sqlite3 aur.db From a8a1f74a9207339bf707bb09e8dba7b2c67abb5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sat, 22 Feb 2020 22:31:26 +0100 Subject: [PATCH 0076/1451] Set up Alembic for database migrations Signed-off-by: Lukas Fleischer --- INSTALL | 4 +- TESTING | 3 +- alembic.ini | 86 +++++++++++++++++++++++++++++++++++++++ aurweb/initdb.py | 9 ++++ aurweb/schema.py | 8 ++++ migrations/README | 48 ++++++++++++++++++++++ migrations/env.py | 73 +++++++++++++++++++++++++++++++++ migrations/script.py.mako | 24 +++++++++++ 8 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 alembic.ini create mode 100644 migrations/README create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako diff --git a/INSTALL b/INSTALL index 68fe5dcd..7087aca2 100644 --- a/INSTALL +++ b/INSTALL @@ -47,8 +47,8 @@ read the instructions below. 4) Install Python modules and dependencies: - # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy - # pacman -S python-bleach python-markdown + # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ + python-bleach python-markdown python-alembic # python3 setup.py install 5) Create a new MySQL database and a user and import the aurweb SQL schema: diff --git a/TESTING b/TESTING index 190043f9..4a1e6f4c 100644 --- a/TESTING +++ b/TESTING @@ -11,7 +11,8 @@ INSTALL. 2) Install the necessary packages: - # pacman -S --needed php php-sqlite sqlite words fortune-mod python python-sqlalchemy + # pacman -S --needed php php-sqlite sqlite words fortune-mod \ + python python-sqlalchemy python-alembic Ensure to enable the pdo_sqlite extension in php.ini. diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..6d3a3929 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,86 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# the database URL is generated in env.py +# sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/aurweb/initdb.py b/aurweb/initdb.py index e3e96503..c02fb961 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -1,6 +1,8 @@ import aurweb.db import aurweb.schema +import alembic.command +import alembic.config import argparse import sqlalchemy @@ -31,10 +33,17 @@ def feed_initial_data(conn): def run(args): + # Ensure Alembic is fine before we do the real work, in order not to fail at + # the last step and leave the database in an inconsistent state. The + # configuration is loaded lazily, so we query it to force its loading. + alembic_config = alembic.config.Config('alembic.ini') + alembic_config.get_main_option('script_location') + engine = sqlalchemy.create_engine(aurweb.db.get_sqlalchemy_url(), echo=(args.verbose >= 1)) aurweb.schema.metadata.create_all(engine) feed_initial_data(engine.connect()) + alembic.command.stamp(alembic_config, 'head') if __name__ == '__main__': diff --git a/aurweb/schema.py b/aurweb/schema.py index b1261e86..fde6512f 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -1,3 +1,11 @@ +""" +Schema of aurweb's database. + +Changes here should always be accompanied by an Alembic migration, which can be +usually be automatically generated. See `migrations/README` for details. +""" + + from sqlalchemy import CHAR, Column, ForeignKey, Index, MetaData, String, TIMESTAMP, Table, Text, text from sqlalchemy.dialects.mysql import BIGINT, DECIMAL, INTEGER, TINYINT from sqlalchemy.ext.compiler import compiles diff --git a/migrations/README b/migrations/README new file mode 100644 index 00000000..301d0e54 --- /dev/null +++ b/migrations/README @@ -0,0 +1,48 @@ +This directory contains Alembic's environment for managing database migrations. + +From Alembic's documentation: Alembic is a lightweight database migration tool +for usage with the SQLAlchemy Database Toolkit for Python. +https://alembic.sqlalchemy.org/en/latest/index.html + + +Upgrading to the latest version +------------------------------- + +Simply run `alembic upgrade head` from aurweb's root. + + +Creating new migrations +----------------------- + +When working with Alembic and SQLAlchemy, you should never edit the database +schema manually. Please proceed like this instead: + +1. Edit `aurweb/schema.py` to your liking. +2. Run `alembic revision --autogenerate -m "your message"` +3. Proofread the generated migration. +4. Run `alembic upgrade head` to apply the changes to the database. +5. Commit the new migration. + +To revert a migration, you may run `alembic downgrade -1` and then manually +delete the migration file. Note that SQLite is limited and that it's sometimes +easier to recreate the database. + +For anything more complicated, please read Alembic's documentation. + + +Troubleshooting +--------------- + +- `ModuleNotFoundError: No module named 'aurweb'` + + You may either install the aurweb module with pip, or set PYTHONPATH to your + aurweb repository. Since alembic must be run from the aurweb root, you may + simply use: `PYTHONPATH=. alembic [...]`. + +- `FAILED: No config file 'alembic.ini' found, or file has no '[alembic]' section` + + You need to run Alembic from the project's root, and not from `migrations/`. + +- `configparser.NoSectionError: No section: 'database'` + + You need to set AUR_CONFIG, as explained in `TESTING`. diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 00000000..1627e693 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,73 @@ +import aurweb.db +import aurweb.schema + +from alembic import context +import logging.config +import sqlalchemy + + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +logging.config.fileConfig(config.config_file_name) + +# model MetaData for autogenerating migrations +target_metadata = aurweb.schema.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + context.configure( + url=aurweb.db.get_sqlalchemy_url(), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = sqlalchemy.create_engine( + aurweb.db.get_sqlalchemy_url(), + poolclass=sqlalchemy.pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} From e4cbe264cf6949f82338b7de705cba90c15f4d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 23 Feb 2020 19:52:12 +0100 Subject: [PATCH 0077/1451] Create an initial Alembic migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This way the database will get stamped, and Git will create the `versions` directory without which Alembic won’t work. Signed-off-by: Lukas Fleischer --- .../versions/f47cad5d6d03_initial_revision.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 migrations/versions/f47cad5d6d03_initial_revision.py diff --git a/migrations/versions/f47cad5d6d03_initial_revision.py b/migrations/versions/f47cad5d6d03_initial_revision.py new file mode 100644 index 00000000..9e99490f --- /dev/null +++ b/migrations/versions/f47cad5d6d03_initial_revision.py @@ -0,0 +1,24 @@ +"""initial revision + +Revision ID: f47cad5d6d03 +Revises: +Create Date: 2020-02-23 13:23:32.331396 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f47cad5d6d03' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass From 81d55e70ee0469018af86d203ceaf2fece691ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 23 Feb 2020 19:52:36 +0100 Subject: [PATCH 0078/1451] Disable Alembic support on test databases Signed-off-by: Lukas Fleischer --- aurweb/initdb.py | 12 +++++++++--- test/setup.sh | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/aurweb/initdb.py b/aurweb/initdb.py index c02fb961..91777f7e 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -36,14 +36,17 @@ def run(args): # Ensure Alembic is fine before we do the real work, in order not to fail at # the last step and leave the database in an inconsistent state. The # configuration is loaded lazily, so we query it to force its loading. - alembic_config = alembic.config.Config('alembic.ini') - alembic_config.get_main_option('script_location') + if args.use_alembic: + alembic_config = alembic.config.Config('alembic.ini') + alembic_config.get_main_option('script_location') engine = sqlalchemy.create_engine(aurweb.db.get_sqlalchemy_url(), echo=(args.verbose >= 1)) aurweb.schema.metadata.create_all(engine) feed_initial_data(engine.connect()) - alembic.command.stamp(alembic_config, 'head') + + if args.use_alembic: + alembic.command.stamp(alembic_config, 'head') if __name__ == '__main__': @@ -52,5 +55,8 @@ if __name__ == '__main__': description='Initialize the aurweb database.') parser.add_argument('-v', '--verbose', action='count', default=0, help='increase verbosity') + parser.add_argument('--no-alembic', + help='disable Alembic migrations support', + dest='use_alembic', action='store_false') args = parser.parse_args() run(args) diff --git a/test/setup.sh b/test/setup.sh index 12f6edcc..cad5cd66 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -110,7 +110,7 @@ SSH_TTY=/dev/pts/0 export SSH_CLIENT SSH_CONNECTION SSH_TTY # Initialize the test database. -python -m aurweb.initdb +python -m aurweb.initdb --no-alembic echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (1, 'user', '!', 'user@localhost', 'en', 1);" | sqlite3 aur.db echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (2, 'tu', '!', 'tu@localhost', 'en', 2);" | sqlite3 aur.db From e374a91febe53b72ff4cb73b153348f067374c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 23 Feb 2020 19:53:13 +0100 Subject: [PATCH 0079/1451] Change the extension of TAP test suites to .t MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the common convention for TAP, and makes harnesses like prove automatically detect them. Plus, test suites don’t have to be shell scripts anymore. Signed-off-by: Lukas Fleischer --- test/Makefile | 2 +- test/{t1100-git-auth.sh => t1100-git-auth.t} | 0 test/{t1200-git-serve.sh => t1200-git-serve.t} | 0 test/{t1300-git-update.sh => t1300-git-update.t} | 0 test/{t2100-mkpkglists.sh => t2100-mkpkglists.t} | 0 test/{t2200-tuvotereminder.sh => t2200-tuvotereminder.t} | 0 test/{t2300-pkgmaint.sh => t2300-pkgmaint.t} | 0 test/{t2400-aurblup.sh => t2400-aurblup.t} | 0 test/{t2500-notify.sh => t2500-notify.t} | 0 test/{t2600-rendercomment.sh => t2600-rendercomment.t} | 0 test/{t2700-usermaint.sh => t2700-usermaint.t} | 0 11 files changed, 1 insertion(+), 1 deletion(-) rename test/{t1100-git-auth.sh => t1100-git-auth.t} (100%) rename test/{t1200-git-serve.sh => t1200-git-serve.t} (100%) rename test/{t1300-git-update.sh => t1300-git-update.t} (100%) rename test/{t2100-mkpkglists.sh => t2100-mkpkglists.t} (100%) rename test/{t2200-tuvotereminder.sh => t2200-tuvotereminder.t} (100%) rename test/{t2300-pkgmaint.sh => t2300-pkgmaint.t} (100%) rename test/{t2400-aurblup.sh => t2400-aurblup.t} (100%) rename test/{t2500-notify.sh => t2500-notify.t} (100%) rename test/{t2600-rendercomment.sh => t2600-rendercomment.t} (100%) rename test/{t2700-usermaint.sh => t2700-usermaint.t} (100%) diff --git a/test/Makefile b/test/Makefile index f559e169..d310c8f5 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,4 +1,4 @@ -T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)) +T = $(sort $(wildcard *.t)) check: $(T) diff --git a/test/t1100-git-auth.sh b/test/t1100-git-auth.t similarity index 100% rename from test/t1100-git-auth.sh rename to test/t1100-git-auth.t diff --git a/test/t1200-git-serve.sh b/test/t1200-git-serve.t similarity index 100% rename from test/t1200-git-serve.sh rename to test/t1200-git-serve.t diff --git a/test/t1300-git-update.sh b/test/t1300-git-update.t similarity index 100% rename from test/t1300-git-update.sh rename to test/t1300-git-update.t diff --git a/test/t2100-mkpkglists.sh b/test/t2100-mkpkglists.t similarity index 100% rename from test/t2100-mkpkglists.sh rename to test/t2100-mkpkglists.t diff --git a/test/t2200-tuvotereminder.sh b/test/t2200-tuvotereminder.t similarity index 100% rename from test/t2200-tuvotereminder.sh rename to test/t2200-tuvotereminder.t diff --git a/test/t2300-pkgmaint.sh b/test/t2300-pkgmaint.t similarity index 100% rename from test/t2300-pkgmaint.sh rename to test/t2300-pkgmaint.t diff --git a/test/t2400-aurblup.sh b/test/t2400-aurblup.t similarity index 100% rename from test/t2400-aurblup.sh rename to test/t2400-aurblup.t diff --git a/test/t2500-notify.sh b/test/t2500-notify.t similarity index 100% rename from test/t2500-notify.sh rename to test/t2500-notify.t diff --git a/test/t2600-rendercomment.sh b/test/t2600-rendercomment.t similarity index 100% rename from test/t2600-rendercomment.sh rename to test/t2600-rendercomment.t diff --git a/test/t2700-usermaint.sh b/test/t2700-usermaint.t similarity index 100% rename from test/t2700-usermaint.sh rename to test/t2700-usermaint.t From 90c0a361b5cb8f72a9c908104b399451712fb7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sat, 29 Feb 2020 01:01:38 +0100 Subject: [PATCH 0080/1451] Support running tests from any directory Signed-off-by: Lukas Fleischer --- test/setup.sh | 5 ++--- test/t1100-git-auth.t | 2 +- test/t1200-git-serve.t | 2 +- test/t1300-git-update.t | 2 +- test/t2100-mkpkglists.t | 2 +- test/t2200-tuvotereminder.t | 2 +- test/t2300-pkgmaint.t | 2 +- test/t2400-aurblup.t | 2 +- test/t2500-notify.t | 2 +- test/t2600-rendercomment.t | 2 +- test/t2700-usermaint.t | 2 +- 11 files changed, 12 insertions(+), 13 deletions(-) diff --git a/test/setup.sh b/test/setup.sh index cad5cd66..4a6eb3b1 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -1,7 +1,6 @@ -TEST_DIRECTORY="$(pwd)" -TOPLEVEL="$(cd .. && pwd)" +TOPLEVEL="$(cd "$(dirname "$0")/.." && pwd)" -. ./sharness.sh +. "$TOPLEVEL/test/sharness.sh" # Configure python search path. PYTHONPATH="$TOPLEVEL" diff --git a/test/t1100-git-auth.t b/test/t1100-git-auth.t index 71d526f2..cbf16aed 100755 --- a/test/t1100-git-auth.t +++ b/test/t1100-git-auth.t @@ -2,7 +2,7 @@ test_description='git-auth tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test basic authentication.' ' "$GIT_AUTH" "$AUTH_KEYTYPE_USER" "$AUTH_KEYTEXT_USER" >out && diff --git a/test/t1200-git-serve.t b/test/t1200-git-serve.t index e817b2cf..1893cdcd 100755 --- a/test/t1200-git-serve.t +++ b/test/t1200-git-serve.t @@ -2,7 +2,7 @@ test_description='git-serve tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test interactive shell.' ' "$GIT_SERVE" 2>&1 | grep -q "Interactive shell is disabled." diff --git a/test/t1300-git-update.t b/test/t1300-git-update.t index 06d14984..82c0fb99 100755 --- a/test/t1300-git-update.t +++ b/test/t1300-git-update.t @@ -2,7 +2,7 @@ test_description='git-update tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" dump_package_info() { for t in Packages Licenses PackageLicenses Groups PackageGroups \ diff --git a/test/t2100-mkpkglists.t b/test/t2100-mkpkglists.t index fc11d073..5bf13de8 100755 --- a/test/t2100-mkpkglists.t +++ b/test/t2100-mkpkglists.t @@ -2,7 +2,7 @@ test_description='mkpkglists tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test package list generation with no packages.' ' echo "DELETE FROM Packages;" | sqlite3 aur.db && diff --git a/test/t2200-tuvotereminder.t b/test/t2200-tuvotereminder.t index c82ce874..5a8f3a25 100755 --- a/test/t2200-tuvotereminder.t +++ b/test/t2200-tuvotereminder.t @@ -2,7 +2,7 @@ test_description='tuvotereminder tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test Trusted User vote reminders.' ' now=$(date -d now +%s) && diff --git a/test/t2300-pkgmaint.t b/test/t2300-pkgmaint.t index 478df526..c390f5db 100755 --- a/test/t2300-pkgmaint.t +++ b/test/t2300-pkgmaint.t @@ -2,7 +2,7 @@ test_description='pkgmaint tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test package base cleanup script.' ' now=$(date -d now +%s) && diff --git a/test/t2400-aurblup.t b/test/t2400-aurblup.t index 708281c6..cc287a0f 100755 --- a/test/t2400-aurblup.t +++ b/test/t2400-aurblup.t @@ -2,7 +2,7 @@ test_description='aurblup tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test official provider update script.' ' mkdir -p remote/test/foobar-1.0-1 && diff --git a/test/t2500-notify.t b/test/t2500-notify.t index 380e65b8..5ef64c18 100755 --- a/test/t2500-notify.t +++ b/test/t2500-notify.t @@ -2,7 +2,7 @@ test_description='notify tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test out-of-date notifications.' ' cat <<-EOD | sqlite3 aur.db && diff --git a/test/t2600-rendercomment.t b/test/t2600-rendercomment.t index be408b80..e01904c6 100755 --- a/test/t2600-rendercomment.t +++ b/test/t2600-rendercomment.t @@ -2,7 +2,7 @@ test_description='rendercomment tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test comment rendering.' ' cat <<-EOD | sqlite3 aur.db && diff --git a/test/t2700-usermaint.t b/test/t2700-usermaint.t index 4f625142..f0bb449b 100755 --- a/test/t2700-usermaint.t +++ b/test/t2700-usermaint.t @@ -2,7 +2,7 @@ test_description='usermaint tests' -. ./setup.sh +. "$(dirname "$0")/setup.sh" test_expect_success 'Test removal of login IP addresses.' ' now=$(date -d now +%s) && From bf7c49158c360690f79b31b5a65f0bb42e3fccb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sat, 29 Feb 2020 01:02:04 +0100 Subject: [PATCH 0081/1451] test/Makefile: Run tests with prove when available Signed-off-by: Lukas Fleischer --- test/Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/Makefile b/test/Makefile index d310c8f5..758befa3 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,6 +1,13 @@ T = $(sort $(wildcard *.t)) +PROVE := $(shell command -v prove 2> /dev/null) + +ifdef PROVE +check: + prove . +else check: $(T) +endif clean: $(RM) -r test-results/ From 28ba3f77dcd3741b2cb8dc82f47790e130063da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sat, 29 Feb 2020 01:02:42 +0100 Subject: [PATCH 0082/1451] Write test/README.md to help working with tests Signed-off-by: Lukas Fleischer --- test/README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/README.md diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..de7eff18 --- /dev/null +++ b/test/README.md @@ -0,0 +1,37 @@ +Running tests +------------- + +To run all the tests, you may run `make check` under `test/`. + +For more control, you may use the `prove` command, which receives a directory +or a list of files to run, and produces a report. + +Each test script is standalone, so you may run them individually. Some tests +may receive command-line options to help debugging. See for example sharness's +documentation for shell test scripts: +https://github.com/chriscool/sharness/blob/master/README.git + +### Dependencies + +For all the test to run, the following Arch packages should be installed: + +- pyalpm +- python-alembic +- python-bleach +- python-markdown +- python-pygit2 +- python-sqlalchemy +- python-srcinfo + +Writing tests +------------- + +Test scripts must follow the Test Anything Protocol specification: +http://testanything.org/tap-specification.html + +Tests must support being run from any directory. They may use $0 to determine +their location. Python scripts should expect aurweb to be installed and +importable without toying with os.path or PYTHONPATH. + +Tests written in shell should use sharness. In general, new tests should be +consistent with existing tests unless they have a good reason not to. From 31a5b40b5cf355f3648d5e13d9dbd09b51f7cb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sat, 21 Mar 2020 19:13:45 +0100 Subject: [PATCH 0083/1451] Map BIGINT to INTEGER for SQLite Signed-off-by: Lukas Fleischer --- aurweb/schema.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/aurweb/schema.py b/aurweb/schema.py index fde6512f..6792cf1d 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -17,6 +17,17 @@ def compile_tinyint_sqlite(type_, compiler, **kw): return 'INTEGER' +@compiles(BIGINT, 'sqlite') +def compile_bigint_sqlite(type_, compiler, **kw): + """ + For SQLite's AUTOINCREMENT to work on BIGINT columns, we need to map BIGINT + to INTEGER. Aside from that, BIGINT is the same as INTEGER for SQLite. + + See https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#allowing-autoincrement-behavior-sqlalchemy-types-other-than-integer-integer + """ + return 'INTEGER' + + metadata = MetaData() # Define the Account Types for the AUR. From a09c4d81682a1ec76925315b497c475cadebf3ea Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 27 Mar 2020 08:31:46 -0400 Subject: [PATCH 0084/1451] Translation updates from Transifex Signed-off-by: Lukas Fleischer --- po/ast.po | 502 +++++++++++++++++++++++++++--------------------------- po/ja.po | 62 +++---- 2 files changed, 282 insertions(+), 282 deletions(-) diff --git a/po/ast.po b/po/ast.po index 5e08e86b..16c363a6 100644 --- a/po/ast.po +++ b/po/ast.po @@ -11,8 +11,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-01-31 08:29+0000\n" -"Last-Translator: Lukas Fleischer\n" +"PO-Revision-Date: 2020-03-07 17:55+0000\n" +"Last-Translator: enolp \n" "Language-Team: Asturian (http://www.transifex.com/lfleischer/aurweb/language/ast/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -22,15 +22,15 @@ msgstr "" #: html/404.php msgid "Page Not Found" -msgstr "Nun s'alcontró la páxina" +msgstr "" #: html/404.php msgid "Sorry, the page you've requested does not exist." -msgstr "Perdón, la páxina que pidisti nun esiste." +msgstr "" #: html/404.php template/pkgreq_close_form.php msgid "Note" -msgstr "Nota" +msgstr "" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." @@ -48,32 +48,32 @@ msgstr "" #: html/503.php msgid "Service Unavailable" -msgstr "Serviciu non disponible" +msgstr "" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "¡Asela! Esti sitiu ta cayíu debío a caltenimientu. Volverémos ceo." +msgstr "" #: html/account.php msgid "Account" -msgstr "Cuenta" +msgstr "" #: html/account.php template/header.php msgid "Accounts" -msgstr "Cuentes" +msgstr "" #: html/account.php html/addvote.php msgid "You are not allowed to access this area." -msgstr "Nun tienes permisu p'acceder a esta area." +msgstr "" #: html/account.php msgid "Could not retrieve information for the specified user." -msgstr "Nun pudo recibise la información pal usuariu especificáu." +msgstr "" #: html/account.php msgid "You do not have permission to edit this account." -msgstr "Nun tienes permisu pa editar esta cuenta." +msgstr "" #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." @@ -81,40 +81,40 @@ msgstr "" #: html/account.php msgid "Use this form to search existing accounts." -msgstr "Usa esti formulariu pa guetar cuentes esistentes." +msgstr "" #: html/account.php msgid "You must log in to view user information." -msgstr "Tienes d'aniciar sesión pa ver la información del usuariu." +msgstr "" #: html/addvote.php template/tu_list.php msgid "Add Proposal" -msgstr "Amestar propuesta" +msgstr "" #: html/addvote.php msgid "Invalid token for user action." -msgstr "Token non válidu pa la aición del usuariu" +msgstr "" #: html/addvote.php msgid "Username does not exist." -msgstr "El nome d'usuariu nun esiste." +msgstr "" #: html/addvote.php #, php-format msgid "%s already has proposal running for them." -msgstr "%s yá tien la propuesta que cuerre pa ellos." +msgstr "" #: html/addvote.php msgid "Invalid type." -msgstr "Triba non válida." +msgstr "" #: html/addvote.php msgid "Proposal cannot be empty." -msgstr "La propuesta nun pue tar balera" +msgstr "" #: html/addvote.php msgid "New proposal submitted." -msgstr "Unvióse la propuesta nueva." +msgstr "" #: html/addvote.php msgid "Submit a proposal to vote on." @@ -122,20 +122,20 @@ msgstr "" #: html/addvote.php msgid "Applicant/TU" -msgstr "Aplicante/Usuariu d'Enfotu." +msgstr "" #: html/addvote.php msgid "(empty if not applicable)" -msgstr "(baletu si nun s'aplica)" +msgstr "" #: html/addvote.php template/account_search_results.php #: template/pkgreq_results.php msgid "Type" -msgstr "Triba" +msgstr "" #: html/addvote.php msgid "Addition of a TU" -msgstr "Añedir un Usuariu d'Enfotu" +msgstr "" #: html/addvote.php msgid "Removal of a TU" @@ -151,11 +151,11 @@ msgstr "" #: html/addvote.php template/tu_list.php msgid "Proposal" -msgstr "Propuesta" +msgstr "" #: html/addvote.php msgid "Submit" -msgstr "Unviar" +msgstr "" #: html/comaintainers.php template/comaintainers_form.php msgid "Manage Co-maintainers" @@ -171,7 +171,7 @@ msgstr "" #: html/home.php template/header.php msgid "Home" -msgstr "Aniciu" +msgstr "" #: html/home.php msgid "My Flagged Packages" @@ -183,7 +183,7 @@ msgstr "" #: html/home.php msgid "My Packages" -msgstr "Los mios paquetes" +msgstr "" #: html/home.php msgid "Search for packages I maintain" @@ -202,44 +202,44 @@ msgstr "" msgid "" "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " "Guidelines%s for more information." -msgstr "¡Bienllegáu al AUR! Llei la %sGuía d'usuariu del AUR%s y la %sGuía d'usuariu TU del AUR%s pa más información." +msgstr "" #: html/home.php #, php-format msgid "" "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " "otherwise they will be deleted!" -msgstr "Los PKGBUILD contribuyíos %stienen%s de ser compatibles col %sEstándar de empaquetado de Arch%s d'otra forma van ser esaniciaos." +msgstr "" #: html/home.php msgid "Remember to vote for your favourite packages!" -msgstr "¡Recuerda votar los tos paquetes favoritos!" +msgstr "" #: html/home.php msgid "Some packages may be provided as binaries in [community]." -msgstr "Dellos paquetes puen apurrise como binarios en [community]." +msgstr "" #: html/home.php msgid "DISCLAIMER" -msgstr "ACLARATORIA" +msgstr "" #: html/home.php template/footer.php msgid "" "AUR packages are user produced content. Any use of the provided files is at " "your own risk." -msgstr "Los paquetes d'AUR son conteníu producíu polos usuarios. Cualesquier usu de los ficheros forníos ta sol to propiu riesgu." +msgstr "" #: html/home.php msgid "Learn more..." -msgstr "Llei más..." +msgstr "" #: html/home.php msgid "Support" -msgstr "Sofitu" +msgstr "" #: html/home.php msgid "Package Requests" -msgstr "Solicitúes de paquetes" +msgstr "" #: html/home.php #, php-format @@ -250,7 +250,7 @@ msgstr "" #: html/home.php msgid "Orphan Request" -msgstr "Solicitú güérfana" +msgstr "" #: html/home.php msgid "" @@ -260,7 +260,7 @@ msgstr "" #: html/home.php msgid "Deletion Request" -msgstr "Solicitú de desaniciu" +msgstr "" #: html/home.php msgid "" @@ -271,7 +271,7 @@ msgstr "" #: html/home.php msgid "Merge Request" -msgstr "Solicitú de desanciu" +msgstr "" #: html/home.php msgid "" @@ -288,7 +288,7 @@ msgstr "" #: html/home.php msgid "Submitting Packages" -msgstr "Unviu de paquetes" +msgstr "" #: html/home.php #, php-format @@ -296,15 +296,15 @@ msgid "" "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting" " packages%s section of the Arch User Repository ArchWiki page for more " "details." -msgstr "Agora úsase GIT sobro SSH pa unviar paquetes a AUR. Mira la estaya %sUnviu de paquetes%s de la páxina ArchWiki del AUR pa más detalles." +msgstr "" #: html/home.php msgid "The following SSH fingerprints are used for the AUR:" -msgstr "Les buelgues SSH de darréu úsense pal AUR:" +msgstr "" #: html/home.php msgid "Discussion" -msgstr "Discutiniu" +msgstr "" #: html/home.php #, php-format @@ -316,7 +316,7 @@ msgstr "" #: html/home.php msgid "Bug Reporting" -msgstr "Informe de fallos" +msgstr "" #: html/home.php #, php-format @@ -329,23 +329,23 @@ msgstr "" #: html/home.php msgid "Package Search" -msgstr "Gueta de paquetes" +msgstr "" #: html/index.php msgid "Adopt" -msgstr "Adoptar" +msgstr "" #: html/index.php msgid "Vote" -msgstr "Votar" +msgstr "" #: html/index.php msgid "UnVote" -msgstr "Retirar votu" +msgstr "" #: html/index.php template/pkg_search_form.php template/pkg_search_results.php msgid "Notify" -msgstr "Avisar" +msgstr "" #: html/index.php template/pkg_search_results.php msgid "UnNotify" @@ -357,7 +357,7 @@ msgstr "" #: html/login.php template/header.php msgid "Login" -msgstr "Aniciar sesión" +msgstr "" #: html/login.php html/tos.php #, php-format @@ -366,11 +366,11 @@ msgstr "" #: html/login.php template/header.php msgid "Logout" -msgstr "Zarrar sesión" +msgstr "" #: html/login.php msgid "Enter login credentials" -msgstr "Introduz les tos credenciales d'aniciu sesión" +msgstr "" #: html/login.php msgid "User name or primary email address" @@ -378,15 +378,15 @@ msgstr "" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" -msgstr "Contraseña" +msgstr "" #: html/login.php msgid "Remember me" -msgstr "Recordáime" +msgstr "" #: html/login.php msgid "Forgot Password" -msgstr "Escaecí la contraseña" +msgstr "" #: html/login.php #, php-format @@ -396,45 +396,45 @@ msgstr "" #: html/packages.php template/pkg_search_form.php msgid "Search Criteria" -msgstr "Criteriu de gueta" +msgstr "" #: html/packages.php template/header.php template/pkgbase_details.php #: template/stats/general_stats_table.php template/stats/user_table.php msgid "Packages" -msgstr "Paquetes" +msgstr "" #: html/packages.php msgid "Error trying to retrieve package details." -msgstr "Fallu intentando recibir los detalles del paquete." +msgstr "" #: html/passreset.php lib/acctfuncs.inc.php msgid "Missing a required field." -msgstr "Falta un campu riquíu." +msgstr "" #: html/passreset.php lib/acctfuncs.inc.php msgid "Password fields do not match." -msgstr "Nun concasen los campos de contraseñes" +msgstr "" #: html/passreset.php lib/acctfuncs.inc.php #, php-format msgid "Your password must be at least %s characters." -msgstr "La to contraseña tien de tener polo menos %s carauteres." +msgstr "" #: html/passreset.php msgid "Invalid e-mail." -msgstr "Corréu electrónicu non válidu" +msgstr "" #: html/passreset.php msgid "Password Reset" -msgstr "Reaniciu de contraseña" +msgstr "" #: html/passreset.php msgid "Check your e-mail for the confirmation link." -msgstr "Comprueba'l to corréu pal enllaz de confirmación." +msgstr "" #: html/passreset.php msgid "Your password has been reset successfully." -msgstr "La to contraseña reanicióse con ésitu." +msgstr "" #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" @@ -442,15 +442,15 @@ msgstr "" #: html/passreset.php msgid "Enter your new password:" -msgstr "Introduz la to contraseña nueva:" +msgstr "" #: html/passreset.php msgid "Confirm your new password:" -msgstr "Confirma la to contraseña nueva:" +msgstr "" #: html/passreset.php html/tos.php msgid "Continue" -msgstr "Siguir" +msgstr "" #: html/passreset.php #, php-format @@ -489,7 +489,7 @@ msgstr "" #: html/pkgdel.php msgid "Package Deletion" -msgstr "Desaniciu de paquete" +msgstr "" #: html/pkgdel.php template/pkgbase_actions.php msgid "Delete Package" @@ -500,11 +500,11 @@ msgstr "" msgid "" "Use this form to delete the package base %s%s%s and the following packages " "from the AUR: " -msgstr "Usa esti formulariu pa desaniciar el paquete base %s%s%s y los paquetes siguientes d'AUR:" +msgstr "" #: html/pkgdel.php msgid "Deletion of a package is permanent. " -msgstr "El desaniciu d'un paquete ye permanente." +msgstr "" #: html/pkgdel.php html/pkgmerge.php msgid "Select the checkbox to confirm action." @@ -512,11 +512,11 @@ msgstr "" #: html/pkgdel.php msgid "Confirm package deletion" -msgstr "Confirmar desaniciu de paquete" +msgstr "" #: html/pkgdel.php template/account_delete.php msgid "Delete" -msgstr "Desaniciar" +msgstr "" #: html/pkgdel.php msgid "Only Trusted Users and Developers can delete packages." @@ -583,7 +583,7 @@ msgstr "" msgid "" "Please do %snot%s use this form to report bugs. Use the package comments " "instead." -msgstr "%sNun%s uses esti formulariu pa informar de fallos, por favor. Usa nel so llugar los comentarios del paquete." +msgstr "" #: html/pkgflag.php msgid "" @@ -594,7 +594,7 @@ msgstr "" #: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php #: template/pkgreq_results.php msgid "Comments" -msgstr "Comentarios" +msgstr "" #: html/pkgflag.php msgid "Flag" @@ -619,7 +619,7 @@ msgstr "" #: html/pkgmerge.php msgid "The following packages will be deleted: " -msgstr "Desaniciaránse los paquetes de darréu:" +msgstr "" #: html/pkgmerge.php msgid "Once the package has been merged it cannot be reversed. " @@ -627,7 +627,7 @@ msgstr "" #: html/pkgmerge.php msgid "Enter the package name you wish to merge the package into. " -msgstr "Introduz el nome del paquete al que deseyes amestar" +msgstr "" #: html/pkgmerge.php msgid "Merge into:" @@ -651,7 +651,7 @@ msgstr "" #: html/pkgreq.php template/pkgreq_close_form.php msgid "Close Request" -msgstr "Zarar solicitú" +msgstr "" #: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" @@ -663,7 +663,7 @@ msgstr "" #: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php msgid "Next" -msgstr "Siguiente" +msgstr "" #: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "Last" @@ -671,15 +671,15 @@ msgstr "" #: html/pkgreq.php template/header.php msgid "Requests" -msgstr "Solicitúes" +msgstr "" #: html/register.php template/header.php msgid "Register" -msgstr "Rexistrase" +msgstr "" #: html/register.php msgid "Use this form to create an account." -msgstr "Usa esti formulariu pa crear una cuenta." +msgstr "" #: html/tos.php msgid "Terms of Service" @@ -725,19 +725,19 @@ msgstr "" #: html/tu.php msgid "Vote ID not valid." -msgstr "Nun ye válida la ID del votu." +msgstr "" #: html/tu.php template/tu_list.php msgid "Current Votes" -msgstr "Votos actuales" +msgstr "" #: html/tu.php msgid "Past Votes" -msgstr "Votos pasaos" +msgstr "" #: html/voters.php template/tu_details.php msgid "Voters" -msgstr "Votantes" +msgstr "" #: lib/acctfuncs.inc.php msgid "" @@ -747,16 +747,16 @@ msgstr "" #: lib/acctfuncs.inc.php msgid "Missing User ID" -msgstr "Falta la ID d'usuariu" +msgstr "" #: lib/acctfuncs.inc.php msgid "The username is invalid." -msgstr "El nome d'usuariu nun ye válidu" +msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "It must be between %s and %s characters long" -msgstr "Tien de tar ente %s y %s carauteres de llargor" +msgstr "" #: lib/acctfuncs.inc.php msgid "Start and end with a letter or number" @@ -772,7 +772,7 @@ msgstr "" #: lib/acctfuncs.inc.php msgid "The email address is invalid." -msgstr "La direición de corréu nun ye válida." +msgstr "" #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." @@ -784,19 +784,19 @@ msgstr "" #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." -msgstr "La buelga de la clave PGP nun ye válida." +msgstr "" #: lib/acctfuncs.inc.php msgid "The SSH public key is invalid." -msgstr "La clave SSH pública nun ye válida." +msgstr "" #: lib/acctfuncs.inc.php msgid "Cannot increase account permissions." -msgstr "Nun puen aumentase los permisos de la cuenta." +msgstr "" #: lib/acctfuncs.inc.php msgid "Language is not currently supported." -msgstr "La llingua nun ta anguaño sofitada." +msgstr "" #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." @@ -805,17 +805,17 @@ msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "The username, %s%s%s, is already in use." -msgstr "El nome d'usuariu, %s%s%s, yá ta n'usu." +msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "The address, %s%s%s, is already in use." -msgstr "La direición, %s%s%s, yá ta n'usu." +msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "The SSH public key, %s%s%s, is already in use." -msgstr "La llave pública SSH, %s%s%s, ye yá n'usu." +msgstr "" #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." @@ -832,12 +832,12 @@ msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." -msgstr "Fallu intentando crear la cuenta, %s%s%s." +msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully created." -msgstr "La cuenta, %s%s%s, creóse con ésitu." +msgstr "" #: lib/acctfuncs.inc.php msgid "A password reset key has been sent to your e-mail address." @@ -845,17 +845,17 @@ msgstr "" #: lib/acctfuncs.inc.php msgid "Click on the Login link above to use your account." -msgstr "Primi nel enllaz d'aniciu de sesión d'enriba pa usar la to cuenta" +msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "No changes were made to the account, %s%s%s." -msgstr "Nun se fixeron camudancies na cuenta, %s%s%s." +msgstr "" #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully modified." -msgstr "La cuenta, %s%s%s, modificóse con ésitu." +msgstr "" #: lib/acctfuncs.inc.php msgid "" @@ -865,7 +865,7 @@ msgstr "" #: lib/acctfuncs.inc.php msgid "Account suspended" -msgstr "Cuenta suspendida" +msgstr "" #: lib/acctfuncs.inc.php #, php-format @@ -873,15 +873,15 @@ msgid "" "Your password has been reset. If you just created a new account, please use " "the link from the confirmation email to set an initial password. Otherwise, " "please request a reset key on the %sPassword Reset%s page." -msgstr "Reanicióse la to contraseña. Si tas acabantes crear una cuenta nueva, usa l'enllaz del corréu de confirmación p'afitar una contraseña inicial, por favor. D'otramiente, solicita una clave de reaniciu na páxina %sReaniciu de contraseña%s, por favor." +msgstr "" #: lib/acctfuncs.inc.php msgid "Bad username or password." -msgstr "Nome d'usuariu o contraseña incorreutos" +msgstr "" #: lib/acctfuncs.inc.php msgid "An error occurred trying to generate a user session." -msgstr "Asocedió un fallu intentando xenerar una sesión d'usuariu." +msgstr "" #: lib/acctfuncs.inc.php msgid "Invalid e-mail and reset key combination." @@ -902,19 +902,19 @@ msgstr "" #: lib/aurjson.class.php lib/pkgbasefuncs.inc.php msgid "You are not allowed to edit this comment." -msgstr "Nun tienes permisu pa editar esti comentariu." +msgstr "" #: lib/aurjson.class.php msgid "Comment does not exist." -msgstr "Nun esiste'l comentariu." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "Comment cannot be empty." -msgstr "Nun pue tar baleru'l comentariu." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "Comment has been added." -msgstr "Amestóse'l comentariu." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can edit package information." @@ -922,7 +922,7 @@ msgstr "" #: lib/pkgbasefuncs.inc.php msgid "Missing comment ID." -msgstr "Falta la ID del comentariu." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "No more than 5 comments can be pinned." @@ -946,11 +946,11 @@ msgstr "" #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Error retrieving package details." -msgstr "Fallu recibiendo los detalles de paquete." +msgstr "" #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Package details could not be found." -msgstr "Nun pudieron alcontrase los detalles del paquete." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -986,11 +986,11 @@ msgstr "" #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to delete." -msgstr "Nun esbillesti dengún ficheru pa desaniciar." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." -msgstr "Desaniciáronse los paquetes esbillaos." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can adopt packages." @@ -1038,7 +1038,7 @@ msgstr "" #: lib/pkgbasefuncs.inc.php msgid "Couldn't add to notification list." -msgstr "Nun pudo amestase al llistáu d'avisos." +msgstr "" #: lib/pkgbasefuncs.inc.php #, php-format @@ -1060,15 +1060,15 @@ msgstr "" #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to delete this comment." -msgstr "Nun tienes permisu pa desaniciar esti comentariu." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "Comment has been deleted." -msgstr "El comentariu amestóse." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "Comment has been edited." -msgstr "Editóse'l comentariu." +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to edit the keywords of this package base." @@ -1085,7 +1085,7 @@ msgstr "" #: lib/pkgbasefuncs.inc.php #, php-format msgid "Invalid user name: %s" -msgstr "Nome d'usuariu non válidu: %s" +msgstr "" #: lib/pkgbasefuncs.inc.php msgid "The package base co-maintainers have been updated." @@ -1093,7 +1093,7 @@ msgstr "" #: lib/pkgfuncs.inc.php template/pkgbase_details.php msgid "View packages details for" -msgstr "Ver detalles de paquetes pa" +msgstr "" #: lib/pkgfuncs.inc.php #, php-format @@ -1110,19 +1110,19 @@ msgstr "" #: lib/pkgreqfuncs.inc.php msgid "The comment field must not be empty." -msgstr "El campu del comentariu nun tien de tar baleru." +msgstr "" #: lib/pkgreqfuncs.inc.php msgid "Invalid request type." -msgstr "Triba de solicitú non válida." +msgstr "" #: lib/pkgreqfuncs.inc.php msgid "Added request successfully." -msgstr "Amestada con ésitu la solicitú." +msgstr "" #: lib/pkgreqfuncs.inc.php msgid "Invalid reason." -msgstr "Razón non válida." +msgstr "" #: lib/pkgreqfuncs.inc.php msgid "Only TUs and developers can close requests." @@ -1130,41 +1130,41 @@ msgstr "" #: lib/pkgreqfuncs.inc.php msgid "Request closed successfully." -msgstr "Solicitú zarrada con ésitu." +msgstr "" #: template/account_delete.php #, php-format msgid "You can use this form to permanently delete the AUR account %s." -msgstr "Pues usar esti formulariu pa desaniciar la cuenta del AUR %s dafechu." +msgstr "" #: template/account_delete.php #, php-format msgid "%sWARNING%s: This action cannot be undone." -msgstr "%sAVISU%s: Esta aición nun pue desfacese." +msgstr "" #: template/account_delete.php msgid "Confirm deletion" -msgstr "Confirmar desaniciu" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php msgid "Username" -msgstr "Nome d'usuariu" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Account Type" -msgstr "Triba de cuenta" +msgstr "" #: template/account_details.php template/tu_details.php #: template/tu_last_votes_list.php template/tu_list.php msgid "User" -msgstr "Usuariu" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Developer" -msgstr "Desendolcador" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1174,7 +1174,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Email Address" -msgstr "Direición de corréu" +msgstr "" #: template/account_details.php msgid "hidden" @@ -1183,7 +1183,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php msgid "Real Name" -msgstr "Nome real" +msgstr "" #: template/account_details.php template/account_edit_form.php msgid "Homepage" @@ -1192,25 +1192,25 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php msgid "IRC Nick" -msgstr "Alcuñu nel IRC" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php msgid "PGP Key Fingerprint" -msgstr "Buelga de clave PGP" +msgstr "" #: template/account_details.php template/account_search_results.php #: template/pkgreq_results.php msgid "Status" -msgstr "Estáu" +msgstr "" #: template/account_details.php msgid "Inactive since" -msgstr "Inactivu dende" +msgstr "" #: template/account_details.php template/account_search_results.php msgid "Active" -msgstr "Activu" +msgstr "" #: template/account_details.php msgid "Registration date:" @@ -1220,7 +1220,7 @@ msgstr "" #: template/pkg_details.php template/pkgreq_results.php #: template/tu_details.php msgid "unknown" -msgstr "Desconocíu" +msgstr "" #: template/account_details.php msgid "Last Login" @@ -1228,7 +1228,7 @@ msgstr "" #: template/account_details.php msgid "Never" -msgstr "Enxamás" +msgstr "" #: template/account_details.php msgid "View this user's packages" @@ -1245,7 +1245,7 @@ msgstr "" #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." -msgstr "Primi %sequí%s si quies desaniciar esta cuenta dafechu." +msgstr "" #: template/account_edit_form.php #, php-format @@ -1259,7 +1259,7 @@ msgstr "" #: template/account_edit_form.php msgid "required" -msgstr "riquíu" +msgstr "" #: template/account_edit_form.php msgid "" @@ -1269,19 +1269,19 @@ msgstr "" #: template/account_edit_form.php template/search_accounts_form.php msgid "Normal user" -msgstr "Usuariu normal" +msgstr "" #: template/account_edit_form.php template/search_accounts_form.php msgid "Trusted user" -msgstr "Usuariu d'Enfotu" +msgstr "" #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" -msgstr "Cuenta suspendida" +msgstr "" #: template/account_edit_form.php msgid "Inactive" -msgstr "Inactivu" +msgstr "" #: template/account_edit_form.php msgid "" @@ -1325,7 +1325,7 @@ msgstr "" #: template/account_edit_form.php msgid "Language" -msgstr "Llingua" +msgstr "" #: template/account_edit_form.php msgid "Timezone" @@ -1339,17 +1339,17 @@ msgstr "" #: template/account_edit_form.php msgid "Re-type password" -msgstr "Teclexa de nueves la contraseña" +msgstr "" #: template/account_edit_form.php msgid "" "The following information is only required if you want to submit packages to" " the Arch User Repository." -msgstr "La información de darréu namái se rique si quies xubir paquetes al AUR." +msgstr "" #: template/account_edit_form.php msgid "SSH Public Key" -msgstr "Clave SSH pública" +msgstr "" #: template/account_edit_form.php msgid "Notification settings" @@ -1388,15 +1388,15 @@ msgstr "" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" -msgstr "Anovar" +msgstr "" #: template/account_edit_form.php msgid "Create" -msgstr "Crear" +msgstr "" #: template/account_edit_form.php template/search_accounts_form.php msgid "Reset" -msgstr "Reafitar" +msgstr "" #: template/account_search_results.php msgid "No results matched your search criteria." @@ -1404,27 +1404,27 @@ msgstr "" #: template/account_search_results.php msgid "Edit Account" -msgstr "Editar cuenta" +msgstr "" #: template/account_search_results.php msgid "Suspended" -msgstr "Suspendíu" +msgstr "" #: template/account_search_results.php msgid "Edit" -msgstr "Editar" +msgstr "" #: template/account_search_results.php msgid "Less" -msgstr "Menos" +msgstr "" #: template/account_search_results.php msgid "More" -msgstr "Más" +msgstr "" #: template/account_search_results.php msgid "No more results to display." -msgstr "Nun hai más resultaos p'amosar." +msgstr "" #: template/comaintainers_form.php #, php-format @@ -1434,11 +1434,11 @@ msgstr "" #: template/comaintainers_form.php msgid "Users" -msgstr "Usuarios" +msgstr "" #: template/comaintainers_form.php template/pkg_comment_form.php msgid "Save" -msgstr "Guardar" +msgstr "" #: template/flag_comment.php #, php-format @@ -1466,27 +1466,27 @@ msgstr "" #: template/header.php msgid " My Account" -msgstr "La mio cuenta" +msgstr "" #: template/pkgbase_actions.php msgid "Package Actions" -msgstr "Aiciones de paquete" +msgstr "" #: template/pkgbase_actions.php msgid "View PKGBUILD" -msgstr "Ver PKGBUILD" +msgstr "" #: template/pkgbase_actions.php msgid "View Changes" -msgstr "Ver camudancies" +msgstr "" #: template/pkgbase_actions.php msgid "Download snapshot" -msgstr "Baxar instantánea" +msgstr "" #: template/pkgbase_actions.php msgid "Search wiki" -msgstr "Guetar na wiki" +msgstr "" #: template/pkgbase_actions.php #, php-format @@ -1495,19 +1495,19 @@ msgstr "" #: template/pkgbase_actions.php msgid "Flag package out-of-date" -msgstr "Marcáu como non actualizáu" +msgstr "" #: template/pkgbase_actions.php msgid "Unflag package" -msgstr "Quitar marca de non actualizáu" +msgstr "" #: template/pkgbase_actions.php msgid "Remove vote" -msgstr "Quitar votu" +msgstr "" #: template/pkgbase_actions.php msgid "Vote for this package" -msgstr "Votar pol paquete" +msgstr "" #: template/pkgbase_actions.php scripts/notify.py msgid "Disable notifications" @@ -1519,7 +1519,7 @@ msgstr "" #: template/pkgbase_actions.php msgid "Manage Co-Maintainers" -msgstr "Alministra comantenedores" +msgstr "" #: template/pkgbase_actions.php #, php-format @@ -1534,11 +1534,11 @@ msgstr "" #: template/pkgbase_details.php msgid "Package Base Details" -msgstr "Detalles del paquete base" +msgstr "" #: template/pkgbase_details.php template/pkg_details.php msgid "Git Clone URL" -msgstr "URL pa clonar con Git" +msgstr "" #: template/pkgbase_details.php template/pkg_details.php msgid "read-only" @@ -1551,7 +1551,7 @@ msgstr "" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" -msgstr "Pallabres clave" +msgstr "" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1561,7 +1561,7 @@ msgstr "" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php msgid "Maintainer" -msgstr "Caltenedor" +msgstr "" #: template/pkgbase_details.php template/pkg_details.php msgid "Last Packager" @@ -1570,7 +1570,7 @@ msgstr "" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php msgid "Votes" -msgstr "Votos" +msgstr "" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php @@ -1592,7 +1592,7 @@ msgstr "" #: template/pkg_comment_box.php template/pkg_comment_form.php msgid "Add Comment" -msgstr "Amestar comentariu" +msgstr "" #: template/pkg_comment_form.php msgid "" @@ -1611,7 +1611,7 @@ msgstr "" #: template/pkg_comments.php msgid "Latest Comments" -msgstr "Comentarios caberos" +msgstr "" #: template/pkg_comments.php msgid "Comments for" @@ -1658,7 +1658,7 @@ msgstr "" #: template/pkg_comments.php msgid "Delete comment" -msgstr "Desaniciar comentariu" +msgstr "" #: template/pkg_comments.php msgid "Pin comment" @@ -1670,15 +1670,15 @@ msgstr "" #: template/pkg_details.php msgid "Package Details" -msgstr "Detalles del paquete" +msgstr "" #: template/pkg_details.php template/pkg_search_form.php msgid "Package Base" -msgstr "Paquete base" +msgstr "" #: template/pkg_details.php template/pkg_search_results.php msgid "Description" -msgstr "Descripción" +msgstr "" #: template/pkg_details.php msgid "Upstream URL" @@ -1690,11 +1690,11 @@ msgstr "" #: template/pkg_details.php msgid "Licenses" -msgstr "Llicencies" +msgstr "" #: template/pkg_details.php msgid "Groups" -msgstr "Grupos" +msgstr "" #: template/pkg_details.php msgid "Conflicts" @@ -1702,48 +1702,48 @@ msgstr "" #: template/pkg_details.php msgid "Provides" -msgstr "Apurre" +msgstr "" #: template/pkg_details.php msgid "Replaces" -msgstr "Troca" +msgstr "" #: template/pkg_details.php msgid "Dependencies" -msgstr "Dependencies" +msgstr "" #: template/pkg_details.php msgid "Required by" -msgstr "Riquíu por" +msgstr "" #: template/pkg_details.php msgid "Sources" -msgstr "Fontes" +msgstr "" #: template/pkgreq_close_form.php #, php-format msgid "Use this form to close the request for package base %s%s%s." -msgstr "Usa esti formulariu pa zarrar la solicitú pal paquete base %s%s%s." +msgstr "" #: template/pkgreq_close_form.php msgid "" "The comments field can be left empty. However, it is highly recommended to " "add a comment when rejecting a request." -msgstr "El campu de comentarios pue dexase baleru, Por embargu, encamiéntase amestar un comentariu al refugar una solicitú." +msgstr "" #: template/pkgreq_close_form.php msgid "Reason" -msgstr "Razón" +msgstr "" #: template/pkgreq_close_form.php template/pkgreq_results.php #: template/tu_details.php msgid "Accepted" -msgstr "Aceutáu" +msgstr "" #: template/pkgreq_close_form.php template/pkgreq_results.php #: template/tu_details.php msgid "Rejected" -msgstr "Refugáu" +msgstr "" #: template/pkgreq_form.php #, php-format @@ -1754,15 +1754,15 @@ msgstr "" #: template/pkgreq_form.php msgid "Request type" -msgstr "Triba de solicitú" +msgstr "" #: template/pkgreq_form.php msgid "Deletion" -msgstr "Desaniciu" +msgstr "" #: template/pkgreq_form.php msgid "Orphan" -msgstr "Güérfanu" +msgstr "" #: template/pkgreq_form.php template/pkg_search_results.php msgid "Merge into" @@ -1805,11 +1805,11 @@ msgstr[1] "" #: template/pkgreq_results.php template/pkg_search_results.php #, php-format msgid "Page %d of %d." -msgstr "Páxina %d de %d" +msgstr "" #: template/pkgreq_results.php msgid "Package" -msgstr "Paquete" +msgstr "" #: template/pkgreq_results.php msgid "Filed by" @@ -1817,7 +1817,7 @@ msgstr "" #: template/pkgreq_results.php msgid "Date" -msgstr "Data" +msgstr "" #: template/pkgreq_results.php #, php-format @@ -1830,24 +1830,24 @@ msgstr[1] "" #, php-format msgid "~%d hour left" msgid_plural "~%d hours left" -msgstr[0] "Falta ~%d hora" -msgstr[1] "Falten ~%d hores" +msgstr[0] "" +msgstr[1] "" #: template/pkgreq_results.php msgid "<1 hour left" -msgstr "Falta menos d'una hora" +msgstr "" #: template/pkgreq_results.php msgid "Accept" -msgstr "Aceutar" +msgstr "" #: template/pkgreq_results.php msgid "Locked" -msgstr "Bloquiáu" +msgstr "" #: template/pkgreq_results.php msgid "Close" -msgstr "Zarrar" +msgstr "" #: template/pkgreq_results.php msgid "Pending" @@ -1855,23 +1855,23 @@ msgstr "" #: template/pkgreq_results.php msgid "Closed" -msgstr "Zarráu" +msgstr "" #: template/pkg_search_form.php msgid "Name, Description" -msgstr "Nome, descripción" +msgstr "" #: template/pkg_search_form.php msgid "Name Only" -msgstr "Namái nome" +msgstr "" #: template/pkg_search_form.php msgid "Exact Name" -msgstr "Nome exautu" +msgstr "" #: template/pkg_search_form.php msgid "Exact Package Base" -msgstr "Paquete base exautu" +msgstr "" #: template/pkg_search_form.php msgid "Co-maintainer" @@ -1883,7 +1883,7 @@ msgstr "" #: template/pkg_search_form.php msgid "All" -msgstr "Too" +msgstr "" #: template/pkg_search_form.php msgid "Flagged" @@ -1895,7 +1895,7 @@ msgstr "" #: template/pkg_search_form.php template/pkg_search_results.php msgid "Name" -msgstr "Nome" +msgstr "" #: template/pkg_search_form.php template/pkg_search_results.php #: template/tu_details.php template/tu_list.php @@ -1908,11 +1908,11 @@ msgstr "" #: template/pkg_search_form.php msgid "Ascending" -msgstr "Ascendente" +msgstr "" #: template/pkg_search_form.php msgid "Descending" -msgstr "Descendente" +msgstr "" #: template/pkg_search_form.php msgid "Enter search criteria" @@ -1920,31 +1920,31 @@ msgstr "" #: template/pkg_search_form.php msgid "Search by" -msgstr "Guetar per" +msgstr "" #: template/pkg_search_form.php template/stats/user_table.php msgid "Out of Date" -msgstr "Ensin anovar" +msgstr "" #: template/pkg_search_form.php template/search_accounts_form.php msgid "Sort by" -msgstr "Ordenar per" +msgstr "" #: template/pkg_search_form.php msgid "Sort order" -msgstr "Mou d'ordenación" +msgstr "" #: template/pkg_search_form.php msgid "Per page" -msgstr "Per páxina" +msgstr "" #: template/pkg_search_form.php template/pkg_search_results.php msgid "Go" -msgstr "Dir" +msgstr "" #: template/pkg_search_form.php msgid "Orphans" -msgstr "Güérfanos" +msgstr "" #: template/pkg_search_results.php msgid "Error retrieving package list." @@ -1952,18 +1952,18 @@ msgstr "" #: template/pkg_search_results.php msgid "No packages matched your search criteria." -msgstr "Nun hai paquetes que concasen col criteriu de gueta." +msgstr "" #: template/pkg_search_results.php #, php-format msgid "%d package found." msgid_plural "%d packages found." -msgstr[0] "Alcontróse %d paquete." -msgstr[1] "Alcontráronse %d paquetes" +msgstr[0] "" +msgstr[1] "" #: template/pkg_search_results.php msgid "Version" -msgstr "Versión" +msgstr "" #: template/pkg_search_results.php #, php-format @@ -1975,15 +1975,15 @@ msgstr "" #: template/pkg_search_results.php template/tu_details.php #: template/tu_list.php msgid "Yes" -msgstr "Sí" +msgstr "" #: template/pkg_search_results.php msgid "orphan" -msgstr "güérfanu" +msgstr "" #: template/pkg_search_results.php msgid "Actions" -msgstr "Aiciones" +msgstr "" #: template/pkg_search_results.php msgid "Unflag Out-of-date" @@ -2015,39 +2015,39 @@ msgstr "" #: template/stats/general_stats_table.php msgid "Statistics" -msgstr "Estadístiques" +msgstr "" #: template/stats/general_stats_table.php msgid "Orphan Packages" -msgstr "Paquetes güérfanos" +msgstr "" #: template/stats/general_stats_table.php msgid "Packages added in the past 7 days" -msgstr "Paquetes amestaos nos pasaos 7 díes" +msgstr "" #: template/stats/general_stats_table.php msgid "Packages updated in the past 7 days" -msgstr "Paquetes anovaos nos pasaos 7 díes" +msgstr "" #: template/stats/general_stats_table.php msgid "Packages updated in the past year" -msgstr "Paquetes anovaos nel añu caberu" +msgstr "" #: template/stats/general_stats_table.php msgid "Packages never updated" -msgstr "Paquetes qu'enxamás s'anovaron" +msgstr "" #: template/stats/general_stats_table.php msgid "Registered Users" -msgstr "Usuarios rexistraos." +msgstr "" #: template/stats/general_stats_table.php msgid "Trusted Users" -msgstr "Usuarios d'enfotu." +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" -msgstr "Anovamientos recientes" +msgstr "" #: template/stats/updates_table.php msgid "more" @@ -2055,7 +2055,7 @@ msgstr "" #: template/stats/user_table.php msgid "My Statistics" -msgstr "Les mios estadístiques" +msgstr "" #: template/tu_details.php msgid "Proposal Details" @@ -2072,27 +2072,27 @@ msgstr "" #: template/tu_details.php template/tu_list.php msgid "End" -msgstr "Fin" +msgstr "" #: template/tu_details.php msgid "Result" -msgstr "Resultáu" +msgstr "" #: template/tu_details.php template/tu_list.php msgid "No" -msgstr "Non" +msgstr "" #: template/tu_details.php msgid "Abstain" -msgstr "Astención" +msgstr "" #: template/tu_details.php msgid "Total" -msgstr "Total" +msgstr "" #: template/tu_details.php msgid "Participation" -msgstr "Participación" +msgstr "" #: template/tu_last_votes_list.php msgid "Last Votes by TU" @@ -2104,7 +2104,7 @@ msgstr "" #: template/tu_last_votes_list.php template/tu_list.php msgid "No results found." -msgstr "Nun s'alcontró dengún resultáu." +msgstr "" #: template/tu_list.php msgid "Start" diff --git a/po/ja.po b/po/ja.po index 337e7ee8..d51319d2 100644 --- a/po/ja.po +++ b/po/ja.po @@ -5,15 +5,15 @@ # Translators: # kusakata, 2013 # kusakata, 2013 -# kusakata, 2013-2018 +# kusakata, 2013-2018,2020 # 尾ノ上卓朗 , 2017 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-01-31 08:29+0000\n" -"Last-Translator: Lukas Fleischer\n" +"PO-Revision-Date: 2020-02-26 12:49+0000\n" +"Last-Translator: kusakata\n" "Language-Team: Japanese (http://www.transifex.com/lfleischer/aurweb/language/ja/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -78,7 +78,7 @@ msgstr "あなたはこのアカウントを編集する権利を持っていま #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "不正なパスワード。" #: html/account.php msgid "Use this form to search existing accounts." @@ -375,7 +375,7 @@ msgstr "ログイン情報を入力してください" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "ユーザー名またはメインのメールアドレス" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -439,7 +439,7 @@ msgstr "パスワードのリセットが成功しました。" #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "ユーザー名またはメインのメールアドレスの確認:" #: html/passreset.php msgid "Enter your new password:" @@ -458,11 +458,11 @@ msgstr "続行" msgid "" "If you have forgotten the user name and the primary e-mail address you used " "to register, please send a message to the %saur-general%s mailing list." -msgstr "" +msgstr "登録したときに使用したユーザー名とメインのメールアドレスを忘れてしまった場合、メッセージを %saur-general%s メーリングリストに送信してください。" #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "ユーザー名またはメインのメールアドレスを入力:" #: html/pkgbase.php msgid "Package Bases" @@ -769,7 +769,7 @@ msgstr "ピリオド、アンダーライン、ハイフンはひとつだけ含 #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "新しいパスワードを確認してください。" #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -777,7 +777,7 @@ msgstr "メールアドレスが不正です。" #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "バックアップメールアドレスが不正です。" #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -820,15 +820,15 @@ msgstr "SSH 公開鍵、%s%s%s は既に使われています。" #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "CAPTCHA が入力されていません。" #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "CAPTCHA の有効期限が切れました。もう一度入力してください。" #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "入力された CAPTCHA の答えが合っていません。" #: lib/acctfuncs.inc.php #, php-format @@ -1241,7 +1241,7 @@ msgstr "このユーザーのアカウントを編集" #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "このユーザーのコメントを表示" #: template/account_edit_form.php #, php-format @@ -1256,7 +1256,7 @@ msgstr "ユーザーの詳細は%sこちら%sをクリック。" #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "このアカウントによって作成されたコメントを表示するには %sこちら%s をクリック。" #: template/account_edit_form.php msgid "required" @@ -1299,30 +1299,30 @@ msgid "" "If you do not hide your email address, it is visible to all registered AUR " "users. If you hide your email address, it is visible to members of the Arch " "Linux staff only." -msgstr "" +msgstr "メールアドレスを非公開にしない場合、登録されている AUR ユーザー全てから閲覧できる状態になります。メールアドレスを非表示にした場合、Arch Linux のスタッフからしか表示されません。" #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "バックアップメールアドレス" #: template/account_edit_form.php msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "" +msgstr "メールアドレスをふたつ登録することで、メインのメールアドレスが使えなくなってしまった場合にアカウントを復帰することができます。" #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "パスワードのリセットリンクはメインとバックアップ両方のメールアドレスに送信されます。" #: template/account_edit_form.php #, php-format msgid "" "Your backup email address is always only visible to members of the Arch " "Linux staff, independent of the %s setting." -msgstr "" +msgstr "%s の設定に関わらず、バックアップメールアドレスを確認できるのは Arch Linux のスタッフメンバーだけです。" #: template/account_edit_form.php msgid "Language" @@ -1336,7 +1336,7 @@ msgstr "タイムゾーン" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "パスワードを変更したい場合、新しいパスワードを入力して、もう一度確認のため新しいパスワードを入力してください。" #: template/account_edit_form.php msgid "Re-type password" @@ -1370,21 +1370,21 @@ msgstr "所有者の変更の通知" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "プロフィールの変更を確認するため、現在のパスワードを入力してください:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "現在のパスワード" #: template/account_edit_form.php msgid "" "To protect the AUR against automated account creation, we kindly ask you to " "provide the output of the following command:" -msgstr "" +msgstr "機械的なアカウント作成から AUR を保護するため、次のコマンドの出力結果を入力してください:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "答え" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1546,7 +1546,7 @@ msgstr "リードオンリー" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "クリックしてコピー" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1598,12 +1598,12 @@ msgstr "コメントを投稿する" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "AUR パッケージリポジトリの Git コミット ID や URL は自動的にリンクに変換されます。" #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "%sMarkdown 構文%s が一部サポートされています。" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1615,7 +1615,7 @@ msgstr "最新のコメント" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "コメント履歴" #: template/pkg_comments.php #, php-format @@ -1630,7 +1630,7 @@ msgstr "匿名ユーザーが %s にコメントを投稿しました" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "%s パッケージのコメント欄に %s に投稿したコメント" #: template/pkg_comments.php #, php-format @@ -2212,7 +2212,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] によって {old} [2] は {new} [3] にマージされました。\n\n-- \n新しいパッケージの通知を受け取りたくない場合、[3] を開いて \"{label}\" をクリックしてください。" #: scripts/notify.py #, python-brace-format From 279d8042e3889402079a4d0eaf10a386fdd4a2a9 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 27 Mar 2020 08:37:46 -0400 Subject: [PATCH 0085/1451] Add new upgrade instructions Signed-off-by: Lukas Fleischer --- upgrading/{4.9.0.txt => 5.x.x.txt} | 8 ++++++++ 1 file changed, 8 insertions(+) rename upgrading/{4.9.0.txt => 5.x.x.txt} (54%) diff --git a/upgrading/4.9.0.txt b/upgrading/5.x.x.txt similarity index 54% rename from upgrading/4.9.0.txt rename to upgrading/5.x.x.txt index 241f24af..94f91c69 100644 --- a/upgrading/4.9.0.txt +++ b/upgrading/5.x.x.txt @@ -1,3 +1,11 @@ +Starting from release 5.0.0, Alembic is used for managing database migrations. + +Run `alembic upgrade head` from the aurweb root directory to upgrade your +database after upgrading the source code to a new release. + +When upgrading from 4.8.0, you also need to execute the following manual SQL +statements before doing so. + 1. Add new columns to store the timestamp and UID when closing requests: ---- From 853ed9a950cc3b70b57be3deb6d20c131c0cbfe3 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Fri, 27 Mar 2020 08:51:15 -0400 Subject: [PATCH 0086/1451] Release 5.0.0 Signed-off-by: Lukas Fleischer --- web/lib/version.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/version.inc.php b/web/lib/version.inc.php index 93ab51b2..27eef718 100644 --- a/web/lib/version.inc.php +++ b/web/lib/version.inc.php @@ -1,3 +1,3 @@ Date: Sun, 5 Apr 2020 10:56:35 -0400 Subject: [PATCH 0087/1451] Fix invalid session ID check Signed-off-by: Lukas Fleischer --- web/lib/aur.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/aur.inc.php b/web/lib/aur.inc.php index dbcc23a4..f4ad6b47 100644 --- a/web/lib/aur.inc.php +++ b/web/lib/aur.inc.php @@ -50,7 +50,7 @@ function check_sid() { $result = $dbh->query($q); $row = $result->fetch(PDO::FETCH_NUM); - if (!$row[0]) { + if (!$row) { # Invalid SessionID - hacker alert! # $failed = 1; From 169607f153f1dcb7bb8e7ebea1c53bac93d376b3 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Sun, 5 Apr 2020 11:00:18 -0400 Subject: [PATCH 0088/1451] Fix PHP notices in the account form Signed-off-by: Lukas Fleischer --- web/html/account.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/html/account.php b/web/html/account.php index c05d136d..d70f4ced 100644 --- a/web/html/account.php +++ b/web/html/account.php @@ -25,7 +25,7 @@ if ($action == "UpdateAccount") { $update_account_message = ''; /* Details for account being updated */ /* Verify user permissions and that the request is a valid POST */ - if (can_edit_account($row) && check_token()) { + if ($row && can_edit_account($row) && check_token()) { /* Update the details for the existing account */ list($success, $update_account_message) = process_account_form( "edit", "UpdateAccount", @@ -55,7 +55,7 @@ if ($action == "UpdateAccount") { } } -if ($action == "AccountInfo") { +if ($row && $action == "AccountInfo") { html_header(__('Account') . ' ' . $row['Username']); } else { html_header(__('Accounts')); @@ -122,7 +122,7 @@ if (isset($_COOKIE["AURSID"])) { } elseif ($action == "DeleteAccount") { /* Details for account being deleted. */ - if (can_edit_account($row)) { + if ($row && can_edit_account($row)) { $uid_removal = $row['ID']; $uid_session = uid_from_sid($_COOKIE['AURSID']); $username = $row['Username']; @@ -155,7 +155,7 @@ if (isset($_COOKIE["AURSID"])) { } elseif ($action == "UpdateAccount") { print $update_account_message; - if (!$success) { + if ($row && !$success) { display_account_form("UpdateAccount", in_request("U"), in_request("T"), @@ -181,7 +181,7 @@ if (isset($_COOKIE["AURSID"])) { } } elseif ($action == "ListComments") { - if (has_credential(CRED_ACCOUNT_LIST_COMMENTS, array($row["ID"]))) { + if ($row && has_credential(CRED_ACCOUNT_LIST_COMMENTS, array($row["ID"]))) { # display the comment list if they're a TU/dev $total_comment_count = account_comments_count($row["ID"]); From 03a6fa2f7ec927625c64f980c3408ed395a9dcfc Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sat, 22 Aug 2020 22:08:43 +0200 Subject: [PATCH 0089/1451] Call sendmail with to, not recipient After f7a57c8 (Localize notification emails, 2018-05-17), the server.sendmail line was not updated to now send the to the email address but instead sends to (email, 'en') and as sendmail accepts an iterable an email is also send to 'en'. Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 5b18a476..899f8acc 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -116,7 +116,7 @@ class Notification: server.login(user, passwd) server.set_debuglevel(0) - server.sendmail(sender, recipient, msg.as_bytes()) + server.sendmail(sender, to, msg.as_bytes()) server.quit() From c4f4ac510be1898e6969d13dd4f37c0a3f807aff Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Thu, 27 Aug 2020 07:11:17 -0400 Subject: [PATCH 0090/1451] Deliver emails to Cc in smtplib code path When using the sendmail() function with smtplib.SMTP or smtplib.SMTP_SSL, the list of actual recipients for the email (to be translated to RCPT commands) has to be provided as a parameter. Update the notification script and add all Cc recipients to that parameter. Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 18 ++++++++++++------ test/t2500-notify.t | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 899f8acc..9d4f3bde 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -14,10 +14,6 @@ import aurweb.l10n aur_location = aurweb.config.get('options', 'aur_location') -def headers_cc(cclist): - return {'Cc': str.join(', ', cclist)} - - def headers_msgid(thread_id): return {'Message-ID': thread_id} @@ -53,6 +49,9 @@ class Notification: def get_headers(self): return {} + def get_cc(self): + return [] + def get_body_fmt(self, lang): body = '' for line in self.get_body(lang).splitlines(): @@ -80,6 +79,8 @@ class Notification: msg['From'] = sender msg['Reply-to'] = reply_to msg['To'] = to + if self.get_cc(): + msg['Cc'] = str.join(', ', self.get_cc()) msg['X-AUR-Reason'] = reason msg['Date'] = email.utils.formatdate(localtime=True) @@ -116,6 +117,7 @@ class Notification: server.login(user, passwd) server.set_debuglevel(0) + deliver_to = [to] + self.get_cc() server.sendmail(sender, to, msg.as_bytes()) server.quit() @@ -444,6 +446,9 @@ class RequestOpenNotification(Notification): def get_recipients(self): return [(self._to, 'en')] + def get_cc(self): + return self._cc + def get_subject(self, lang): return '[PRQ#%d] %s Request for %s' % \ (self._reqid, self._reqtype.title(), self._pkgbase) @@ -472,7 +477,6 @@ class RequestOpenNotification(Notification): # Use a deterministic Message-ID for the first email referencing a # request. headers = headers_msgid(thread_id) - headers.update(headers_cc(self._cc)) return headers @@ -502,6 +506,9 @@ class RequestCloseNotification(Notification): def get_recipients(self): return [(self._to, 'en')] + def get_cc(self): + return self._cc + def get_subject(self, lang): return '[PRQ#%d] %s Request for %s %s' % (self._reqid, self._reqtype.title(), @@ -531,7 +538,6 @@ class RequestCloseNotification(Notification): def get_headers(self): thread_id = '' headers = headers_reply(thread_id) - headers.update(headers_cc(self._cc)) return headers diff --git a/test/t2500-notify.t b/test/t2500-notify.t index 5ef64c18..713b31e3 100755 --- a/test/t2500-notify.t +++ b/test/t2500-notify.t @@ -277,13 +277,18 @@ test_expect_success 'Test subject and body of merge notifications.' ' test_cmp actual expected ' -test_expect_success 'Test subject and body of request open notifications.' ' +test_expect_success 'Test Cc, subject and body of request open notifications.' ' cat <<-EOD | sqlite3 aur.db && /* Use package request IDs which can be distinguished from other IDs. */ - INSERT INTO PackageRequests (ID, PackageBaseID, PackageBaseName, UsersID, ReqTypeID, Comments, ClosureComment) VALUES (3001, 1001, "foobar", 1, 1, "This is a request test comment.", ""); + INSERT INTO PackageRequests (ID, PackageBaseID, PackageBaseName, UsersID, ReqTypeID, Comments, ClosureComment) VALUES (3001, 1001, "foobar", 2, 1, "This is a request test comment.", ""); EOD >sendmail.out && "$NOTIFY" request-open 1 3001 orphan 1001 && + grep ^Cc: sendmail.out >actual && + cat <<-EOD >expected && + Cc: user@localhost, tu@localhost + EOD + test_cmp actual expected && grep ^Subject: sendmail.out >actual && cat <<-EOD >expected && Subject: [PRQ#3001] Orphan Request for foobar @@ -324,9 +329,14 @@ test_expect_success 'Test subject and body of request open notifications for mer test_cmp actual expected ' -test_expect_success 'Test subject and body of request close notifications.' ' +test_expect_success 'Test Cc, subject and body of request close notifications.' ' >sendmail.out && "$NOTIFY" request-close 1 3001 accepted && + grep ^Cc: sendmail.out >actual && + cat <<-EOD >expected && + Cc: user@localhost, tu@localhost + EOD + test_cmp actual expected && grep ^Subject: sendmail.out >actual && cat <<-EOD >expected && Subject: [PRQ#3001] Deletion Request for foobar Accepted From 613364b773c6352ae17aea3d2a74786fe0ca607d Mon Sep 17 00:00:00 2001 From: Morten Linderud Date: Fri, 4 Sep 2020 09:27:34 +0200 Subject: [PATCH 0091/1451] pkg_search_page: Limit number of results on package search The current package search query is quite poorly optimized and becomes a resource hog when the offsets gets large enough. This DoSes the service. A quick fix is to just ensure we have some limit to the number of hits we return. The current hardcoding of 2500 is based on the following: * 250 hits per page max * 10 pages We can maybe consider having it lower, but it seems easier to just have this a multiple of 250 in the first iteration. Signed-off-by: Morten Linderud Signed-off-by: Lukas Fleischer --- web/lib/pkgfuncs.inc.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index 8c915711..80758005 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -619,7 +619,7 @@ function pkg_search_page($params, $show_headers=true, $SID="") { /* Sanitize paging variables. */ if (isset($params['O'])) { - $params['O'] = max(intval($params['O']), 0); + $params['O'] = bound(intval($params['O']), 0, 2500); } else { $params['O'] = 0; } @@ -771,9 +771,8 @@ function pkg_search_page($params, $show_headers=true, $SID="") { $result_t = $dbh->query($q_total); if ($result_t) { $row = $result_t->fetch(PDO::FETCH_NUM); - $total = $row[0]; - } - else { + $total = min($row[0], 2500); + } else { $total = 0; } From d5e308550ad4682829c01feb32212540a6699100 Mon Sep 17 00:00:00 2001 From: Frederik Schwan Date: Wed, 14 Oct 2020 02:22:08 +0200 Subject: [PATCH 0092/1451] Fix requests not being sent to the Cc recipients Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 9d4f3bde..edae76f8 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -118,7 +118,7 @@ class Notification: server.set_debuglevel(0) deliver_to = [to] + self.get_cc() - server.sendmail(sender, to, msg.as_bytes()) + server.sendmail(sender, deliver_to, msg.as_bytes()) server.quit() From d92dd69aa3c23acc7e2e409decf42c3b3e37749c Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Tue, 16 Feb 2021 21:42:23 -0500 Subject: [PATCH 0093/1451] fix broken SQL query that always failed Due to missing whitespace at the end of strings during joining, we ended up with the query fragment "DelTS IS NULLAND NOT PinnedTS" which should be "DelTS IS NULL AND NOT PinnedTS" So the check for pinned comments > 5 likely always failed. In php 7, a completely broken query that raises exceptions in the database engine was silently ignored... in php 8, it raises Uncaught PDOException: SQLSTATE[HY000]: General error: 1 near "PinnedTS": syntax error in and aborts the page building. End result: users with permission to pin comments cannot see any comments, or indeed page content below the first comment header Signed-off-by: Eli Schwartz Signed-off-by: Lukas Fleischer --- web/lib/pkgbasefuncs.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php index a4925891..4c8abba7 100644 --- a/web/lib/pkgbasefuncs.inc.php +++ b/web/lib/pkgbasefuncs.inc.php @@ -21,7 +21,7 @@ function pkgbase_comments_count($base_id, $include_deleted, $only_pinned=false) $q = "SELECT COUNT(*) FROM PackageComments "; $q.= "WHERE PackageBaseID = " . $base_id . " "; if (!$include_deleted) { - $q.= "AND DelTS IS NULL"; + $q.= "AND DelTS IS NULL "; } if ($only_pinned) { $q.= "AND NOT PinnedTS = 0"; From be5197a5fe11d93ebce0044179c6f04fa8ff4cbb Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Tue, 16 Feb 2021 21:50:23 -0500 Subject: [PATCH 0094/1451] prevent running mysql-specific query in sqlite We usually guard such queries and have both mysql and sqlite branches. But I have not implemented the sqlite branch. Given sqlite is typically used for local dev setups, the fact that "users with more than the configured max simultaneous logins" can avoid getting some logins annulled is probably not a huge risk. And this always *used* to fail on sqlite, silently. Now, in php 8, it raises PDOException, which prevents running the test server Document this as a FIXME for now, until someone reimplements the query for sqlite. Signed-off-by: Eli Schwartz Signed-off-by: Lukas Fleischer --- web/lib/acctfuncs.inc.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index d238c0e0..30c4cfe0 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -597,7 +597,9 @@ function try_login() { /* Generate a session ID and store it. */ while (!$logged_in && $num_tries < 5) { $session_limit = config_get_int('options', 'max_sessions_per_user'); - if ($session_limit) { + # FIXME: this does not work for sqlite (JOIN in a DELETE clause) + # hence non-prod instances can have a naughty amount of simultaneous logins + if ($backend == "mysql" && $session_limit) { /* * Delete all user sessions except the * last ($session_limit - 1). From 71740a75a210907cee418a6c404e05ef4710fa9b Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Tue, 16 Feb 2021 22:09:36 -0500 Subject: [PATCH 0095/1451] rewrite query to support both mysql/sqlite Signed-off-by: Eli Schwartz Signed-off-by: Lukas Fleischer --- web/lib/acctfuncs.inc.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index 30c4cfe0..752abe97 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -597,21 +597,17 @@ function try_login() { /* Generate a session ID and store it. */ while (!$logged_in && $num_tries < 5) { $session_limit = config_get_int('options', 'max_sessions_per_user'); - # FIXME: this does not work for sqlite (JOIN in a DELETE clause) - # hence non-prod instances can have a naughty amount of simultaneous logins - if ($backend == "mysql" && $session_limit) { + if ($session_limit) { /* * Delete all user sessions except the * last ($session_limit - 1). */ - $q = "DELETE s.* FROM Sessions s "; - $q.= "LEFT JOIN (SELECT SessionID FROM Sessions "; + $q = "DELETE FROM Sessions "; $q.= "WHERE UsersId = " . $userID . " "; + $q.= "AND SessionID NOT IN (SELECT SessionID FROM Sessions "; + $q.= "WHERE UsersID = " . $userID . " "; $q.= "ORDER BY LastUpdateTS DESC "; - $q.= "LIMIT " . ($session_limit - 1) . ") q "; - $q.= "ON s.SessionID = q.SessionID "; - $q.= "WHERE s.UsersId = " . $userID . " "; - $q.= "AND q.SessionID IS NULL;"; + $q.= "LIMIT " . ($session_limit - 1) . ")"; $dbh->query($q); } From db02227cc467ac9b9abcd9ee28bb10b2ef62cf4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 20 May 2020 23:15:16 +0100 Subject: [PATCH 0096/1451] ci: add gitlab ci MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- .gitlab-ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..d463b109 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,12 @@ +image: archlinux + +before_script: + - pacman -Syu --noconfirm --noprogressbar --needed + base-devel git gpgme protobuf pyalpm python-mysql-connector + python-pygit2 python-srcinfo python-bleach python-markdown + python-sqlalchemy python-alembic python-pytest python-werkzeug + python-pytest-tap + +test: + script: + - make -C test From 23f6dd16a7c8b6f81c229e2307838edd213d6149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 20 May 2020 23:27:50 +0100 Subject: [PATCH 0097/1451] ci: add cache to gitlab ci MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- .gitlab-ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d463b109..74784fce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,13 @@ image: archlinux +cache: + key: system-v1 + paths: + # For some reason Gitlab CI only supports storing cache/artifacts in a path relative to the build directory + - .pkg-cache + before_script: - - pacman -Syu --noconfirm --noprogressbar --needed + - pacman -Syu --noconfirm --noprogressbar --needed --cachedir .pkg-cache base-devel git gpgme protobuf pyalpm python-mysql-connector python-pygit2 python-srcinfo python-bleach python-markdown python-sqlalchemy python-alembic python-pytest python-werkzeug From 8a13500535942a1c99b97ae4de46e5a1c0297cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Sun, 19 Apr 2020 20:11:02 +0200 Subject: [PATCH 0098/1451] Create aurweb.spawn for spawing the test server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This program makes it easier for developers to spawn the PHP server since it fetches automatically what it needs from the configuration file, rather than having the user explicitly pass arguments to the php executable. When the setup gets more complicated as we introduce Python, aurweb.spawn will keep providing the same interface, while under the hood it is planned to support running multiple sub-processes. Its Python interface provides an way for the test suite to spawn the test server when it needs to perform HTTP requests to the test server. The current implementation is somewhat weak as it doesn’t detect when a child process dies, but this is not supposed to happen often, and it is only meant for aurweb developers. In the long term, aurweb.spawn will eventually become obsolete, and replaced by Docker or Flask’s tools. Signed-off-by: Lukas Fleischer --- TESTING | 7 +-- aurweb/spawn.py | 107 +++++++++++++++++++++++++++++++++++++++++++ conf/config.defaults | 3 ++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 aurweb/spawn.py diff --git a/TESTING b/TESTING index 4a1e6f4c..a5e08cb8 100644 --- a/TESTING +++ b/TESTING @@ -17,7 +17,8 @@ INSTALL. Ensure to enable the pdo_sqlite extension in php.ini. 3) Copy conf/config.defaults to conf/config and adjust the configuration - (pay attention to disable_http_login, enable_maintenance and aur_location). + Pay attention to disable_http_login, enable_maintenance, aur_location and + htmldir. Be sure to change backend to sqlite and name to the file location of your created test database. @@ -31,6 +32,6 @@ INSTALL. $ ./gendummydata.py out.sql $ sqlite3 path/to/aurweb.sqlite3 < out.sql -5) Run the PHP built-in web server: +5) Run the test server: - $ AUR_CONFIG='/path/to/aurweb/conf/config' php -S localhost:8080 -t /path/to/aurweb/web/html + $ AUR_CONFIG='/path/to/aurweb/conf/config' python -m aurweb.spawn diff --git a/aurweb/spawn.py b/aurweb/spawn.py new file mode 100644 index 00000000..5fa646b5 --- /dev/null +++ b/aurweb/spawn.py @@ -0,0 +1,107 @@ +""" +Provide an automatic way of spawing an HTTP test server running aurweb. + +It can be called from the command-line or from another Python module. + +This module uses a global state, since you can’t open two servers with the same +configuration anyway. +""" + + +import atexit +import argparse +import subprocess +import sys +import time +import urllib + +import aurweb.config +import aurweb.schema + + +children = [] +verbosity = 0 + + +class ProcessExceptions(Exception): + """ + Compound exception used by stop() to list all the errors that happened when + terminating child processes. + """ + def __init__(self, message, exceptions): + self.message = message + self.exceptions = exceptions + messages = [message] + [str(e) for e in exceptions] + super().__init__("\n- ".join(messages)) + + +def spawn_child(args): + """Open a subprocess and add it to the global state.""" + if verbosity >= 1: + print(f"Spawning {args}", file=sys.stderr) + children.append(subprocess.Popen(args)) + + +def start(): + """ + Spawn the test server. If it is already running, do nothing. + + The server can be stopped with stop(), or is automatically stopped when the + Python process ends using atexit. + """ + if children: + return + atexit.register(stop) + aur_location = aurweb.config.get("options", "aur_location") + aur_location_parts = urllib.parse.urlsplit(aur_location) + htmldir = aurweb.config.get("options", "htmldir") + spawn_child(["php", "-S", aur_location_parts.netloc, "-t", htmldir]) + + +def stop(): + """ + Stop all the child processes. + + If an exception occurs during the process, the process continues anyway + because we don’t want to leave runaway processes around, and all the + exceptions are finally raised as a single ProcessExceptions. + """ + global children + atexit.unregister(stop) + exceptions = [] + for p in children: + try: + p.terminate() + if verbosity >= 1: + print(f"Sent SIGTERM to {p.args}", file=sys.stderr) + except Exception as e: + exceptions.append(e) + for p in children: + try: + rc = p.wait() + if rc != 0 and rc != -15: + # rc = -15 indicates the process was terminated with SIGTERM, + # which is to be expected since we called terminate on them. + raise Exception(f"Process {p.args} exited with {rc}") + except Exception as e: + exceptions.append(e) + children = [] + if exceptions: + raise ProcessExceptions("Errors terminating the child processes:", + exceptions) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + prog='python -m aurweb.spawn', + description='Start aurweb\'s test server.') + parser.add_argument('-v', '--verbose', action='count', default=0, + help='increase verbosity') + args = parser.parse_args() + verbosity = args.verbose + start() + try: + while True: + time.sleep(60) + except KeyboardInterrupt: + stop() diff --git a/conf/config.defaults b/conf/config.defaults index 447dacac..86fe765c 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -41,6 +41,9 @@ cache = none cache_pkginfo_ttl = 86400 memcache_servers = 127.0.0.1:11211 +; Directory containing aurweb's PHP code, required by aurweb.spawn. +;htmldir = /path/to/web/html + [ratelimit] request_limit = 4000 window_length = 86400 From 48b58b1c2f74df0906231d2affd9f2b352a8e330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 23 May 2020 17:54:07 +0100 Subject: [PATCH 0099/1451] ci: remove Travis CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are are moving to Gitlab CI. Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- .travis.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5bbfda1f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python - -python: 3.6 - -addons: - apt: - packages: - - bsdtar - - libarchive-dev - - libgpgme11-dev - - libprotobuf-dev - -install: - - curl https://codeload.github.com/libgit2/libgit2/tar.gz/v0.26.0 | tar -xz - - curl https://sources.archlinux.org/other/pacman/pacman-5.0.2.tar.gz | tar -xz - - curl https://git.archlinux.org/pyalpm.git/snapshot/pyalpm-0.8.1.tar.gz | tar -xz - - ( cd libgit2-0.26.0 && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && make && sudo make install ) - - ( cd pacman-5.0.2 && ./configure --prefix=/usr && make && sudo make install ) - - ( cd pyalpm-0.8.1 && python setup.py build && python setup.py install ) - - pip install mysql-connector-python-rf pygit2==0.26 srcinfo - - pip install bleach Markdown - -script: make -C test From 8d1be7ea8a8d7c270f692a6c375ef2614c5ac601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Mon, 1 Jun 2020 23:35:25 +0100 Subject: [PATCH 0100/1451] Refactor code to comply with flake8 and isort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- aurweb/git/auth.py | 3 +- aurweb/git/serve.py | 14 +- aurweb/git/update.py | 6 +- aurweb/initdb.py | 7 +- aurweb/l10n.py | 2 +- aurweb/schema.py | 4 +- aurweb/scripts/aurblup.py | 3 +- aurweb/scripts/rendercomment.py | 6 +- migrations/env.py | 9 +- schema/gendummydata.py | 346 ++++++++++++++++---------------- setup.py | 3 +- 11 files changed, 206 insertions(+), 197 deletions(-) diff --git a/aurweb/git/auth.py b/aurweb/git/auth.py index 3b1e485f..abecd276 100755 --- a/aurweb/git/auth.py +++ b/aurweb/git/auth.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 -import os -import shlex import re +import shlex import sys import aurweb.config diff --git a/aurweb/git/serve.py b/aurweb/git/serve.py index 64d51b9e..b91f1a13 100755 --- a/aurweb/git/serve.py +++ b/aurweb/git/serve.py @@ -175,11 +175,11 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged): i += 1 for userid in uids_rem: - cur = conn.execute("DELETE FROM PackageComaintainers " + - "WHERE PackageBaseID = ? AND UsersID = ?", - [pkgbase_id, userid]) - subprocess.Popen((notify_cmd, 'comaintainer-remove', - str(userid), str(pkgbase_id))) + cur = conn.execute("DELETE FROM PackageComaintainers " + + "WHERE PackageBaseID = ? AND UsersID = ?", + [pkgbase_id, userid]) + subprocess.Popen((notify_cmd, 'comaintainer-remove', + str(userid), str(pkgbase_id))) conn.commit() conn.close() @@ -268,7 +268,7 @@ def pkgbase_disown(pkgbase, user, privileged): cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user]) userid = cur.fetchone()[0] if userid == 0: - raise aurweb.exceptions.InvalidUserException(user) + raise aurweb.exceptions.InvalidUserException(user) subprocess.Popen((notify_cmd, 'disown', str(userid), str(pkgbase_id))) @@ -472,7 +472,7 @@ def checkarg(cmdargv, *argdesc): checkarg_atmost(cmdargv, *argdesc) -def serve(action, cmdargv, user, privileged, remote_addr): +def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901 if enable_maintenance: if remote_addr not in maintenance_exc: raise aurweb.exceptions.MaintenanceException diff --git a/aurweb/git/update.py b/aurweb/git/update.py index 39128f8b..929b254e 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 import os -import pygit2 import re import subprocess import sys import time +import pygit2 import srcinfo.parse import srcinfo.utils @@ -75,7 +75,7 @@ def create_pkgbase(conn, pkgbase, user): return pkgbase_id -def save_metadata(metadata, conn, user): +def save_metadata(metadata, conn, user): # noqa: C901 # Obtain package base ID and previous maintainer. pkgbase = metadata['pkgbase'] cur = conn.execute("SELECT ID, MaintainerUID FROM PackageBases " @@ -232,7 +232,7 @@ def die_commit(msg, commit): exit(1) -def main(): +def main(): # noqa: C901 repo = pygit2.Repository(repo_path) user = os.environ.get("AUR_USER") diff --git a/aurweb/initdb.py b/aurweb/initdb.py index 91777f7e..c8d0b2ae 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -1,11 +1,12 @@ -import aurweb.db -import aurweb.schema +import argparse import alembic.command import alembic.config -import argparse import sqlalchemy +import aurweb.db +import aurweb.schema + def feed_initial_data(conn): conn.execute(aurweb.schema.AccountTypes.insert(), [ diff --git a/aurweb/l10n.py b/aurweb/l10n.py index a7c0103e..492200b3 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -16,4 +16,4 @@ class Translator: self._localedir, languages=[lang]) self._translator[lang].install() - return _(s) + return _(s) # _ is not defined, what is this? # noqa: F821 diff --git a/aurweb/schema.py b/aurweb/schema.py index 6792cf1d..20f3e5ce 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -6,7 +6,7 @@ usually be automatically generated. See `migrations/README` for details. """ -from sqlalchemy import CHAR, Column, ForeignKey, Index, MetaData, String, TIMESTAMP, Table, Text, text +from sqlalchemy import CHAR, TIMESTAMP, Column, ForeignKey, Index, MetaData, String, Table, Text, text from sqlalchemy.dialects.mysql import BIGINT, DECIMAL, INTEGER, TINYINT from sqlalchemy.ext.compiler import compiles @@ -24,7 +24,7 @@ def compile_bigint_sqlite(type_, compiler, **kw): to INTEGER. Aside from that, BIGINT is the same as INTEGER for SQLite. See https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#allowing-autoincrement-behavior-sqlalchemy-types-other-than-integer-integer - """ + """ # noqa: E501 return 'INTEGER' diff --git a/aurweb/scripts/aurblup.py b/aurweb/scripts/aurblup.py index a7d43f12..e32937ce 100755 --- a/aurweb/scripts/aurblup.py +++ b/aurweb/scripts/aurblup.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -import pyalpm import re +import pyalpm + import aurweb.config import aurweb.db diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 76865d27..422dd33b 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -import re -import pygit2 import sys + import bleach import markdown +import pygit2 import aurweb.config import aurweb.db @@ -47,7 +47,7 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): class FlysprayLinksExtension(markdown.extensions.Extension): def extendMarkdown(self, md, md_globals): - processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b',md) + processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b', md) md.inlinePatterns.register(processor, 'flyspray-links', 118) diff --git a/migrations/env.py b/migrations/env.py index 1627e693..c2ff58c1 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,10 +1,11 @@ -import aurweb.db -import aurweb.schema - -from alembic import context import logging.config + import sqlalchemy +from alembic import context + +import aurweb.db +import aurweb.schema # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 1f3d0476..b3a73ef2 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -10,33 +10,32 @@ usage: gendummydata.py outputfilename.sql # insert these users/packages into the AUR database. # import hashlib -import random -import time -import os -import sys -import io import logging +import os +import random +import sys +import time -LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output +LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" -DB_HOST = os.getenv("DB_HOST", "localhost") -DB_NAME = os.getenv("DB_NAME", "AUR") -DB_USER = os.getenv("DB_USER", "aur") -DB_PASS = os.getenv("DB_PASS", "aur") -USER_ID = 5 # Users.ID of first bogus user -PKG_ID = 1 # Packages.ID of first package +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_NAME = os.getenv("DB_NAME", "AUR") +DB_USER = os.getenv("DB_USER", "aur") +DB_PASS = os.getenv("DB_PASS", "aur") +USER_ID = 5 # Users.ID of first bogus user +PKG_ID = 1 # Packages.ID of first package MAX_USERS = 300 # how many users to 'register' -MAX_DEVS = .1 # what percentage of MAX_USERS are Developers -MAX_TUS = .2 # what percentage of MAX_USERS are Trusted Users -MAX_PKGS = 900 # how many packages to load -PKG_DEPS = (1, 15) # min/max depends a package has -PKG_RELS = (1, 5) # min/max relations a package has -PKG_SRC = (1, 3) # min/max sources a package has +MAX_DEVS = .1 # what percentage of MAX_USERS are Developers +MAX_TUS = .2 # what percentage of MAX_USERS are Trusted Users +MAX_PKGS = 900 # how many packages to load +PKG_DEPS = (1, 15) # min/max depends a package has +PKG_RELS = (1, 5) # min/max relations a package has +PKG_SRC = (1, 3) # min/max sources a package has PKG_CMNTS = (1, 5) # min/max number of comments a package has CATEGORIES_COUNT = 17 # the number of categories from aur-schema -VOTING = (0, .30) # percentage range for package voting -OPEN_PROPOSALS = 5 # number of open trusted user proposals -CLOSE_PROPOSALS = 15 # number of closed trusted user proposals +VOTING = (0, .30) # percentage range for package voting +OPEN_PROPOSALS = 5 # number of open trusted user proposals +CLOSE_PROPOSALS = 15 # number of closed trusted user proposals RANDOM_TLDS = ("edu", "com", "org", "net", "tw", "ru", "pl", "de", "es") RANDOM_URL = ("http://www.", "ftp://ftp.", "http://", "ftp://") RANDOM_LOCS = ("pub", "release", "files", "downloads", "src") @@ -48,20 +47,20 @@ logging.basicConfig(format=logformat, level=LOG_LEVEL) log = logging.getLogger() if len(sys.argv) != 2: - log.error("Missing output filename argument") - raise SystemExit + log.error("Missing output filename argument") + raise SystemExit # make sure the seed file exists # if not os.path.exists(SEED_FILE): - log.error("Please install the 'words' Arch package") - raise SystemExit + log.error("Please install the 'words' Arch package") + raise SystemExit # make sure comments can be created # if not os.path.exists(FORTUNE_FILE): - log.error("Please install the 'fortune-mod' Arch package") - raise SystemExit + log.error("Please install the 'fortune-mod' Arch package") + raise SystemExit # track what users/package names have been used # @@ -69,21 +68,28 @@ seen_users = {} seen_pkgs = {} user_keys = [] + # some functions to generate random data # def genVersion(): - ver = [] - ver.append("%d" % random.randrange(0,10)) - ver.append("%d" % random.randrange(0,20)) - if random.randrange(0,2) == 0: - ver.append("%d" % random.randrange(0,100)) - return ".".join(ver) + "-%d" % random.randrange(1,11) + ver = [] + ver.append("%d" % random.randrange(0, 10)) + ver.append("%d" % random.randrange(0, 20)) + if random.randrange(0, 2) == 0: + ver.append("%d" % random.randrange(0, 100)) + return ".".join(ver) + "-%d" % random.randrange(1, 11) + + def genCategory(): - return random.randrange(1,CATEGORIES_COUNT) + return random.randrange(1, CATEGORIES_COUNT) + + def genUID(): - return seen_users[user_keys[random.randrange(0,len(user_keys))]] + return seen_users[user_keys[random.randrange(0, len(user_keys))]] + + def genFortune(): - return fortunes[random.randrange(0,len(fortunes))].replace("'", "") + return fortunes[random.randrange(0, len(fortunes))].replace("'", "") # load the words, and make sure there are enough words for users/pkgs @@ -93,25 +99,25 @@ fp = open(SEED_FILE, "r", encoding="utf-8") contents = fp.readlines() fp.close() if MAX_USERS > len(contents): - MAX_USERS = len(contents) + MAX_USERS = len(contents) if MAX_PKGS > len(contents): - MAX_PKGS = len(contents) + MAX_PKGS = len(contents) if len(contents) - MAX_USERS > MAX_PKGS: - need_dupes = 0 + need_dupes = 0 else: - need_dupes = 1 + need_dupes = 1 # select random usernames # log.debug("Generating random user names...") user_id = USER_ID while len(seen_users) < MAX_USERS: - user = random.randrange(0, len(contents)) - word = contents[user].replace("'", "").replace(".","").replace(" ", "_") - word = word.strip().lower() - if word not in seen_users: - seen_users[word] = user_id - user_id += 1 + user = random.randrange(0, len(contents)) + word = contents[user].replace("'", "").replace(".", "").replace(" ", "_") + word = word.strip().lower() + if word not in seen_users: + seen_users[word] = user_id + user_id += 1 user_keys = list(seen_users.keys()) # select random package names @@ -119,17 +125,17 @@ user_keys = list(seen_users.keys()) log.debug("Generating random package names...") num_pkgs = PKG_ID while len(seen_pkgs) < MAX_PKGS: - pkg = random.randrange(0, len(contents)) - word = contents[pkg].replace("'", "").replace(".","").replace(" ", "_") - word = word.strip().lower() - if not need_dupes: - if word not in seen_pkgs and word not in seen_users: - seen_pkgs[word] = num_pkgs - num_pkgs += 1 - else: - if word not in seen_pkgs: - seen_pkgs[word] = num_pkgs - num_pkgs += 1 + pkg = random.randrange(0, len(contents)) + word = contents[pkg].replace("'", "").replace(".", "").replace(" ", "_") + word = word.strip().lower() + if not need_dupes: + if word not in seen_pkgs and word not in seen_users: + seen_pkgs[word] = num_pkgs + num_pkgs += 1 + else: + if word not in seen_pkgs: + seen_pkgs[word] = num_pkgs + num_pkgs += 1 # free up contents memory # @@ -151,32 +157,32 @@ out.write("BEGIN;\n") # log.debug("Creating SQL statements for users.") for u in user_keys: - account_type = 1 # default to normal user - if not has_devs or not has_tus: - account_type = random.randrange(1, 4) - if account_type == 3 and not has_devs: - # this will be a dev account - # - developers.append(seen_users[u]) - if len(developers) >= MAX_DEVS * MAX_USERS: - has_devs = 1 - elif account_type == 2 and not has_tus: - # this will be a trusted user account - # - trustedusers.append(seen_users[u]) - if len(trustedusers) >= MAX_TUS * MAX_USERS: - has_tus = 1 - else: - # a normal user account - # - pass + account_type = 1 # default to normal user + if not has_devs or not has_tus: + account_type = random.randrange(1, 4) + if account_type == 3 and not has_devs: + # this will be a dev account + # + developers.append(seen_users[u]) + if len(developers) >= MAX_DEVS * MAX_USERS: + has_devs = 1 + elif account_type == 2 and not has_tus: + # this will be a trusted user account + # + trustedusers.append(seen_users[u]) + if len(trustedusers) >= MAX_TUS * MAX_USERS: + has_tus = 1 + else: + # a normal user account + # + pass - h = hashlib.new('md5') - h.update(u.encode()); - s = ("INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd)" - " VALUES (%d, %d, '%s', '%s@example.com', '%s');\n") - s = s % (seen_users[u], account_type, u, u, h.hexdigest()) - out.write(s) + h = hashlib.new('md5') + h.update(u.encode()) + s = ("INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd)" + " VALUES (%d, %d, '%s', '%s@example.com', '%s');\n") + s = s % (seen_users[u], account_type, u, u, h.hexdigest()) + out.write(s) log.debug("Number of developers: %d" % len(developers)) log.debug("Number of trusted users: %d" % len(trustedusers)) @@ -193,123 +199,123 @@ fp.close() log.debug("Creating SQL statements for packages.") count = 0 for p in list(seen_pkgs.keys()): - NOW = int(time.time()) - if count % 2 == 0: - muid = developers[random.randrange(0,len(developers))] - puid = developers[random.randrange(0,len(developers))] - else: - muid = trustedusers[random.randrange(0,len(trustedusers))] - puid = trustedusers[random.randrange(0,len(trustedusers))] - if count % 20 == 0: # every so often, there are orphans... - muid = "NULL" + NOW = int(time.time()) + if count % 2 == 0: + muid = developers[random.randrange(0, len(developers))] + puid = developers[random.randrange(0, len(developers))] + else: + muid = trustedusers[random.randrange(0, len(trustedusers))] + puid = trustedusers[random.randrange(0, len(trustedusers))] + if count % 20 == 0: # every so often, there are orphans... + muid = "NULL" - uuid = genUID() # the submitter/user + uuid = genUID() # the submitter/user - s = ("INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " + s = ("INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " "SubmitterUID, MaintainerUID, PackagerUID) VALUES (%d, '%s', '', %d, %d, %d, %s, %s);\n") - s = s % (seen_pkgs[p], p, NOW, NOW, uuid, muid, puid) - out.write(s) + s = s % (seen_pkgs[p], p, NOW, NOW, uuid, muid, puid) + out.write(s) - s = ("INSERT INTO Packages (ID, PackageBaseID, Name, Version) VALUES " + s = ("INSERT INTO Packages (ID, PackageBaseID, Name, Version) VALUES " "(%d, %d, '%s', '%s');\n") - s = s % (seen_pkgs[p], seen_pkgs[p], p, genVersion()) - out.write(s) + s = s % (seen_pkgs[p], seen_pkgs[p], p, genVersion()) + out.write(s) - count += 1 + count += 1 - # create random comments for this package - # - num_comments = random.randrange(PKG_CMNTS[0], PKG_CMNTS[1]) - for i in range(0, num_comments): - now = NOW + random.randrange(400, 86400*3) - s = ("INSERT INTO PackageComments (PackageBaseID, UsersID," - " Comments, RenderedComment, CommentTS) VALUES (%d, %d, '%s', '', %d);\n") - s = s % (seen_pkgs[p], genUID(), genFortune(), now) - out.write(s) + # create random comments for this package + # + num_comments = random.randrange(PKG_CMNTS[0], PKG_CMNTS[1]) + for i in range(0, num_comments): + now = NOW + random.randrange(400, 86400*3) + s = ("INSERT INTO PackageComments (PackageBaseID, UsersID," + " Comments, RenderedComment, CommentTS) VALUES (%d, %d, '%s', '', %d);\n") + s = s % (seen_pkgs[p], genUID(), genFortune(), now) + out.write(s) # Cast votes # track_votes = {} log.debug("Casting votes for packages.") for u in user_keys: - num_votes = random.randrange(int(len(seen_pkgs)*VOTING[0]), - int(len(seen_pkgs)*VOTING[1])) - pkgvote = {} - for v in range(num_votes): - pkg = random.randrange(1, len(seen_pkgs) + 1) - if pkg not in pkgvote: - s = ("INSERT INTO PackageVotes (UsersID, PackageBaseID)" - " VALUES (%d, %d);\n") - s = s % (seen_users[u], pkg) - pkgvote[pkg] = 1 - if pkg not in track_votes: - track_votes[pkg] = 0 - track_votes[pkg] += 1 - out.write(s) + num_votes = random.randrange(int(len(seen_pkgs)*VOTING[0]), + int(len(seen_pkgs)*VOTING[1])) + pkgvote = {} + for v in range(num_votes): + pkg = random.randrange(1, len(seen_pkgs) + 1) + if pkg not in pkgvote: + s = ("INSERT INTO PackageVotes (UsersID, PackageBaseID)" + " VALUES (%d, %d);\n") + s = s % (seen_users[u], pkg) + pkgvote[pkg] = 1 + if pkg not in track_votes: + track_votes[pkg] = 0 + track_votes[pkg] += 1 + out.write(s) # Update statements for package votes # for p in list(track_votes.keys()): - s = "UPDATE PackageBases SET NumVotes = %d WHERE ID = %d;\n" - s = s % (track_votes[p], p) - out.write(s) + s = "UPDATE PackageBases SET NumVotes = %d WHERE ID = %d;\n" + s = s % (track_votes[p], p) + out.write(s) # Create package dependencies and sources # log.debug("Creating statements for package depends/sources.") for p in list(seen_pkgs.keys()): - num_deps = random.randrange(PKG_DEPS[0], PKG_DEPS[1]) - for i in range(0, num_deps): - dep = random.choice([k for k in seen_pkgs]) - deptype = random.randrange(1, 5) - if deptype == 4: - dep += ": for " + random.choice([k for k in seen_pkgs]) - s = "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) VALUES (%d, %d, '%s');\n" - s = s % (seen_pkgs[p], deptype, dep) - out.write(s) + num_deps = random.randrange(PKG_DEPS[0], PKG_DEPS[1]) + for i in range(0, num_deps): + dep = random.choice([k for k in seen_pkgs]) + deptype = random.randrange(1, 5) + if deptype == 4: + dep += ": for " + random.choice([k for k in seen_pkgs]) + s = "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) VALUES (%d, %d, '%s');\n" + s = s % (seen_pkgs[p], deptype, dep) + out.write(s) - num_rels = random.randrange(PKG_RELS[0], PKG_RELS[1]) - for i in range(0, num_deps): - rel = random.choice([k for k in seen_pkgs]) - reltype = random.randrange(1, 4) - s = "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) VALUES (%d, %d, '%s');\n" - s = s % (seen_pkgs[p], reltype, rel) - out.write(s) + num_rels = random.randrange(PKG_RELS[0], PKG_RELS[1]) + for i in range(0, num_deps): + rel = random.choice([k for k in seen_pkgs]) + reltype = random.randrange(1, 4) + s = "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) VALUES (%d, %d, '%s');\n" + s = s % (seen_pkgs[p], reltype, rel) + out.write(s) - num_sources = random.randrange(PKG_SRC[0], PKG_SRC[1]) - for i in range(num_sources): - src_file = user_keys[random.randrange(0, len(user_keys))] - src = "%s%s.%s/%s/%s-%s.tar.gz" % ( - RANDOM_URL[random.randrange(0,len(RANDOM_URL))], - p, RANDOM_TLDS[random.randrange(0,len(RANDOM_TLDS))], - RANDOM_LOCS[random.randrange(0,len(RANDOM_LOCS))], - src_file, genVersion()) - s = "INSERT INTO PackageSources(PackageID, Source) VALUES (%d, '%s');\n" - s = s % (seen_pkgs[p], src) - out.write(s) + num_sources = random.randrange(PKG_SRC[0], PKG_SRC[1]) + for i in range(num_sources): + src_file = user_keys[random.randrange(0, len(user_keys))] + src = "%s%s.%s/%s/%s-%s.tar.gz" % ( + RANDOM_URL[random.randrange(0, len(RANDOM_URL))], + p, RANDOM_TLDS[random.randrange(0, len(RANDOM_TLDS))], + RANDOM_LOCS[random.randrange(0, len(RANDOM_LOCS))], + src_file, genVersion()) + s = "INSERT INTO PackageSources(PackageID, Source) VALUES (%d, '%s');\n" + s = s % (seen_pkgs[p], src) + out.write(s) # Create trusted user proposals # log.debug("Creating SQL statements for trusted user proposals.") -count=0 +count = 0 for t in range(0, OPEN_PROPOSALS+CLOSE_PROPOSALS): - now = int(time.time()) - if count < CLOSE_PROPOSALS: - start = now - random.randrange(3600*24*7, 3600*24*21) - end = now - random.randrange(0, 3600*24*7) - else: - start = now - end = now + random.randrange(3600*24, 3600*24*7) - if count % 5 == 0: # Don't make the vote about anyone once in a while - user = "" - else: - user = user_keys[random.randrange(0,len(user_keys))] - suid = trustedusers[random.randrange(0,len(trustedusers))] - s = ("INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End," - " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n") - s = s % (genFortune(), user, start, end, suid) - out.write(s) - count += 1 + now = int(time.time()) + if count < CLOSE_PROPOSALS: + start = now - random.randrange(3600*24*7, 3600*24*21) + end = now - random.randrange(0, 3600*24*7) + else: + start = now + end = now + random.randrange(3600*24, 3600*24*7) + if count % 5 == 0: # Don't make the vote about anyone once in a while + user = "" + else: + user = user_keys[random.randrange(0, len(user_keys))] + suid = trustedusers[random.randrange(0, len(trustedusers))] + s = ("INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End," + " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n") + s = s % (genFortune(), user, start, end, suid) + out.write(s) + count += 1 # close output file # diff --git a/setup.py b/setup.py index ca26f0d8..cf88488c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ import re -from setuptools import setup, find_packages import sys +from setuptools import find_packages, setup + version = None with open('web/lib/version.inc.php', 'r') as f: for line in f.readlines(): From 4cf94816ae022be88540a25356a3abbe10a452eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Mon, 1 Jun 2020 23:35:26 +0100 Subject: [PATCH 0101/1451] flake8: add initial config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..04f5b8ba --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 127 +max-complexity = 10 + From 8f47b8d731e0d650ea671e169cddaafa64c44055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Mon, 1 Jun 2020 23:35:27 +0100 Subject: [PATCH 0102/1451] isort: add initial config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.cfg b/setup.cfg index 04f5b8ba..b868c096 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,3 +2,7 @@ max-line-length = 127 max-complexity = 10 +[isort] +line_length = 127 +lines_between_types = 1 + From 41a84934114f5d77f51d7064c4a64b2e279c1ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Mon, 1 Jun 2020 23:35:28 +0100 Subject: [PATCH 0103/1451] pre-commit: add initial config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- .pre-commit-config.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..525c7eb8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +hooks: + - &base + language: python + types: [python] + require_serial: true + exclude: ^migrations/versions + - &flake8 + id: flake8 + name: flake8 + entry: flake8 + <<: *base + - &isort + id: isort + name: isort + entry: isort + <<: *base + +repos: + - repo: local + hooks: + - <<: *flake8 + - <<: *isort + args: ['--check-only', '--diff'] + From d4abe0b72d1215906806ee84474115961e79f7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Mon, 1 Jun 2020 23:35:29 +0100 Subject: [PATCH 0104/1451] Add CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- CONTRIBUTING.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a37d980a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing + +Patches should be sent to the [aur-dev@archlinux.org][1] mailing list. + +Before sending patched you are recomended to run the `flake8` and `isort`. + +You can add git hook to do this by installing `python-pre-install` and running +`pre-install install`. + +[1] https://lists.archlinux.org/listinfo/aur-dev From 5be07a8a9e9777d54cf7122be52aa0a7b1b51e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 1 Jun 2020 18:49:37 +0200 Subject: [PATCH 0105/1451] aurweb.spawn: Integrate FastAPI and nginx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aurweb.spawn used to launch only PHP’s built-in server. Now it spawns a dummy FastAPI application too. Since both stacks spawn their own HTTP server, aurweb.spawn also spawns nginx as a reverse proxy to mount them under the same base URL, defined by aur_location in the configuration. Signed-off-by: Lukas Fleischer --- .gitlab-ci.yml | 2 +- TESTING | 3 +- aurweb/asgi.py | 8 +++++ aurweb/spawn.py | 80 +++++++++++++++++++++++++++++++++++++------- conf/config.defaults | 7 ++++ 5 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 aurweb/asgi.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 74784fce..f6260ebb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,7 @@ before_script: base-devel git gpgme protobuf pyalpm python-mysql-connector python-pygit2 python-srcinfo python-bleach python-markdown python-sqlalchemy python-alembic python-pytest python-werkzeug - python-pytest-tap + python-pytest-tap python-fastapi uvicorn nginx test: script: diff --git a/TESTING b/TESTING index a5e08cb8..31e3bcbd 100644 --- a/TESTING +++ b/TESTING @@ -12,7 +12,8 @@ INSTALL. 2) Install the necessary packages: # pacman -S --needed php php-sqlite sqlite words fortune-mod \ - python python-sqlalchemy python-alembic + python python-sqlalchemy python-alembic \ + python-fastapi uvicorn nginx Ensure to enable the pdo_sqlite extension in php.ini. diff --git a/aurweb/asgi.py b/aurweb/asgi.py new file mode 100644 index 00000000..5f30471a --- /dev/null +++ b/aurweb/asgi.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/hello/") +async def hello(): + return {"message": "Hello from FastAPI!"} diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 5fa646b5..0506afa4 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -10,8 +10,10 @@ configuration anyway. import atexit import argparse +import os import subprocess import sys +import tempfile import time import urllib @@ -20,6 +22,7 @@ import aurweb.schema children = [] +temporary_dir = None verbosity = 0 @@ -35,10 +38,42 @@ class ProcessExceptions(Exception): super().__init__("\n- ".join(messages)) +def generate_nginx_config(): + """ + Generate an nginx configuration based on aurweb's configuration. + The file is generated under `temporary_dir`. + Returns the path to the created configuration file. + """ + aur_location = aurweb.config.get("options", "aur_location") + aur_location_parts = urllib.parse.urlsplit(aur_location) + config_path = os.path.join(temporary_dir, "nginx.conf") + config = open(config_path, "w") + # We double nginx's braces because they conflict with Python's f-strings. + config.write(f""" + events {{}} + daemon off; + error_log /dev/stderr info; + pid {os.path.join(temporary_dir, "nginx.pid")}; + http {{ + access_log /dev/stdout; + server {{ + listen {aur_location_parts.netloc}; + location / {{ + proxy_pass http://{aurweb.config.get("php", "bind_address")}; + }} + location /hello {{ + proxy_pass http://{aurweb.config.get("fastapi", "bind_address")}; + }} + }} + }} + """) + return config_path + + def spawn_child(args): """Open a subprocess and add it to the global state.""" if verbosity >= 1: - print(f"Spawning {args}", file=sys.stderr) + print(f":: Spawning {args}", file=sys.stderr) children.append(subprocess.Popen(args)) @@ -52,10 +87,29 @@ def start(): if children: return atexit.register(stop) - aur_location = aurweb.config.get("options", "aur_location") - aur_location_parts = urllib.parse.urlsplit(aur_location) - htmldir = aurweb.config.get("options", "htmldir") - spawn_child(["php", "-S", aur_location_parts.netloc, "-t", htmldir]) + + print("{ruler}\n" + "Spawing PHP and FastAPI, then nginx as a reverse proxy.\n" + "Check out {aur_location}\n" + "Hit ^C to terminate everything.\n" + "{ruler}" + .format(ruler=("-" * os.get_terminal_size().columns), + aur_location=aurweb.config.get('options', 'aur_location'))) + + # PHP + php_address = aurweb.config.get("php", "bind_address") + htmldir = aurweb.config.get("php", "htmldir") + spawn_child(["php", "-S", php_address, "-t", htmldir]) + + # FastAPI + host, port = aurweb.config.get("fastapi", "bind_address").rsplit(":", 1) + spawn_child(["python", "-m", "uvicorn", + "--host", host, + "--port", port, + "aurweb.asgi:app"]) + + # nginx + spawn_child(["nginx", "-p", temporary_dir, "-c", generate_nginx_config()]) def stop(): @@ -73,7 +127,7 @@ def stop(): try: p.terminate() if verbosity >= 1: - print(f"Sent SIGTERM to {p.args}", file=sys.stderr) + print(f":: Sent SIGTERM to {p.args}", file=sys.stderr) except Exception as e: exceptions.append(e) for p in children: @@ -99,9 +153,11 @@ if __name__ == '__main__': help='increase verbosity') args = parser.parse_args() verbosity = args.verbose - start() - try: - while True: - time.sleep(60) - except KeyboardInterrupt: - stop() + with tempfile.TemporaryDirectory(prefix="aurweb-") as tmpdirname: + temporary_dir = tmpdirname + start() + try: + while True: + time.sleep(60) + except KeyboardInterrupt: + stop() diff --git a/conf/config.defaults b/conf/config.defaults index 86fe765c..ed495168 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -41,9 +41,16 @@ cache = none cache_pkginfo_ttl = 86400 memcache_servers = 127.0.0.1:11211 +[php] +; Address PHP should bind when spawned in development mode by aurweb.spawn. +bind_address = 127.0.0.1:8081 ; Directory containing aurweb's PHP code, required by aurweb.spawn. ;htmldir = /path/to/web/html +[fastapi] +; Address uvicorn should bind when spawned in development mode by aurweb.spawn. +bind_address = 127.0.0.1:8082 + [ratelimit] request_limit = 4000 window_length = 86400 From 8c868e088c8becc7640327db2e5e2a1cb10bab41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Wed, 3 Jun 2020 02:04:02 +0200 Subject: [PATCH 0106/1451] Introduce conf/config.dev for development MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit conf/config.dev’s purpose is to provide a lighter configuration template for developers, and split development-specific options off the default configuration file. Signed-off-by: Lukas Fleischer --- TESTING | 11 ++++++----- conf/config.defaults | 10 ---------- conf/config.dev | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 conf/config.dev diff --git a/TESTING b/TESTING index 31e3bcbd..7261df92 100644 --- a/TESTING +++ b/TESTING @@ -17,12 +17,13 @@ INSTALL. Ensure to enable the pdo_sqlite extension in php.ini. -3) Copy conf/config.defaults to conf/config and adjust the configuration - Pay attention to disable_http_login, enable_maintenance, aur_location and - htmldir. +3) Copy conf/config.dev to conf/config and replace YOUR_AUR_ROOT by the absolute + path to the root of your aurweb clone. sed can do both tasks for you: - Be sure to change backend to sqlite and name to the file location of your - created test database. + $ sed -e "s;YOUR_AUR_ROOT;$PWD;g" conf/config.dev > conf/config + + Note that when the upstream config.dev is updated, you should compare it to + your conf/config, or regenerate your configuration with the command above. 4) Prepare the testing database: diff --git a/conf/config.defaults b/conf/config.defaults index ed495168..447dacac 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -41,16 +41,6 @@ cache = none cache_pkginfo_ttl = 86400 memcache_servers = 127.0.0.1:11211 -[php] -; Address PHP should bind when spawned in development mode by aurweb.spawn. -bind_address = 127.0.0.1:8081 -; Directory containing aurweb's PHP code, required by aurweb.spawn. -;htmldir = /path/to/web/html - -[fastapi] -; Address uvicorn should bind when spawned in development mode by aurweb.spawn. -bind_address = 127.0.0.1:8082 - [ratelimit] request_limit = 4000 window_length = 86400 diff --git a/conf/config.dev b/conf/config.dev new file mode 100644 index 00000000..d752f61f --- /dev/null +++ b/conf/config.dev @@ -0,0 +1,32 @@ +; Configuration file for aurweb development. +; +; Options are implicitly inherited from conf/config.defaults, which lists all +; available options for productions, and their default values. This current file +; overrides only options useful for development, and introduces +; development-specific options too. + +[database] +backend = sqlite +name = YOUR_AUR_ROOT/aurweb.sqlite3 + +; Alternative MySQL configuration +;backend = mysql +;name = aurweb +;user = aur +;password = aur + +[options] +aur_location = http://127.0.0.1:8080 +disable_http_login = 0 +enable-maintenance = 0 + +[php] +; Address PHP should bind when spawned in development mode by aurweb.spawn. +bind_address = 127.0.0.1:8081 + +; Directory containing aurweb's PHP code, required by aurweb.spawn. +htmldir = YOUR_AUR_ROOT/web/html + +[fastapi] +; Address uvicorn should bind when spawned in development mode by aurweb.spawn. +bind_address = 127.0.0.1:8082 From 0e3bd8b5969f3c3d6ba9273b15ae1e093fc934e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Thu, 4 Jun 2020 21:59:34 +0200 Subject: [PATCH 0107/1451] Remove the FastAPI /hello test route Signed-off-by: Lukas Fleischer --- aurweb/asgi.py | 5 ----- aurweb/spawn.py | 3 --- 2 files changed, 8 deletions(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 5f30471a..9bb71ecc 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -1,8 +1,3 @@ from fastapi import FastAPI app = FastAPI() - - -@app.get("/hello/") -async def hello(): - return {"message": "Hello from FastAPI!"} diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 0506afa4..7fe59e65 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -61,9 +61,6 @@ def generate_nginx_config(): location / {{ proxy_pass http://{aurweb.config.get("php", "bind_address")}; }} - location /hello {{ - proxy_pass http://{aurweb.config.get("fastapi", "bind_address")}; - }} }} }} """) From b1300117ac6fc0f5e9cf1048576db8fb97470bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Thu, 4 Jun 2020 21:59:48 +0200 Subject: [PATCH 0108/1451] aurweb.spawn: Fix isort errors Signed-off-by: Lukas Fleischer --- aurweb/spawn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 7fe59e65..e86f29fe 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -8,8 +8,8 @@ configuration anyway. """ -import atexit import argparse +import atexit import os import subprocess import sys @@ -20,7 +20,6 @@ import urllib import aurweb.config import aurweb.schema - children = [] temporary_dir = None verbosity = 0 From 3b347d3989592293661a47a5bac7645afb8d61d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Thu, 4 Jun 2020 22:00:20 +0200 Subject: [PATCH 0109/1451] Crude OpenID Connect client using Authlib Developers can go to /sso/login to get redirected to the SSO. On successful login, the ID token is displayed. Signed-off-by: Lukas Fleischer --- .gitlab-ci.yml | 3 ++- TESTING | 3 ++- aurweb/asgi.py | 13 +++++++++++++ aurweb/routers/__init__.py | 5 +++++ aurweb/routers/sso.py | 30 ++++++++++++++++++++++++++++++ aurweb/spawn.py | 3 +++ conf/config.defaults | 8 ++++++++ conf/config.dev | 9 +++++++++ 8 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 aurweb/routers/__init__.py create mode 100644 aurweb/routers/sso.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f6260ebb..9dc951aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,8 @@ before_script: base-devel git gpgme protobuf pyalpm python-mysql-connector python-pygit2 python-srcinfo python-bleach python-markdown python-sqlalchemy python-alembic python-pytest python-werkzeug - python-pytest-tap python-fastapi uvicorn nginx + python-pytest-tap python-fastapi uvicorn nginx python-authlib + python-itsdangerous python-httpx test: script: diff --git a/TESTING b/TESTING index 7261df92..d7df3672 100644 --- a/TESTING +++ b/TESTING @@ -13,7 +13,8 @@ INSTALL. # pacman -S --needed php php-sqlite sqlite words fortune-mod \ python python-sqlalchemy python-alembic \ - python-fastapi uvicorn nginx + python-fastapi uvicorn nginx \ + python-authlib python-itsdangerous python-httpx Ensure to enable the pdo_sqlite extension in php.ini. diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 9bb71ecc..60c7ade7 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -1,3 +1,16 @@ from fastapi import FastAPI +from starlette.middleware.sessions import SessionMiddleware + +import aurweb.config + +from aurweb.routers import sso app = FastAPI() + +session_secret = aurweb.config.get("fastapi", "session_secret") +if not session_secret: + raise Exception("[fastapi] session_secret must not be empty") + +app.add_middleware(SessionMiddleware, secret_key=session_secret) + +app.include_router(sso.router) diff --git a/aurweb/routers/__init__.py b/aurweb/routers/__init__.py new file mode 100644 index 00000000..35d43c03 --- /dev/null +++ b/aurweb/routers/__init__.py @@ -0,0 +1,5 @@ +""" +API routers for FastAPI. + +See https://fastapi.tiangolo.com/tutorial/bigger-applications/ +""" diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py new file mode 100644 index 00000000..b16edffb --- /dev/null +++ b/aurweb/routers/sso.py @@ -0,0 +1,30 @@ +import fastapi + +from authlib.integrations.starlette_client import OAuth +from starlette.requests import Request + +import aurweb.config + +router = fastapi.APIRouter() + +oauth = OAuth() +oauth.register( + name="sso", + server_metadata_url=aurweb.config.get("sso", "openid_configuration"), + client_kwargs={"scope": "openid"}, + client_id=aurweb.config.get("sso", "client_id"), + client_secret=aurweb.config.get("sso", "client_secret"), +) + + +@router.get("/sso/login") +async def login(request: Request): + redirect_uri = aurweb.config.get("options", "aur_location") + "/sso/authenticate" + return await oauth.sso.authorize_redirect(request, redirect_uri, prompt="login") + + +@router.get("/sso/authenticate") +async def authenticate(request: Request): + token = await oauth.sso.authorize_access_token(request) + user = await oauth.sso.parse_id_token(request, token) + return dict(user) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index e86f29fe..5da8587e 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -60,6 +60,9 @@ def generate_nginx_config(): location / {{ proxy_pass http://{aurweb.config.get("php", "bind_address")}; }} + location /sso {{ + proxy_pass http://{aurweb.config.get("fastapi", "bind_address")}; + }} }} }} """) diff --git a/conf/config.defaults b/conf/config.defaults index 447dacac..49259754 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -68,6 +68,14 @@ username-regex = [a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$ git-serve-cmd = /usr/local/bin/aurweb-git-serve ssh-options = restrict +[sso] +openid_configuration = +client_id = +client_secret = + +[fastapi] +session_secret = + [serve] repo-path = /srv/http/aurweb/aur.git/ repo-regex = [a-z0-9][a-z0-9.+_-]*$ diff --git a/conf/config.dev b/conf/config.dev index d752f61f..893e8fd6 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -20,6 +20,12 @@ aur_location = http://127.0.0.1:8080 disable_http_login = 0 enable-maintenance = 0 +; Single sign-on +[sso] +openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/openid-configuration +client_id = aurweb +client_secret = + [php] ; Address PHP should bind when spawned in development mode by aurweb.spawn. bind_address = 127.0.0.1:8081 @@ -30,3 +36,6 @@ htmldir = YOUR_AUR_ROOT/web/html [fastapi] ; Address uvicorn should bind when spawned in development mode by aurweb.spawn. bind_address = 127.0.0.1:8082 + +; Passphrase FastAPI uses to sign client-side sessions. +session_secret = secret From 2b439b819908a7f6e9cbd9029d82de617230312f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Thu, 4 Jun 2020 22:00:34 +0200 Subject: [PATCH 0110/1451] Guide to setting up Keycloak for the SSO Signed-off-by: Lukas Fleischer --- conf/config.dev | 2 +- doc/sso.txt | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 doc/sso.txt diff --git a/conf/config.dev b/conf/config.dev index 893e8fd6..37f38c45 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -20,7 +20,7 @@ aur_location = http://127.0.0.1:8080 disable_http_login = 0 enable-maintenance = 0 -; Single sign-on +; Single sign-on; see doc/sso.txt. [sso] openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/openid-configuration client_id = aurweb diff --git a/doc/sso.txt b/doc/sso.txt new file mode 100644 index 00000000..1b0b1f7d --- /dev/null +++ b/doc/sso.txt @@ -0,0 +1,38 @@ +Single Sign-On (SSO) +==================== + +This guide will walk you through setting up Keycloak for use with aurweb. For +extensive documentation, see . + +Installing Keycloak +------------------- + +Keycloak is in the official Arch repositories: + + # pacman -S keycloak + +The default port is 8080, which conflicts with aurweb’s default port. You need +to edit `/etc/keycloak/standalone.xml`, looking for this line: + + + +The default developer configuration assumes it is set to 8083. Alternatively, +you may customize [options] aur_location and [sso] openid_configuration in +`conf/config`. + +You may then start `keycloak.service` through systemd. + +See also ArchWiki . + +Configuring a realm +------------------- + +Go to and log in as administrator. Then, hover the +text right below the Keycloak logo at the top left, by default *Master*. Click +*Add realm* and name it *aurweb*. + +Open the *Clients* tab, and create a new *openid-connect* client. Call it +*aurweb*, and set the root URL to (your aur_location). + +Create a user from the *Users* tab and try logging in from +. From 3f31d149a6dd736007c6583a6162aeda1bcc37b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 9 Jun 2020 20:25:22 +0200 Subject: [PATCH 0111/1451] aurweb.l10n: Translate without side effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install method in Python’s gettext API aliases the translator’s gettext method to an application-global _(). We don’t use that anywhere, and it’s clear from aurweb’s Translator interface that we want to translate a piece of text without affecting any global namespace. Signed-off-by: Lukas Fleischer --- aurweb/l10n.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index 492200b3..51b56abb 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -15,5 +15,4 @@ class Translator: self._translator[lang] = gettext.translation("aurweb", self._localedir, languages=[lang]) - self._translator[lang].install() - return _(s) # _ is not defined, what is this? # noqa: F821 + return self._translator[lang].gettext(s) From a5554c19a9712ede5fe5a996bd1bec11cfc9f66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 8 Jun 2020 20:16:27 +0200 Subject: [PATCH 0112/1451] Add SSO account ID in table Users This column holds a user ID issed by the single sign-on provider. For Keycloak, it is an UUID. For more flexibility, we will be using a standardly-sized VARCHAR field. Signed-off-by: Lukas Fleischer --- aurweb/schema.py | 1 + ...6e1cd_add_sso_account_id_in_table_users.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py diff --git a/aurweb/schema.py b/aurweb/schema.py index 20f3e5ce..a1d56281 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -67,6 +67,7 @@ Users = Table( Column('CommentNotify', TINYINT(1), nullable=False, server_default=text("1")), Column('UpdateNotify', TINYINT(1), nullable=False, server_default=text("0")), Column('OwnershipNotify', TINYINT(1), nullable=False, server_default=text("1")), + Column('SSOAccountID', String(255), nullable=True, unique=True), Index('UsersAccountTypeID', 'AccountTypeID'), mysql_engine='InnoDB', ) diff --git a/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py new file mode 100644 index 00000000..9e125165 --- /dev/null +++ b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py @@ -0,0 +1,30 @@ +"""Add SSO account ID in table Users + +Revision ID: ef39fcd6e1cd +Revises: f47cad5d6d03 +Create Date: 2020-06-08 10:04:13.898617 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ef39fcd6e1cd' +down_revision = 'f47cad5d6d03' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('Users', sa.Column('SSOAccountID', sa.String(length=255), nullable=True)) + op.create_unique_constraint(None, 'Users', ['SSOAccountID']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'Users', type_='unique') + op.drop_column('Users', 'SSOAccountID') + # ### end Alembic commands ### From c77e9d1de0d14253ab3c3b958f459b04b233aeb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 8 Jun 2020 20:16:36 +0200 Subject: [PATCH 0113/1451] Integrate SQLAlchemy into FastAPI Signed-off-by: Lukas Fleischer --- aurweb/db.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/aurweb/db.py b/aurweb/db.py index 1ccd9a07..02aeba38 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -10,6 +10,8 @@ except ImportError: import aurweb.config +engine = None # See get_engine + def get_sqlalchemy_url(): """ @@ -38,6 +40,34 @@ def get_sqlalchemy_url(): raise ValueError('unsupported database backend') +def get_engine(): + """ + Return the global SQLAlchemy engine. + + The engine is created on the first call to get_engine and then stored in the + `engine` global variable for the next calls. + """ + from sqlalchemy import create_engine + global engine + if engine is None: + engine = create_engine(get_sqlalchemy_url(), + # check_same_thread is for a SQLite technicality + # https://fastapi.tiangolo.com/tutorial/sql-databases/#note + connect_args={"check_same_thread": False}) + return engine + + +def connect(): + """ + Return an SQLAlchemy connection. Connections are usually pooled. See + . + + Since SQLAlchemy connections are context managers too, you should use it + with Python’s `with` operator, or with FastAPI’s dependency injection. + """ + return get_engine().connect() + + class Connection: _conn = None _paramstyle = None From 42f8f160b6c556a8c2006788a25b6fafe7be1c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 8 Jun 2020 20:16:49 +0200 Subject: [PATCH 0114/1451] Open AUR sessions from SSO Only the core functionality is implemented here. See the TODOs. Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 51 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index b16edffb..d0802c34 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -1,9 +1,18 @@ +import time +import uuid + import fastapi from authlib.integrations.starlette_client import OAuth +from fastapi import Depends, HTTPException +from fastapi.responses import RedirectResponse +from sqlalchemy.sql import select from starlette.requests import Request import aurweb.config +import aurweb.db + +from aurweb.schema import Sessions, Users router = fastapi.APIRouter() @@ -23,8 +32,46 @@ async def login(request: Request): return await oauth.sso.authorize_redirect(request, redirect_uri, prompt="login") +def open_session(conn, user_id): + """ + Create a new user session into the database. Return its SID. + """ + # TODO check for account suspension + # TODO apply [options] max_sessions_per_user + sid = uuid.uuid4().hex + conn.execute(Sessions.insert().values( + UsersID=user_id, + SessionID=sid, + LastUpdateTS=time.time(), + )) + # TODO update Users.LastLogin and Users.LastLoginIPAddress + return sid + + @router.get("/sso/authenticate") -async def authenticate(request: Request): +async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): + """ + Receive an OpenID Connect ID token, validate it, then process it to create + an new AUR session. + """ + # TODO check for banned IPs token = await oauth.sso.authorize_access_token(request) user = await oauth.sso.parse_id_token(request, token) - return dict(user) + sub = user.get("sub") # this is the SSO account ID in JWT terminology + if not sub: + raise HTTPException(status_code=400, detail="JWT is missing its `sub` field.") + + aur_accounts = conn.execute(select([Users.c.ID]).where(Users.c.SSOAccountID == sub)) \ + .fetchall() + if not aur_accounts: + return "Sorry, we don’t seem to know you Sir " + sub + elif len(aur_accounts) == 1: + sid = open_session(conn, aur_accounts[0][Users.c.ID]) + response = RedirectResponse("/") + # TODO redirect to the referrer + response.set_cookie(key="AURSID", value=sid, httponly=True, + secure=request.url.scheme == "https") + return response + else: + # We’ve got a severe integrity violation. + raise Exception("Multiple accounts found for SSO account " + sub) From 8d5244d0c0c3b38f29b9b087c5e85082f0bb1f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 13 Jul 2020 17:05:37 +0200 Subject: [PATCH 0115/1451] Fix typos in CONTRIBUTING.md Signed-off-by: Lukas Fleischer --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a37d980a..7b9ff466 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,9 @@ Patches should be sent to the [aur-dev@archlinux.org][1] mailing list. -Before sending patched you are recomended to run the `flake8` and `isort`. +Before sending patches, you are recommended to run `flake8` and `isort`. -You can add git hook to do this by installing `python-pre-install` and running -`pre-install install`. +You can add a git hook to do this by installing `python-pre-commit` and running +`pre-commit install`. [1] https://lists.archlinux.org/listinfo/aur-dev From 4bf8228324e4c3811b48069a4b2ae7fd840c78a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 14 Jul 2020 15:34:06 +0200 Subject: [PATCH 0116/1451] SSO: Explain the rationale behind prompt=login We might reconsider it in the future. Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index d0802c34..e1ec7efe 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -28,6 +28,13 @@ oauth.register( @router.get("/sso/login") async def login(request: Request): + """ + Redirect the user to the SSO provider’s login page. + + We specify prompt=login to force the user to input their credentials even + if they’re already logged on the SSO. This is less practical, but given AUR + has the potential to impact many users, better safe than sorry. + """ redirect_uri = aurweb.config.get("options", "aur_location") + "/sso/authenticate" return await oauth.sso.authorize_redirect(request, redirect_uri, prompt="login") From d12ea08fcaa62211cbf4d83bba91124b90f861cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 14 Jul 2020 15:34:23 +0200 Subject: [PATCH 0117/1451] SSO: Add an SSO option in the login page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ll probably change the whole login page in the future, but this makes development easier. Signed-off-by: Lukas Fleischer --- web/html/login.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/html/login.php b/web/html/login.php index 01454414..3a146f60 100644 --- a/web/html/login.php +++ b/web/html/login.php @@ -40,6 +40,9 @@ html_header('AUR ' . __("Login"));

    " /> [] + + [] + From 4d0f2d2279ed9fcdf6bb76015ac0da9c6e938d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 14 Jul 2020 15:35:05 +0200 Subject: [PATCH 0118/1451] Implement SSO logout Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 18 ++++++++++++++++++ web/html/logout.php | 14 +++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index e1ec7efe..a8d4b141 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -1,6 +1,8 @@ import time import uuid +from urllib.parse import urlencode + import fastapi from authlib.integrations.starlette_client import OAuth @@ -82,3 +84,19 @@ async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): else: # We’ve got a severe integrity violation. raise Exception("Multiple accounts found for SSO account " + sub) + + +@router.get("/sso/logout") +async def logout(): + """ + Disconnect the user from the SSO provider, potentially affecting every + other Arch service. AUR logout is performed by `/logout`, before it + redirects to `/sso/logout`. + + Based on the OpenID Connect Session Management specification: + https://openid.net/specs/openid-connect-session-1_0.html#RPLogout + """ + metadata = await oauth.sso.load_server_metadata() + # TODO Supply id_token_hint to the end session endpoint. + query = urlencode({'post_logout_redirect_uri': aurweb.config.get('options', 'aur_location')}) + return RedirectResponse(metadata["end_session_endpoint"] + '?' + query) diff --git a/web/html/logout.php b/web/html/logout.php index 14022001..9fd63943 100644 --- a/web/html/logout.php +++ b/web/html/logout.php @@ -5,16 +5,28 @@ set_include_path(get_include_path() . PATH_SEPARATOR . '../lib'); include_once("aur.inc.php"); # access AUR common functions include_once("acctfuncs.inc.php"); # access AUR common functions +$redirect_uri = '/'; + # if they've got a cookie, log them out - need to do this before # sending any HTML output. # if (isset($_COOKIE["AURSID"])) { + $uid = uid_from_sid($_COOKIE['AURSID']); delete_session_id($_COOKIE["AURSID"]); # setting expiration to 1 means '1 second after midnight January 1, 1970' setcookie("AURSID", "", 1, "/", null, !empty($_SERVER['HTTPS']), true); unset($_COOKIE['AURSID']); clear_expired_sessions(); + + # If the account is linked to an SSO account, disconnect the user from the SSO too. + if (isset($uid)) { + $dbh = DB::connect(); + $sso_account_id = $dbh->query("SELECT SSOAccountID FROM Users WHERE ID = " . $dbh->quote($uid)) + ->fetchColumn(); + if ($sso_account_id) + $redirect_uri = '/sso/logout'; + } } -header('Location: /'); +header("Location: $redirect_uri"); From 357dba87b3ee784a4201a7bb56befb105b81bbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 14 Jul 2020 15:35:24 +0200 Subject: [PATCH 0119/1451] Save id_token for the SSO logout As far as I can see, Keycloak ignores it entirely. I can login in as SSO user A, then disconnect from the SSO directly and reconnect as user B, but when I disconnect user A from AUR, Keycloak disconnects B even though AUR passed it an ID token for A. Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index a8d4b141..04ecdca6 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -80,6 +80,11 @@ async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): # TODO redirect to the referrer response.set_cookie(key="AURSID", value=sid, httponly=True, secure=request.url.scheme == "https") + if "id_token" in token: + # We save the id_token for the SSO logout. It’s not too important + # though, so if we can’t find it, we can live without it. + response.set_cookie(key="SSO_ID_TOKEN", value=token["id_token"], path="/sso/", + httponly=True, secure=request.url.scheme == "https") return response else: # We’ve got a severe integrity violation. @@ -87,7 +92,7 @@ async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): @router.get("/sso/logout") -async def logout(): +async def logout(request: Request): """ Disconnect the user from the SSO provider, potentially affecting every other Arch service. AUR logout is performed by `/logout`, before it @@ -96,7 +101,13 @@ async def logout(): Based on the OpenID Connect Session Management specification: https://openid.net/specs/openid-connect-session-1_0.html#RPLogout """ + id_token = request.cookies.get("SSO_ID_TOKEN") + if not id_token: + return RedirectResponse("/") + metadata = await oauth.sso.load_server_metadata() - # TODO Supply id_token_hint to the end session endpoint. - query = urlencode({'post_logout_redirect_uri': aurweb.config.get('options', 'aur_location')}) - return RedirectResponse(metadata["end_session_endpoint"] + '?' + query) + query = urlencode({'post_logout_redirect_uri': aurweb.config.get('options', 'aur_location'), + 'id_token_hint': id_token}) + response = RedirectResponse(metadata["end_session_endpoint"] + '?' + query) + response.delete_cookie("SSO_ID_TOKEN", path="/sso/") + return response From 0e08b151e5c3606e573b1f7113466b5dd6efdcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 20 Jul 2020 16:25:11 +0200 Subject: [PATCH 0120/1451] SSO: Port IP ban checking Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 04ecdca6..efd4462c 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -14,7 +14,7 @@ from starlette.requests import Request import aurweb.config import aurweb.db -from aurweb.schema import Sessions, Users +from aurweb.schema import Bans, Sessions, Users router = fastapi.APIRouter() @@ -57,13 +57,28 @@ def open_session(conn, user_id): return sid +def is_ip_banned(conn, ip): + """ + Check if an IP is banned. `ip` is a string and may be an IPv4 as well as an + IPv6, depending on the server’s configuration. + """ + result = conn.execute(Bans.select().where(Bans.c.IPAddress == ip)) + return result.fetchone() is not None + + @router.get("/sso/authenticate") async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): """ Receive an OpenID Connect ID token, validate it, then process it to create an new AUR session. """ - # TODO check for banned IPs + # TODO Handle translations + if is_ip_banned(conn, request.client.host): + raise HTTPException( + status_code=403, + detail='The login form is currently disabled for your IP address, ' + 'probably due to sustained spam attacks. Sorry for the ' + 'inconvenience.') token = await oauth.sso.authorize_access_token(request) user = await oauth.sso.parse_id_token(request, token) sub = user.get("sub") # this is the SSO account ID in JWT terminology From e323156947a93ba65a99f927ed2d99c738c34f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 20 Jul 2020 16:25:22 +0200 Subject: [PATCH 0121/1451] SSO: Port account suspension Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index efd4462c..3e3b743d 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -41,11 +41,20 @@ async def login(request: Request): return await oauth.sso.authorize_redirect(request, redirect_uri, prompt="login") +def is_account_suspended(conn, user_id): + row = conn.execute(select([Users.c.Suspended]).where(Users.c.ID == user_id)).fetchone() + return row is not None and bool(row[0]) + + def open_session(conn, user_id): """ Create a new user session into the database. Return its SID. """ - # TODO check for account suspension + # TODO Handle translations. + if is_account_suspended(conn, user_id): + raise HTTPException(status_code=403, detail='Account suspended') + # TODO This is a terrible message because it could imply the attempt at + # logging in just caused the suspension. # TODO apply [options] max_sessions_per_user sid = uuid.uuid4().hex conn.execute(Sessions.insert().values( From 239988def7479cba6407901ee4671b6794d0b2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 20 Jul 2020 16:25:28 +0200 Subject: [PATCH 0122/1451] Build a translation facility for FastAPI Signed-off-by: Lukas Fleischer --- aurweb/l10n.py | 22 ++++++++++++++++++++++ aurweb/routers/sso.py | 20 +++++++++++--------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index 51b56abb..a476ecd8 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -16,3 +16,25 @@ class Translator: self._localedir, languages=[lang]) return self._translator[lang].gettext(s) + + +def get_translator_for_request(request): + """ + Determine the preferred language from a FastAPI request object and build a + translator function for it. + + Example: + ```python + _ = get_translator_for_request(request) + print(_("Hello")) + ``` + """ + lang = request.cookies.get("AURLANG") + if lang is None: + lang = aurweb.config.get("options", "default_lang") + translator = Translator() + + def translate(message): + return translator.translate(message, lang) + + return translate diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 3e3b743d..7b9c67c8 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -14,6 +14,7 @@ from starlette.requests import Request import aurweb.config import aurweb.db +from aurweb.l10n import get_translator_for_request from aurweb.schema import Bans, Sessions, Users router = fastapi.APIRouter() @@ -46,13 +47,13 @@ def is_account_suspended(conn, user_id): return row is not None and bool(row[0]) -def open_session(conn, user_id): +def open_session(request, conn, user_id): """ Create a new user session into the database. Return its SID. """ - # TODO Handle translations. if is_account_suspended(conn, user_id): - raise HTTPException(status_code=403, detail='Account suspended') + _ = get_translator_for_request(request) + raise HTTPException(status_code=403, detail=_('Account suspended')) # TODO This is a terrible message because it could imply the attempt at # logging in just caused the suspension. # TODO apply [options] max_sessions_per_user @@ -81,25 +82,26 @@ async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): Receive an OpenID Connect ID token, validate it, then process it to create an new AUR session. """ - # TODO Handle translations if is_ip_banned(conn, request.client.host): + _ = get_translator_for_request(request) raise HTTPException( status_code=403, - detail='The login form is currently disabled for your IP address, ' - 'probably due to sustained spam attacks. Sorry for the ' - 'inconvenience.') + detail=_('The login form is currently disabled for your IP address, ' + 'probably due to sustained spam attacks. Sorry for the ' + 'inconvenience.')) token = await oauth.sso.authorize_access_token(request) user = await oauth.sso.parse_id_token(request, token) sub = user.get("sub") # this is the SSO account ID in JWT terminology if not sub: - raise HTTPException(status_code=400, detail="JWT is missing its `sub` field.") + _ = get_translator_for_request(request) + raise HTTPException(status_code=400, detail=_("JWT is missing its `sub` field.")) aur_accounts = conn.execute(select([Users.c.ID]).where(Users.c.SSOAccountID == sub)) \ .fetchall() if not aur_accounts: return "Sorry, we don’t seem to know you Sir " + sub elif len(aur_accounts) == 1: - sid = open_session(conn, aur_accounts[0][Users.c.ID]) + sid = open_session(request, conn, aur_accounts[0][Users.c.ID]) response = RedirectResponse("/") # TODO redirect to the referrer response.set_cookie(key="AURSID", value=sid, httponly=True, From efe99dc16f2a94be1d4e5917a07fee2260f3d547 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 5 Jul 2020 18:19:06 -0700 Subject: [PATCH 0123/1451] Support conjunctive keyword search in RPC interface Newly supported API Version 6 modifies `type=search` for _by_ type `name-desc`: it now behaves the same as `name-desc` search through the https://aur.archlinux.org/packages/ search page. Search for packages containing the literal keyword `blah blah` AND `haha`: https://aur.archlinux.org/rpc/?v=6&type=search&arg="blah blah"%20haha Search for packages containing the literal keyword `abc 123`: https://aur.archlinux.org/rpc/?v=6&type=search&arg="abc 123" The following example searches for packages that contain `blah` AND `abc`: https://aur.archlinux.org/rpc/?v=6&type=search&arg=blah%20abc The legacy method still searches for packages that contain `blah abc`: https://aur.archlinux.org/rpc/?v=5&type=search&arg=blah%20abc https://aur.archlinux.org/rpc/?v=5&type=search&arg=blah%20abc API Version 6 is currently only considered during a `search` of `name-desc`. Note: This change was written as a solution to https://bugs.archlinux.org/task/49133. PS: + Some spacing issues fixed in comments. Signed-off-by: Kevin Morris Signed-off-by: Lukas Fleischer --- doc/rpc.txt | 4 ++++ web/lib/aurjson.class.php | 29 +++++++++++++++++++---------- web/lib/pkgfuncs.inc.php | 34 ++++++++++++++++++++-------------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/doc/rpc.txt b/doc/rpc.txt index 3148ebea..b0f5c4e1 100644 --- a/doc/rpc.txt +++ b/doc/rpc.txt @@ -39,6 +39,10 @@ Examples `/rpc/?v=5&type=search&by=makedepends&arg=boost` `search` with callback:: `/rpc/?v=5&type=search&arg=foobar&callback=jsonp1192244621103` +`search` with API Version 6 for packages containing `cookie` AND `milk`:: + `/rpc/?v=6&type=search&arg=cookie%20milk` +`search` with API Version 6 for packages containing `cookie milk`:: + `/rpc/?v=6&type=search&arg="cookie milk"` `info`:: `/rpc/?v=5&type=info&arg[]=foobar` `info` with multiple packages:: diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php index 0ac586fe..86eae22b 100644 --- a/web/lib/aurjson.class.php +++ b/web/lib/aurjson.class.php @@ -1,6 +1,7 @@ version = intval($http_data['v']); } - if ($this->version < 1 || $this->version > 5) { + if ($this->version < 1 || $this->version > 6) { return $this->json_error('Invalid version specified.'); } @@ -140,7 +141,7 @@ class AurJSON { } /* - * Check if an IP needs to be rate limited. + * Check if an IP needs to be rate limited. * * @param $ip IP of the current request * @@ -192,7 +193,7 @@ class AurJSON { $value = get_cache_value('ratelimit-ws:' . $ip, $status); if (!$status || ($status && $value < $deletion_time)) { if (set_cache_value('ratelimit-ws:' . $ip, $time, $window_length) && - set_cache_value('ratelimit:' . $ip, 1, $window_length)) { + set_cache_value('ratelimit:' . $ip, 1, $window_length)) { return; } } else { @@ -370,7 +371,7 @@ class AurJSON { } elseif ($this->version >= 2) { if ($this->version == 2 || $this->version == 3) { $fields = implode(',', self::$fields_v2); - } else if ($this->version == 4 || $this->version == 5) { + } else if ($this->version >= 4 && $this->version <= 6) { $fields = implode(',', self::$fields_v4); } $query = "SELECT {$fields} " . @@ -492,13 +493,21 @@ class AurJSON { if (strlen($keyword_string) < 2) { return $this->json_error('Query arg too small.'); } - $keyword_string = $this->dbh->quote("%" . addcslashes($keyword_string, '%_') . "%"); - if ($search_by === 'name') { - $where_condition = "(Packages.Name LIKE $keyword_string)"; - } else if ($search_by === 'name-desc') { - $where_condition = "(Packages.Name LIKE $keyword_string OR "; - $where_condition .= "Description LIKE $keyword_string)"; + if ($this->version >= 6 && $search_by === 'name-desc') { + $where_condition = construct_keyword_search($this->dbh, + $keyword_string, true, false); + } else { + $keyword_string = $this->dbh->quote( + "%" . addcslashes($keyword_string, '%_') . "%"); + + if ($search_by === 'name') { + $where_condition = "(Packages.Name LIKE $keyword_string)"; + } else if ($search_by === 'name-desc') { + $where_condition = "(Packages.Name LIKE $keyword_string "; + $where_condition .= "OR Description LIKE $keyword_string)"; + } + } } else if ($search_by === 'maintainer') { if (empty($keyword_string)) { diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index 80758005..ac5c8cfe 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -696,8 +696,10 @@ function pkg_search_page($params, $show_headers=true, $SID="") { $q_where .= "AND (PackageBases.Name LIKE " . $dbh->quote($K) . ") "; } elseif (isset($params["SeB"]) && $params["SeB"] == "k") { - /* Search by keywords. */ - $q_where .= construct_keyword_search($dbh, $params['K'], false); + /* Search by name. */ + $q_where .= "AND ("; + $q_where .= construct_keyword_search($dbh, $params['K'], false, true); + $q_where .= ") "; } elseif (isset($params["SeB"]) && $params["SeB"] == "N") { /* Search by name (exact match). */ @@ -709,7 +711,9 @@ function pkg_search_page($params, $show_headers=true, $SID="") { } else { /* Keyword search (default). */ - $q_where .= construct_keyword_search($dbh, $params['K'], true); + $q_where .= "AND ("; + $q_where .= construct_keyword_search($dbh, $params['K'], true, true); + $q_where .= ") "; } } @@ -832,10 +836,11 @@ function pkg_search_page($params, $show_headers=true, $SID="") { * @param handle $dbh Database handle * @param string $keywords The search term * @param bool $namedesc Search name and description fields + * @param bool $keyword Search packages with a matching PackageBases.Keyword * * @return string WHERE part of the SQL clause */ -function construct_keyword_search($dbh, $keywords, $namedesc) { +function construct_keyword_search($dbh, $keywords, $namedesc, $keyword=false) { $count = 0; $where_part = ""; $q_keywords = ""; @@ -860,13 +865,18 @@ function construct_keyword_search($dbh, $keywords, $namedesc) { $term = "%" . addcslashes($term, '%_') . "%"; $q_keywords .= $op . " ("; + $q_keywords .= "Packages.Name LIKE " . $dbh->quote($term) . " "; if ($namedesc) { - $q_keywords .= "Packages.Name LIKE " . $dbh->quote($term) . " OR "; - $q_keywords .= "Description LIKE " . $dbh->quote($term) . " OR "; + $q_keywords .= "OR Description LIKE " . $dbh->quote($term) . " "; + } + + if ($keyword) { + $q_keywords .= "OR EXISTS (SELECT * FROM PackageKeywords WHERE "; + $q_keywords .= "PackageKeywords.PackageBaseID = Packages.PackageBaseID AND "; + $q_keywords .= "PackageKeywords.Keyword LIKE " . $dbh->quote($term) . ")) "; + } else { + $q_keywords .= ") "; } - $q_keywords .= "EXISTS (SELECT * FROM PackageKeywords WHERE "; - $q_keywords .= "PackageKeywords.PackageBaseID = Packages.PackageBaseID AND "; - $q_keywords .= "PackageKeywords.Keyword LIKE " . $dbh->quote($term) . ")) "; $count++; if ($count >= 20) { @@ -875,11 +885,7 @@ function construct_keyword_search($dbh, $keywords, $namedesc) { $op = "AND "; } - if (!empty($q_keywords)) { - $where_part = "AND (" . $q_keywords . ") "; - } - - return $where_part; + return $q_keywords; } /** From 445a991ef1b8c76fb1d51837c4fb692dbfb080e6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 15 Jul 2020 11:45:54 -0700 Subject: [PATCH 0124/1451] Exclude suspended Users from being notified The existing notify.py script was grabbing entries regardless of user suspension. This has been modified to only send notifications to unsuspended users. This change was written as a solution to https://bugs.archlinux.org/task/65554. Signed-off-by: Kevin Morris Signed-off-by: Lukas Fleischer --- aurweb/scripts/notify.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index edae76f8..7f8e7168 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -126,7 +126,7 @@ class ResetKeyNotification(Notification): def __init__(self, conn, uid): cur = conn.execute('SELECT UserName, Email, BackupEmail, ' + 'LangPreference, ResetKey ' + - 'FROM Users WHERE ID = ?', [uid]) + 'FROM Users WHERE ID = ? AND Suspended = 0', [uid]) self._username, self._to, self._backup, self._lang, self._resetkey = cur.fetchone() super().__init__() @@ -173,7 +173,8 @@ class CommentNotification(Notification): 'ON PackageNotifications.UserID = Users.ID WHERE ' + 'Users.CommentNotify = 1 AND ' + 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', + 'PackageNotifications.PackageBaseID = ? AND ' + + 'Users.Suspended = 0', [uid, pkgbase_id]) self._recipients = cur.fetchall() cur = conn.execute('SELECT Comments FROM PackageComments WHERE ID = ?', @@ -220,7 +221,8 @@ class UpdateNotification(Notification): 'ON PackageNotifications.UserID = Users.ID WHERE ' + 'Users.UpdateNotify = 1 AND ' + 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', + 'PackageNotifications.PackageBaseID = ? AND ' + + 'Users.Suspended = 0', [uid, pkgbase_id]) self._recipients = cur.fetchall() super().__init__() @@ -266,7 +268,8 @@ class FlagNotification(Notification): 'INNER JOIN PackageBases ' + 'ON PackageBases.MaintainerUID = Users.ID OR ' + 'PackageBases.ID = PackageComaintainers.PackageBaseID ' + - 'WHERE PackageBases.ID = ?', [pkgbase_id]) + 'WHERE PackageBases.ID = ? AND ' + + 'Users.Suspended = 0', [pkgbase_id]) self._recipients = cur.fetchall() cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' + 'ID = ?', [pkgbase_id]) @@ -304,7 +307,8 @@ class OwnershipEventNotification(Notification): 'ON PackageNotifications.UserID = Users.ID WHERE ' + 'Users.OwnershipNotify = 1 AND ' + 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', + 'PackageNotifications.PackageBaseID = ? AND ' + + 'Users.Suspended = 0', [uid, pkgbase_id]) self._recipients = cur.fetchall() cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' + @@ -343,7 +347,7 @@ class ComaintainershipEventNotification(Notification): def __init__(self, conn, uid, pkgbase_id): self._pkgbase = pkgbase_from_id(conn, pkgbase_id) cur = conn.execute('SELECT Email, LangPreference FROM Users ' + - 'WHERE ID = ?', [uid]) + 'WHERE ID = ? AND Suspended = 0', [uid]) self._to, self._lang = cur.fetchone() super().__init__() @@ -386,7 +390,8 @@ class DeleteNotification(Notification): 'INNER JOIN PackageNotifications ' + 'ON PackageNotifications.UserID = Users.ID WHERE ' + 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', + 'PackageNotifications.PackageBaseID = ? AND ' + + 'Users.Suspended = 0', [uid, old_pkgbase_id]) self._recipients = cur.fetchall() super().__init__() @@ -433,7 +438,8 @@ class RequestOpenNotification(Notification): 'INNER JOIN Users ' + 'ON Users.ID = PackageRequests.UsersID ' + 'OR Users.ID = PackageBases.MaintainerUID ' + - 'WHERE PackageRequests.ID = ?', [reqid]) + 'WHERE PackageRequests.ID = ? AND ' + + 'Users.Suspended = 0', [reqid]) self._to = aurweb.config.get('options', 'aur_request_ml') self._cc = [row[0] for row in cur.fetchall()] cur = conn.execute('SELECT Comments FROM PackageRequests WHERE ID = ?', @@ -489,7 +495,8 @@ class RequestCloseNotification(Notification): 'INNER JOIN Users ' + 'ON Users.ID = PackageRequests.UsersID ' + 'OR Users.ID = PackageBases.MaintainerUID ' + - 'WHERE PackageRequests.ID = ?', [reqid]) + 'WHERE PackageRequests.ID = ? AND ' + + 'Users.Suspended = 0', [reqid]) self._to = aurweb.config.get('options', 'aur_request_ml') self._cc = [row[0] for row in cur.fetchall()] cur = conn.execute('SELECT PackageRequests.ClosureComment, ' + @@ -547,7 +554,8 @@ class TUVoteReminderNotification(Notification): cur = conn.execute('SELECT Email, LangPreference FROM Users ' + 'WHERE AccountTypeID IN (2, 4) AND ID NOT IN ' + '(SELECT UserID FROM TU_Votes ' + - 'WHERE TU_Votes.VoteID = ?)', [vote_id]) + 'WHERE TU_Votes.VoteID = ?) AND ' + + 'Users.Suspended = 0', [vote_id]) self._recipients = cur.fetchall() super().__init__() From a1a742b518b7ead1ea32b13dd52d5ea5248e8bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 27 Jul 2020 14:43:48 +0200 Subject: [PATCH 0125/1451] aurweb.spawn: Support stdout redirections to non-tty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only ttys have a terminal size. If we can’t obtain it, we’ll just use 80 as a sane default. Signed-off-by: Lukas Fleischer --- aurweb/spawn.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 5da8587e..46d534d9 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -87,12 +87,16 @@ def start(): return atexit.register(stop) + try: + terminal_width = os.get_terminal_size().columns + except OSError: + terminal_width = 80 print("{ruler}\n" "Spawing PHP and FastAPI, then nginx as a reverse proxy.\n" "Check out {aur_location}\n" "Hit ^C to terminate everything.\n" "{ruler}" - .format(ruler=("-" * os.get_terminal_size().columns), + .format(ruler=("-" * terminal_width), aur_location=aurweb.config.get('options', 'aur_location'))) # PHP From 9290eee1385b4ac9e09fec4a784868789ea5a15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Mon, 27 Jul 2020 14:44:03 +0200 Subject: [PATCH 0126/1451] Stop redirecting stderr with proc_open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Error outputs were piped to a temporary buffer that wasn’t read by anyone, making debugging hard because errors were completely silenced. By not explicitly redirecting stderr on proc_open, the subprocess inherits its parent stderr. Signed-off-by: Lukas Fleischer --- web/lib/acctfuncs.inc.php | 2 -- web/lib/pkgbasefuncs.inc.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index 752abe97..b3822eaf 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -1347,7 +1347,6 @@ function notify($params) { $descspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), - 2 => array('pipe', 'w') ); $p = proc_open($cmd, $descspec, $pipes); @@ -1358,7 +1357,6 @@ function notify($params) { fclose($pipes[0]); fclose($pipes[1]); - fclose($pipes[2]); return proc_close($p); } diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php index 4c8abba7..4a49898c 100644 --- a/web/lib/pkgbasefuncs.inc.php +++ b/web/lib/pkgbasefuncs.inc.php @@ -96,7 +96,6 @@ function render_comment($id) { $descspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), - 2 => array('pipe', 'w') ); $p = proc_open($cmd, $descspec, $pipes); @@ -107,7 +106,6 @@ function render_comment($id) { fclose($pipes[0]); fclose($pipes[1]); - fclose($pipes[2]); return proc_close($p); } From 202ffd8923bc3a08bc6d4f18ac6d91441b0b0cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 28 Jul 2020 16:33:12 +0200 Subject: [PATCH 0127/1451] Update last login information on SSO login Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 7b9c67c8..817adadb 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -63,7 +63,13 @@ def open_session(request, conn, user_id): SessionID=sid, LastUpdateTS=time.time(), )) - # TODO update Users.LastLogin and Users.LastLoginIPAddress + + # Update user’s last login information. + conn.execute(Users.update() + .where(Users.c.ID == user_id) + .values(LastLogin=int(time.time()), + LastLoginIPAddress=request.client.host)) + return sid From 5fb4fc12de1dc374395340724d192271d4aa31f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 28 Jul 2020 16:33:27 +0200 Subject: [PATCH 0128/1451] HTML error pages for FastAPI Signed-off-by: Lukas Fleischer --- aurweb/asgi.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 60c7ade7..9293ed77 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -1,4 +1,7 @@ -from fastapi import FastAPI +import http + +from fastapi import FastAPI, HTTPException +from fastapi.responses import HTMLResponse from starlette.middleware.sessions import SessionMiddleware import aurweb.config @@ -14,3 +17,14 @@ if not session_secret: app.add_middleware(SessionMiddleware, secret_key=session_secret) app.include_router(sso.router) + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request, exc): + """ + Dirty HTML error page to replace the default JSON error responses. + In the future this should use a proper Arch-themed HTML template. + """ + phrase = http.HTTPStatus(exc.status_code).phrase + return HTMLResponse(f"

    {exc.status_code} {phrase}

    {exc.detail}

    ", + status_code=exc.status_code) From be31675b6589e66c8b10a64b44591b594d2eb735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Tue, 28 Jul 2020 16:33:41 +0200 Subject: [PATCH 0129/1451] Guard OAuth exceptions to provide better messages Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 817adadb..2e4fbacc 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -5,7 +5,7 @@ from urllib.parse import urlencode import fastapi -from authlib.integrations.starlette_client import OAuth +from authlib.integrations.starlette_client import OAuth, OAuthError from fastapi import Depends, HTTPException from fastapi.responses import RedirectResponse from sqlalchemy.sql import select @@ -95,8 +95,18 @@ async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): detail=_('The login form is currently disabled for your IP address, ' 'probably due to sustained spam attacks. Sorry for the ' 'inconvenience.')) - token = await oauth.sso.authorize_access_token(request) - user = await oauth.sso.parse_id_token(request, token) + + try: + token = await oauth.sso.authorize_access_token(request) + user = await oauth.sso.parse_id_token(request, token) + except OAuthError: + # Here, most OAuth errors should be caused by forged or expired tokens. + # Let’s give attackers as little information as possible. + _ = get_translator_for_request(request) + raise HTTPException( + status_code=400, + detail=_('Bad OAuth token. Please retry logging in from the start.')) + sub = user.get("sub") # this is the SSO account ID in JWT terminology if not sub: _ = get_translator_for_request(request) From 87815d37c078c315ac3254741973cfba2bfccace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Wed, 29 Jul 2020 13:46:10 +0200 Subject: [PATCH 0130/1451] Remove the per-user session limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature was originally introduced by f961ffd9c7f2d3d51d3e3b060990a4fef9e56c1b as a fix for FS#12898 . As of today, it is broken because of the `q.SessionID IS NULL` condition in the WHERE clause, which can’t be true because SessionID is not nullable. As a consequence, the session limit was not applied. The fact the absence of the session limit hasn’t caused any issue so far, and hadn’t even been noticed, suggests the feature is unneeded. Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 2 +- conf/config.defaults | 1 - web/lib/acctfuncs.inc.php | 15 --------------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 2e4fbacc..73c884a4 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -56,7 +56,7 @@ def open_session(request, conn, user_id): raise HTTPException(status_code=403, detail=_('Account suspended')) # TODO This is a terrible message because it could imply the attempt at # logging in just caused the suspension. - # TODO apply [options] max_sessions_per_user + sid = uuid.uuid4().hex conn.execute(Sessions.insert().values( UsersID=user_id, diff --git a/conf/config.defaults b/conf/config.defaults index 49259754..98e033b7 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -13,7 +13,6 @@ passwd_min_len = 8 default_lang = en default_timezone = UTC sql_debug = 0 -max_sessions_per_user = 8 login_timeout = 7200 persistent_cookie_timeout = 2592000 max_filesize_uncompressed = 8388608 diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index b3822eaf..bc603d3b 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -596,21 +596,6 @@ function try_login() { /* Generate a session ID and store it. */ while (!$logged_in && $num_tries < 5) { - $session_limit = config_get_int('options', 'max_sessions_per_user'); - if ($session_limit) { - /* - * Delete all user sessions except the - * last ($session_limit - 1). - */ - $q = "DELETE FROM Sessions "; - $q.= "WHERE UsersId = " . $userID . " "; - $q.= "AND SessionID NOT IN (SELECT SessionID FROM Sessions "; - $q.= "WHERE UsersID = " . $userID . " "; - $q.= "ORDER BY LastUpdateTS DESC "; - $q.= "LIMIT " . ($session_limit - 1) . ")"; - $dbh->query($q); - } - $new_sid = new_sid(); $q = "INSERT INTO Sessions (UsersID, SessionID, LastUpdateTS)" ." VALUES (" . $userID . ", '" . $new_sid . "', " . strval(time()) . ")"; From 8c28ba6e7f1c99f4b16c651857224b1d19f93466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Mangano-Tarumi?= Date: Wed, 29 Jul 2020 17:25:44 +0200 Subject: [PATCH 0131/1451] Redirect to referer after SSO login Introduce a `redirect` query argument to SSO login endpoints so that users are redirected to the page they were originally on when they clicked the Login link. Signed-off-by: Lukas Fleischer --- aurweb/routers/sso.py | 23 +++++++++++++++++------ web/html/login.php | 18 ++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 73c884a4..4b12b932 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -30,16 +30,21 @@ oauth.register( @router.get("/sso/login") -async def login(request: Request): +async def login(request: Request, redirect: str = None): """ Redirect the user to the SSO provider’s login page. We specify prompt=login to force the user to input their credentials even if they’re already logged on the SSO. This is less practical, but given AUR has the potential to impact many users, better safe than sorry. + + The `redirect` argument is a query parameter specifying the post-login + redirect URL. """ - redirect_uri = aurweb.config.get("options", "aur_location") + "/sso/authenticate" - return await oauth.sso.authorize_redirect(request, redirect_uri, prompt="login") + authenticate_url = aurweb.config.get("options", "aur_location") + "/sso/authenticate" + if redirect: + authenticate_url = authenticate_url + "?" + urlencode([("redirect", redirect)]) + return await oauth.sso.authorize_redirect(request, authenticate_url, prompt="login") def is_account_suspended(conn, user_id): @@ -82,8 +87,15 @@ def is_ip_banned(conn, ip): return result.fetchone() is not None +def is_aur_url(url): + aur_location = aurweb.config.get("options", "aur_location") + if not aur_location.endswith("/"): + aur_location = aur_location + "/" + return url.startswith(aur_location) + + @router.get("/sso/authenticate") -async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): +async def authenticate(request: Request, redirect: str = None, conn=Depends(aurweb.db.connect)): """ Receive an OpenID Connect ID token, validate it, then process it to create an new AUR session. @@ -118,8 +130,7 @@ async def authenticate(request: Request, conn=Depends(aurweb.db.connect)): return "Sorry, we don’t seem to know you Sir " + sub elif len(aur_accounts) == 1: sid = open_session(request, conn, aur_accounts[0][Users.c.ID]) - response = RedirectResponse("/") - # TODO redirect to the referrer + response = RedirectResponse(redirect if redirect and is_aur_url(redirect) else "/") response.set_cookie(key="AURSID", value=sid, httponly=True, secure=request.url.scheme == "https") if "id_token" in token: diff --git a/web/html/login.php b/web/html/login.php index 3a146f60..3f3d66cc 100644 --- a/web/html/login.php +++ b/web/html/login.php @@ -9,6 +9,10 @@ if (!$disable_http_login || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'])) { $login_error = $login['error']; } +$referer = in_request('referer'); +if ($referer === '') + $referer = $_SERVER['HTTP_REFERER']; + html_header('AUR ' . __("Login")); ?>
    @@ -40,13 +44,15 @@ html_header('AUR ' . __("Login"));

    " /> [] - - [] + + [] - - - - + +

    From 83d228d9e8c2b067dfd5565d22437363f7590d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 13 Aug 2020 15:45:58 +0100 Subject: [PATCH 0132/1451] spawn: expand AUR_CONFIG to the full path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows using a relative path for the config. PHP didn't play well with it. Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- aurweb/spawn.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 46d534d9..3c5130d7 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -11,6 +11,7 @@ configuration anyway. import argparse import atexit import os +import os.path import subprocess import sys import tempfile @@ -87,6 +88,9 @@ def start(): return atexit.register(stop) + if 'AUR_CONFIG' in os.environ: + os.environ['AUR_CONFIG'] = os.path.realpath(os.environ['AUR_CONFIG']) + try: terminal_width = os.get_terminal_size().columns except OSError: From 4e4f5855f17acc2bd353093566ea853aa523f933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 13 Aug 2020 15:46:00 +0100 Subject: [PATCH 0133/1451] doc: fix AUR_CONFIG in TESTING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- TESTING | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TESTING b/TESTING index d7df3672..17c6fbc7 100644 --- a/TESTING +++ b/TESTING @@ -29,7 +29,7 @@ INSTALL. 4) Prepare the testing database: $ cd /path/to/aurweb/ - $ python -m aurweb.initdb + $ AUR_CONFIG=conf/config python -m aurweb.initdb $ cd /path/to/aurweb/schema $ ./gendummydata.py out.sql @@ -37,4 +37,4 @@ INSTALL. 5) Run the test server: - $ AUR_CONFIG='/path/to/aurweb/conf/config' python -m aurweb.spawn + $ AUR_CONFIG=conf/config python -m aurweb.spawn From e62d472708e722e6123f9bfa01d2c32ec9838755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 13 Aug 2020 15:46:01 +0100 Subject: [PATCH 0134/1451] doc: add missing gendummydata.py dependencies in TESTING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- TESTING | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TESTING b/TESTING index 17c6fbc7..d666b3ca 100644 --- a/TESTING +++ b/TESTING @@ -14,7 +14,8 @@ INSTALL. # pacman -S --needed php php-sqlite sqlite words fortune-mod \ python python-sqlalchemy python-alembic \ python-fastapi uvicorn nginx \ - python-authlib python-itsdangerous python-httpx + python-authlib python-itsdangerous python-httpx \ + words fortune-mod Ensure to enable the pdo_sqlite extension in php.ini. From db75a5528e45c13a427813c177bf9657a1cb8350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 13 Aug 2020 15:46:02 +0100 Subject: [PATCH 0135/1451] doc: simplify database setup instructions in TESTING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns Signed-off-by: Lukas Fleischer --- TESTING | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TESTING b/TESTING index d666b3ca..972bce2c 100644 --- a/TESTING +++ b/TESTING @@ -30,11 +30,11 @@ INSTALL. 4) Prepare the testing database: $ cd /path/to/aurweb/ + $ AUR_CONFIG=conf/config python -m aurweb.initdb - $ cd /path/to/aurweb/schema - $ ./gendummydata.py out.sql - $ sqlite3 path/to/aurweb.sqlite3 < out.sql + $ schema/gendummydata.py data.sql + $ sqlite3 aurweb.sqlite3 < data.sql 5) Run the test server: From 92e315465b19e18b67ba7bff0b14c9658db579f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Fri, 4 Sep 2020 23:06:43 +0200 Subject: [PATCH 0136/1451] gendummydata.py: remove unused database connection variables Signed-off-by: Lukas Fleischer --- schema/gendummydata.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index b3a73ef2..8b15ac69 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -18,10 +18,6 @@ import time LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" -DB_HOST = os.getenv("DB_HOST", "localhost") -DB_NAME = os.getenv("DB_NAME", "AUR") -DB_USER = os.getenv("DB_USER", "aur") -DB_PASS = os.getenv("DB_PASS", "aur") USER_ID = 5 # Users.ID of first bogus user PKG_ID = 1 # Packages.ID of first package MAX_USERS = 300 # how many users to 'register' From 879c0622d66a04b38b281f879ebbf06ba97c62c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Fri, 4 Sep 2020 23:13:56 +0200 Subject: [PATCH 0137/1451] gendummydata.py: set exit code to 1 when there is an error Of course the default exit code is 0... Signed-off-by: Lukas Fleischer --- schema/gendummydata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 8b15ac69..224c82e5 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -44,19 +44,19 @@ log = logging.getLogger() if len(sys.argv) != 2: log.error("Missing output filename argument") - raise SystemExit + raise SystemExit(1) # make sure the seed file exists # if not os.path.exists(SEED_FILE): log.error("Please install the 'words' Arch package") - raise SystemExit + raise SystemExit(1) # make sure comments can be created # if not os.path.exists(FORTUNE_FILE): log.error("Please install the 'fortune-mod' Arch package") - raise SystemExit + raise SystemExit(1) # track what users/package names have been used # From 51a353582010a45b2121c25a5ad995111f1842a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Fri, 4 Sep 2020 23:10:20 +0200 Subject: [PATCH 0138/1451] gendummydata.py: set MAX_USERS and MAX_PKGS to more realistic values Signed-off-by: Lukas Fleischer --- schema/gendummydata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 224c82e5..91a580c2 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -20,16 +20,16 @@ LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" USER_ID = 5 # Users.ID of first bogus user PKG_ID = 1 # Packages.ID of first package -MAX_USERS = 300 # how many users to 'register' +MAX_USERS = 76000 # how many users to 'register' MAX_DEVS = .1 # what percentage of MAX_USERS are Developers MAX_TUS = .2 # what percentage of MAX_USERS are Trusted Users -MAX_PKGS = 900 # how many packages to load +MAX_PKGS = 64000 # how many packages to load PKG_DEPS = (1, 15) # min/max depends a package has PKG_RELS = (1, 5) # min/max relations a package has PKG_SRC = (1, 3) # min/max sources a package has PKG_CMNTS = (1, 5) # min/max number of comments a package has CATEGORIES_COUNT = 17 # the number of categories from aur-schema -VOTING = (0, .30) # percentage range for package voting +VOTING = (0, .001) # percentage range for package voting OPEN_PROPOSALS = 5 # number of open trusted user proposals CLOSE_PROPOSALS = 15 # number of closed trusted user proposals RANDOM_TLDS = ("edu", "com", "org", "net", "tw", "ru", "pl", "de", "es") From 3062a78a92d32144f423fdb460e96a309732d9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Fri, 4 Sep 2020 23:53:07 +0200 Subject: [PATCH 0139/1451] gendummydata.py: optimize iteration for big numbers of pkgs Signed-off-by: Lukas Fleischer --- schema/gendummydata.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 91a580c2..c7b3a06d 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -259,20 +259,23 @@ for p in list(track_votes.keys()): # Create package dependencies and sources # log.debug("Creating statements for package depends/sources.") -for p in list(seen_pkgs.keys()): +# the keys of seen_pkgs are accessed many times by random.choice, +# so the list has to be created outside the loops to keep it efficient +seen_pkgs_keys = list(seen_pkgs.keys()) +for p in seen_pkgs_keys: num_deps = random.randrange(PKG_DEPS[0], PKG_DEPS[1]) for i in range(0, num_deps): - dep = random.choice([k for k in seen_pkgs]) + dep = random.choice(seen_pkgs_keys) deptype = random.randrange(1, 5) if deptype == 4: - dep += ": for " + random.choice([k for k in seen_pkgs]) + dep += ": for " + random.choice(seen_pkgs_keys) s = "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) VALUES (%d, %d, '%s');\n" s = s % (seen_pkgs[p], deptype, dep) out.write(s) num_rels = random.randrange(PKG_RELS[0], PKG_RELS[1]) for i in range(0, num_deps): - rel = random.choice([k for k in seen_pkgs]) + rel = random.choice(seen_pkgs_keys) reltype = random.randrange(1, 4) s = "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) VALUES (%d, %d, '%s');\n" s = s % (seen_pkgs[p], reltype, rel) From bc972089a158459005700a7eaa8cee3ba666e2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Klinkovsk=C3=BD?= Date: Sat, 5 Sep 2020 22:15:22 +0200 Subject: [PATCH 0140/1451] Fix WHERE clause for keyword search queries with empty keywords When the keyword parameter is empty, the AND clause has to be omitted, otherwise we get an SQL syntax error: ... WHERE PackageBases.PackagerUID IS NOT NULL AND () ... This got broken in commit 9e30013aa4fc6ce3a3c9f6f83a6fe789c1fc2456 Author: Kevin Morris Date: Sun Jul 5 18:19:06 2020 -0700 Support conjunctive keyword search in RPC interface Signed-off-by: Lukas Fleischer --- web/lib/pkgfuncs.inc.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index ac5c8cfe..eb3afab6 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -697,9 +697,7 @@ function pkg_search_page($params, $show_headers=true, $SID="") { } elseif (isset($params["SeB"]) && $params["SeB"] == "k") { /* Search by name. */ - $q_where .= "AND ("; $q_where .= construct_keyword_search($dbh, $params['K'], false, true); - $q_where .= ") "; } elseif (isset($params["SeB"]) && $params["SeB"] == "N") { /* Search by name (exact match). */ @@ -711,9 +709,7 @@ function pkg_search_page($params, $show_headers=true, $SID="") { } else { /* Keyword search (default). */ - $q_where .= "AND ("; $q_where .= construct_keyword_search($dbh, $params['K'], true, true); - $q_where .= ") "; } } @@ -885,7 +881,11 @@ function construct_keyword_search($dbh, $keywords, $namedesc, $keyword=false) { $op = "AND "; } - return $q_keywords; + if (!empty($q_keywords)) { + $where_part = "AND (" . $q_keywords . ") "; + } + + return $where_part; } /** From 568e0d2fa33d17ea4c45d046d9870d8ba0376789 Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Thu, 19 Nov 2020 23:16:17 +0100 Subject: [PATCH 0141/1451] RSS: Add atom self link https://validator.w3.org/feed/docs/warning/MissingAtomSelfLink.html Signed-off-by: Lukas Fleischer --- web/lib/feedcreator.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/lib/feedcreator.class.php b/web/lib/feedcreator.class.php index 802eebbe..e881f252 100644 --- a/web/lib/feedcreator.class.php +++ b/web/lib/feedcreator.class.php @@ -906,12 +906,13 @@ class RSSCreator091 extends FeedCreator { $feed = "encoding."\"?>\n"; $feed.= $this->_createGeneratorComment(); $feed.= $this->_createStylesheetReferences(); - $feed.= "RSSVersion."\">\n"; + $feed.= "RSSVersion."\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n"; $feed.= " \n"; $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."\n"; $this->descriptionTruncSize = 500; $feed.= " ".$this->getDescription()."\n"; $feed.= " ".$this->link."\n"; + $feed.= " syndicationURL."\" rel=\"self\" type=\"application/rss+xml\" />\n"; $now = new FeedDate(); $feed.= " ".htmlspecialchars($now->rfc822())."\n"; $feed.= " ".FEEDCREATOR_VERSION."\n"; From 78dbbd3dfa916e0b054a231ff7cd56049ff7dc2f Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Thu, 19 Nov 2020 23:17:49 +0100 Subject: [PATCH 0142/1451] RSS: Set proper content type header https://validator.w3.org/feed/docs/warning/UnexpectedContentType.html Signed-off-by: Lukas Fleischer --- web/html/rss.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/html/rss.php b/web/html/rss.php index d6e7825a..245a2171 100644 --- a/web/html/rss.php +++ b/web/html/rss.php @@ -11,6 +11,8 @@ $host = $_SERVER['HTTP_HOST']; $feed_key = 'pkg-feed-' . $protocol; +header("Content-Type: application/rss+xml"); + $bool = false; $ret = get_cache_value($feed_key, $bool); if ($bool) { From 1d0c6ffe24c692d4279ce6ad6cb03aeb38a2303e Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Thu, 19 Nov 2020 23:18:33 +0100 Subject: [PATCH 0143/1451] RSS: Make sure image title matches channel title https://validator.w3.org/feed/docs/warning/ImageTitleDoesntMatch.html Signed-off-by: Lukas Fleischer --- web/html/rss.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/html/rss.php b/web/html/rss.php index 245a2171..5720d3d1 100644 --- a/web/html/rss.php +++ b/web/html/rss.php @@ -33,7 +33,7 @@ $rss->description = "The latest and greatest packages in the AUR"; $rss->link = "${protocol}://{$host}"; $rss->syndicationURL = "{$protocol}://{$host}" . get_uri('/rss/'); $image = new FeedImage(); -$image->title = "AUR"; +$image->title = "AUR Newest Packages"; $image->url = "{$protocol}://{$host}/css/archnavbar/aurlogo.png"; $image->link = $rss->link; $image->description = "AUR Newest Packages Feed"; From eb11943fed16462e089891b128fd01f9e460b114 Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Thu, 19 Nov 2020 23:22:11 +0100 Subject: [PATCH 0144/1451] RSS: Always provide a GUID https://validator.w3.org/feed/docs/warning/MissingGuid.html Signed-off-by: Lukas Fleischer --- web/html/rss.php | 1 + 1 file changed, 1 insertion(+) diff --git a/web/html/rss.php b/web/html/rss.php index 5720d3d1..b67f862d 100644 --- a/web/html/rss.php +++ b/web/html/rss.php @@ -50,6 +50,7 @@ foreach ($packages as $indx => $row) { $item->date = intval($row["SubmittedTS"]); $item->source = "{$protocol}://{$host}"; $item->author = username_from_id($row["MaintainerUID"]); + $item->guid = $item->link; $rss->addItem($item); } From d5d333005eafb338fc5aefff9d4426cc23dbd84c Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Thu, 19 Nov 2020 23:27:21 +0100 Subject: [PATCH 0145/1451] RSS: Decrease cache time and increase item count I think after 10-15 years we might want to adjust those values. With a 30min cache and 20 items per creation I would bet some new AUR packages might be swept under the carpet. Signed-off-by: Lukas Fleischer --- web/html/rss.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/html/rss.php b/web/html/rss.php index b67f862d..1e6335cf 100644 --- a/web/html/rss.php +++ b/web/html/rss.php @@ -40,7 +40,7 @@ $image->description = "AUR Newest Packages Feed"; $rss->image = $image; #Get the latest packages and add items for them -$packages = latest_pkgs(20); +$packages = latest_pkgs(100); foreach ($packages as $indx => $row) { $item = new FeedItem(); @@ -56,6 +56,6 @@ foreach ($packages as $indx => $row) { #save it so that useCached() can find it $feedContent = $rss->createFeed(); -set_cache_value($feed_key, $feedContent, 1800); +set_cache_value($feed_key, $feedContent, 600); echo $feedContent; ?> From 62b413f6b76fd852abf728897f9929c4bf7024ea Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Sun, 27 Dec 2020 19:38:48 -0500 Subject: [PATCH 0146/1451] .gitignore: add test/trash directory* Signed-off-by: Lukas Fleischer --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7c9fa60b..4d961d13 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ aur.git/ __pycache__/ *.py[cod] test/test-results/ +test/trash directory* schema/aur-schema-sqlite.sql From 933d2705f935011035ee661e4a7acac37cd7aca3 Mon Sep 17 00:00:00 2001 From: Lukas Fleischer Date: Mon, 28 Dec 2020 13:03:24 -0500 Subject: [PATCH 0147/1451] Fetch Transifex image from https://www.transifex.com Fixes GitLab issue #3. Signed-off-by: Lukas Fleischer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7285a51..77f3b13d 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,4 @@ Translations Translations are welcome via our Transifex project at https://www.transifex.com/lfleischer/aurweb; see `doc/i18n.txt` for details. -![Transifex](http://www.transifex.net/projects/p/aurweb/chart/image_png) +![Transifex](https://www.transifex.com/projects/p/aurweb/chart/image_png) From 21c457817fe8ec5db60a10e2270f9fcf951aca5f Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Sat, 23 Jan 2021 00:43:57 +0800 Subject: [PATCH 0148/1451] Use jsDelivr instead of Google CDN for jquery jsdelivr is another free CDN service for open source projects. The main motivation for this change is that it is the only one that works fairly well across the globe. The Google CDN service is known to be hardly accessible in mainland China, unfortunately. Signed-off-by: Lukas Fleischer --- web/html/home.php | 2 +- web/html/packages.php | 2 +- web/html/pkgmerge.php | 2 +- web/template/pkgreq_form.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/html/home.php b/web/html/home.php index 0ce89f40..8fb05246 100644 --- a/web/html/home.php +++ b/web/html/home.php @@ -202,7 +202,7 @@ if (isset($_COOKIE["AURSID"])) {
    - + + + + ", ""); EOD - "$RENDERCOMMENT" 3 && + cover "$RENDERCOMMENT" 3 && cat <<-EOD >expected && <script>alert("XSS!");</script> EOD @@ -61,7 +61,7 @@ test_expect_success 'Test link conversion.' ' [arch]: https://www.archlinux.org/ ", ""); EOD - "$RENDERCOMMENT" 4 && + cover "$RENDERCOMMENT" 4 && cat <<-EOD >expected &&

    Visit https://www.archlinux.org/#_test_. Visit https://www.archlinux.org/. @@ -89,7 +89,7 @@ test_expect_success 'Test Git commit linkification.' ' http://example.com/$oid ", ""); EOD - "$RENDERCOMMENT" 5 && + cover "$RENDERCOMMENT" 5 && cat <<-EOD >expected &&

    ${oid:0:12} ${oid:0:7} @@ -116,7 +116,7 @@ test_expect_success 'Test Flyspray issue linkification.' ' https://archlinux.org/?test=FS#1234 ", ""); EOD - "$RENDERCOMMENT" 6 && + cover "$RENDERCOMMENT" 6 && cat <<-EOD >expected &&

    FS#1234567. FS#1234 @@ -142,7 +142,7 @@ test_expect_success 'Test headings lowering.' ' ###### Six ", ""); EOD - "$RENDERCOMMENT" 7 && + cover "$RENDERCOMMENT" 7 && cat <<-EOD >expected &&

    One
    Two
    diff --git a/test/t2700-usermaint.t b/test/t2700-usermaint.t index f0bb449b..c119e3f4 100755 --- a/test/t2700-usermaint.t +++ b/test/t2700-usermaint.t @@ -16,7 +16,7 @@ test_expect_success 'Test removal of login IP addresses.' ' UPDATE Users SET LastLogin = 0, LastLoginIPAddress = "5.6.7.8" WHERE ID = 5; UPDATE Users SET LastLogin = $tendaysago, LastLoginIPAddress = "6.7.8.9" WHERE ID = 6; EOD - "$USERMAINT" && + cover "$USERMAINT" && cat <<-EOD >expected && 1.2.3.4 3.4.5.6 @@ -37,7 +37,7 @@ test_expect_success 'Test removal of SSH login IP addresses.' ' UPDATE Users SET LastSSHLogin = 0, LastSSHLoginIPAddress = "5.6.7.8" WHERE ID = 5; UPDATE Users SET LastSSHLogin = $tendaysago, LastSSHLoginIPAddress = "6.7.8.9" WHERE ID = 6; EOD - "$USERMAINT" && + cover "$USERMAINT" && cat <<-EOD >expected && 1.2.3.4 2.3.4.5 From 4b7609681deb8ce6627919b4c43f879601c49d30 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Dec 2020 18:26:32 -0800 Subject: [PATCH 0164/1451] add test_exceptions.py This helps gain coverage over aurweb.exceptions regardless of their actual use in the testing base. Signed-off-by: Kevin Morris --- test/test_exceptions.py | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/test_exceptions.py diff --git a/test/test_exceptions.py b/test/test_exceptions.py new file mode 100644 index 00000000..feac2656 --- /dev/null +++ b/test/test_exceptions.py @@ -0,0 +1,102 @@ +from aurweb.exceptions import (AlreadyVotedException, AurwebException, BannedException, BrokenUpdateHookException, + InvalidArgumentsException, InvalidCommentException, InvalidPackageBaseException, + InvalidReasonException, InvalidRepositoryNameException, InvalidUserException, + MaintenanceException, NotVotedException, PackageBaseExistsException, PermissionDeniedException) + + +def test_aurweb_exception(): + try: + raise AurwebException("test") + except AurwebException as exc: + assert str(exc) == "test" + + +def test_maintenance_exception(): + try: + raise MaintenanceException("test") + except MaintenanceException as exc: + assert str(exc) == "test" + + +def test_banned_exception(): + try: + raise BannedException("test") + except BannedException as exc: + assert str(exc) == "test" + + +def test_already_voted_exception(): + try: + raise AlreadyVotedException("test") + except AlreadyVotedException as exc: + assert str(exc) == "already voted for package base: test" + + +def test_broken_update_hook_exception(): + try: + raise BrokenUpdateHookException("test") + except BrokenUpdateHookException as exc: + assert str(exc) == "broken update hook: test" + + +def test_invalid_arguments_exception(): + try: + raise InvalidArgumentsException("test") + except InvalidArgumentsException as exc: + assert str(exc) == "test" + + +def test_invalid_packagebase_exception(): + try: + raise InvalidPackageBaseException("test") + except InvalidPackageBaseException as exc: + assert str(exc) == "package base not found: test" + + +def test_invalid_comment_exception(): + try: + raise InvalidCommentException("test") + except InvalidCommentException as exc: + assert str(exc) == "comment is too short: test" + + +def test_invalid_reason_exception(): + try: + raise InvalidReasonException("test") + except InvalidReasonException as exc: + assert str(exc) == "invalid reason: test" + + +def test_invalid_user_exception(): + try: + raise InvalidUserException("test") + except InvalidUserException as exc: + assert str(exc) == "unknown user: test" + + +def test_not_voted_exception(): + try: + raise NotVotedException("test") + except NotVotedException as exc: + assert str(exc) == "missing vote for package base: test" + + +def test_packagebase_exists_exception(): + try: + raise PackageBaseExistsException("test") + except PackageBaseExistsException as exc: + assert str(exc) == "package base already exists: test" + + +def test_permission_denied_exception(): + try: + raise PermissionDeniedException("test") + except PermissionDeniedException as exc: + assert str(exc) == "permission denied: test" + + +def test_repository_name_exception(): + try: + raise InvalidRepositoryNameException("test") + except InvalidRepositoryNameException as exc: + assert str(exc) == "invalid repository name: test" From 6d08789ac1f759b66006ab2ec225513863308f91 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 31 Dec 2020 22:00:15 -0800 Subject: [PATCH 0165/1451] add test_popupdate.py We had no coverage over aurweb.scripts.popupdate. This test covers all of its functionality. Signed-off-by: Kevin Morris --- aurweb/db.py | 3 +++ aurweb/scripts/popupdate.py | 1 - test/test_popupdate.py | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 test/test_popupdate.py diff --git a/aurweb/db.py b/aurweb/db.py index 8ca32165..04b40f43 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,3 +1,5 @@ +import math + try: import mysql.connector except ImportError: @@ -95,6 +97,7 @@ class Connection: elif aur_db_backend == 'sqlite': aur_db_name = aurweb.config.get('database', 'name') self._conn = sqlite3.connect(aur_db_name) + self._conn.create_function("POWER", 2, math.pow) self._paramstyle = sqlite3.paramstyle else: raise ValueError('unsupported database backend') diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index b64deedb..b1e70403 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -7,7 +7,6 @@ import aurweb.db def main(): conn = aurweb.db.Connection() - conn.execute("UPDATE PackageBases SET NumVotes = (" + "SELECT COUNT(*) FROM PackageVotes " + "WHERE PackageVotes.PackageBaseID = PackageBases.ID)") diff --git a/test/test_popupdate.py b/test/test_popupdate.py new file mode 100644 index 00000000..93f86f10 --- /dev/null +++ b/test/test_popupdate.py @@ -0,0 +1,5 @@ +from aurweb.scripts import popupdate + + +def test_popupdate(): + popupdate.main() From 57c11ae13fea0276359cf552ab59455fe2c579a3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 20 Jan 2021 14:08:39 -0800 Subject: [PATCH 0166/1451] install aurweb package & init db on GitLab CI Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 71c14457..6db573d2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,5 +18,8 @@ before_script: test: script: + - python setup.py install + - sed -r "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.dev > conf/config + - AUR_CONFIG=conf/config python -m aurweb.initdb - make -C test - coverage report --include='aurweb/*' From 52ab056e1876fa68c70d3ff1b1d63ea9f74d6e31 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 21 Dec 2020 22:33:46 -0800 Subject: [PATCH 0167/1451] update documentation for FastAPI tests and deps. Additionally, we now ask for two more favors from contributors: 1. All source modified or added within a patchset **must** maintain equivalent or increased coverage by providing tests that use the functionality. 2. Please keep your source within an 80 column width. PS: Sneak a few test Makefile and gitlab fixes. Signed-off-by: Kevin Morris --- CONTRIBUTING.md | 9 +++++++++ INSTALL | 4 ++-- README.md | 9 ++++++++- test/README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b9ff466..1d57d742 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,3 +8,12 @@ You can add a git hook to do this by installing `python-pre-commit` and running `pre-commit install`. [1] https://lists.archlinux.org/listinfo/aur-dev + +### Coding Guidelines + +1. All source modified or added within a patchset **must** maintain equivalent + or increased coverage by providing tests that use the functionality. + +2. Please keep your source within an 80 column width. + +Test patches that increase coverage in the codebase are always welcome. diff --git a/INSTALL b/INSTALL index a32d6f5a..8607b07f 100644 --- a/INSTALL +++ b/INSTALL @@ -48,8 +48,8 @@ read the instructions below. 4) Install Python modules and dependencies: # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ - python-bleach python-markdown python-alembic python-jinja \ - python-itsdangerous python-authlib python-httpx hypercorn + python-bleach python-markdown python-alembic hypercorn \ + python-itsdangerous python-authlib python-httpx # python3 setup.py install 5) Create a new MySQL database and a user and import the aurweb SQL schema: diff --git a/README.md b/README.md index 77f3b13d..7521b4d9 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,14 @@ Directory Layout * `aurweb`: aurweb Python modules, Git interface and maintenance scripts * `conf`: configuration and configuration templates +* `static`: static resource files +* `templates`: jinja2 template collection * `doc`: project documentation * `po`: translation files for strings in the aurweb interface * `schema`: schema for the SQL database * `test`: test suite and test cases * `upgrading`: instructions for upgrading setups from one release to another -* `web`: web interface for the AUR +* `web`: PHP-based web interface for the AUR Links ----- @@ -46,3 +48,8 @@ Translations are welcome via our Transifex project at https://www.transifex.com/lfleischer/aurweb; see `doc/i18n.txt` for details. ![Transifex](https://www.transifex.com/projects/p/aurweb/chart/image_png) + +Testing +------- + +See [test/README.md](test/README.md) for details on dependencies and testing. diff --git a/test/README.md b/test/README.md index de7eff18..3261899b 100644 --- a/test/README.md +++ b/test/README.md @@ -1,10 +1,11 @@ Running tests ------------- -To run all the tests, you may run `make check` under `test/`. +To run all tests, you may run `make check` under `test/` (alternative targets: +`make pytest`, `make sh`). -For more control, you may use the `prove` command, which receives a directory -or a list of files to run, and produces a report. +For more control, you may use the `prove` or `pytest` command, which receives a +directory or a list of files to run, and produces a report. Each test script is standalone, so you may run them individually. Some tests may receive command-line options to help debugging. See for example sharness's @@ -22,16 +23,60 @@ For all the test to run, the following Arch packages should be installed: - python-pygit2 - python-sqlalchemy - python-srcinfo +- python-coverage +- python-pytest +- python-pytest-cov +- python-pytest-asyncio + +Running tests +------------- + +Recommended method of running tests: `make check`. + +First, setup the test configuration: + + $ sed -r 's;YOUR_AUR_ROOT;$(pwd);g' conf/config.dev > conf/config + +With those installed, one can run Python tests manually with any AUR config +specified by `AUR_CONFIG`: + + $ AUR_CONFIG=conf/config coverage run --append /usr/bin/pytest test/ + +After tests are run (particularly, with `coverage run` included), one can +produce coverage reports. + + # Print out a CLI coverage report. + $ coverage report + # Produce an HTML-based coverage report. + $ coverage html + +When running `make -C test`, all tests ran will produce coverage data via +`coverage run --append`. After running `make -C test`, one can continue with +coverage reporting steps above. Running tests through `make` will test and +cover code ran by both aurweb scripts and our pytest collection. Writing tests ------------- -Test scripts must follow the Test Anything Protocol specification: +Shell test scripts must follow the Test Anything Protocol specification: http://testanything.org/tap-specification.html +Python tests must be compatible with `pytest` and included in `pytest test/` +execution after setting up a configuration. + Tests must support being run from any directory. They may use $0 to determine their location. Python scripts should expect aurweb to be installed and importable without toying with os.path or PYTHONPATH. Tests written in shell should use sharness. In general, new tests should be consistent with existing tests unless they have a good reason not to. + +Debugging tests +--------------- + +By default, `make -C test` is quiet and does not print out verbose information +about tests being run. If a test is failing, one can look into verbose details +of sharness tests by executing them with the `--verbose` flag. Example: +`./t1100_git_auth.t --verbose`. This is particularly useful when tests happen +to fail in a remote continuous integration environment, where the reader does +not have complete access to the runner. From cd3e880264339ffd73e507775b4fb7e9d740a24d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 23 Dec 2020 16:11:40 -0800 Subject: [PATCH 0168/1451] add Dockerfile This docker file downloads deps, sets up some things beforehand and finishes with running our entire collection of tests. Signed-off-by: Kevin Morris Signed-off-by: Lukas Fleischer --- Dockerfile | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7e981340 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM archlinux +COPY . /aurweb +WORKDIR /aurweb + +# Install dependencies. +RUN pacman -Syu --noconfirm base-devel git gpgme protobuf pyalpm \ + python-mysql-connector python-pygit2 python-srcinfo python-bleach \ + python-markdown python-sqlalchemy python-alembic python-pytest \ + python-werkzeug python-pytest-tap python-fastapi nginx python-authlib \ + python-itsdangerous python-httpx python-jinja python-pytest-cov \ + python-requests python-aiofiles python-python-multipart \ + python-pytest-asyncio python-coverage hypercorn + +# Remove aurweb.sqlite3 if it was copied over via COPY. +RUN rm -fv aurweb.sqlite3 + +# Setup our test config. +RUN sed -r "s;YOUR_AUR_ROOT;/aurweb;g" conf/config.dev > conf/config + +# Install translations. +RUN AUR_CONFIG=conf/config make -C po all install + +# Initialize the database. +RUN AUR_CONFIG=conf/config python -m aurweb.initdb + +# Test everything! +RUN make -C test + +# Produce a coverage report. +RUN coverage report --include='aurweb/*' From 21140e28a8d5aa6ccf7a3366ca0d5fe7de8d2364 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Wed, 7 Apr 2021 09:56:16 +0100 Subject: [PATCH 0169/1451] Filter out current username from co-maintainers list. Closes: #8 Signed-off-by: Leonidas Spyropoulos Signed-off-by: Eli Schwartz --- web/lib/pkgbasefuncs.inc.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php index 4a49898c..a053962e 100644 --- a/web/lib/pkgbasefuncs.inc.php +++ b/web/lib/pkgbasefuncs.inc.php @@ -1189,7 +1189,8 @@ function pkgbase_get_comaintainer_uids($base_ids) { * @return array Tuple of success/failure indicator and error message */ function pkgbase_set_comaintainers($base_id, $users, $override=false) { - if (!$override && !has_credential(CRED_PKGBASE_EDIT_COMAINTAINERS, array(pkgbase_maintainer_uid($base_id)))) { + $maintainer_uid = pkgbase_maintainer_uid($base_id); + if (!$override && !has_credential(CRED_PKGBASE_EDIT_COMAINTAINERS, array($maintainer_uid))) { return array(false, __("You are not allowed to manage co-maintainers of this package base.")); } @@ -1207,9 +1208,12 @@ function pkgbase_set_comaintainers($base_id, $users, $override=false) { if (!$uid) { return array(false, __("Invalid user name: %s", $user)); + } elseif ($uid == $maintainer_uid) { + // silently ignore when maintainer == co-maintainer + continue; + } else { + $uids_new[] = $uid; } - - $uids_new[] = $uid; } $q = sprintf("SELECT UsersID FROM PackageComaintainers WHERE PackageBaseID = %d", $base_id); From c1e29e90ca2a7e58faef573c975ddf47f108e048 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:17:17 -0700 Subject: [PATCH 0170/1451] aurweb: Globalize a Translator instance, add more utility + Added SUPPORTED_LANGUAGES, a global constant dictionary of language => display pairs for languages we support. + Add Translator.get_translator, a function used to retrieve a translator after initializing it (if needed). Use `fallback=True` while creating languages, in case we setup a language that we don't have a translation for, it will noop the translation. This is particularly useful for "en," since we do not translate it, but doing this will allow us to go through our normal translation flow in any case. + Added typing. + Added get_request_language, a function that grabs the language for a request session, defaulting to aurweb.config [options] default_lang. + Added get_raw_translator_for_request, a function that retrieves the concrete translation object for a given language. + Added tr, a jinja2 contextfilter that can be used to inline translate strings in jinja2 templates. + Added `python-jinja` dep to .gitlab-ci.yml. This needs to be included in documentation before this set is merged in. + Introduce pytest units (test_l10n.py) in `test` along with __init__.py, which marks `test` as a test package. + Additionally, fix up notify.py to use the global translator. Also reduce its source width to <= 80 by newlining some code. + Additionally, prepare locale in .gitlab-ci.yml and add aurweb.config [options] localedir to config.dev with YOUR_AUR_ROOT like others. Signed-off-by: Kevin Morris Signed-off-by: Lukas Fleischer --- .gitlab-ci.yml | 1 + aurweb/l10n.py | 79 +++++++++++-- aurweb/scripts/notify.py | 238 ++++++++++++++++++++------------------- conf/config.dev | 1 + test/__init__.py | 0 test/test_l10n.py | 44 ++++++++ 6 files changed, 240 insertions(+), 123 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/test_l10n.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6db573d2..1e287748 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ test: script: - python setup.py install - sed -r "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.dev > conf/config + - AUR_CONFIG=conf/config make -C po all install - AUR_CONFIG=conf/config python -m aurweb.initdb - make -C test - coverage report --include='aurweb/*' diff --git a/aurweb/l10n.py b/aurweb/l10n.py index a476ecd8..030ab274 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -1,24 +1,79 @@ import gettext +import typing + +from collections import OrderedDict + +from fastapi import Request +from jinja2 import contextfilter import aurweb.config +SUPPORTED_LANGUAGES = OrderedDict({ + "ar": "العربية", + "ast": "Asturianu", + "ca": "Català", + "cs": "Český", + "da": "Dansk", + "de": "Deutsch", + "el": "Ελληνικά", + "en": "English", + "es": "Español", + "es_419": "Español (Latinoamérica)", + "fi": "Suomi", + "fr": "Français", + "he": "עברית", + "hr": "Hrvatski", + "hu": "Magyar", + "it": "Italiano", + "ja": "日本語", + "nb": "Norsk", + "nl": "Nederlands", + "pl": "Polski", + "pt_BR": "Português (Brasil)", + "pt_PT": "Português (Portugal)", + "ro": "Română", + "ru": "Русский", + "sk": "Slovenčina", + "sr": "Srpski", + "tr": "Türkçe", + "uk": "Українська", + "zh_CN": "简体中文", + "zh_TW": "正體中文" +}) + class Translator: def __init__(self): self._localedir = aurweb.config.get('options', 'localedir') self._translator = {} - def translate(self, s, lang): - if lang == 'en': - return s + def get_translator(self, lang: str): if lang not in self._translator: self._translator[lang] = gettext.translation("aurweb", self._localedir, - languages=[lang]) - return self._translator[lang].gettext(s) + languages=[lang], + fallback=True) + return self._translator.get(lang) + + def translate(self, s: str, lang: str): + return self.get_translator(lang).gettext(s) -def get_translator_for_request(request): +# Global translator object. +translator = Translator() + + +def get_request_language(request: Request): + return request.cookies.get("AURLANG", + aurweb.config.get("options", "default_lang")) + + +def get_raw_translator_for_request(request: Request): + lang = get_request_language(request) + return translator.get_translator(lang) + + +def get_translator_for_request(request: Request): """ Determine the preferred language from a FastAPI request object and build a translator function for it. @@ -29,12 +84,16 @@ def get_translator_for_request(request): print(_("Hello")) ``` """ - lang = request.cookies.get("AURLANG") - if lang is None: - lang = aurweb.config.get("options", "default_lang") - translator = Translator() + lang = get_request_language(request) def translate(message): return translator.translate(message, lang) return translate + + +@contextfilter +def tr(context: typing.Any, value: str): + """ A translation filter; example: {{ "Hello" | tr("de") }}. """ + _ = get_translator_for_request(context.get("request")) + return _(value) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 7f8e7168..1df0175a 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -40,9 +40,6 @@ def pkgbase_from_pkgreq(conn, reqid): class Notification: - def __init__(self): - self._l10n = aurweb.l10n.Translator() - def get_refs(self): return () @@ -97,9 +94,12 @@ class Notification: else: # send email using smtplib; no local MTA required server_addr = aurweb.config.get('notifications', 'smtp-server') - server_port = aurweb.config.getint('notifications', 'smtp-port') - use_ssl = aurweb.config.getboolean('notifications', 'smtp-use-ssl') - use_starttls = aurweb.config.getboolean('notifications', 'smtp-use-starttls') + server_port = aurweb.config.getint('notifications', + 'smtp-port') + use_ssl = aurweb.config.getboolean('notifications', + 'smtp-use-ssl') + use_starttls = aurweb.config.getboolean('notifications', + 'smtp-use-starttls') user = aurweb.config.get('notifications', 'smtp-user') passwd = aurweb.config.get('notifications', 'smtp-password') @@ -127,7 +127,8 @@ class ResetKeyNotification(Notification): cur = conn.execute('SELECT UserName, Email, BackupEmail, ' + 'LangPreference, ResetKey ' + 'FROM Users WHERE ID = ? AND Suspended = 0', [uid]) - self._username, self._to, self._backup, self._lang, self._resetkey = cur.fetchone() + self._username, self._to, self._backup, self._lang, self._resetkey = \ + cur.fetchone() super().__init__() def get_recipients(self): @@ -137,15 +138,15 @@ class ResetKeyNotification(Notification): return [(self._to, self._lang)] def get_subject(self, lang): - return self._l10n.translate('AUR Password Reset', lang) + return aurweb.l10n.translator.translate('AUR Password Reset', lang) def get_body(self, lang): - return self._l10n.translate( - 'A password reset request was submitted for the account ' - '{user} associated with your email address. If you wish to ' - 'reset your password follow the link [1] below, otherwise ' - 'ignore this message and nothing will happen.', - lang).format(user=self._username) + return aurweb.l10n.translator.translate( + 'A password reset request was submitted for the account ' + '{user} associated with your email address. If you wish to ' + 'reset your password follow the link [1] below, otherwise ' + 'ignore this message and nothing will happen.', + lang).format(user=self._username) def get_refs(self): return (aur_location + '/passreset/?resetkey=' + self._resetkey,) @@ -153,15 +154,16 @@ class ResetKeyNotification(Notification): class WelcomeNotification(ResetKeyNotification): def get_subject(self, lang): - return self._l10n.translate('Welcome to the Arch User Repository', - lang) + return aurweb.l10n.translator.translate( + 'Welcome to the Arch User Repository', + lang) def get_body(self, lang): - return self._l10n.translate( - 'Welcome to the Arch User Repository! In order to set an ' - 'initial password for your new account, please click the ' - 'link [1] below. If the link does not work, try copying and ' - 'pasting it into your browser.', lang) + return aurweb.l10n.translator.translate( + 'Welcome to the Arch User Repository! In order to set an ' + 'initial password for your new account, please click the ' + 'link [1] below. If the link does not work, try copying and ' + 'pasting it into your browser.', lang) class CommentNotification(Notification): @@ -186,19 +188,21 @@ class CommentNotification(Notification): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Comment for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Comment for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_body(self, lang): - body = self._l10n.translate( - '{user} [1] added the following comment to {pkgbase} [2]:', - lang).format(user=self._user, pkgbase=self._pkgbase) + body = aurweb.l10n.translator.translate( + '{user} [1] added the following comment to {pkgbase} [2]:', + lang).format(user=self._user, pkgbase=self._pkgbase) body += '\n\n' + self._text + '\n\n-- \n' - dnlabel = self._l10n.translate('Disable notifications', lang) - body += self._l10n.translate( - 'If you no longer wish to receive notifications about this ' - 'package, please go to the package page [2] and select ' - '"{label}".', lang).format(label=dnlabel) + dnlabel = aurweb.l10n.translator.translate( + 'Disable notifications', lang) + body += aurweb.l10n.translator.translate( + 'If you no longer wish to receive notifications about this ' + 'package, please go to the package page [2] and select ' + '"{label}".', lang).format(label=dnlabel) return body def get_refs(self): @@ -231,20 +235,21 @@ class UpdateNotification(Notification): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Package Update: {pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Package Update: {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_body(self, lang): - body = self._l10n.translate('{user} [1] pushed a new commit to ' - '{pkgbase} [2].', lang).format( - user=self._user, - pkgbase=self._pkgbase) + body = aurweb.l10n.translator.translate( + '{user} [1] pushed a new commit to {pkgbase} [2].', + lang).format(user=self._user, pkgbase=self._pkgbase) body += '\n\n-- \n' - dnlabel = self._l10n.translate('Disable notifications', lang) - body += self._l10n.translate( - 'If you no longer wish to receive notifications about this ' - 'package, please go to the package page [2] and select ' - '"{label}".', lang).format(label=dnlabel) + dnlabel = aurweb.l10n.translator.translate( + 'Disable notifications', lang) + body += aurweb.l10n.translator.translate( + 'If you no longer wish to receive notifications about this ' + 'package, please go to the package page [2] and select ' + '"{label}".', lang).format(label=dnlabel) return body def get_refs(self): @@ -261,15 +266,16 @@ class FlagNotification(Notification): def __init__(self, conn, uid, pkgbase_id): self._user = username_from_id(conn, uid) self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'LEFT JOIN PackageComaintainers ' + - 'ON PackageComaintainers.UsersID = Users.ID ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.MaintainerUID = Users.ID OR ' + - 'PackageBases.ID = PackageComaintainers.PackageBaseID ' + - 'WHERE PackageBases.ID = ? AND ' + - 'Users.Suspended = 0', [pkgbase_id]) + cur = conn.execute( + 'SELECT DISTINCT Users.Email, ' + + 'Users.LangPreference FROM Users ' + + 'LEFT JOIN PackageComaintainers ' + + 'ON PackageComaintainers.UsersID = Users.ID ' + + 'INNER JOIN PackageBases ' + + 'ON PackageBases.MaintainerUID = Users.ID OR ' + + 'PackageBases.ID = PackageComaintainers.PackageBaseID ' + + 'WHERE PackageBases.ID = ? AND ' + + 'Users.Suspended = 0', [pkgbase_id]) self._recipients = cur.fetchall() cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' + 'ID = ?', [pkgbase_id]) @@ -280,15 +286,15 @@ class FlagNotification(Notification): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Out-of-date Notification for ' - '{pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Out-of-date Notification for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_body(self, lang): - body = self._l10n.translate( - 'Your package {pkgbase} [1] has been flagged out-of-date by ' - '{user} [2]:', lang).format(pkgbase=self._pkgbase, - user=self._user) + body = aurweb.l10n.translator.translate( + 'Your package {pkgbase} [1] has been flagged out-of-date by ' + '{user} [2]:', lang).format(pkgbase=self._pkgbase, + user=self._user) body += '\n\n' + self._text return body @@ -320,8 +326,9 @@ class OwnershipEventNotification(Notification): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Ownership Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Ownership Notification for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_refs(self): return (aur_location + '/pkgbase/' + self._pkgbase + '/', @@ -330,17 +337,17 @@ class OwnershipEventNotification(Notification): class AdoptNotification(OwnershipEventNotification): def get_body(self, lang): - return self._l10n.translate( - 'The package {pkgbase} [1] was adopted by {user} [2].', - lang).format(pkgbase=self._pkgbase, user=self._user) + return aurweb.l10n.translator.translate( + 'The package {pkgbase} [1] was adopted by {user} [2].', + lang).format(pkgbase=self._pkgbase, user=self._user) class DisownNotification(OwnershipEventNotification): def get_body(self, lang): - return self._l10n.translate( - 'The package {pkgbase} [1] was disowned by {user} ' - '[2].', lang).format(pkgbase=self._pkgbase, - user=self._user) + return aurweb.l10n.translator.translate( + 'The package {pkgbase} [1] was disowned by {user} ' + '[2].', lang).format(pkgbase=self._pkgbase, + user=self._user) class ComaintainershipEventNotification(Notification): @@ -355,9 +362,9 @@ class ComaintainershipEventNotification(Notification): return [(self._to, self._lang)] def get_subject(self, lang): - return self._l10n.translate('AUR Co-Maintainer Notification for ' - '{pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Co-Maintainer Notification for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_refs(self): return (aur_location + '/pkgbase/' + self._pkgbase + '/',) @@ -365,16 +372,16 @@ class ComaintainershipEventNotification(Notification): class ComaintainerAddNotification(ComaintainershipEventNotification): def get_body(self, lang): - return self._l10n.translate( - 'You were added to the co-maintainer list of {pkgbase} [1].', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'You were added to the co-maintainer list of {pkgbase} [1].', + lang).format(pkgbase=self._pkgbase) class ComaintainerRemoveNotification(ComaintainershipEventNotification): def get_body(self, lang): - return self._l10n.translate( - 'You were removed from the co-maintainer list of {pkgbase} ' - '[1].', lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'You were removed from the co-maintainer list of {pkgbase} ' + '[1].', lang).format(pkgbase=self._pkgbase) class DeleteNotification(Notification): @@ -400,25 +407,27 @@ class DeleteNotification(Notification): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Package deleted: {pkgbase}', - lang).format(pkgbase=self._old_pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Package deleted: {pkgbase}', + lang).format(pkgbase=self._old_pkgbase) def get_body(self, lang): if self._new_pkgbase: - dnlabel = self._l10n.translate('Disable notifications', lang) - return self._l10n.translate( - '{user} [1] merged {old} [2] into {new} [3].\n\n' - '-- \n' - 'If you no longer wish receive notifications about the ' - 'new package, please go to [3] and click "{label}".', - lang).format(user=self._user, old=self._old_pkgbase, - new=self._new_pkgbase, label=dnlabel) + dnlabel = aurweb.l10n.translator.translate( + 'Disable notifications', lang) + return aurweb.l10n.translator.translate( + '{user} [1] merged {old} [2] into {new} [3].\n\n' + '-- \n' + 'If you no longer wish receive notifications about the ' + 'new package, please go to [3] and click "{label}".', + lang).format(user=self._user, old=self._old_pkgbase, + new=self._new_pkgbase, label=dnlabel) else: - return self._l10n.translate( - '{user} [1] deleted {pkgbase} [2].\n\n' - 'You will no longer receive notifications about this ' - 'package.', lang).format(user=self._user, - pkgbase=self._old_pkgbase) + return aurweb.l10n.translator.translate( + '{user} [1] deleted {pkgbase} [2].\n\n' + 'You will no longer receive notifications about this ' + 'package.', lang).format(user=self._user, + pkgbase=self._old_pkgbase) def get_refs(self): refs = (aur_location + '/account/' + self._user + '/', @@ -432,14 +441,15 @@ class RequestOpenNotification(Notification): def __init__(self, conn, uid, reqid, reqtype, pkgbase_id, merge_into=None): self._user = username_from_id(conn, uid) self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + - 'INNER JOIN Users ' + - 'ON Users.ID = PackageRequests.UsersID ' + - 'OR Users.ID = PackageBases.MaintainerUID ' + - 'WHERE PackageRequests.ID = ? AND ' + - 'Users.Suspended = 0', [reqid]) + cur = conn.execute( + 'SELECT DISTINCT Users.Email FROM PackageRequests ' + + 'INNER JOIN PackageBases ' + + 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + + 'INNER JOIN Users ' + + 'ON Users.ID = PackageRequests.UsersID ' + + 'OR Users.ID = PackageBases.MaintainerUID ' + + 'WHERE PackageRequests.ID = ? AND ' + + 'Users.Suspended = 0', [reqid]) self._to = aurweb.config.get('options', 'aur_request_ml') self._cc = [row[0] for row in cur.fetchall()] cur = conn.execute('SELECT Comments FROM PackageRequests WHERE ID = ?', @@ -489,14 +499,15 @@ class RequestOpenNotification(Notification): class RequestCloseNotification(Notification): def __init__(self, conn, uid, reqid, reason): self._user = username_from_id(conn, uid) if int(uid) else None - cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + - 'INNER JOIN Users ' + - 'ON Users.ID = PackageRequests.UsersID ' + - 'OR Users.ID = PackageBases.MaintainerUID ' + - 'WHERE PackageRequests.ID = ? AND ' + - 'Users.Suspended = 0', [reqid]) + cur = conn.execute( + 'SELECT DISTINCT Users.Email FROM PackageRequests ' + + 'INNER JOIN PackageBases ' + + 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + + 'INNER JOIN Users ' + + 'ON Users.ID = PackageRequests.UsersID ' + + 'OR Users.ID = PackageBases.MaintainerUID ' + + 'WHERE PackageRequests.ID = ? AND ' + + 'Users.Suspended = 0', [reqid]) self._to = aurweb.config.get('options', 'aur_request_ml') self._cc = [row[0] for row in cur.fetchall()] cur = conn.execute('SELECT PackageRequests.ClosureComment, ' + @@ -563,14 +574,15 @@ class TUVoteReminderNotification(Notification): return self._recipients def get_subject(self, lang): - return self._l10n.translate('TU Vote Reminder: Proposal {id}', - lang).format(id=self._vote_id) + return aurweb.l10n.translator.translate( + 'TU Vote Reminder: Proposal {id}', + lang).format(id=self._vote_id) def get_body(self, lang): - return self._l10n.translate( - 'Please remember to cast your vote on proposal {id} [1]. ' - 'The voting period ends in less than 48 hours.', - lang).format(id=self._vote_id) + return aurweb.l10n.translator.translate( + 'Please remember to cast your vote on proposal {id} [1]. ' + 'The voting period ends in less than 48 hours.', + lang).format(id=self._vote_id) def get_refs(self): return (aur_location + '/tu/?id=' + str(self._vote_id),) diff --git a/conf/config.dev b/conf/config.dev index 37f38c45..ef7b5ed7 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -19,6 +19,7 @@ name = YOUR_AUR_ROOT/aurweb.sqlite3 aur_location = http://127.0.0.1:8080 disable_http_login = 0 enable-maintenance = 0 +localedir = YOUR_AUR_ROOT/web/locale ; Single sign-on; see doc/sso.txt. [sso] diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_l10n.py b/test/test_l10n.py new file mode 100644 index 00000000..1a1ef3e6 --- /dev/null +++ b/test/test_l10n.py @@ -0,0 +1,44 @@ +""" Test our l10n module. """ +from aurweb import l10n + + +class FakeRequest: + """ A fake Request doppleganger; use this to change request.cookies + easily and with no side-effects. """ + + def __init__(self, *args, **kwargs): + self.cookies = kwargs.pop("cookies", dict()) + + +def test_translator(): + """ Test creating l10n translation tools. """ + de_home = l10n.translator.translate("Home", "de") + assert de_home == "Startseite" + + +def test_get_request_language(): + """ First, tests default_lang, then tests a modified AURLANG cookie. """ + request = FakeRequest() + assert l10n.get_request_language(request) == "en" + + request.cookies["AURLANG"] = "de" + assert l10n.get_request_language(request) == "de" + + +def test_get_raw_translator_for_request(): + """ Make sure that get_raw_translator_for_request is giving us + the translator we expect. """ + request = FakeRequest(cookies={"AURLANG": "de"}) + + translator = l10n.get_raw_translator_for_request(request) + assert translator.gettext("Home") == \ + l10n.translator.translate("Home", "de") + + +def test_get_translator_for_request(): + """ Make sure that get_translator_for_request is giving us back + our expected translation function. """ + request = FakeRequest(cookies={"AURLANG": "de"}) + + translate = l10n.get_translator_for_request(request) + assert translate("Home") == "Startseite" From bda9256ab11101397977282236af6cbc6d664181 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Fri, 7 May 2021 18:50:41 +0200 Subject: [PATCH 0171/1451] Add error color when package is orphaned Signed-off-by: Eli Schwartz From 1ff822bb1492497adb284f278e0e537ea9be00f3 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Tue, 11 May 2021 00:01:13 +0200 Subject: [PATCH 0172/1451] Use the clipboard API for copy paste The Document.execCommand API is deprecated and no longer recommended to be used. It's replacement is the much simpler navigator.clipboard API which is supported in all browsers except internet explorer. Signed-off-by: Eli Schwartz --- web/template/pkg_details.php | 10 +++------- web/template/pkgbase_details.php | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/web/template/pkg_details.php b/web/template/pkg_details.php index c6bb32d8..047de9a7 100644 --- a/web/template/pkg_details.php +++ b/web/template/pkg_details.php @@ -308,14 +308,10 @@ endif; diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php index a6857c4e..35ad217a 100644 --- a/web/template/pkgbase_details.php +++ b/web/template/pkgbase_details.php @@ -137,14 +137,10 @@ endif; From 2df90ce28087d02e7b1dbd0e8efd5d5f99407793 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:20:04 -0700 Subject: [PATCH 0173/1451] port over base HTML layout from PHP to FastAPI+Jinja2 + Mounted static files (at web/html) to /static. + Added AURWEB_VERSION to aurweb.config (this is used around HTML to refer back to aurweb's release on git.archlinux.org), so we need it easily accessible in the Python codebase. + Implemented basic Jinja2 partials to put together whole aurweb pages. This may be missing some things currently and is a WIP until this set is ready to be merged. + Added config [options] aurwebdir = YOUR_AUR_ROOT; this configuration option should specify the root directory of the aurweb project. It is used by various parts of the FastAPI codebase to target project directories. Added routes via aurweb.routers.html: * POST /language: Set your session language. * GET /favicon.ico: Redirect to /static/images/favicon.ico. * Some browsers always look for $ROOT/favicon.ico to get an icon for the page being loaded, regardless of a specified "shortcut icon" given in a directive. * GET /: Home page; WIP. * Updated aurweb.routers.html.language passes query parameters to its next redirection. When calling aurweb.templates.render_template, the context passed should be formed via the aurweb.templates.make_context. See aurweb.routers.html.index for an example of this. Signed-off-by: Kevin Morris --- .coveragerc | 1 + INSTALL | 4 +- aurweb/asgi.py | 23 ++++++++- aurweb/config.py | 5 ++ aurweb/routers/html.py | 50 +++++++++++++++++++ aurweb/templates.py | 57 +++++++++++++++++++++ conf/config.dev | 1 + templates/index.html | 4 ++ templates/partials/archdev-navbar.html | 8 +++ templates/partials/body.html | 10 ++++ templates/partials/footer.html | 5 ++ templates/partials/head.html | 16 ++++++ templates/partials/layout.html | 10 ++++ templates/partials/meta.html | 1 + templates/partials/navbar.html | 19 +++++++ templates/partials/set_lang.html | 28 +++++++++++ templates/partials/typeahead.html | 30 +++++++++++ test/test_routes.py | 69 ++++++++++++++++++++++++++ 18 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 aurweb/routers/html.py create mode 100644 aurweb/templates.py create mode 100644 templates/index.html create mode 100644 templates/partials/archdev-navbar.html create mode 100644 templates/partials/body.html create mode 100644 templates/partials/footer.html create mode 100644 templates/partials/head.html create mode 100644 templates/partials/layout.html create mode 100644 templates/partials/meta.html create mode 100644 templates/partials/navbar.html create mode 100644 templates/partials/set_lang.html create mode 100644 templates/partials/typeahead.html create mode 100644 test/test_routes.py diff --git a/.coveragerc b/.coveragerc index 144a9f5c..9dcfca18 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,5 +3,6 @@ disable_warnings = already-imported [report] include = aurweb/* +fail_under = 85 exclude_lines = if __name__ == .__main__.: diff --git a/INSTALL b/INSTALL index 8607b07f..e4c52480 100644 --- a/INSTALL +++ b/INSTALL @@ -49,7 +49,9 @@ read the instructions below. # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ python-bleach python-markdown python-alembic hypercorn \ - python-itsdangerous python-authlib python-httpx + python-itsdangerous python-authlib python-httpx \ + python-jinja python-aiofiles python-python-multipart \ + python-requests # python3 setup.py install 5) Create a new MySQL database and a user and import the aurweb SQL schema: diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 9293ed77..00d7c595 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -2,13 +2,26 @@ import http from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware import aurweb.config -from aurweb.routers import sso +from aurweb.routers import html, sso +routes = set() + +# Setup the FastAPI app. app = FastAPI() +app.mount("/static/css", + StaticFiles(directory="web/html/css"), + name="static_css") +app.mount("/static/js", + StaticFiles(directory="web/html/js"), + name="static_js") +app.mount("/static/images", + StaticFiles(directory="web/html/images"), + name="static_images") session_secret = aurweb.config.get("fastapi", "session_secret") if not session_secret: @@ -17,6 +30,14 @@ if not session_secret: app.add_middleware(SessionMiddleware, secret_key=session_secret) app.include_router(sso.router) +app.include_router(html.router) + +# NOTE: Always keep this dictionary updated with all routes +# that the application contains. We use this to check for +# parameter value verification. +routes = {route.path for route in app.routes} +routes.update({route.path for route in sso.router.routes}) +routes.update({route.path for route in html.router.routes}) @app.exception_handler(HTTPException) diff --git a/aurweb/config.py b/aurweb/config.py index 52ec461e..020c3b80 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -1,6 +1,11 @@ import configparser import os +# Publicly visible version of aurweb. This is used to display +# aurweb versioning in the footer and must be maintained. +# Todo: Make this dynamic/automated. +AURWEB_VERSION = "v5.0.0" + _parser = None diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py new file mode 100644 index 00000000..ae08c764 --- /dev/null +++ b/aurweb/routers/html.py @@ -0,0 +1,50 @@ +""" AURWeb's primary routing module. Define all routes via @app.app.{get,post} +decorators in some way; more complex routes should be defined in their +own modules and imported here. """ +from http import HTTPStatus +from urllib.parse import unquote + +from fastapi import APIRouter, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse + +from aurweb.templates import make_context, render_template + +router = APIRouter() + + +@router.get("/favicon.ico") +async def favicon(request: Request): + """ Some browsers attempt to find a website's favicon via root uri at + /favicon.ico, so provide a redirection here to our static icon. """ + return RedirectResponse("/static/images/favicon.ico") + + +@router.post("/language", response_class=RedirectResponse) +async def language(request: Request, + set_lang: str = Form(...), + next: str = Form(...), + q: str = Form(default=None)): + """ A POST route used to set a session's language. + + Return a 303 See Other redirect to {next}?next={next}. If we are + setting the language on any page, we want to preserve query + parameters across the redirect. + """ + from aurweb.asgi import routes + if unquote(next) not in routes: + return HTMLResponse( + b"Invalid 'next' parameter.", + status_code=400) + + query_string = "?" + q if q else str() + response = RedirectResponse(url=f"{next}{query_string}", + status_code=int(HTTPStatus.SEE_OTHER)) + response.set_cookie("AURLANG", set_lang) + return response + + +@router.get("/", response_class=HTMLResponse) +async def index(request: Request): + """ Homepage route. """ + context = make_context(request, "Home") + return render_template("index.html", context) diff --git a/aurweb/templates.py b/aurweb/templates.py new file mode 100644 index 00000000..c05dce79 --- /dev/null +++ b/aurweb/templates.py @@ -0,0 +1,57 @@ +import copy +import os + +from datetime import datetime +from http import HTTPStatus + +import jinja2 + +from fastapi import Request +from fastapi.responses import HTMLResponse + +import aurweb.config + +from aurweb import l10n + +# Prepare jinja2 objects. +loader = jinja2.FileSystemLoader(os.path.join( + aurweb.config.get("options", "aurwebdir"), "templates")) +env = jinja2.Environment(loader=loader, autoescape=True, + extensions=["jinja2.ext.i18n"]) + +# Add tr translation filter. +env.filters["tr"] = l10n.tr + + +def make_context(request: Request, title: str, next: str = None): + """ Create a context for a jinja2 TemplateResponse. """ + + return { + "request": request, + "language": l10n.get_request_language(request), + "languages": l10n.SUPPORTED_LANGUAGES, + "title": title, + # The 'now' context variable will not show proper datetimes + # until we've implemented timezone support here. + "now": datetime.now(), + "config": aurweb.config, + "next": next if next else request.url.path + } + + +def render_template(path: str, context: dict, status_code=int(HTTPStatus.OK)): + """ Render a Jinja2 multi-lingual template with some context. """ + + # Create a deep copy of our jinja2 environment. The environment in + # total by itself is 48 bytes large (according to sys.getsizeof). + # This is done so we can install gettext translations on the template + # environment being rendered without installing them into a global + # which is reused in this function. + templates = copy.copy(env) + + translator = l10n.get_raw_translator_for_request(context.get("request")) + templates.install_gettext_translations(translator) + + template = templates.get_template(path) + rendered = template.render(context) + return HTMLResponse(rendered, status_code=status_code) diff --git a/conf/config.dev b/conf/config.dev index ef7b5ed7..ccb01f4f 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -16,6 +16,7 @@ name = YOUR_AUR_ROOT/aurweb.sqlite3 ;password = aur [options] +aurwebdir = YOUR_AUR_ROOT aur_location = http://127.0.0.1:8080 disable_http_login = 0 enable-maintenance = 0 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..27d3375d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,4 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +{% endblock %} diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html new file mode 100644 index 00000000..55338bc4 --- /dev/null +++ b/templates/partials/archdev-navbar.html @@ -0,0 +1,8 @@ + diff --git a/templates/partials/body.html b/templates/partials/body.html new file mode 100644 index 00000000..ccae0fe3 --- /dev/null +++ b/templates/partials/body.html @@ -0,0 +1,10 @@ +
    + {% include 'partials/set_lang.html' %} + {% include 'partials/archdev-navbar.html' %} + + {% block pageContent %} + + {% endblock %} + + {% include 'partials/footer.html' %} +
    diff --git a/templates/partials/footer.html b/templates/partials/footer.html new file mode 100644 index 00000000..0ac4d089 --- /dev/null +++ b/templates/partials/footer.html @@ -0,0 +1,5 @@ + diff --git a/templates/partials/head.html b/templates/partials/head.html new file mode 100644 index 00000000..0351fd6e --- /dev/null +++ b/templates/partials/head.html @@ -0,0 +1,16 @@ + + {% include 'partials/meta.html' %} + + + + + + + + + + + + AUR ({{ language }}) - {{ title | tr }} + diff --git a/templates/partials/layout.html b/templates/partials/layout.html new file mode 100644 index 00000000..d30208a9 --- /dev/null +++ b/templates/partials/layout.html @@ -0,0 +1,10 @@ + + + {% include 'partials/head.html' %} + + + {% include 'partials/navbar.html' %} + {% extends 'partials/body.html' %} + {% include 'partials/typeahead.html' %} + + diff --git a/templates/partials/meta.html b/templates/partials/meta.html new file mode 100644 index 00000000..727100b9 --- /dev/null +++ b/templates/partials/meta.html @@ -0,0 +1 @@ + diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html new file mode 100644 index 00000000..199b2067 --- /dev/null +++ b/templates/partials/navbar.html @@ -0,0 +1,19 @@ + diff --git a/templates/partials/set_lang.html b/templates/partials/set_lang.html new file mode 100644 index 00000000..e9590050 --- /dev/null +++ b/templates/partials/set_lang.html @@ -0,0 +1,28 @@ +
    +
    +
    +
    + + + + + + + + + +
    +
    + +
    diff --git a/templates/partials/typeahead.html b/templates/partials/typeahead.html new file mode 100644 index 00000000..d943dbc4 --- /dev/null +++ b/templates/partials/typeahead.html @@ -0,0 +1,30 @@ + + + diff --git a/test/test_routes.py b/test/test_routes.py new file mode 100644 index 00000000..46ba39f5 --- /dev/null +++ b/test/test_routes.py @@ -0,0 +1,69 @@ +import urllib.parse + +from http import HTTPStatus + +import pytest + +from fastapi.testclient import TestClient + +from aurweb.asgi import app +from aurweb.testing import setup_test_db + +client = TestClient(app) + + +@pytest.fixture +def setup(): + setup_test_db("Users", "Session") + + +def test_index(): + """ Test the index route at '/'. """ + # Use `with` to trigger FastAPI app events. + with client as req: + response = req.get("/") + assert response.status_code == int(HTTPStatus.OK) + + +def test_favicon(): + """ Test the favicon route at '/favicon.ico'. """ + response1 = client.get("/static/images/favicon.ico") + response2 = client.get("/favicon.ico") + assert response1.status_code == int(HTTPStatus.OK) + assert response1.content == response2.content + + +def test_language(): + """ Test the language post route at '/language'. """ + post_data = { + "set_lang": "de", + "next": "/" + } + with client as req: + response = req.post("/language", data=post_data) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_language_invalid_next(): + """ Test an invalid next route at '/language'. """ + post_data = { + "set_lang": "de", + "next": "/BLAHBLAHFAKE" + } + with client as req: + response = req.post("/language", data=post_data) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + +def test_language_query_params(): + """ Test the language post route with query params. """ + next = urllib.parse.quote_plus("/") + post_data = { + "set_lang": "de", + "next": "/", + "q": f"next={next}" + } + q = post_data.get("q") + with client as req: + response = req.post("/language", data=post_data) + assert response.headers.get("location") == f"/?{q}" + assert response.status_code == int(HTTPStatus.SEE_OTHER) From 7c65604dad8d9c68bee59129b03af05d26db1582 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:20:19 -0700 Subject: [PATCH 0174/1451] move off env.py's active code to __name__ == "__main__" * Moved migrations/env.py's logging initialization and migration execution into a `__name__ == "__main__"` stanza so it doesn't immediately happen when imported by another module. Signed-off-by: Kevin Morris --- migrations/env.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/migrations/env.py b/migrations/env.py index c2ff58c1..23759123 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -11,10 +11,6 @@ import aurweb.schema # access to the values within the .ini file in use. config = context.config -# Interpret the config file for Python logging. -# This line sets up loggers basically. -logging.config.fileConfig(config.config_file_name) - # model MetaData for autogenerating migrations target_metadata = aurweb.schema.metadata @@ -68,7 +64,12 @@ def run_migrations_online(): context.run_migrations() -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() +if __name__ == "__main__": + # Interpret the config file for Python logging. + # This line sets up loggers basically. + logging.config.fileConfig(config.config_file_name) + + if context.is_offline_mode(): + run_migrations_offline() + else: + run_migrations_online() From 4238a9fc6855242121402b392581ba5b695e2f90 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:20:23 -0700 Subject: [PATCH 0175/1451] add aurweb.db.session + Added Session class and global session object to aurweb.db, these are sessions created by sqlalchemy ORM's sessionmaker and will allow us to use declarative/imperative models. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 36 +++++---- aurweb/config.py | 7 ++ aurweb/db.py | 33 ++++----- test/test_asgi.py | 29 ++++++++ test/test_config.py | 13 ++++ test/test_db.py | 174 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 260 insertions(+), 32 deletions(-) create mode 100644 test/test_asgi.py create mode 100644 test/test_config.py create mode 100644 test/test_db.py diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 00d7c595..b6e15582 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -7,30 +7,36 @@ from starlette.middleware.sessions import SessionMiddleware import aurweb.config +from aurweb.db import get_engine from aurweb.routers import html, sso routes = set() # Setup the FastAPI app. app = FastAPI() -app.mount("/static/css", - StaticFiles(directory="web/html/css"), - name="static_css") -app.mount("/static/js", - StaticFiles(directory="web/html/js"), - name="static_js") -app.mount("/static/images", - StaticFiles(directory="web/html/images"), - name="static_images") -session_secret = aurweb.config.get("fastapi", "session_secret") -if not session_secret: - raise Exception("[fastapi] session_secret must not be empty") -app.add_middleware(SessionMiddleware, secret_key=session_secret) +@app.on_event("startup") +async def app_startup(): + session_secret = aurweb.config.get("fastapi", "session_secret") + if not session_secret: + raise Exception("[fastapi] session_secret must not be empty") -app.include_router(sso.router) -app.include_router(html.router) + app.mount("/static/css", + StaticFiles(directory="web/html/css"), + name="static_css") + app.mount("/static/js", + StaticFiles(directory="web/html/js"), + name="static_js") + app.mount("/static/images", + StaticFiles(directory="web/html/images"), + name="static_images") + + app.add_middleware(SessionMiddleware, secret_key=session_secret) + app.include_router(sso.router) + app.include_router(html.router) + + get_engine() # NOTE: Always keep this dictionary updated with all routes # that the application contains. We use this to check for diff --git a/aurweb/config.py b/aurweb/config.py index 020c3b80..49a2765a 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -25,6 +25,13 @@ def _get_parser(): return _parser +def rehash(): + """ Globally rehash the configuration parser. """ + global _parser + _parser = None + _get_parser() + + def get(section, option): return _get_parser().get(section, option) diff --git a/aurweb/db.py b/aurweb/db.py index 04b40f43..7993dfdb 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,19 +1,15 @@ import math -try: - import mysql.connector -except ImportError: - pass - -try: - import sqlite3 -except ImportError: - pass - import aurweb.config engine = None # See get_engine +# ORM Session class. +Session = None + +# Global ORM Session object. +session = None + def get_sqlalchemy_url(): """ @@ -49,14 +45,15 @@ def get_engine(): `engine` global variable for the next calls. """ from sqlalchemy import create_engine - global engine + from sqlalchemy.orm import sessionmaker + + global engine, session, Session + if engine is None: - connect_args = dict() - if aurweb.config.get("database", "backend") == "sqlite": - # check_same_thread is for a SQLite technicality - # https://fastapi.tiangolo.com/tutorial/sql-databases/#note - connect_args["check_same_thread"] = False - engine = create_engine(get_sqlalchemy_url(), connect_args=connect_args) + engine = create_engine(get_sqlalchemy_url(), + # check_same_thread is for a SQLite technicality + # https://fastapi.tiangolo.com/tutorial/sql-databases/#note + connect_args={"check_same_thread": False}) Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) session = Session() @@ -82,6 +79,7 @@ class Connection: aur_db_backend = aurweb.config.get('database', 'backend') if aur_db_backend == 'mysql': + import mysql.connector aur_db_host = aurweb.config.get('database', 'host') aur_db_name = aurweb.config.get('database', 'name') aur_db_user = aurweb.config.get('database', 'user') @@ -95,6 +93,7 @@ class Connection: buffered=True) self._paramstyle = mysql.connector.paramstyle elif aur_db_backend == 'sqlite': + import sqlite3 aur_db_name = aurweb.config.get('database', 'name') self._conn = sqlite3.connect(aur_db_name) self._conn.create_function("POWER", 2, math.pow) diff --git a/test/test_asgi.py b/test/test_asgi.py new file mode 100644 index 00000000..79b34daf --- /dev/null +++ b/test/test_asgi.py @@ -0,0 +1,29 @@ +import http +import os + +from unittest import mock + +import pytest + +from fastapi import HTTPException + +import aurweb.asgi +import aurweb.config + + +@pytest.mark.asyncio +async def test_asgi_startup_exception(monkeypatch): + with mock.patch.dict(os.environ, {"AUR_CONFIG": "conf/config.defaults"}): + aurweb.config.rehash() + with pytest.raises(Exception): + await aurweb.asgi.app_startup() + aurweb.config.rehash() + + +@pytest.mark.asyncio +async def test_asgi_http_exception_handler(): + exc = HTTPException(status_code=422, detail="EXCEPTION!") + phrase = http.HTTPStatus(exc.status_code).phrase + response = await aurweb.asgi.http_exception_handler(None, exc) + assert response.body.decode() == \ + f"

    {exc.status_code} {phrase}

    {exc.detail}

    " diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 00000000..4f10b60d --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,13 @@ +from aurweb import config + + +def test_get(): + assert config.get("options", "disable_http_login") == "0" + + +def test_getboolean(): + assert not config.getboolean("options", "disable_http_login") + + +def test_getint(): + assert config.getint("options", "disable_http_login") == 0 diff --git a/test/test_db.py b/test/test_db.py new file mode 100644 index 00000000..0a134541 --- /dev/null +++ b/test/test_db.py @@ -0,0 +1,174 @@ +import os +import re +import sqlite3 +import tempfile + +from unittest import mock + +import mysql.connector +import pytest + +import aurweb.config + +from aurweb import db +from aurweb.testing import setup_test_db + + +class DBCursor: + """ A fake database cursor object used in tests. """ + items = [] + + def execute(self, *args, **kwargs): + self.items = list(args) + return self + + def fetchall(self): + return self.items + + +class DBConnection: + """ A fake database connection object used in tests. """ + @staticmethod + def cursor(): + return DBCursor() + + @staticmethod + def create_function(name, num_args, func): + pass + + +@pytest.fixture(autouse=True) +def setup_db(): + setup_test_db() + + +def test_sqlalchemy_sqlite_url(): + with mock.patch.dict(os.environ, {"AUR_CONFIG": "conf/config.dev"}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def test_sqlalchemy_mysql_url(): + with mock.patch.dict(os.environ, {"AUR_CONFIG": "conf/config.defaults"}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def make_temp_config(backend): + if not os.path.isdir("/tmp"): + os.mkdir("/tmp") + tmpdir = tempfile.mkdtemp() + tmp = os.path.join(tmpdir, "config.tmp") + with open("conf/config") as f: + config = re.sub(r'backend = sqlite', f'backend = {backend}', f.read()) + with open(tmp, "w") as o: + o.write(config) + return (tmpdir, tmp) + + +def test_sqlalchemy_unknown_backend(): + tmpdir, tmp = make_temp_config("blah") + + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + with pytest.raises(ValueError): + db.get_sqlalchemy_url() + aurweb.config.rehash() + + os.remove(tmp) + os.removedirs(tmpdir) + + +def test_db_connects_without_fail(): + db.connect() + assert db.engine is not None + + +def test_connection_class_without_fail(): + conn = db.Connection() + + cur = conn.execute( + "SELECT AccountType FROM AccountTypes WHERE ID = ?", (1,)) + account_type = cur.fetchone()[0] + + assert account_type == "User" + + +def test_connection_class_unsupported_backend(): + tmpdir, tmp = make_temp_config("blah") + + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + with pytest.raises(ValueError): + db.Connection() + aurweb.config.rehash() + + os.remove(tmp) + os.removedirs(tmpdir) + + +@mock.patch("mysql.connector.connect", mock.MagicMock(return_value=True)) +@mock.patch.object(mysql.connector, "paramstyle", "qmark") +def test_connection_mysql(): + tmpdir, tmp = make_temp_config("mysql") + with mock.patch.dict(os.environ, { + "AUR_CONFIG": tmp, + "AUR_CONFIG_DEFAULTS": "conf/config.defaults" + }): + aurweb.config.rehash() + db.Connection() + aurweb.config.rehash() + + os.remove(tmp) + os.removedirs(tmpdir) + + +@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) +@mock.patch.object(sqlite3, "paramstyle", "qmark") +def test_connection_sqlite(): + db.Connection() + + +@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) +@mock.patch.object(sqlite3, "paramstyle", "format") +def test_connection_execute_paramstyle_format(): + conn = db.Connection() + + # First, test ? to %s format replacement. + account_types = conn\ + .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"])\ + .fetchall() + assert account_types == \ + ["SELECT * FROM AccountTypes WHERE AccountType = %s", ["User"]] + + # Test other format replacement. + account_types = conn\ + .execute("SELECT * FROM AccountTypes WHERE AccountType = %", ["User"])\ + .fetchall() + assert account_types == \ + ["SELECT * FROM AccountTypes WHERE AccountType = %%", ["User"]] + + +@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) +@mock.patch.object(sqlite3, "paramstyle", "qmark") +def test_connection_execute_paramstyle_qmark(): + conn = db.Connection() + # We don't modify anything when using qmark, so test equality. + account_types = conn\ + .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"])\ + .fetchall() + assert account_types == \ + ["SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"]] + + +@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) +@mock.patch.object(sqlite3, "paramstyle", "unsupported") +def test_connection_execute_paramstyle_unsupported(): + conn = db.Connection() + with pytest.raises(ValueError, match="unsupported paramstyle"): + conn.execute( + "SELECT * FROM AccountTypes WHERE AccountType = ?", + ["User"] + ).fetchall() From 32f289309579554d85a9971be59ccc24c973840c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:20:26 -0700 Subject: [PATCH 0176/1451] add aurweb.models.account_type.AccountType Signed-off-by: Kevin Morris --- aurweb/models/__init__.py | 0 aurweb/models/account_type.py | 20 ++++++++++++++++++++ test/test_account_type.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 aurweb/models/__init__.py create mode 100644 aurweb/models/account_type.py create mode 100644 test/test_account_type.py diff --git a/aurweb/models/__init__.py b/aurweb/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py new file mode 100644 index 00000000..44225e35 --- /dev/null +++ b/aurweb/models/account_type.py @@ -0,0 +1,20 @@ +from sqlalchemy.orm import mapper + +from aurweb.schema import AccountTypes + + +class AccountType: + """ An ORM model of a single AccountTypes record. """ + + def __init__(self, **kwargs): + self.AccountType = kwargs.pop("AccountType") + + def __str__(self): + return str(self.AccountType) + + def __repr__(self): + return "" % ( + self.ID, str(self)) + + +mapper(AccountType, AccountTypes, confirm_deleted_rows=False) diff --git a/test/test_account_type.py b/test/test_account_type.py new file mode 100644 index 00000000..b6a12363 --- /dev/null +++ b/test/test_account_type.py @@ -0,0 +1,28 @@ +import pytest + +from aurweb.models.account_type import AccountType +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db() + + +def test_account_type(): + """ Test creating an AccountType, and reading its columns. """ + from aurweb.db import session + account_type = AccountType(AccountType="TestUser") + session.add(account_type) + session.commit() + + # Make sure it got created and was given an ID. + assert bool(account_type.ID) + + # Next, test our string functions. + assert str(account_type) == "TestUser" + assert repr(account_type) == \ + "" % ( + account_type.ID) + + session.delete(account_type) From e860d828b6f9babaa5829db92951087de3774b78 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:20:28 -0700 Subject: [PATCH 0177/1451] add aurweb.testing, a module with testing utilities Signed-off-by: Kevin Morris --- aurweb/testing.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 aurweb/testing.py diff --git a/aurweb/testing.py b/aurweb/testing.py new file mode 100644 index 00000000..7516d918 --- /dev/null +++ b/aurweb/testing.py @@ -0,0 +1,30 @@ +from aurweb.db import get_engine + + +def setup_test_db(*args): + """ This function is to be used to setup a test database before + using it. It takes a variable number of table strings, and for + each table in that set of table strings, it deletes all records. + + The primary goal of this method is to configure empty tables + that tests can use from scratch. This means that tests using + this function should make sure they do not depend on external + records and keep their logic self-contained. + + Generally used inside of pytest fixtures, this function + can be used anywhere, but keep in mind its functionality when + doing so. + + Examples: + setup_test_db("Users", "Sessions") + + test_tables = ["Users", "Sessions"]; + setup_test_db(*test_tables) + """ + engine = get_engine() + conn = engine.connect() + + tables = list(args) + for table in tables: + conn.execute(f"DELETE FROM {table}") + conn.close() From 8a47afd2ea8ce56b17aba95503d7c97f22023dff Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:20:32 -0700 Subject: [PATCH 0178/1451] add aurweb.models.user.User + Added aurweb.models.user.User class. This is the first example of an sqlalchemy ORM model. We can search for users via for example: `session.query(User).filter(User.ID==1).first()`, where `session` is a configured `aurweb.db.session` object. + Along with the User class, defined the AccountType class. Each User maintains a relationship to its AccountType via User.AccountType. + Added AccountType.users backref. Signed-off-by: Kevin Morris --- aurweb/models/user.py | 43 +++++++++++++++++++++++++++++++++++ test/test_user.py | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 aurweb/models/user.py create mode 100644 test/test_user.py diff --git a/aurweb/models/user.py b/aurweb/models/user.py new file mode 100644 index 00000000..ba91c439 --- /dev/null +++ b/aurweb/models/user.py @@ -0,0 +1,43 @@ +from sqlalchemy.orm import backref, mapper, relationship + +from aurweb.models.account_type import AccountType +from aurweb.schema import Users + + +class User: + """ An ORM model of a single Users record. """ + + def __init__(self, **kwargs): + self.AccountTypeID = kwargs.get("AccountTypeID") + + account_type = kwargs.get("AccountType") + if account_type: + self.AccountType = account_type + + self.Username = kwargs.get("Username") + self.Email = kwargs.get("Email") + self.BackupEmail = kwargs.get("BackupEmail") + self.Passwd = kwargs.get("Passwd") + self.Salt = kwargs.get("Salt") + self.RealName = kwargs.get("RealName") + self.LangPreference = kwargs.get("LangPreference") + self.Timezone = kwargs.get("Timezone") + self.Homepage = kwargs.get("Homepage") + self.IRCNick = kwargs.get("IRCNick") + self.PGPKey = kwargs.get("PGPKey") + self.RegistrationTS = kwargs.get("RegistrationTS") + self.CommentNotify = kwargs.get("CommentNotify") + self.UpdateNotify = kwargs.get("UpdateNotify") + self.OwnershipNotify = kwargs.get("OwnershipNotify") + self.SSOAccountID = kwargs.get("SSOAccountID") + + def __repr__(self): + return "" % ( + self.ID, str(self.AccountType), self.Username) + + +# Map schema.Users to User and give it some relationships. +mapper(User, Users, properties={ + "AccountType": relationship(AccountType, + backref=backref("users", lazy="dynamic")) +}) diff --git a/test/test_user.py b/test/test_user.py new file mode 100644 index 00000000..8ac9b00b --- /dev/null +++ b/test/test_user.py @@ -0,0 +1,52 @@ +import pytest + +from aurweb.models.account_type import AccountType +from aurweb.models.user import User +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db("Users") + + +def test_user(): + """ Test creating a user and reading its columns. """ + from aurweb.db import session + + # First, grab our target AccountType. + account_type = session.query(AccountType).filter( + AccountType.AccountType == "User").first() + + user = User( + AccountType=account_type, + RealName="Test User", Username="test", + Email="test@example.org", Passwd="abcd", + IRCNick="tester", + Salt="efgh", ResetKey="blahblah") + session.add(user) + session.commit() + + assert user in account_type.users + + # Make sure the user was created and given an ID. + assert bool(user.ID) + + # Search for the user via query API. + result = session.query(User).filter(User.ID == user.ID).first() + + # Compare the result and our original user. + assert result.ID == user.ID + assert result.AccountType.ID == user.AccountType.ID + assert result.Username == user.Username + assert result.Email == user.Email + + # Ensure we've got the correct account type. + assert user.AccountType.ID == account_type.ID + assert user.AccountType.AccountType == account_type.AccountType + + # Test out user string functions. + assert repr(user) == f"" + + session.delete(user) From 02311eab7604d29de7b70721cb1e10329178cfc7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Mar 2021 15:20:34 -0700 Subject: [PATCH 0179/1451] add test_initdb.py IMPORTANT: This test completely wipes out the database it's using. Make sure you've got AUR_CONFIG set to a test database configuration! Signed-off-by: Kevin Morris --- test/test_initdb.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/test_initdb.py diff --git a/test/test_initdb.py b/test/test_initdb.py new file mode 100644 index 00000000..ff089b63 --- /dev/null +++ b/test/test_initdb.py @@ -0,0 +1,27 @@ +import pytest + +import aurweb.config +import aurweb.db +import aurweb.initdb + +from aurweb.models.account_type import AccountType +from aurweb.schema import metadata +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db() + + tables = metadata.tables.keys() + for table in tables: + aurweb.db.session.execute(f"DROP TABLE IF EXISTS {table}") + + +def test_run(): + class Args: + use_alembic = True + verbose = False + aurweb.initdb.run(Args()) + assert aurweb.db.session.query(AccountType).filter( + AccountType.AccountType == "User").first() is not None From 81856f3b646ad2bf27008a97b1604a5325eea03c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 13 May 2021 21:05:34 -0700 Subject: [PATCH 0180/1451] Fix incorrect construction of MySQL SQLAlchemy URL Signed-off-by: Kevin Morris From 82f3871a83bf234983f8d63d2f1861d876a52fb1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 13 May 2021 21:11:57 -0700 Subject: [PATCH 0181/1451] Support SQLAlchemy 1.4 URL.create recommendation This fixes a deprecating warning when using SQLAlchemy 1.4. Signed-off-by: Kevin Morris --- .coveragerc | 1 + aurweb/db.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9dcfca18..69c153ce 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,3 +6,4 @@ include = aurweb/* fail_under = 85 exclude_lines = if __name__ == .__main__.: + pragma: no cover diff --git a/aurweb/db.py b/aurweb/db.py index 7993dfdb..49e0abd2 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -16,9 +16,18 @@ def get_sqlalchemy_url(): Build an SQLAlchemy for use with create_engine based on the aurweb configuration. """ import sqlalchemy + + constructor = sqlalchemy.engine.url.URL + + parts = sqlalchemy.__version__.split('.') + major = int(parts[0]) + minor = int(parts[1]) + if major == 1 and minor >= 4: # pragma: no cover + constructor = sqlalchemy.engine.url.URL.create + aur_db_backend = aurweb.config.get('database', 'backend') if aur_db_backend == 'mysql': - return sqlalchemy.engine.url.URL( + return constructor( 'mysql+mysqlconnector', username=aurweb.config.get('database', 'user'), password=aurweb.config.get('database', 'password'), @@ -29,7 +38,7 @@ def get_sqlalchemy_url(): }, ) elif aur_db_backend == 'sqlite': - return sqlalchemy.engine.url.URL( + return constructor( 'sqlite', database=aurweb.config.get('database', 'name'), ) From cdf75ced9abe855753e33b48cc869c4ac64506ba Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Thu, 13 May 2021 00:08:15 +0200 Subject: [PATCH 0182/1451] Adding error 404 catcher --- aurweb/routers/errors.py | 14 ++++++++++++++ templates/errors/404.html | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 aurweb/routers/errors.py create mode 100644 templates/errors/404.html diff --git a/aurweb/routers/errors.py b/aurweb/routers/errors.py new file mode 100644 index 00000000..5d4ca4ce --- /dev/null +++ b/aurweb/routers/errors.py @@ -0,0 +1,14 @@ +from aurweb.templates import make_context, render_template +from aurweb import l10n + + +async def not_found(request, exc): + _ = l10n.get_translator_for_request(request) + context = make_context(request, f"404 - {_('Page Not Found')}") + return render_template("errors/404.html", context) + + +# Maps HTTP errors to functions +exceptions = { + 404: not_found, +} diff --git a/templates/errors/404.html b/templates/errors/404.html new file mode 100644 index 00000000..0afdd2fa --- /dev/null +++ b/templates/errors/404.html @@ -0,0 +1,8 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +
    +

    404 - {% trans %}Page Not Found{% endtrans %}

    +

    {% trans %}Sorry, the page you've requested does not exist.{% endtrans %}

    +
    + {% endblock %} From f6744d3e39fd044f797f9a702d8c9883fe40c527 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Thu, 13 May 2021 00:08:15 +0200 Subject: [PATCH 0183/1451] Adding error 503 catcher --- aurweb/asgi.py | 4 ++-- aurweb/routers/errors.py | 6 ++++++ templates/errors/503.html | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 templates/errors/503.html diff --git a/aurweb/asgi.py b/aurweb/asgi.py index b6e15582..c03a00f7 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -8,12 +8,12 @@ from starlette.middleware.sessions import SessionMiddleware import aurweb.config from aurweb.db import get_engine -from aurweb.routers import html, sso +from aurweb.routers import html, sso, errors routes = set() # Setup the FastAPI app. -app = FastAPI() +app = FastAPI(exception_handlers=errors.exceptions) @app.on_event("startup") diff --git a/aurweb/routers/errors.py b/aurweb/routers/errors.py index 5d4ca4ce..3bdaeb9d 100644 --- a/aurweb/routers/errors.py +++ b/aurweb/routers/errors.py @@ -8,7 +8,13 @@ async def not_found(request, exc): return render_template("errors/404.html", context) +async def service_unavailable(request, exc): + _ = l10n.get_translator_for_request(request) + context = make_context(request, "503 - {_('Service Unavailable')}") + return render_template("errors/503.html", context) + # Maps HTTP errors to functions exceptions = { 404: not_found, + 503: service_unavailable } diff --git a/templates/errors/503.html b/templates/errors/503.html new file mode 100644 index 00000000..d31666a1 --- /dev/null +++ b/templates/errors/503.html @@ -0,0 +1,8 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +
    +

    503 - {% trans %}Service Unavailable{% endtrans %}

    +

    {% trans %}Don't panic! This site is down due to maintenance. We will be back soon.{% endtrans %}

    +
    +{% endblock %} From 1d5827007f205c8972d07eee138372e8f9303684 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Thu, 13 May 2021 22:02:50 +0200 Subject: [PATCH 0184/1451] Adding route tests Removing status code from 404 title Removing status code from 503 title Adding id to 503 error box Indatation fix --- aurweb/routers/errors.py | 11 ++++------- aurweb/routers/html.py | 8 +++++++- templates/errors/404.html | 4 ++-- templates/errors/503.html | 2 +- test/test_routes.py | 7 +++++++ 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/aurweb/routers/errors.py b/aurweb/routers/errors.py index 3bdaeb9d..111d802a 100644 --- a/aurweb/routers/errors.py +++ b/aurweb/routers/errors.py @@ -1,17 +1,14 @@ from aurweb.templates import make_context, render_template -from aurweb import l10n async def not_found(request, exc): - _ = l10n.get_translator_for_request(request) - context = make_context(request, f"404 - {_('Page Not Found')}") - return render_template("errors/404.html", context) + context = make_context(request, "Page Not Found") + return render_template("errors/404.html", context, 404) async def service_unavailable(request, exc): - _ = l10n.get_translator_for_request(request) - context = make_context(request, "503 - {_('Service Unavailable')}") - return render_template("errors/503.html", context) + context = make_context(request, "Service Unavailable") + return render_template("errors/503.html", context, 503) # Maps HTTP errors to functions exceptions = { diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index ae08c764..50b62450 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -4,7 +4,7 @@ own modules and imported here. """ from http import HTTPStatus from urllib.parse import unquote -from fastapi import APIRouter, Form, Request +from fastapi import APIRouter, Form, Request, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse from aurweb.templates import make_context, render_template @@ -48,3 +48,9 @@ async def index(request: Request): """ Homepage route. """ context = make_context(request, "Home") return render_template("index.html", context) + + +# A route that returns a error 503. For testing purposes. +@router.get("/raisefivethree", response_class=HTMLResponse) +async def raise_service_unavailable(request: Request): + raise HTTPException(status_code=503) diff --git a/templates/errors/404.html b/templates/errors/404.html index 0afdd2fa..4926aff6 100644 --- a/templates/errors/404.html +++ b/templates/errors/404.html @@ -1,8 +1,8 @@ {% extends 'partials/layout.html' %} {% block pageContent %} -
    +

    404 - {% trans %}Page Not Found{% endtrans %}

    {% trans %}Sorry, the page you've requested does not exist.{% endtrans %}

    - {% endblock %} +{% endblock %} diff --git a/templates/errors/503.html b/templates/errors/503.html index d31666a1..9a0ed56a 100644 --- a/templates/errors/503.html +++ b/templates/errors/503.html @@ -1,7 +1,7 @@ {% extends 'partials/layout.html' %} {% block pageContent %} -
    +

    503 - {% trans %}Service Unavailable{% endtrans %}

    {% trans %}Don't panic! This site is down due to maintenance. We will be back soon.{% endtrans %}

    diff --git a/test/test_routes.py b/test/test_routes.py index 46ba39f5..86221108 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -67,3 +67,10 @@ def test_language_query_params(): response = req.post("/language", data=post_data) assert response.headers.get("location") == f"/?{q}" assert response.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_error_messages(): + response1 = client.get("/thisroutedoesnotexist") + response2 = client.get("/raisefivethree") + assert response1.status_code == int(HTTPStatus.NOT_FOUND) + assert response2.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) \ No newline at end of file From e0eb6b0e76311bc4e2df02e083edea28c42c178c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 16 May 2021 06:26:38 -0700 Subject: [PATCH 0185/1451] test_db: remove use of mkdtemp and os.removedirs Signed-off-by: Kevin Morris --- test/test_db.py | 52 +++++++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/test/test_db.py b/test/test_db.py index 0a134541..f5902e4c 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -57,10 +57,8 @@ def test_sqlalchemy_mysql_url(): def make_temp_config(backend): - if not os.path.isdir("/tmp"): - os.mkdir("/tmp") - tmpdir = tempfile.mkdtemp() - tmp = os.path.join(tmpdir, "config.tmp") + tmpdir = tempfile.TemporaryDirectory() + tmp = os.path.join(tmpdir.name, "config.tmp") with open("conf/config") as f: config = re.sub(r'backend = sqlite', f'backend = {backend}', f.read()) with open(tmp, "w") as o: @@ -69,16 +67,14 @@ def make_temp_config(backend): def test_sqlalchemy_unknown_backend(): - tmpdir, tmp = make_temp_config("blah") + tmpctx, tmp = make_temp_config("blah") - with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + with pytest.raises(ValueError): + db.get_sqlalchemy_url() aurweb.config.rehash() - with pytest.raises(ValueError): - db.get_sqlalchemy_url() - aurweb.config.rehash() - - os.remove(tmp) - os.removedirs(tmpdir) def test_db_connects_without_fail(): @@ -97,32 +93,28 @@ def test_connection_class_without_fail(): def test_connection_class_unsupported_backend(): - tmpdir, tmp = make_temp_config("blah") + tmpctx, tmp = make_temp_config("blah") - with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + with pytest.raises(ValueError): + db.Connection() aurweb.config.rehash() - with pytest.raises(ValueError): - db.Connection() - aurweb.config.rehash() - - os.remove(tmp) - os.removedirs(tmpdir) @mock.patch("mysql.connector.connect", mock.MagicMock(return_value=True)) @mock.patch.object(mysql.connector, "paramstyle", "qmark") def test_connection_mysql(): - tmpdir, tmp = make_temp_config("mysql") - with mock.patch.dict(os.environ, { - "AUR_CONFIG": tmp, - "AUR_CONFIG_DEFAULTS": "conf/config.defaults" - }): + tmpctx, tmp = make_temp_config("mysql") + with tmpctx: + with mock.patch.dict(os.environ, { + "AUR_CONFIG": tmp, + "AUR_CONFIG_DEFAULTS": "conf/config.defaults" + }): + aurweb.config.rehash() + db.Connection() aurweb.config.rehash() - db.Connection() - aurweb.config.rehash() - - os.remove(tmp) - os.removedirs(tmpdir) @mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) From 3f1f03e03c904f1c8202a692bc18ca153c529f50 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 16 May 2021 01:40:19 -0700 Subject: [PATCH 0186/1451] aurweb.db: only pass check_same_thread with sqlite Signed-off-by: Kevin Morris --- aurweb/db.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/aurweb/db.py b/aurweb/db.py index 49e0abd2..f5530bcf 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -59,10 +59,12 @@ def get_engine(): global engine, session, Session if engine is None: - engine = create_engine(get_sqlalchemy_url(), - # check_same_thread is for a SQLite technicality - # https://fastapi.tiangolo.com/tutorial/sql-databases/#note - connect_args={"check_same_thread": False}) + connect_args = dict() + if aurweb.config.get("database", "backend") == "sqlite": + # check_same_thread is for a SQLite technicality + # https://fastapi.tiangolo.com/tutorial/sql-databases/#note + connect_args["check_same_thread"] = False + engine = create_engine(get_sqlalchemy_url(), connect_args=connect_args) Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) session = Session() From 66189c4460d1516344b13ad6a381aca6a7a0786e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 18 May 2021 02:46:56 -0700 Subject: [PATCH 0187/1451] alembic: restore logging, fix pytest conflicts In this case, when running pytests, we do not allow alembic to configure loggers. Signed-off-by: Kevin Morris --- aurweb/initdb.py | 1 + migrations/env.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/aurweb/initdb.py b/aurweb/initdb.py index c8d0b2ae..5f55bfc9 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -40,6 +40,7 @@ def run(args): if args.use_alembic: alembic_config = alembic.config.Config('alembic.ini') alembic_config.get_main_option('script_location') + alembic_config.attributes["configure_logger"] = False engine = sqlalchemy.create_engine(aurweb.db.get_sqlalchemy_url(), echo=(args.verbose >= 1)) diff --git a/migrations/env.py b/migrations/env.py index 23759123..dfe14804 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -20,6 +20,12 @@ target_metadata = aurweb.schema.metadata # ... etc. +# If configure_logger is either True or not specified, +# configure the logger via fileConfig. +if config.attributes.get("configure_logger", True): + logging.config.fileConfig(config.config_file_name) + + def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -64,12 +70,7 @@ def run_migrations_online(): context.run_migrations() -if __name__ == "__main__": - # Interpret the config file for Python logging. - # This line sets up loggers basically. - logging.config.fileConfig(config.config_file_name) - - if context.is_offline_mode(): - run_migrations_offline() - else: - run_migrations_online() +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() From 72f755817c012a5c38255175522f32a059f976c0 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 17 May 2021 19:12:05 +0100 Subject: [PATCH 0188/1451] Adds Alembic migration for DB/Tables conversion to utf8mb4 MySql defaults to `utf8` and case insensitive collation so migrate these to case sensitive and `utf8mb4` Closes #21 Signed-off-by: Leonidas Spyropoulos From 7b7c3abbe2dab35bf4a7bb67c7ab4121a0ee7566 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 18 May 2021 13:04:20 +0100 Subject: [PATCH 0189/1451] Conditionally apply SSOAccountId migration to support misaligned databases Closes: #34 Signed-off-by: Leonidas Spyropoulos From ac31f520ea25d31f675dd4e2ac236078418c2f69 Mon Sep 17 00:00:00 2001 From: Kristian Klausen Date: Mon, 10 May 2021 22:34:19 +0200 Subject: [PATCH 0190/1451] Add coverage report for "Test Coverage Visualization"[1] [1] https://docs.gitlab.com/ee/user/project/merge_requests/test_coverage_visualization.html --- .gitlab-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1e287748..ca3055ad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,3 +24,7 @@ test: - AUR_CONFIG=conf/config python -m aurweb.initdb - make -C test - coverage report --include='aurweb/*' + - coverage xml --include='aurweb/*' + artifacts: + reports: + cobertura: coverage.xml From 64bc93926f87aa9a0a29b4f014af2374e527fc8a Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 18 May 2021 13:15:47 +0100 Subject: [PATCH 0191/1451] Add support for configuring database with port instead of socket Signed-off-by: Leonidas Spyropoulos --- .gitignore | 1 + aurweb/config.py | 4 ++++ aurweb/db.py | 11 ++++++++--- conf/config.defaults | 1 + conf/config.dev | 5 ++++- test/test_db.py | 24 +++++++++++++++++------- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 372fa105..35b571d7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ fastapi_aw/ .vim/ .pylintrc .coverage +.idea diff --git a/aurweb/config.py b/aurweb/config.py index 49a2765a..2a6cfc3e 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -32,6 +32,10 @@ def rehash(): _get_parser() +def get_with_fallback(section, option, fallback): + return _get_parser().get(section, option, fallback=fallback) + + def get(section, option): return _get_parser().get(section, option) diff --git a/aurweb/db.py b/aurweb/db.py index f5530bcf..491ce9e2 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -27,15 +27,20 @@ def get_sqlalchemy_url(): aur_db_backend = aurweb.config.get('database', 'backend') if aur_db_backend == 'mysql': + if aurweb.config.get_with_fallback('database', 'port', fallback=None): + port = aurweb.config.get('database', 'port') + param_query = None + else: + port = None + param_query = {'unix_socket': aurweb.config.get('database', 'socket')} return constructor( 'mysql+mysqlconnector', username=aurweb.config.get('database', 'user'), password=aurweb.config.get('database', 'password'), host=aurweb.config.get('database', 'host'), database=aurweb.config.get('database', 'name'), - query={ - 'unix_socket': aurweb.config.get('database', 'socket'), - }, + port=port, + query=param_query ) elif aur_db_backend == 'sqlite': return constructor( diff --git a/conf/config.defaults b/conf/config.defaults index 98e033b7..c05648d5 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -2,6 +2,7 @@ backend = mysql host = localhost socket = /var/run/mysqld/mysqld.sock +;port = 3306 name = AUR user = aur password = aur diff --git a/conf/config.dev b/conf/config.dev index ccb01f4f..194a3bf8 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -9,11 +9,14 @@ backend = sqlite name = YOUR_AUR_ROOT/aurweb.sqlite3 -; Alternative MySQL configuration +; Alternative MySQL configuration (Use either port of socket, if both defined port takes priority) ;backend = mysql ;name = aurweb ;user = aur ;password = aur +;host = localhost +;port = 3306 +;socket = /var/run/mysqld/mysqld.sock [options] aurwebdir = YOUR_AUR_ROOT diff --git a/test/test_db.py b/test/test_db.py index f5902e4c..41936321 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -56,18 +56,28 @@ def test_sqlalchemy_mysql_url(): aurweb.config.rehash() -def make_temp_config(backend): +def test_sqlalchemy_mysql_port_url(): + tmpctx, tmp = make_temp_config("conf/config.defaults", ";port = 3306", "port = 3306") + + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def make_temp_config(config_file, src_str, replace_with): tmpdir = tempfile.TemporaryDirectory() tmp = os.path.join(tmpdir.name, "config.tmp") - with open("conf/config") as f: - config = re.sub(r'backend = sqlite', f'backend = {backend}', f.read()) + with open(config_file) as f: + config = re.sub(src_str, f'{replace_with}', f.read()) with open(tmp, "w") as o: o.write(config) - return (tmpdir, tmp) + return tmpdir, tmp def test_sqlalchemy_unknown_backend(): - tmpctx, tmp = make_temp_config("blah") + tmpctx, tmp = make_temp_config("conf/config", "backend = sqlite", "backend = blah") with tmpctx: with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): @@ -93,7 +103,7 @@ def test_connection_class_without_fail(): def test_connection_class_unsupported_backend(): - tmpctx, tmp = make_temp_config("blah") + tmpctx, tmp = make_temp_config("conf/config", "backend = sqlite", "backend = blah") with tmpctx: with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): @@ -106,7 +116,7 @@ def test_connection_class_unsupported_backend(): @mock.patch("mysql.connector.connect", mock.MagicMock(return_value=True)) @mock.patch.object(mysql.connector, "paramstyle", "qmark") def test_connection_mysql(): - tmpctx, tmp = make_temp_config("mysql") + tmpctx, tmp = make_temp_config("conf/config", "backend = sqlite", "backend = mysql") with tmpctx: with mock.patch.dict(os.environ, { "AUR_CONFIG": tmp, From 5185df629ee7d2190fac7f0268935e3f4477d114 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 24 Dec 2020 20:48:35 -0800 Subject: [PATCH 0192/1451] move aurweb.testing to its own package + Added aurweb.testing.setup_test_db(*tables) + Added aurweb.testing.models.make_user(**kwargs) + Added aurweb.testing.models.make_session(**kwargs) + Added aurweb.testing.requests.Client + Added aurweb.testing.requests.Request * Updated test_l10n.py to use our new Request Signed-off-by: Kevin Morris --- aurweb/{testing.py => testing/__init__.py} | 4 ++-- aurweb/testing/models.py | 25 ++++++++++++++++++++++ aurweb/testing/requests.py | 8 +++++++ test/test_l10n.py | 18 ++++++---------- 4 files changed, 41 insertions(+), 14 deletions(-) rename aurweb/{testing.py => testing/__init__.py} (93%) create mode 100644 aurweb/testing/models.py create mode 100644 aurweb/testing/requests.py diff --git a/aurweb/testing.py b/aurweb/testing/__init__.py similarity index 93% rename from aurweb/testing.py rename to aurweb/testing/__init__.py index 7516d918..0a807b40 100644 --- a/aurweb/testing.py +++ b/aurweb/testing/__init__.py @@ -1,4 +1,4 @@ -from aurweb.db import get_engine +import aurweb.db def setup_test_db(*args): @@ -21,7 +21,7 @@ def setup_test_db(*args): test_tables = ["Users", "Sessions"]; setup_test_db(*test_tables) """ - engine = get_engine() + engine = aurweb.db.get_engine() conn = engine.connect() tables = list(args) diff --git a/aurweb/testing/models.py b/aurweb/testing/models.py new file mode 100644 index 00000000..8a27c409 --- /dev/null +++ b/aurweb/testing/models.py @@ -0,0 +1,25 @@ +import warnings + +from sqlalchemy import exc + +import aurweb.db + + +def make_user(**kwargs): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", exc.SAWarning) + from aurweb.models.user import User + user = User(**kwargs) + aurweb.db.session.add(user) + aurweb.db.session.commit() + return user + + +def make_session(**kwargs): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", exc.SAWarning) + from aurweb.models.session import Session + session = Session(**kwargs) + aurweb.db.session.add(session) + aurweb.db.session.commit() + return session diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py new file mode 100644 index 00000000..2839c93f --- /dev/null +++ b/aurweb/testing/requests.py @@ -0,0 +1,8 @@ +class Client: + host = "127.0.0.1" + + +class Request: + client = Client() + cookies = dict() + headers = dict() diff --git a/test/test_l10n.py b/test/test_l10n.py index 1a1ef3e6..e833cd44 100644 --- a/test/test_l10n.py +++ b/test/test_l10n.py @@ -1,13 +1,6 @@ """ Test our l10n module. """ from aurweb import l10n - - -class FakeRequest: - """ A fake Request doppleganger; use this to change request.cookies - easily and with no side-effects. """ - - def __init__(self, *args, **kwargs): - self.cookies = kwargs.pop("cookies", dict()) +from aurweb.testing.requests import Request def test_translator(): @@ -18,7 +11,7 @@ def test_translator(): def test_get_request_language(): """ First, tests default_lang, then tests a modified AURLANG cookie. """ - request = FakeRequest() + request = Request() assert l10n.get_request_language(request) == "en" request.cookies["AURLANG"] = "de" @@ -28,8 +21,8 @@ def test_get_request_language(): def test_get_raw_translator_for_request(): """ Make sure that get_raw_translator_for_request is giving us the translator we expect. """ - request = FakeRequest(cookies={"AURLANG": "de"}) - + request = Request() + request.cookies["AURLANG"] = "de" translator = l10n.get_raw_translator_for_request(request) assert translator.gettext("Home") == \ l10n.translator.translate("Home", "de") @@ -38,7 +31,8 @@ def test_get_raw_translator_for_request(): def test_get_translator_for_request(): """ Make sure that get_translator_for_request is giving us back our expected translation function. """ - request = FakeRequest(cookies={"AURLANG": "de"}) + request = Request() + request.cookies["AURLANG"] = "de" translate = l10n.get_translator_for_request(request) assert translate("Home") == "Startseite" From a836892cde9a8f89fb7cb9e159bc8d4711f88439 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 14 Jan 2021 21:06:41 -0800 Subject: [PATCH 0193/1451] aurweb.db: add query, create, delete helpers Takes sqlalchemy kwargs or stanzas: query(Model, Model.Column == value) query(Model, and_(Model.Column == value, Model.Column != "BAD!")) Updated tests to reflect the new utility and a comment about upcoming function deprecation is added to get_account_type(). From here on, phase out the use of get_account_type(). + aurweb.db: Added create utility function + aurweb.db: Added delete utility function The `delete` function can be used to delete a record by search kwargs directly. Example: delete(User, User.ID == 6) All three functions added in this commit are typically useful to perform these operations without having to import aurweb.db.session. Removes a bit of redundancy overall. Signed-off-by: Kevin Morris --- aurweb/db.py | 18 ++++++++++++++++++ test/test_account_type.py | 40 ++++++++++++++++++++++++++++++++++----- test/test_db.py | 13 ++++++++++++- test/test_routes.py | 18 ++++++++++++++++-- test/test_user.py | 4 +++- 5 files changed, 84 insertions(+), 9 deletions(-) diff --git a/aurweb/db.py b/aurweb/db.py index 491ce9e2..9ca51de2 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -11,6 +11,24 @@ Session = None session = None +def query(model, *args, **kwargs): + return session.query(model).filter(*args, **kwargs) + + +def create(model, *args, **kwargs): + instance = model(*args, **kwargs) + session.add(instance) + session.commit() + return instance + + +def delete(model, *args, **kwargs): + instance = session.query(model).filter(*args, **kwargs) + for record in instance: + session.delete(record) + session.commit() + + def get_sqlalchemy_url(): """ Build an SQLAlchemy for use with create_engine based on the aurweb configuration. diff --git a/test/test_account_type.py b/test/test_account_type.py index b6a12363..9419970c 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -1,20 +1,34 @@ import pytest from aurweb.models.account_type import AccountType +from aurweb.models.user import User from aurweb.testing import setup_test_db +from aurweb.testing.models import make_user + +account_type = None @pytest.fixture(autouse=True) def setup(): - setup_test_db() + setup_test_db("Users") + + from aurweb.db import session + + global account_type + + account_type = AccountType(AccountType="TestUser") + session.add(account_type) + session.commit() + + yield account_type + + session.delete(account_type) + session.commit() def test_account_type(): """ Test creating an AccountType, and reading its columns. """ from aurweb.db import session - account_type = AccountType(AccountType="TestUser") - session.add(account_type) - session.commit() # Make sure it got created and was given an ID. assert bool(account_type.ID) @@ -25,4 +39,20 @@ def test_account_type(): "" % ( account_type.ID) - session.delete(account_type) + record = session.query(AccountType).filter( + AccountType.AccountType == "TestUser").first() + assert account_type == record + + +def test_user_account_type_relationship(): + from aurweb.db import session + + user = make_user(Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + assert user.AccountType == account_type + assert account_type.users.filter(User.ID == user.ID).first() + + session.delete(user) + session.commit() diff --git a/test/test_db.py b/test/test_db.py index 41936321..1eb0dc28 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -3,6 +3,7 @@ import re import sqlite3 import tempfile +from datetime import datetime from unittest import mock import mysql.connector @@ -11,6 +12,7 @@ import pytest import aurweb.config from aurweb import db +from aurweb.models.account_type import AccountType from aurweb.testing import setup_test_db @@ -39,7 +41,7 @@ class DBConnection: @pytest.fixture(autouse=True) def setup_db(): - setup_test_db() + setup_test_db("Bans") def test_sqlalchemy_sqlite_url(): @@ -174,3 +176,12 @@ def test_connection_execute_paramstyle_unsupported(): "SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"] ).fetchall() + + +def test_create_delete(): + db.create(AccountType, AccountType="test") + record = db.query(AccountType, AccountType.AccountType == "test").first() + assert record is not None + db.delete(AccountType, AccountType.AccountType == "test") + record = db.query(AccountType, AccountType.AccountType == "test").first() + assert record is None diff --git a/test/test_routes.py b/test/test_routes.py index 86221108..950d9b71 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -7,14 +7,26 @@ import pytest from fastapi.testclient import TestClient from aurweb.asgi import app +from aurweb.db import query +from aurweb.models.account_type import AccountType from aurweb.testing import setup_test_db client = TestClient(app) +user = None + @pytest.fixture def setup(): - setup_test_db("Users", "Session") + global user + + setup_test_db("Users", "Sessions") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = make_user(Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) def test_index(): @@ -54,6 +66,7 @@ def test_language_invalid_next(): response = req.post("/language", data=post_data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) + def test_language_query_params(): """ Test the language post route with query params. """ next = urllib.parse.quote_plus("/") @@ -73,4 +86,5 @@ def test_error_messages(): response1 = client.get("/thisroutedoesnotexist") response2 = client.get("/raisefivethree") assert response1.status_code == int(HTTPStatus.NOT_FOUND) - assert response2.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) \ No newline at end of file + assert response2.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) + diff --git a/test/test_user.py b/test/test_user.py index 8ac9b00b..5a56a035 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -1,5 +1,8 @@ import pytest +import aurweb.config + +from aurweb.db import query from aurweb.models.account_type import AccountType from aurweb.models.user import User from aurweb.testing import setup_test_db @@ -26,7 +29,6 @@ def test_user(): Salt="efgh", ResetKey="blahblah") session.add(user) session.commit() - assert user in account_type.users # Make sure the user was created and given an ID. From adc9fccb7d0e984cd780cd1a785911e36a6316b1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Dec 2020 19:29:19 -0800 Subject: [PATCH 0194/1451] add aurweb.models.ban.Ban ORM mapping Signed-off-by: Kevin Morris --- aurweb/models/ban.py | 19 +++++++++++++ test/test_ban.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 aurweb/models/ban.py create mode 100644 test/test_ban.py diff --git a/aurweb/models/ban.py b/aurweb/models/ban.py new file mode 100644 index 00000000..be030380 --- /dev/null +++ b/aurweb/models/ban.py @@ -0,0 +1,19 @@ +from fastapi import Request +from sqlalchemy.orm import mapper + +from aurweb.schema import Bans + + +class Ban: + def __init__(self, **kwargs): + self.IPAddress = kwargs.get("IPAddress") + self.BanTS = kwargs.get("BanTS") + + +def is_banned(request: Request): + from aurweb.db import session + ip = request.client.host + return session.query(Ban).filter(Ban.IPAddress == ip).first() is not None + + +mapper(Ban, Bans) diff --git a/test/test_ban.py b/test/test_ban.py new file mode 100644 index 00000000..de4f5b1b --- /dev/null +++ b/test/test_ban.py @@ -0,0 +1,63 @@ +import warnings + +from datetime import datetime, timedelta + +import pytest + +from sqlalchemy import exc as sa_exc + +from aurweb.models.ban import Ban, is_banned +from aurweb.testing import setup_test_db +from aurweb.testing.requests import Request + +ban = None + +request = Request() + + +@pytest.fixture(autouse=True) +def setup(): + from aurweb.db import session + + global ban + + setup_test_db("Bans") + + ban = Ban(IPAddress="127.0.0.1", + BanTS=datetime.utcnow() + timedelta(seconds=30)) + session.add(ban) + session.commit() + + +def test_ban(): + assert ban.IPAddress == "127.0.0.1" + assert bool(ban.BanTS) + + +def test_invalid_ban(): + from aurweb.db import session + + with pytest.raises(sa_exc.IntegrityError, + match="NOT NULL constraint failed: Bans.IPAddress"): + bad_ban = Ban(BanTS=datetime.utcnow()) + session.add(bad_ban) + + # We're adding a ban with no primary key; this causes an + # SQLAlchemy warnings when committing to the DB. + # Ignore them. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", sa_exc.SAWarning) + session.commit() + + # Since we got a transaction failure, we need to rollback. + session.rollback() + + +def test_banned(): + request.client.host = "127.0.0.1" + assert is_banned(request) + + +def test_not_banned(): + request.client.host = "192.168.0.1" + assert not is_banned(request) From 1922e5380d819501b1ee3f9b50ff69bc583dbf6c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Dec 2020 20:55:43 -0800 Subject: [PATCH 0195/1451] add aurweb.models.session.Session ORM database object + Added aurweb.util module. - Added make_random_string function. + Added aurweb.db.make_random_value function. - Takes a model and a column and introspects them to figure out the proper column length to create a random string for; then creates a unique string for that column. Signed-off-by: Kevin Morris --- aurweb/db.py | 42 +++++++++++++++++++++++++++++- aurweb/models/session.py | 25 ++++++++++++++++++ aurweb/util.py | 7 +++++ test/test_session.py | 56 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 aurweb/models/session.py create mode 100644 aurweb/util.py create mode 100644 test/test_session.py diff --git a/aurweb/db.py b/aurweb/db.py index 9ca51de2..3f5731a9 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,8 +1,10 @@ import math import aurweb.config +import aurweb.util -engine = None # See get_engine +# See get_engine. +engine = None # ORM Session class. Session = None @@ -10,6 +12,44 @@ Session = None # Global ORM Session object. session = None +# Global introspected object memo. +introspected = dict() + + +def make_random_value(table: str, column: str): + """ Generate a unique, random value for a string column in a table. + + This can be used to generate for example, session IDs that + align with the properties of the database column with regards + to size. + + Internally, we use SQLAlchemy introspection to look at column + to decide which length to use for random string generation. + + :return: A unique string that is not in the database + """ + global introspected + + # Make sure column is converted to a string for memo interaction. + scolumn = str(column) + + # If the target column is not yet introspected, store its introspection + # object into our global `introspected` memo. + if scolumn not in introspected: + from sqlalchemy import inspect + target_column = scolumn.split('.')[-1] + col = list(filter(lambda c: c.name == target_column, + inspect(table).columns))[0] + introspected[scolumn] = col + + col = introspected.get(scolumn) + length = col.type.length + + string = aurweb.util.make_random_string(length) + while session.query(table).filter(column == string).first(): + string = aurweb.util.make_random_string(length) + return string + def query(model, *args, **kwargs): return session.query(model).filter(*args, **kwargs) diff --git a/aurweb/models/session.py b/aurweb/models/session.py new file mode 100644 index 00000000..60749303 --- /dev/null +++ b/aurweb/models/session.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, Integer +from sqlalchemy.orm import backref, mapper, relationship + +from aurweb.db import make_random_value +from aurweb.models.user import User +from aurweb.schema import Sessions + + +class Session: + UsersID = Column(Integer, nullable=True) + + def __init__(self, **kwargs): + self.UsersID = kwargs.get("UsersID") + self.SessionID = kwargs.get("SessionID") + self.LastUpdateTS = kwargs.get("LastUpdateTS") + + +mapper(Session, Sessions, primary_key=[Sessions.c.SessionID], properties={ + "User": relationship(User, backref=backref("session", + uselist=False)) +}) + + +def generate_unique_sid(): + return make_random_value(Session, Session.SessionID) diff --git a/aurweb/util.py b/aurweb/util.py new file mode 100644 index 00000000..65f18a4c --- /dev/null +++ b/aurweb/util.py @@ -0,0 +1,7 @@ +import random +import string + + +def make_random_string(length): + return ''.join(random.choices(string.ascii_lowercase + + string.digits, k=length)) diff --git a/test/test_session.py b/test/test_session.py new file mode 100644 index 00000000..560f628c --- /dev/null +++ b/test/test_session.py @@ -0,0 +1,56 @@ +""" Test our Session model. """ +from datetime import datetime +from unittest import mock + +import pytest + +from aurweb.models.account_type import AccountType +from aurweb.models.session import generate_unique_sid +from aurweb.testing import setup_test_db +from aurweb.testing.models import make_session, make_user + +user, _session = None, None + + +@pytest.fixture(autouse=True) +def setup(): + from aurweb.db import session + + global user, _session + + setup_test_db("Users", "Sessions") + + account_type = session.query(AccountType).filter( + AccountType.AccountType == "User").first() + user = make_user(Username="test", Email="test@example.org", + ResetKey="testReset", Passwd="testPassword", + AccountType=account_type) + _session = make_session(UsersID=user.ID, SessionID="testSession", + LastUpdateTS=datetime.utcnow()) + + +def test_session(): + assert _session.SessionID == "testSession" + assert _session.UsersID == user.ID + + +def test_session_user_association(): + # Make sure that the Session user attribute is correct. + assert _session.User == user + + +def test_generate_unique_sid(): + # Mock up aurweb.models.session.generate_sid by returning + # sids[i % 2] from 0 .. n. This will swap between each sid + # between each call. + sids = ["testSession", "realSession"] + i = 0 + + def mock_generate_sid(length): + nonlocal i + sid = sids[i % 2] + i += 1 + return sid + + with mock.patch("aurweb.util.make_random_string", mock_generate_sid): + assert generate_unique_sid() == "realSession" From 137c050f99e29f3d039c42f3b693dd9ef7ed4bd1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Jan 2021 16:43:35 -0800 Subject: [PATCH 0196/1451] add python-bcrypt dependency Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 +- Dockerfile | 2 +- INSTALL | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ca3055ad..4ad97393 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,7 @@ before_script: python-pytest-tap python-fastapi hypercorn nginx python-authlib python-itsdangerous python-httpx python-jinja python-pytest-cov python-requests python-aiofiles python-python-multipart - python-pytest-asyncio python-coverage + python-pytest-asyncio python-coverage python-bcrypt test: script: diff --git a/Dockerfile b/Dockerfile index 7e981340..6638f9a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN pacman -Syu --noconfirm base-devel git gpgme protobuf pyalpm \ python-werkzeug python-pytest-tap python-fastapi nginx python-authlib \ python-itsdangerous python-httpx python-jinja python-pytest-cov \ python-requests python-aiofiles python-python-multipart \ - python-pytest-asyncio python-coverage hypercorn + python-pytest-asyncio python-coverage hypercorn python-bcrypt # Remove aurweb.sqlite3 if it was copied over via COPY. RUN rm -fv aurweb.sqlite3 diff --git a/INSTALL b/INSTALL index e4c52480..6c43fec8 100644 --- a/INSTALL +++ b/INSTALL @@ -51,7 +51,7 @@ read the instructions below. python-bleach python-markdown python-alembic hypercorn \ python-itsdangerous python-authlib python-httpx \ python-jinja python-aiofiles python-python-multipart \ - python-requests + python-requests hypercorn python-bcrypt # python3 setup.py install 5) Create a new MySQL database and a user and import the aurweb SQL schema: From 56f2798279f3cbde46389aa65a27fb58bfb0bcfc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Dec 2020 20:54:53 -0800 Subject: [PATCH 0197/1451] add aurweb.auth and authentication to User + Added aurweb.auth.AnonymousUser * An instance of this model is returned as the request user when the request is not authenticated + Added aurweb.auth.BasicAuthBackend + Add starlette's AuthenticationMiddleware to app middleware, which uses our BasicAuthBackend facility + Added User.is_authenticated() + Added User.authenticate(password) + Added User.login(request, password) + Added User.logout(request) + Added repr(User(...)) representation + Added aurweb.auth.auth_required decorator. This change uses the same AURSID logic in the PHP implementation. Additionally, introduce a few helpers for authentication, one of which being `User.update_password(password, rounds = 12)` where `rounds` is a configurable number of salt rounds. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 8 +++ aurweb/auth.py | 77 +++++++++++++++++++++++ aurweb/models/user.py | 125 ++++++++++++++++++++++++++++++++++++- test/test_auth.py | 80 ++++++++++++++++++++++++ test/test_user.py | 142 +++++++++++++++++++++++++++++++++++++----- 5 files changed, 412 insertions(+), 20 deletions(-) create mode 100644 aurweb/auth.py create mode 100644 test/test_auth.py diff --git a/aurweb/asgi.py b/aurweb/asgi.py index c03a00f7..4d21ad03 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -1,12 +1,15 @@ import http +import os from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles +from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.sessions import SessionMiddleware import aurweb.config +from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine from aurweb.routers import html, sso, errors @@ -32,10 +35,15 @@ async def app_startup(): StaticFiles(directory="web/html/images"), name="static_images") + # Add application middlewares. + app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend()) app.add_middleware(SessionMiddleware, secret_key=session_secret) + + # Add application routes. app.include_router(sso.router) app.include_router(html.router) + # Initialize the database engine and ORM. get_engine() # NOTE: Always keep this dictionary updated with all routes diff --git a/aurweb/auth.py b/aurweb/auth.py new file mode 100644 index 00000000..8608a82a --- /dev/null +++ b/aurweb/auth.py @@ -0,0 +1,77 @@ +import functools + +from datetime import datetime +from http import HTTPStatus + +from fastapi.responses import RedirectResponse +from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError +from starlette.requests import HTTPConnection + +from aurweb.models.session import Session +from aurweb.models.user import User +from aurweb.templates import make_context, render_template + + +class AnonymousUser: + @staticmethod + def is_authenticated(): + return False + + +class BasicAuthBackend(AuthenticationBackend): + async def authenticate(self, conn: HTTPConnection): + from aurweb.db import session + + sid = conn.cookies.get("AURSID") + if not sid: + return None, AnonymousUser() + + now_ts = datetime.utcnow().timestamp() + record = session.query(Session).filter( + Session.SessionID == sid, Session.LastUpdateTS >= now_ts).first() + if not record: + return None, AnonymousUser() + + user = session.query(User).filter(User.ID == record.UsersID).first() + if not user: + raise AuthenticationError(f"Invalid User ID: {record.UsersID}") + + user.authenticated = True + return AuthCredentials(["authenticated"]), user + + +def auth_required(is_required: bool = True, + redirect: str = "/", + template: tuple = None): + """ Authentication route decorator. + + If redirect is given, the user will be redirected if the auth state + does not match is_required. + + If template is given, it will be rendered with Unauthorized if + is_required does not match and take priority over redirect. + + :param is_required: A boolean indicating whether the function requires auth + :param redirect: Path to redirect to if is_required isn't True + :param template: A template tuple: ("template.html", "Template Page") + """ + + def decorator(func): + @functools.wraps(func) + async def wrapper(request, *args, **kwargs): + if request.user.is_authenticated() != is_required: + status_code = int(HTTPStatus.UNAUTHORIZED) + url = "/" + if redirect: + status_code = int(HTTPStatus.SEE_OTHER) + url = redirect + if template: + path, title = template + context = make_context(request, title) + return render_template(request, path, context, + status_code=int(HTTPStatus.UNAUTHORIZED)) + return RedirectResponse(url=url, status_code=status_code) + return await func(request, *args, **kwargs) + return wrapper + + return decorator diff --git a/aurweb/models/user.py b/aurweb/models/user.py index ba91c439..aff4ce6b 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -1,13 +1,25 @@ +import hashlib + +from datetime import datetime + +import bcrypt + +from fastapi import Request from sqlalchemy.orm import backref, mapper, relationship +import aurweb.config + from aurweb.models.account_type import AccountType +from aurweb.models.ban import is_banned from aurweb.schema import Users class User: """ An ORM model of a single Users record. """ + authenticated = False def __init__(self, **kwargs): + # Set AccountTypeID if it was passed. self.AccountTypeID = kwargs.get("AccountTypeID") account_type = kwargs.get("AccountType") @@ -15,22 +27,129 @@ class User: self.AccountType = account_type self.Username = kwargs.get("Username") + + self.ResetKey = kwargs.get("ResetKey") self.Email = kwargs.get("Email") self.BackupEmail = kwargs.get("BackupEmail") - self.Passwd = kwargs.get("Passwd") - self.Salt = kwargs.get("Salt") self.RealName = kwargs.get("RealName") self.LangPreference = kwargs.get("LangPreference") self.Timezone = kwargs.get("Timezone") self.Homepage = kwargs.get("Homepage") self.IRCNick = kwargs.get("IRCNick") self.PGPKey = kwargs.get("PGPKey") - self.RegistrationTS = kwargs.get("RegistrationTS") + self.RegistrationTS = datetime.utcnow() self.CommentNotify = kwargs.get("CommentNotify") self.UpdateNotify = kwargs.get("UpdateNotify") self.OwnershipNotify = kwargs.get("OwnershipNotify") self.SSOAccountID = kwargs.get("SSOAccountID") + self.Salt = None + self.Passwd = str() + + passwd = kwargs.get("Passwd") + if passwd: + self.update_password(passwd) + + def update_password(self, password, salt_rounds=12): + from aurweb.db import session + self.Passwd = bcrypt.hashpw( + password.encode(), + bcrypt.gensalt(rounds=salt_rounds)).decode() + session.commit() + + @staticmethod + def minimum_passwd_length(): + return aurweb.config.getint("options", "passwd_min_len") + + def is_authenticated(self): + """ Return internal authenticated state. """ + return self.authenticated + + def valid_password(self, password: str): + """ Check authentication against a given password. """ + from aurweb.db import session + + if password is None: + return False + + password_is_valid = False + + try: + password_is_valid = bcrypt.checkpw(password.encode(), + self.Passwd.encode()) + except ValueError: + pass + + # If our Salt column is not empty, we're using a legacy password. + if not password_is_valid and self.Salt != str(): + # Try to login with legacy method. + password_is_valid = hashlib.md5( + f"{self.Salt}{password}".encode() + ).hexdigest() == self.Passwd + + # We got here, we passed the legacy authentication. + # Update the password to our modern hash style. + if password_is_valid: + self.update_password(password) + + return password_is_valid + + def _login_approved(self, request: Request): + return not is_banned(request) and not self.Suspended + + def login(self, request: Request, password: str, session_time=0): + """ Login and authenticate a request. """ + + from aurweb.db import session + from aurweb.models.session import Session, generate_unique_sid + + if not self._login_approved(request): + return None + + self.authenticated = self.valid_password(password) + if not self.authenticated: + return None + + self.LastLogin = now_ts = datetime.utcnow().timestamp() + self.LastLoginIPAddress = request.client.host + session.commit() + + session_ts = now_ts + ( + session_time if session_time + else aurweb.config.getint("options", "login_timeout") + ) + + sid = None + + if not self.session: + sid = generate_unique_sid() + self.session = Session(UsersID=self.ID, SessionID=sid, + LastUpdateTS=session_ts) + session.add(self.session) + else: + last_updated = self.session.LastUpdateTS + if last_updated and last_updated < now_ts: + self.session.SessionID = sid = generate_unique_sid() + else: + # Session is still valid; retrieve the current SID. + sid = self.session.SessionID + + self.session.LastUpdateTS = session_ts + + session.commit() + + request.cookies["AURSID"] = self.session.SessionID + return self.session.SessionID + + def logout(self, request): + from aurweb.db import session + + del request.cookies["AURSID"] + self.authenticated = False + if self.session: + session.delete(self.session) + session.commit() + def __repr__(self): return "" % ( self.ID, str(self.AccountType), self.Username) diff --git a/test/test_auth.py b/test/test_auth.py new file mode 100644 index 00000000..d2251de4 --- /dev/null +++ b/test/test_auth.py @@ -0,0 +1,80 @@ +from datetime import datetime + +import pytest + +from starlette.authentication import AuthenticationError + +from aurweb.db import query +from aurweb.auth import BasicAuthBackend +from aurweb.models.account_type import AccountType +from aurweb.testing import setup_test_db +from aurweb.testing.models import make_session, make_user +from aurweb.testing.requests import Request + +# Persistent user object, initialized in our setup fixture. +user = None +backend = None +request = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, backend, request + + setup_test_db("Users", "Sessions") + + from aurweb.db import session + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = make_user(Username="test", Email="test@example.com", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + session.add(user) + session.commit() + + backend = BasicAuthBackend() + request = Request() + + +@pytest.mark.asyncio +async def test_auth_backend_missing_sid(): + # The request has no AURSID cookie, so authentication fails, and + # AnonymousUser is returned. + _, result = await backend.authenticate(request) + assert not result.is_authenticated() + + +@pytest.mark.asyncio +async def test_auth_backend_invalid_sid(): + # Provide a fake AURSID that won't be found in the database. + # This results in our path going down the invalid sid route, + # which gives us an AnonymousUser. + request.cookies["AURSID"] = "fake" + _, result = await backend.authenticate(request) + assert not result.is_authenticated() + + +@pytest.mark.asyncio +async def test_auth_backend_invalid_user_id(): + # Create a new session with a fake user id. + now_ts = datetime.utcnow().timestamp() + make_session(UsersID=666, SessionID="realSession", + LastUpdateTS=now_ts + 5) + + # Here, we specify a real SID; but it's user is not there. + request.cookies["AURSID"] = "realSession" + with pytest.raises(AuthenticationError, match="Invalid User ID: 666"): + await backend.authenticate(request) + + +@pytest.mark.asyncio +async def test_basic_auth_backend(): + # This time, everything matches up. We expect the user to + # equal the real_user. + now_ts = datetime.utcnow().timestamp() + make_session(UsersID=user.ID, SessionID="realSession", + LastUpdateTS=now_ts + 5) + _, result = await backend.authenticate(request) + assert result == user diff --git a/test/test_user.py b/test/test_user.py index 5a56a035..b8d4248a 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -1,48 +1,86 @@ +import hashlib + +from datetime import datetime, timedelta + +import bcrypt import pytest +import aurweb.auth import aurweb.config from aurweb.db import query from aurweb.models.account_type import AccountType +from aurweb.models.ban import Ban +from aurweb.models.session import Session from aurweb.models.user import User from aurweb.testing import setup_test_db +from aurweb.testing.models import make_session, make_user +from aurweb.testing.requests import Request + +account_type, user = None, None @pytest.fixture(autouse=True) def setup(): - setup_test_db("Users") - - -def test_user(): - """ Test creating a user and reading its columns. """ from aurweb.db import session - # First, grab our target AccountType. + global account_type, user + + setup_test_db("Users", "Sessions", "Bans") + account_type = session.query(AccountType).filter( AccountType.AccountType == "User").first() - user = User( - AccountType=account_type, - RealName="Test User", Username="test", - Email="test@example.org", Passwd="abcd", - IRCNick="tester", - Salt="efgh", ResetKey="blahblah") - session.add(user) - session.commit() + user = make_user(Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + +def test_user_login_logout(): + """ Test creating a user and reading its columns. """ + from aurweb.db import session + + # Assert that make_user created a valid user. + assert bool(user.ID) + + # Test authentication. + assert user.valid_password("testPassword") + assert not user.valid_password("badPassword") + assert user in account_type.users - # Make sure the user was created and given an ID. - assert bool(user.ID) + # Make a raw request. + request = Request() + assert not user.login(request, "badPassword") + assert not user.is_authenticated() + + sid = user.login(request, "testPassword") + assert sid is not None + assert user.is_authenticated() + assert "AURSID" in request.cookies + + # Expect that User session relationships work right. + user_session = session.query(Session).filter( + Session.UsersID == user.ID).first() + assert user_session == user.session + assert user.session.SessionID == sid + assert user.session.User == user # Search for the user via query API. result = session.query(User).filter(User.ID == user.ID).first() # Compare the result and our original user. + assert result == user assert result.ID == user.ID assert result.AccountType.ID == user.AccountType.ID assert result.Username == user.Username assert result.Email == user.Email + # Test result authenticate methods to ensure they work the same. + assert not result.valid_password("badPassword") + assert result.valid_password("testPassword") + assert result.is_authenticated() + # Ensure we've got the correct account type. assert user.AccountType.ID == account_type.ID assert user.AccountType.AccountType == account_type.AccountType @@ -51,4 +89,74 @@ def test_user(): assert repr(user) == f"" - session.delete(user) + # Test logout. + user.logout(request) + assert "AURSID" not in request.cookies + assert not user.is_authenticated() + + +def test_user_login_twice(): + request = Request() + assert user.login(request, "testPassword") + assert user.login(request, "testPassword") + + +def test_user_login_banned(): + from aurweb.db import session + + # Add ban for the next 30 seconds. + banned_timestamp = datetime.utcnow() + timedelta(seconds=30) + ban = Ban(IPAddress="127.0.0.1", BanTS=banned_timestamp) + session.add(ban) + session.commit() + + request = Request() + request.client.host = "127.0.0.1" + assert not user.login(request, "testPassword") + + +def test_user_login_suspended(): + from aurweb.db import session + user.Suspended = True + session.commit() + assert not user.login(Request(), "testPassword") + + +def test_legacy_user_authentication(): + from aurweb.db import session + + user.Salt = bcrypt.gensalt().decode() + user.Passwd = hashlib.md5(f"{user.Salt}testPassword".encode()).hexdigest() + session.commit() + + assert not user.valid_password("badPassword") + assert user.valid_password("testPassword") + + # Test by passing a password of None value in. + assert not user.valid_password(None) + + +def test_user_login_with_outdated_sid(): + from aurweb.db import session + + # Make a session with a LastUpdateTS 5 seconds ago, causing + # user.login to update it with a new sid. + _session = make_session(UsersID=user.ID, SessionID="stub", + LastUpdateTS=datetime.utcnow().timestamp() - 5) + sid = user.login(Request(), "testPassword") + assert sid and user.is_authenticated() + assert sid != "stub" + + session.delete(_session) + session.commit() + + +def test_user_update_password(): + user.update_password("secondPassword") + assert not user.valid_password("testPassword") + assert user.valid_password("secondPassword") + + +def test_user_minimum_passwd_length(): + passwd_min_len = aurweb.config.getint("options", "passwd_min_len") + assert User.minimum_passwd_length() == passwd_min_len From 5d4a5deddf59806a691cda8d6933c7049b84db53 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 31 Dec 2020 20:44:59 -0800 Subject: [PATCH 0198/1451] implement login + logout routes and templates + Added route: GET `/login` via `aurweb.routers.auth.login_get` + Added route: POST `/login` via `aurweb.routers.auth.login_post` + Added route: GET `/logout` via `aurweb.routers.auth.logout` + Added route: POST `/logout` via `aurweb.routers.auth.logout_post` * Modify archdev-navbar.html template to toggle displays on auth state + Added login.html template Signed-off-by: Kevin Morris --- aurweb/asgi.py | 3 +- aurweb/routers/auth.py | 85 +++++++++++++++++ templates/login.html | 84 +++++++++++++++++ templates/partials/archdev-navbar.html | 18 +++- test/test_auth_routes.py | 126 +++++++++++++++++++++++++ 5 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 aurweb/routers/auth.py create mode 100644 templates/login.html create mode 100644 test/test_auth_routes.py diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 4d21ad03..b15e5874 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -11,7 +11,7 @@ import aurweb.config from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine -from aurweb.routers import html, sso, errors +from aurweb.routers import auth, html, sso, errors routes = set() @@ -42,6 +42,7 @@ async def app_startup(): # Add application routes. app.include_router(sso.router) app.include_router(html.router) + app.include_router(auth.router) # Initialize the database engine and ORM. get_engine() diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py new file mode 100644 index 00000000..24f5d4e3 --- /dev/null +++ b/aurweb/routers/auth.py @@ -0,0 +1,85 @@ +from datetime import datetime +from http import HTTPStatus + +from fastapi import APIRouter, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse + +import aurweb.config + +from aurweb.models.user import User +from aurweb.templates import make_context, render_template + +router = APIRouter() + + +def login_template(request: Request, next: str, errors: list = None): + """ Provide login-specific template context to render_template. """ + context = make_context(request, "Login", next) + context["errors"] = errors + context["url_base"] = f"{request.url.scheme}://{request.url.netloc}" + return render_template("login.html", context) + + +@router.get("/login", response_class=HTMLResponse) +async def login_get(request: Request, next: str = "/"): + """ Homepage route. """ + return login_template(request, next) + + +@router.post("/login", response_class=HTMLResponse) +async def login_post(request: Request, + next: str = Form(...), + user: str = Form(default=str()), + passwd: str = Form(default=str()), + remember_me: bool = Form(default=False)): + from aurweb.db import session + + user = session.query(User).filter(User.Username == user).first() + if not user: + return login_template(request, next, + errors=["Bad username or password."]) + + cookie_timeout = 0 + + if remember_me: + cookie_timeout = aurweb.config.getint( + "options", "persistent_cookie_timeout") + + _, sid = user.login(request, passwd, cookie_timeout) + if not _: + return login_template(request, next, + errors=["Bad username or password."]) + + login_timeout = aurweb.config.getint("options", "login_timeout") + + expires_at = int(datetime.utcnow().timestamp() + + max(cookie_timeout, login_timeout)) + + response = RedirectResponse(url=next, + status_code=int(HTTPStatus.SEE_OTHER)) + response.set_cookie("AURSID", sid, expires=expires_at) + return response + + +@router.get("/logout") +async def logout(request: Request, next: str = "/"): + """ A GET and POST route for logging out. + + @param request FastAPI request + @param next Route to redirect to + """ + if request.user.is_authenticated(): + request.user.logout(request) + + # Use 303 since we may be handling a post request, that'll get it + # to redirect to a get request. + response = RedirectResponse(url=next, + status_code=int(HTTPStatus.SEE_OTHER)) + response.delete_cookie("AURSID") + response.delete_cookie("AURTZ") + return response + + +@router.post("/logout") +async def logout_post(request: Request, next: str = "/"): + return await logout(request=request, next=next) diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 00000000..da7bd722 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,84 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} + +
    +

    AUR {% trans %}Login{% endtrans %}

    + + {% if request.url.scheme == "http" and config.getboolean("options", "disable_http_login") %} + {% set https_login = url_base.replace("http://", "https://") + "/login/" %} +

    + {{ "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." + | tr + | format( + '' | format(https_login), + "") + | safe + }} +

    + {% else %} + {% if request.user.is_authenticated() %} +

    + {{ "Logged-in as: %s" + | tr + | format("%s" | format(request.user.Username)) + | safe + }} + [{% trans %}Logout{% endtrans %}] +

    + {% else %} +
    +
    + {% trans %}Enter login credentials{% endtrans %} + + {% if errors %} +
      + {% for error in errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% endif %} + +

    + + + +

    + +

    + + +

    + +

    + + +

    + +

    + + + [{% trans %}Forgot Password{% endtrans %}] + + + +

    + +
    + + {% endif %} + {% endif %} + +
    + +{% endblock %} diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index 55338bc4..7662e3a4 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -1,8 +1,22 @@ diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py new file mode 100644 index 00000000..adf75329 --- /dev/null +++ b/test/test_auth_routes.py @@ -0,0 +1,126 @@ +from datetime import datetime +from http import HTTPStatus + +import pytest + +from fastapi.testclient import TestClient + +import aurweb.config + +from aurweb.asgi import app +from aurweb.db import query +from aurweb.models.account_type import AccountType +from aurweb.models.session import Session +from aurweb.testing import setup_test_db +from aurweb.testing.models import make_user + +client = TestClient(app) + +user = None + + +@pytest.fixture(autouse=True) +def setup(): + global user + + setup_test_db("Users", "Sessions", "Bans") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = make_user(Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + +def test_login_logout(): + post_data = { + "user": "test", + "passwd": "testPassword", + "next": "/" + } + + with client as request: + response = client.get("/login") + assert response.status_code == int(HTTPStatus.OK) + + response = request.post("/login", data=post_data, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + response = request.get(response.headers.get("location"), cookies={ + "AURSID": response.cookies.get("AURSID") + }) + assert response.status_code == int(HTTPStatus.OK) + + response = request.post("/logout", data=post_data, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + response = request.post("/logout", data=post_data, cookies={ + "AURSID": response.cookies.get("AURSID") + }, allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert "AURSID" not in response.cookies + + +def test_login_missing_username(): + post_data = { + "passwd": "testPassword", + "next": "/" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + +def test_login_remember_me(): + from aurweb.db import session + + post_data = { + "user": "test", + "passwd": "testPassword", + "next": "/", + "remember_me": True + } + + with client as request: + response = request.post("/login", data=post_data, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert "AURSID" in response.cookies + + cookie_timeout = aurweb.config.getint( + "options", "persistent_cookie_timeout") + expected_ts = datetime.utcnow().timestamp() + cookie_timeout + + _session = session.query(Session).filter( + Session.UsersID == user.ID).first() + + # Expect that LastUpdateTS was within 5 seconds of the expected_ts, + # which is equal to the current timestamp + persistent_cookie_timeout. + assert _session.LastUpdateTS > expected_ts - 5 + assert _session.LastUpdateTS < expected_ts + 5 + + +def test_login_missing_password(): + post_data = { + "user": "test", + "next": "/" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + +def test_login_incorrect_password(): + post_data = { + "user": "test", + "passwd": "badPassword", + "next": "/" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies From 4423326cec91dbfc9cd90294fc09ca40e917bc63 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 8 Jan 2021 20:24:22 -0800 Subject: [PATCH 0199/1451] add the request parameter to render_template This allows us to inspect things about the request we're rendering from. * Use render_template(request, ...) in aurweb.routers.auth Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 2 +- aurweb/routers/errors.py | 4 ++-- aurweb/routers/html.py | 2 +- aurweb/templates.py | 10 ++++++++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 24f5d4e3..3a1c7192 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -17,7 +17,7 @@ def login_template(request: Request, next: str, errors: list = None): context = make_context(request, "Login", next) context["errors"] = errors context["url_base"] = f"{request.url.scheme}://{request.url.netloc}" - return render_template("login.html", context) + return render_template(request, "login.html", context) @router.get("/login", response_class=HTMLResponse) diff --git a/aurweb/routers/errors.py b/aurweb/routers/errors.py index 111d802a..eb935b57 100644 --- a/aurweb/routers/errors.py +++ b/aurweb/routers/errors.py @@ -3,12 +3,12 @@ from aurweb.templates import make_context, render_template async def not_found(request, exc): context = make_context(request, "Page Not Found") - return render_template("errors/404.html", context, 404) + return render_template(request, "errors/404.html", context, 404) async def service_unavailable(request, exc): context = make_context(request, "Service Unavailable") - return render_template("errors/503.html", context, 503) + return render_template(request, "errors/503.html", context, 503) # Maps HTTP errors to functions exceptions = { diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 50b62450..32a7e630 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -47,7 +47,7 @@ async def language(request: Request, async def index(request: Request): """ Homepage route. """ context = make_context(request, "Home") - return render_template("index.html", context) + return render_template(request, "index.html", context) # A route that returns a error 503. For testing purposes. diff --git a/aurweb/templates.py b/aurweb/templates.py index c05dce79..c5f378b8 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -39,7 +39,10 @@ def make_context(request: Request, title: str, next: str = None): } -def render_template(path: str, context: dict, status_code=int(HTTPStatus.OK)): +def render_template(request: Request, + path: str, + context: dict, + status_code=int(HTTPStatus.OK)): """ Render a Jinja2 multi-lingual template with some context. """ # Create a deep copy of our jinja2 environment. The environment in @@ -54,4 +57,7 @@ def render_template(path: str, context: dict, status_code=int(HTTPStatus.OK)): template = templates.get_template(path) rendered = template.render(context) - return HTMLResponse(rendered, status_code=status_code) + + response = HTMLResponse(rendered, status_code=status_code) + response.set_cookie("AURLANG", context.get("language")) + return response From a33d076d8bae9ab2e988aaa5bc3ab5d8eabd44d3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 6 Jan 2021 21:00:12 -0800 Subject: [PATCH 0200/1451] add passreset routes Introduced `get|post` `/passreset` routes. These routes mimic the behavior of the existing PHP implementation, with the exception of HTTP status code returns. Routes added: GET /passreset POST /passreset Routers added: aurweb.routers.accounts * On an unknown user or mismatched resetkey (where resetkey must == user.resetkey), return HTTP status NOT_FOUND (404). * On another error in the request, return HTTP status BAD_REQUEST (400). Both `get|post` routes requires that the current user is **not** authenticated, hence `@auth_required(False, redirect="/")`. + Added auth_required decorator to aurweb.auth. + Added some more utility to aurweb.models.user.User. + Added `partials/error.html` template. + Added `passreset.html` template. + Added aurweb.db.ConnectionExecutor functor for paramstyle logic. Decoupling the executor logic from the database connection logic is needed for us to easily use the same logic with a fastapi database session, when we need to use aurweb.scripts modules. At this point, notification configuration is now required to complete tests involved with notifications properly, like passreset. `conf/config.dev` has been modified to include [notifications] sendmail, sender and reply-to overrides. Dockerfile and .gitlab-ci.yml have been updated to setup /etc/hosts and start postfix before running tests. * setup.cfg: ignore E741, C901 in aurweb.routers.accounts These two warnings (shown in the commit) are not dangerous and a bi-product of maintaining compatibility with our current code flow. Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 + aurweb/asgi.py | 4 +- aurweb/db.py | 70 +++++++---- aurweb/routers/accounts.py | 102 ++++++++++++++++ aurweb/routers/auth.py | 10 +- conf/config.dev | 6 + setup.cfg | 13 ++ templates/partials/error.html | 15 +++ templates/passreset.html | 76 ++++++++++++ test/README.md | 5 + test/test_accounts_routes.py | 218 ++++++++++++++++++++++++++++++++++ test/test_auth_routes.py | 51 ++++++-- test/test_db.py | 13 +- test/test_user.py | 6 +- util/sendmail | 2 + 15 files changed, 552 insertions(+), 41 deletions(-) create mode 100644 aurweb/routers/accounts.py create mode 100644 templates/partials/error.html create mode 100644 templates/passreset.html create mode 100644 test/test_accounts_routes.py create mode 100755 util/sendmail diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4ad97393..db7dec9b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,8 @@ before_script: python-itsdangerous python-httpx python-jinja python-pytest-cov python-requests python-aiofiles python-python-multipart python-pytest-asyncio python-coverage python-bcrypt + - bash -c "echo '127.0.0.1' > /etc/hosts" + - bash -c "echo '::1' >> /etc/hosts" test: script: diff --git a/aurweb/asgi.py b/aurweb/asgi.py index b15e5874..1a61b1f4 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -1,5 +1,4 @@ import http -import os from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse @@ -11,7 +10,7 @@ import aurweb.config from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine -from aurweb.routers import auth, html, sso, errors +from aurweb.routers import accounts, auth, errors, html, sso routes = set() @@ -43,6 +42,7 @@ async def app_startup(): app.include_router(sso.router) app.include_router(html.router) app.include_router(auth.router) + app.include_router(accounts.router) # Initialize the database engine and ORM. get_engine() diff --git a/aurweb/db.py b/aurweb/db.py index 3f5731a9..7dab6c4a 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -145,35 +145,21 @@ def connect(): return get_engine().connect() -class Connection: +class ConnectionExecutor: _conn = None _paramstyle = None - def __init__(self): - aur_db_backend = aurweb.config.get('database', 'backend') - - if aur_db_backend == 'mysql': + def __init__(self, conn, backend=aurweb.config.get("database", "backend")): + self._conn = conn + if backend == "mysql": import mysql.connector - aur_db_host = aurweb.config.get('database', 'host') - aur_db_name = aurweb.config.get('database', 'name') - aur_db_user = aurweb.config.get('database', 'user') - aur_db_pass = aurweb.config.get('database', 'password') - aur_db_socket = aurweb.config.get('database', 'socket') - self._conn = mysql.connector.connect(host=aur_db_host, - user=aur_db_user, - passwd=aur_db_pass, - db=aur_db_name, - unix_socket=aur_db_socket, - buffered=True) self._paramstyle = mysql.connector.paramstyle - elif aur_db_backend == 'sqlite': + elif backend == "sqlite": import sqlite3 - aur_db_name = aurweb.config.get('database', 'name') - self._conn = sqlite3.connect(aur_db_name) - self._conn.create_function("POWER", 2, math.pow) self._paramstyle = sqlite3.paramstyle - else: - raise ValueError('unsupported database backend') + + def paramstyle(self): + return self._paramstyle def execute(self, query, params=()): if self._paramstyle in ('format', 'pyformat'): @@ -193,3 +179,43 @@ class Connection: def close(self): self._conn.close() + + +class Connection: + _executor = None + _conn = None + + def __init__(self): + aur_db_backend = aurweb.config.get('database', 'backend') + + if aur_db_backend == 'mysql': + import mysql.connector + aur_db_host = aurweb.config.get('database', 'host') + aur_db_name = aurweb.config.get('database', 'name') + aur_db_user = aurweb.config.get('database', 'user') + aur_db_pass = aurweb.config.get('database', 'password') + aur_db_socket = aurweb.config.get('database', 'socket') + self._conn = mysql.connector.connect(host=aur_db_host, + user=aur_db_user, + passwd=aur_db_pass, + db=aur_db_name, + unix_socket=aur_db_socket, + buffered=True) + elif aur_db_backend == 'sqlite': + import sqlite3 + aur_db_name = aurweb.config.get('database', 'name') + self._conn = sqlite3.connect(aur_db_name) + self._conn.create_function("POWER", 2, math.pow) + else: + raise ValueError('unsupported database backend') + + self._conn = ConnectionExecutor(self._conn) + + def execute(self, query, params=()): + return self._conn.execute(query, params) + + def commit(self): + self._conn.commit() + + def close(self): + self._conn.close() diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py new file mode 100644 index 00000000..0839f64e --- /dev/null +++ b/aurweb/routers/accounts.py @@ -0,0 +1,102 @@ +from http import HTTPStatus + +from fastapi import APIRouter, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from sqlalchemy import or_ + +from aurweb import db +from aurweb.auth import auth_required +from aurweb.l10n import get_translator_for_request +from aurweb.models.user import User +from aurweb.scripts.notify import ResetKeyNotification +from aurweb.templates import make_context, render_template + +router = APIRouter() + + +@router.get("/passreset", response_class=HTMLResponse) +@auth_required(False) +async def passreset(request: Request): + context = make_context(request, "Password Reset") + + for k, v in request.query_params.items(): + context[k] = v + + return render_template(request, "passreset.html", context) + + +@router.post("/passreset", response_class=HTMLResponse) +@auth_required(False) +async def passreset_post(request: Request, + user: str = Form(...), + resetkey: str = Form(default=None), + password: str = Form(default=None), + confirm: str = Form(default=None)): + from aurweb.db import session + + context = make_context(request, "Password Reset") + + for k, v in dict(await request.form()).items(): + context[k] = v + + # The user parameter being required, we can match against + user = db.query(User, or_(User.Username == user, + User.Email == user)).first() + if not user: + context["errors"] = ["Invalid e-mail."] + return render_template(request, "passreset.html", context, + status_code=int(HTTPStatus.NOT_FOUND)) + + if resetkey: + context["resetkey"] = resetkey + + if not user.ResetKey or resetkey != user.ResetKey: + context["errors"] = ["Invalid e-mail."] + return render_template(request, "passreset.html", context, + status_code=int(HTTPStatus.NOT_FOUND)) + + if not user or not password: + context["errors"] = ["Missing a required field."] + return render_template(request, "passreset.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + + if password != confirm: + # If the provided password does not match the provided confirm. + context["errors"] = ["Password fields do not match."] + return render_template(request, "passreset.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + + if len(password) < User.minimum_passwd_length(): + # Translate the error here, which simplifies error output + # in the jinja2 template. + _ = get_translator_for_request(request) + context["errors"] = [_( + "Your password must be at least %s characters.") % ( + str(User.minimum_passwd_length()))] + return render_template(request, "passreset.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + + # We got to this point; everything matched up. Update the password + # and remove the ResetKey. + user.ResetKey = str() + user.update_password(password) + + if user.session: + session.delete(user.session) + session.commit() + + # Render ?step=complete. + return RedirectResponse(url="/passreset?step=complete", + status_code=int(HTTPStatus.SEE_OTHER)) + + # If we got here, we continue with issuing a resetkey for the user. + resetkey = db.make_random_value(User, User.ResetKey) + user.ResetKey = resetkey + session.commit() + + executor = db.ConnectionExecutor(db.get_engine().raw_connection()) + ResetKeyNotification(executor, user.ID).send() + + # Render ?step=confirm. + return RedirectResponse(url="/passreset?step=confirm", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 3a1c7192..e4864424 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse import aurweb.config +from aurweb.auth import auth_required from aurweb.models.user import User from aurweb.templates import make_context, render_template @@ -21,12 +22,13 @@ def login_template(request: Request, next: str, errors: list = None): @router.get("/login", response_class=HTMLResponse) +@auth_required(False) async def login_get(request: Request, next: str = "/"): - """ Homepage route. """ return login_template(request, next) @router.post("/login", response_class=HTMLResponse) +@auth_required(False) async def login_post(request: Request, next: str = Form(...), user: str = Form(default=str()), @@ -45,8 +47,8 @@ async def login_post(request: Request, cookie_timeout = aurweb.config.getint( "options", "persistent_cookie_timeout") - _, sid = user.login(request, passwd, cookie_timeout) - if not _: + sid = user.login(request, passwd, cookie_timeout) + if not sid: return login_template(request, next, errors=["Bad username or password."]) @@ -62,6 +64,7 @@ async def login_post(request: Request, @router.get("/logout") +@auth_required() async def logout(request: Request, next: str = "/"): """ A GET and POST route for logging out. @@ -81,5 +84,6 @@ async def logout(request: Request, next: str = "/"): @router.post("/logout") +@auth_required() async def logout_post(request: Request, next: str = "/"): return await logout(request=request, next=next) diff --git a/conf/config.dev b/conf/config.dev index 194a3bf8..94775a92 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -25,6 +25,12 @@ disable_http_login = 0 enable-maintenance = 0 localedir = YOUR_AUR_ROOT/web/locale +[notifications] +; For development/testing, use /usr/bin/sendmail +sendmail = YOUR_AUR_ROOT/util/sendmail +sender = notify@localhost +reply-to = noreply@localhost + ; Single sign-on; see doc/sso.txt. [sso] openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/openid-configuration diff --git a/setup.cfg b/setup.cfg index b868c096..98261651 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,19 @@ max-line-length = 127 max-complexity = 10 +# Ignore some unavoidable flake8 warnings; we know this is against +# pycodestyle, but some of the existing codebase uses `I` variables, +# so specifically silence warnings about it in pre-defined files. +# In E741, the 'I', 'O', 'l' are ambiguous variable names. +# Our current implementation uses these variables through HTTP +# and the FastAPI form specification wants them named as such. +# In C901's case, our process_account_form function is way too +# complex for PEP (too many if statements). However, we need to +# process these anyways, and making it any more complex would +# just add confusion to the implementation. +per-file-ignores = + aurweb/routers/accounts.py:E741,C901 + [isort] line_length = 127 lines_between_types = 1 diff --git a/templates/partials/error.html b/templates/partials/error.html new file mode 100644 index 00000000..6043dfd1 --- /dev/null +++ b/templates/partials/error.html @@ -0,0 +1,15 @@ +{% if errors %} +
      + {% for error in errors %} + {% if error is string %} +
    • {{ error | tr | safe }}
    • + {% elif error is iterable %} +
        + {% for e in error %} +
      • {{ e | tr | safe }}
      • + {% endfor %} +
      + {% endif %} + {% endfor %} +
    +{% endif %} diff --git a/templates/passreset.html b/templates/passreset.html new file mode 100644 index 00000000..d2c3c2ee --- /dev/null +++ b/templates/passreset.html @@ -0,0 +1,76 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {% trans %}Password Reset{% endtrans %}

    + +

    + {% if step == "confirm" %} + {% trans %}Check your e-mail for the confirmation link.{% endtrans %} + {% elif step == "complete" %} + {% trans %}Your password has been reset successfully.{% endtrans %} + {% elif resetkey %} + + {% include "partials/error.html" %} + +
    +

    + + + + + + + + + + + + + + +
    {% trans %}Confirm your user name or primary e-mail address:{% endtrans %} + +
    {% trans %}Enter your new password:{% endtrans %} + +
    {% trans %}Confirm your new password:{% endtrans %} + +
    +
    + + + + {% else %} + + {% set url = "https://mailman.archlinux.org/mailman/listinfo/aur-general" %} + {{ "If you have forgotten the user name and the primary e-mail " + "address you used to register, please send a message to the " + "%saur-general%s mailing list." + | tr + | format( + '' | format(url), + "") + | safe + }} +

    + + {% include "partials/error.html" %} + +
    +

    + {% trans %}Enter your user name or your primary e-mail address:{% endtrans %} + +

    + +
    + {% endif %} +

    + +{% endblock %} diff --git a/test/README.md b/test/README.md index 3261899b..872d980b 100644 --- a/test/README.md +++ b/test/README.md @@ -27,6 +27,7 @@ For all the test to run, the following Arch packages should be installed: - python-pytest - python-pytest-cov - python-pytest-asyncio +- postfix Running tests ------------- @@ -37,6 +38,10 @@ First, setup the test configuration: $ sed -r 's;YOUR_AUR_ROOT;$(pwd);g' conf/config.dev > conf/config +You'll need to make sure that emails can be sent out by aurweb.scripts.notify. +If you don't have anything setup, just install postfix and start it before +running tests. + With those installed, one can run Python tests manually with any AUR config specified by `AUR_CONFIG`: diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py new file mode 100644 index 00000000..0f548805 --- /dev/null +++ b/test/test_accounts_routes.py @@ -0,0 +1,218 @@ +from http import HTTPStatus + +import pytest + +from fastapi.testclient import TestClient + +from aurweb.asgi import app +from aurweb.db import query +from aurweb.models.account_type import AccountType +from aurweb.models.session import Session +from aurweb.models.user import User +from aurweb.testing import setup_test_db +from aurweb.testing.models import make_user +from aurweb.testing.requests import Request + +# Some test global constants. +TEST_USERNAME = "test" +TEST_EMAIL = "test@example.org" + +# Global mutables. +client = TestClient(app) +user = None + + +@pytest.fixture(autouse=True) +def setup(): + global user + + setup_test_db("Users", "Sessions", "Bans") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = make_user(Username=TEST_USERNAME, Email=TEST_EMAIL, + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + +def test_get_passreset_authed_redirects(): + sid = user.login(Request(), "testPassword") + assert sid is not None + + with client as request: + response = request.get("/passreset", cookies={"AURSID": sid}, + allow_redirects=False) + + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + +def test_get_passreset(): + with client as request: + response = request.get("/passreset") + assert response.status_code == int(HTTPStatus.OK) + + +def test_get_passreset_translation(): + # Test that translation works. + with client as request: + response = request.get("/passreset", cookies={"AURLANG": "de"}) + + # The header title should be translated. + assert "Passwort zurücksetzen".encode("utf-8") in response.content + + # The form input label should be translated. + assert "Benutzername oder primäre E-Mail-Adresse eingeben:".encode( + "utf-8") in response.content + + # And the button. + assert "Weiter".encode("utf-8") in response.content + + +def test_get_passreset_with_resetkey(): + with client as request: + response = request.get("/passreset", data={"resetkey": "abcd"}) + assert response.status_code == int(HTTPStatus.OK) + + +def test_post_passreset_authed_redirects(): + sid = user.login(Request(), "testPassword") + assert sid is not None + + with client as request: + response = request.post("/passreset", + cookies={"AURSID": sid}, + data={"user": "blah"}, + allow_redirects=False) + + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + +def test_post_passreset_user(): + # With username. + with client as request: + response = request.post("/passreset", data={"user": TEST_USERNAME}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + + # With e-mail. + with client as request: + response = request.post("/passreset", data={"user": TEST_EMAIL}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + + +def test_post_passreset_resetkey(): + from aurweb.db import session + + user.session = Session(UsersID=user.ID, SessionID="blah", + LastUpdateTS=datetime.utcnow().timestamp()) + session.commit() + + # Prepare a password reset. + with client as request: + response = request.post("/passreset", data={"user": TEST_USERNAME}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + + # Now that we've prepared the password reset, prepare a POST + # request with the user's ResetKey. + resetkey = user.ResetKey + post_data = { + "user": TEST_USERNAME, + "resetkey": resetkey, + "password": "abcd1234", + "confirm": "abcd1234" + } + + with client as request: + response = request.post("/passreset", data=post_data) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=complete" + + +def test_post_passreset_error_invalid_email(): + # First, test with a user that doesn't even exist. + with client as request: + response = request.post("/passreset", data={"user": "invalid"}) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + error = "Invalid e-mail." + assert error in response.content.decode("utf-8") + + # Then, test with an invalid resetkey for a real user. + _ = make_resetkey() + post_data = make_passreset_data("fake") + post_data["password"] = "abcd1234" + post_data["confirm"] = "abcd1234" + + with client as request: + response = request.post("/passreset", data=post_data) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + assert error in response.content.decode("utf-8") + + +def make_resetkey(): + with client as request: + response = request.post("/passreset", data={"user": TEST_USERNAME}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + return user.ResetKey + + +def make_passreset_data(resetkey): + return { + "user": user.Username, + "resetkey": resetkey + } + + +def test_post_passreset_error_missing_field(): + # Now that we've prepared the password reset, prepare a POST + # request with the user's ResetKey. + resetkey = make_resetkey() + post_data = make_passreset_data(resetkey) + + with client as request: + response = request.post("/passreset", data=post_data) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + error = "Missing a required field." + assert error in response.content.decode("utf-8") + + +def test_post_passreset_error_password_mismatch(): + resetkey = make_resetkey() + post_data = make_passreset_data(resetkey) + + post_data["password"] = "abcd1234" + post_data["confirm"] = "mismatched" + + with client as request: + response = request.post("/passreset", data=post_data) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + error = "Password fields do not match." + assert error in response.content.decode("utf-8") + + +def test_post_passreset_error_password_requirements(): + resetkey = make_resetkey() + post_data = make_passreset_data(resetkey) + + passwd_min_len = User.minimum_passwd_length() + assert passwd_min_len >= 4 + + post_data["password"] = "x" + post_data["confirm"] = "x" + + with client as request: + response = request.post("/passreset", data=post_data) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + error = f"Your password must be at least {passwd_min_len} characters." + assert error in response.content.decode("utf-8") diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index adf75329..ff8a08e9 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -14,8 +14,12 @@ from aurweb.models.session import Session from aurweb.testing import setup_test_db from aurweb.testing.models import make_user -client = TestClient(app) +# Some test global constants. +TEST_USERNAME = "test" +TEST_EMAIL = "test@example.org" +# Global mutables. +client = TestClient(app) user = None @@ -27,7 +31,8 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.org", + + user = make_user(Username=TEST_USERNAME, Email=TEST_EMAIL, RealName="Test User", Passwd="testPassword", AccountType=account_type) @@ -40,16 +45,16 @@ def test_login_logout(): } with client as request: - response = client.get("/login") + # First, let's test get /login. + response = request.get("/login") assert response.status_code == int(HTTPStatus.OK) response = request.post("/login", data=post_data, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) - response = request.get(response.headers.get("location"), cookies={ - "AURSID": response.cookies.get("AURSID") - }) + # Simulate following the redirect location from above's response. + response = request.get(response.headers.get("location")) assert response.status_code == int(HTTPStatus.OK) response = request.post("/logout", data=post_data, @@ -60,9 +65,37 @@ def test_login_logout(): "AURSID": response.cookies.get("AURSID") }, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert "AURSID" not in response.cookies +def test_authenticated_login_forbidden(): + post_data = { + "user": "test", + "passwd": "testPassword", + "next": "/" + } + + with client as request: + # Login. + response = request.post("/login", data=post_data, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + # Now, let's verify that we receive 403 Forbidden when we + # try to get /login as an authenticated user. + response = request.get("/login", allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_unauthenticated_logout_unauthorized(): + with client as request: + # Alright, let's verify that attempting to /logout when not + # authenticated returns 401 Unauthorized. + response = request.get("/logout", allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + def test_login_missing_username(): post_data = { "passwd": "testPassword", @@ -75,8 +108,6 @@ def test_login_missing_username(): def test_login_remember_me(): - from aurweb.db import session - post_data = { "user": "test", "passwd": "testPassword", @@ -94,8 +125,8 @@ def test_login_remember_me(): "options", "persistent_cookie_timeout") expected_ts = datetime.utcnow().timestamp() + cookie_timeout - _session = session.query(Session).filter( - Session.UsersID == user.ID).first() + _session = query(Session, + Session.UsersID == user.ID).first() # Expect that LastUpdateTS was within 5 seconds of the expected_ts, # which is equal to the current timestamp + persistent_cookie_timeout. diff --git a/test/test_db.py b/test/test_db.py index 1eb0dc28..e0946ed5 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -3,7 +3,6 @@ import re import sqlite3 import tempfile -from datetime import datetime from unittest import mock import mysql.connector @@ -185,3 +184,15 @@ def test_create_delete(): db.delete(AccountType, AccountType.AccountType == "test") record = db.query(AccountType, AccountType.AccountType == "test").first() assert record is None + + +@mock.patch("mysql.connector.paramstyle", "qmark") +def test_connection_executor_mysql_paramstyle(): + executor = db.ConnectionExecutor(None, backend="mysql") + assert executor.paramstyle() == "qmark" + + +@mock.patch("sqlite3.paramstyle", "pyformat") +def test_connection_executor_sqlite_paramstyle(): + executor = db.ConnectionExecutor(None, backend="sqlite") + assert executor.paramstyle() == "pyformat" diff --git a/test/test_user.py b/test/test_user.py index b8d4248a..4f144819 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -28,8 +28,8 @@ def setup(): setup_test_db("Users", "Sessions", "Bans") - account_type = session.query(AccountType).filter( - AccountType.AccountType == "User").first() + account_type = query(AccountType, + AccountType.AccountType == "User").first() user = make_user(Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", @@ -67,7 +67,7 @@ def test_user_login_logout(): assert user.session.User == user # Search for the user via query API. - result = session.query(User).filter(User.ID == user.ID).first() + result = query(User, User.ID == user.ID).first() # Compare the result and our original user. assert result == user diff --git a/util/sendmail b/util/sendmail new file mode 100755 index 00000000..06bd9865 --- /dev/null +++ b/util/sendmail @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 From 9fdbe3f775a3d13e92a02a11e5eb4830e6daf875 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 8 Jan 2021 20:10:45 -0800 Subject: [PATCH 0201/1451] add authenticated User LangPreference tracking + Use User.LangPreference when there is no set AURSID if request.user.is_authenticated is true. + Updated post /language to update LangPreference when request.user.is_authenticated. + Restore language during test where we change it. + Added the user attribute to aurweb.testing.requests.Request. Signed-off-by: Kevin Morris --- aurweb/l10n.py | 12 ++++-------- aurweb/routers/html.py | 11 ++++++++++- aurweb/testing/requests.py | 19 +++++++++++++++++++ test/test_accounts_routes.py | 6 +++++- test/test_routes.py | 25 +++++++++++++++++++++---- 5 files changed, 59 insertions(+), 14 deletions(-) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index 030ab274..4a5c1a46 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -64,8 +64,10 @@ translator = Translator() def get_request_language(request: Request): - return request.cookies.get("AURLANG", - aurweb.config.get("options", "default_lang")) + if request.user.is_authenticated(): + return request.user.LangPreference + default_lang = aurweb.config.get("options", "default_lang") + return request.cookies.get("AURLANG", default_lang) def get_raw_translator_for_request(request: Request): @@ -77,12 +79,6 @@ def get_translator_for_request(request: Request): """ Determine the preferred language from a FastAPI request object and build a translator function for it. - - Example: - ```python - _ = get_translator_for_request(request) - print(_("Hello")) - ``` """ lang = get_request_language(request) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 32a7e630..e947d213 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -24,12 +24,14 @@ async def language(request: Request, set_lang: str = Form(...), next: str = Form(...), q: str = Form(default=None)): - """ A POST route used to set a session's language. + """ + A POST route used to set a session's language. Return a 303 See Other redirect to {next}?next={next}. If we are setting the language on any page, we want to preserve query parameters across the redirect. """ + from aurweb.db import session from aurweb.asgi import routes if unquote(next) not in routes: return HTMLResponse( @@ -37,6 +39,13 @@ async def language(request: Request, status_code=400) query_string = "?" + q if q else str() + + # If the user is authenticated, update the user's LangPreference. + if request.user.is_authenticated(): + request.user.LangPreference = set_lang + session.commit() + + # In any case, set the response's AURLANG cookie that never expires. response = RedirectResponse(url=f"{next}{query_string}", status_code=int(HTTPStatus.SEE_OTHER)) response.set_cookie("AURLANG", set_lang) diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py index 2839c93f..2e64fd3d 100644 --- a/aurweb/testing/requests.py +++ b/aurweb/testing/requests.py @@ -1,8 +1,27 @@ +import aurweb.config + + +class User: + """ A fake User model. """ + # Fake columns. + LangPreference = aurweb.config.get("options", "default_lang") + + # A fake authenticated flag. + authenticated = False + + def is_authenticated(self): + return self.authenticated + + class Client: + """ A fake FastAPI Request.client object. """ + # A fake host. host = "127.0.0.1" class Request: + """ A fake Request object which mimics a FastAPI Request for tests. """ client = Client() cookies = dict() headers = dict() + user = User() diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 0f548805..69896a0f 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -54,7 +54,7 @@ def test_get_passreset(): def test_get_passreset_translation(): - # Test that translation works. + # Test that translation works; set it to de. with client as request: response = request.get("/passreset", cookies={"AURLANG": "de"}) @@ -68,6 +68,10 @@ def test_get_passreset_translation(): # And the button. assert "Weiter".encode("utf-8") in response.content + # Restore english. + with client as request: + response = request.get("/passreset", cookies={"AURLANG": "en"}) + def test_get_passreset_with_resetkey(): with client as request: diff --git a/test/test_routes.py b/test/test_routes.py index 950d9b71..d512a172 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -10,13 +10,14 @@ from aurweb.asgi import app from aurweb.db import query from aurweb.models.account_type import AccountType from aurweb.testing import setup_test_db +from aurweb.testing.models import make_user +from aurweb.testing.requests import Request client = TestClient(app) - user = None -@pytest.fixture +@pytest.fixture(autouse=True) def setup(): global user @@ -46,7 +47,7 @@ def test_favicon(): def test_language(): - """ Test the language post route at '/language'. """ + """ Test the language post route as a guest user. """ post_data = { "set_lang": "de", "next": "/" @@ -67,6 +68,23 @@ def test_language_invalid_next(): assert response.status_code == int(HTTPStatus.BAD_REQUEST) +def test_user_language(): + """ Test the language post route as an authenticated user. """ + post_data = { + "set_lang": "de", + "next": "/" + } + + sid = user.login(Request(), "testPassword") + assert sid is not None + + with client as req: + response = req.post("/language", data=post_data, + cookies={"AURSID": sid}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert user.LangPreference == "de" + + def test_language_query_params(): """ Test the language post route with query params. """ next = urllib.parse.quote_plus("/") @@ -87,4 +105,3 @@ def test_error_messages(): response2 = client.get("/raisefivethree") assert response1.status_code == int(HTTPStatus.NOT_FOUND) assert response2.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) - From 670f711b593ed6c040ed3facb6212d327a3c38af Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 13 Jan 2021 06:46:30 -0800 Subject: [PATCH 0202/1451] add SSHPubKey ORM model Includes `aurweb.models.ssh_pub_key.get_fingerprint(pubkey)` helper. Signed-off-by: Kevin Morris --- aurweb/models/ssh_pub_key.py | 41 +++++++++++++++++++++++++ test/test_ssh_pub_key.py | 58 ++++++++++++++++++++++++++++++++++++ test/test_user.py | 20 ++++++++++++- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 aurweb/models/ssh_pub_key.py create mode 100644 test/test_ssh_pub_key.py diff --git a/aurweb/models/ssh_pub_key.py b/aurweb/models/ssh_pub_key.py new file mode 100644 index 00000000..01ff558e --- /dev/null +++ b/aurweb/models/ssh_pub_key.py @@ -0,0 +1,41 @@ +import os +import tempfile + +from subprocess import PIPE, Popen + +from sqlalchemy.orm import backref, mapper, relationship + +from aurweb.models.user import User +from aurweb.schema import SSHPubKeys + + +class SSHPubKey: + def __init__(self, **kwargs): + self.UserID = kwargs.get("UserID") + self.Fingerprint = kwargs.get("Fingerprint") + self.PubKey = kwargs.get("PubKey") + + +def get_fingerprint(pubkey): + with tempfile.TemporaryDirectory() as tmpdir: + pk = os.path.join(tmpdir, "ssh.pub") + + with open(pk, "w") as f: + f.write(pubkey) + + proc = Popen(["ssh-keygen", "-l", "-f", pk], stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + + # Invalid SSH Public Key. Return None to the caller. + if proc.returncode != 0: + return None + + parts = out.decode().split() + fp = parts[1].replace("SHA256:", "") + + return fp + + +mapper(SSHPubKey, SSHPubKeys, properties={ + "User": relationship(User, backref=backref("ssh_pub_key", uselist=False)) +}) diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py new file mode 100644 index 00000000..fe9df047 --- /dev/null +++ b/test/test_ssh_pub_key.py @@ -0,0 +1,58 @@ +import pytest + +from aurweb.db import query +from aurweb.models.account_type import AccountType +from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint +from aurweb.testing import setup_test_db +from aurweb.testing.models import make_user + +TEST_SSH_PUBKEY = """ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCycoCi5yGCvSclH2wmNBUuwsYEzRZZBJaQquRc4ysl+Tg+/jiDkR3Zn9fIznC4KnFoyrIHzkKuePZ3bNDYwkZxkJKoWBCh4hXKDXSm87FMN0+VDC+1QxF/z0XaAGr/P6f4XukabyddypBdnHcZiplbw+YOSqcAE2TCqOlSXwNMOcF9U89UsR/Q9i9I52hlvU0q8+fZVGhou1KCowFSnHYtrr5KYJ04CXkJ13DkVf3+pjQWyrByvBcf1hGEaczlgfobrrv/y96jDhgfXucxliNKLdufDPPkii3LhhsNcDmmI1VZ3v0irKvd9WZuauqloobY84zEFcDTyjn0hxGjVeYFejm4fBnvjga0yZXORuWksdNfXWLDxFk6MDDd1jF0ExRbP+OxDuU4IVyIuDL7S3cnbf2YjGhkms/8voYT2OBE7FwNlfv98Kr0NUp51zpf55Arxn9j0Rz9xTA7FiODQgCn6iQ0SDtzUNL0IKTCw26xJY5gzMxbfpvzPQGeulx/ioM= kevr@volcano +""" + +user, ssh_pub_key = None, None + + +@pytest.fixture(autouse=True) +def setup(): + from aurweb.db import session + + global user, ssh_pub_key + + setup_test_db("Users", "SSHPubKeys") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = make_user(Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + assert account_type == user.AccountType + assert account_type.ID == user.AccountTypeID + + ssh_pub_key = SSHPubKey(UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey") + + session.add(ssh_pub_key) + session.commit() + + yield ssh_pub_key + + session.delete(ssh_pub_key) + session.commit() + + +def test_ssh_pub_key(): + assert ssh_pub_key.UserID == user.ID + assert ssh_pub_key.User == user + assert ssh_pub_key.Fingerprint == "testFingerprint" + assert ssh_pub_key.PubKey == "testPubKey" + + +def test_ssh_pub_key_fingerprint(): + assert get_fingerprint(TEST_SSH_PUBKEY) is not None + + +def test_ssh_pub_key_invalid_fingerprint(): + assert get_fingerprint("ssh-rsa fake and invalid") is None diff --git a/test/test_user.py b/test/test_user.py index 4f144819..473b035a 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -12,6 +12,7 @@ from aurweb.db import query from aurweb.models.account_type import AccountType from aurweb.models.ban import Ban from aurweb.models.session import Session +from aurweb.models.ssh_pub_key import SSHPubKey from aurweb.models.user import User from aurweb.testing import setup_test_db from aurweb.testing.models import make_session, make_user @@ -26,7 +27,7 @@ def setup(): global account_type, user - setup_test_db("Users", "Sessions", "Bans") + setup_test_db("Users", "Sessions", "Bans", "SSHPubKeys") account_type = query(AccountType, AccountType.AccountType == "User").first() @@ -160,3 +161,20 @@ def test_user_update_password(): def test_user_minimum_passwd_length(): passwd_min_len = aurweb.config.getint("options", "passwd_min_len") assert User.minimum_passwd_length() == passwd_min_len + + +def test_user_ssh_pub_key(): + from aurweb.db import session + + assert user.ssh_pub_key is None + + ssh_pub_key = SSHPubKey(UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey") + session.add(ssh_pub_key) + session.commit() + + assert user.ssh_pub_key == ssh_pub_key + + session.delete(ssh_pub_key) + session.commit() From 07d5907ecda5e93ebe44bd591a7f0ce87fb73cc2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 25 Jan 2021 16:30:47 -0800 Subject: [PATCH 0203/1451] aurweb.auth: add user credentials and matcher functions This clones the behavior already present in the PHP implementation, but it uses a global dict with credential constant keys to validation functions to determine if a given user has a credential. Signed-off-by: Kevin Morris --- aurweb/auth.py | 101 ++++++++++++++++++++++++++++++++++++++++++ aurweb/models/user.py | 5 +++ test/test_auth.py | 7 ++- test/test_user.py | 42 ++++++++++++++++++ 4 files changed, 154 insertions(+), 1 deletion(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 8608a82a..53c853de 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -17,6 +17,10 @@ class AnonymousUser: def is_authenticated(): return False + @staticmethod + def has_credential(credential): + return False + class BasicAuthBackend(AuthenticationBackend): async def authenticate(self, conn: HTTPConnection): @@ -75,3 +79,100 @@ def auth_required(is_required: bool = True, return wrapper return decorator + + +CRED_ACCOUNT_CHANGE_TYPE = 1 +CRED_ACCOUNT_EDIT = 2 +CRED_ACCOUNT_EDIT_DEV = 3 +CRED_ACCOUNT_LAST_LOGIN = 4 +CRED_ACCOUNT_SEARCH = 5 +CRED_ACCOUNT_LIST_COMMENTS = 28 +CRED_COMMENT_DELETE = 6 +CRED_COMMENT_UNDELETE = 27 +CRED_COMMENT_VIEW_DELETED = 22 +CRED_COMMENT_EDIT = 25 +CRED_COMMENT_PIN = 26 +CRED_PKGBASE_ADOPT = 7 +CRED_PKGBASE_SET_KEYWORDS = 8 +CRED_PKGBASE_DELETE = 9 +CRED_PKGBASE_DISOWN = 10 +CRED_PKGBASE_EDIT_COMAINTAINERS = 24 +CRED_PKGBASE_FLAG = 11 +CRED_PKGBASE_LIST_VOTERS = 12 +CRED_PKGBASE_NOTIFY = 13 +CRED_PKGBASE_UNFLAG = 15 +CRED_PKGBASE_VOTE = 16 +CRED_PKGREQ_FILE = 23 +CRED_PKGREQ_CLOSE = 17 +CRED_PKGREQ_LIST = 18 +CRED_TU_ADD_VOTE = 19 +CRED_TU_LIST_VOTES = 20 +CRED_TU_VOTE = 21 + + +def has_any(user, *account_types): + return str(user.AccountType) in set(account_types) + + +def user_developer_or_trusted_user(user): + return has_any(user, "User", "Trusted User", "Developer", + "Trusted User & Developer") + + +def trusted_user(user): + return has_any(user, "Trusted User", "Trusted User & Developer") + + +def developer(user): + return has_any(user, "Developer", "Trusted User & Developer") + + +def trusted_user_or_dev(user): + return has_any(user, "Trusted User", "Developer", + "Trusted User & Developer") + + +# A mapping of functions that users must pass to have credentials. +cred_filters = { + CRED_PKGBASE_FLAG: user_developer_or_trusted_user, + CRED_PKGBASE_NOTIFY: user_developer_or_trusted_user, + CRED_PKGBASE_VOTE: user_developer_or_trusted_user, + CRED_PKGREQ_FILE: user_developer_or_trusted_user, + CRED_ACCOUNT_CHANGE_TYPE: trusted_user_or_dev, + CRED_ACCOUNT_EDIT: trusted_user_or_dev, + CRED_ACCOUNT_LAST_LOGIN: trusted_user_or_dev, + CRED_ACCOUNT_LIST_COMMENTS: trusted_user_or_dev, + CRED_ACCOUNT_SEARCH: trusted_user_or_dev, + CRED_COMMENT_DELETE: trusted_user_or_dev, + CRED_COMMENT_UNDELETE: trusted_user_or_dev, + CRED_COMMENT_VIEW_DELETED: trusted_user_or_dev, + CRED_COMMENT_EDIT: trusted_user_or_dev, + CRED_COMMENT_PIN: trusted_user_or_dev, + CRED_PKGBASE_ADOPT: trusted_user_or_dev, + CRED_PKGBASE_SET_KEYWORDS: trusted_user_or_dev, + CRED_PKGBASE_DELETE: trusted_user_or_dev, + CRED_PKGBASE_EDIT_COMAINTAINERS: trusted_user_or_dev, + CRED_PKGBASE_DISOWN: trusted_user_or_dev, + CRED_PKGBASE_LIST_VOTERS: trusted_user_or_dev, + CRED_PKGBASE_UNFLAG: trusted_user_or_dev, + CRED_PKGREQ_CLOSE: trusted_user_or_dev, + CRED_PKGREQ_LIST: trusted_user_or_dev, + CRED_TU_ADD_VOTE: trusted_user, + CRED_TU_LIST_VOTES: trusted_user, + CRED_TU_VOTE: trusted_user, + CRED_ACCOUNT_EDIT_DEV: developer, +} + + +def has_credential(user: User, + credential: int, + approved_users: list = tuple()): + + if user in approved_users: + return True + + if credential in cred_filters: + cred_filter = cred_filters.get(credential) + return cred_filter(user) + + return False diff --git a/aurweb/models/user.py b/aurweb/models/user.py index aff4ce6b..3983e098 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -141,6 +141,11 @@ class User: request.cookies["AURSID"] = self.session.SessionID return self.session.SessionID + def has_credential(self, credential: str, approved: list = tuple()): + import aurweb.auth + cred = getattr(aurweb.auth, credential) + return aurweb.auth.has_credential(self, cred, approved) + def logout(self, request): from aurweb.db import session diff --git a/test/test_auth.py b/test/test_auth.py index d2251de4..d43459cd 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -4,8 +4,8 @@ import pytest from starlette.authentication import AuthenticationError +from aurweb.auth import BasicAuthBackend, has_credential from aurweb.db import query -from aurweb.auth import BasicAuthBackend from aurweb.models.account_type import AccountType from aurweb.testing import setup_test_db from aurweb.testing.models import make_session, make_user @@ -78,3 +78,8 @@ async def test_basic_auth_backend(): LastUpdateTS=now_ts + 5) _, result = await backend.authenticate(request) assert result == user + + +def test_has_fake_credential_fails(): + # Fake credential 666 does not exist. + assert not has_credential(user, 666) diff --git a/test/test_user.py b/test/test_user.py index 473b035a..e8056681 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -163,6 +163,11 @@ def test_user_minimum_passwd_length(): assert User.minimum_passwd_length() == passwd_min_len +def test_user_has_credential(): + assert user.has_credential("CRED_PKGBASE_FLAG") + assert not user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") + + def test_user_ssh_pub_key(): from aurweb.db import session @@ -178,3 +183,40 @@ def test_user_ssh_pub_key(): session.delete(ssh_pub_key) session.commit() + + +def test_user_credential_types(): + from aurweb.db import session + + assert aurweb.auth.user_developer_or_trusted_user(user) + assert not aurweb.auth.trusted_user(user) + assert not aurweb.auth.developer(user) + assert not aurweb.auth.trusted_user_or_dev(user) + + trusted_user_type = query(AccountType, + AccountType.AccountType == "Trusted User")\ + .first() + user.AccountType = trusted_user_type + session.commit() + + assert aurweb.auth.trusted_user(user) + assert aurweb.auth.trusted_user_or_dev(user) + + developer_type = query(AccountType, + AccountType.AccountType == "Developer")\ + .first() + user.AccountType = developer_type + session.commit() + + assert aurweb.auth.developer(user) + assert aurweb.auth.trusted_user_or_dev(user) + + type_str = "Trusted User & Developer" + elevated_type = query(AccountType, + AccountType.AccountType == type_str).first() + user.AccountType = elevated_type + session.commit() + + assert aurweb.auth.trusted_user(user) + assert aurweb.auth.developer(user) + assert aurweb.auth.trusted_user_or_dev(user) From 9052688ed247bccda516ffa84183b7ed442f0a04 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 25 Jan 2021 16:52:14 -0800 Subject: [PATCH 0204/1451] add aurweb.time module This module includes timezone-based utilities for a FastAPI request. This commit introduces use of the AURTZ cookie within get_request_timezone. This cookie should be set to the user or session's timezone. * `make_context` has been modified to parse the request's timezone and include the "timezone" and "timezones" variables, along with a timezone specified "now" date. + Added `Timezone` attribute to aurweb.testing.requests.Request.user. Signed-off-by: Kevin Morris --- aurweb/templates.py | 11 ++++--- aurweb/testing/requests.py | 1 + aurweb/time.py | 63 ++++++++++++++++++++++++++++++++++++++ test/test_time.py | 33 ++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 aurweb/time.py create mode 100644 test/test_time.py diff --git a/aurweb/templates.py b/aurweb/templates.py index c5f378b8..564f3149 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -1,5 +1,6 @@ import copy import os +import zoneinfo from datetime import datetime from http import HTTPStatus @@ -11,7 +12,7 @@ from fastapi.responses import HTMLResponse import aurweb.config -from aurweb import l10n +from aurweb import l10n, time # Prepare jinja2 objects. loader = jinja2.FileSystemLoader(os.path.join( @@ -26,14 +27,15 @@ env.filters["tr"] = l10n.tr def make_context(request: Request, title: str, next: str = None): """ Create a context for a jinja2 TemplateResponse. """ + timezone = time.get_request_timezone(request) return { "request": request, "language": l10n.get_request_language(request), "languages": l10n.SUPPORTED_LANGUAGES, + "timezone": timezone, + "timezones": time.SUPPORTED_TIMEZONES, "title": title, - # The 'now' context variable will not show proper datetimes - # until we've implemented timezone support here. - "now": datetime.now(), + "now": datetime.now(tz=zoneinfo.ZoneInfo(timezone)), "config": aurweb.config, "next": next if next else request.url.path } @@ -60,4 +62,5 @@ def render_template(request: Request, response = HTMLResponse(rendered, status_code=status_code) response.set_cookie("AURLANG", context.get("language")) + response.set_cookie("AURTZ", context.get("timezone")) return response diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py index 2e64fd3d..9976b6fb 100644 --- a/aurweb/testing/requests.py +++ b/aurweb/testing/requests.py @@ -5,6 +5,7 @@ class User: """ A fake User model. """ # Fake columns. LangPreference = aurweb.config.get("options", "default_lang") + Timezone = aurweb.config.get("options", "default_timezone") # A fake authenticated flag. authenticated = False diff --git a/aurweb/time.py b/aurweb/time.py new file mode 100644 index 00000000..0b1dff11 --- /dev/null +++ b/aurweb/time.py @@ -0,0 +1,63 @@ +import zoneinfo + +from collections import OrderedDict +from datetime import datetime + +from fastapi import Request + +import aurweb.config + + +def tz_offset(name: str): + """ Get a timezone offset in the form "+00:00" by its name. + + Example: tz_offset('America/Los_Angeles') + + :param name: Timezone name + :return: UTC offset in the form "+00:00" + """ + dt = datetime.now(tz=zoneinfo.ZoneInfo(name)) + + # Our offset in hours. + offset = dt.utcoffset().total_seconds() / 60 / 60 + + # Prefix the offset string with a - or +. + offset_string = '-' if offset < 0 else '+' + + # Remove any negativity from the offset. We want a good offset. :) + offset = abs(offset) + + # Truncate the floating point digits, giving the hours. + hours = int(offset) + + # Subtract hours from the offset, and multiply the remaining fraction + # (0 - 0.99[repeated]) with 60 minutes to get the number of minutes + # remaining in the hour. + minutes = int((offset - hours) * 60) + + # Pad the hours and minutes by two places. + offset_string += "{:0>2}:{:0>2}".format(hours, minutes) + return offset_string + + +SUPPORTED_TIMEZONES = OrderedDict({ + # Flatten out the list of tuples into an OrderedDict. + timezone: offset for timezone, offset in sorted([ + # Comprehend a list of tuples (timezone, offset display string) + # and sort them by (offset, timezone). + (tz, "(UTC%s) %s" % (tz_offset(tz), tz)) + for tz in zoneinfo.available_timezones() + ], key=lambda element: (tz_offset(element[0]), element[0])) +}) + + +def get_request_timezone(request: Request): + """ Get a request's timezone by its AURTZ cookie. We use the + configuration's [options] default_timezone otherwise. + + @param request FastAPI request + """ + if request.user.is_authenticated(): + return request.user.Timezone + default_tz = aurweb.config.get("options", "default_timezone") + return request.cookies.get("AURTZ", default_tz) diff --git a/test/test_time.py b/test/test_time.py new file mode 100644 index 00000000..2134d217 --- /dev/null +++ b/test/test_time.py @@ -0,0 +1,33 @@ +import aurweb.config + +from aurweb.testing.requests import Request +from aurweb.time import get_request_timezone, tz_offset + + +def test_tz_offset_utc(): + offset = tz_offset("UTC") + assert offset == "+00:00" + + +def test_tz_offset_mst(): + offset = tz_offset("MST") + assert offset == "-07:00" + + +def test_request_timezone(): + request = Request() + tz = get_request_timezone(request) + assert tz == aurweb.config.get("options", "default_timezone") + + +def test_authenticated_request_timezone(): + # Modify a fake request to be authenticated with the + # America/Los_Angeles timezone. + request = Request() + request.user.authenticated = True + request.user.Timezone = "America/Los_Angeles" + + # Get the request's timezone, it should be America/Los_Angeles. + tz = get_request_timezone(request) + assert tz == request.user.Timezone + assert tz == "America/Los_Angeles" From a5be6fc9beaa8a195b9c8e382d25b5bb51225412 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 27 Jan 2021 18:30:57 -0800 Subject: [PATCH 0205/1451] aurweb.templates: add make_variable_context A new make_context wrapper which additionally includes either query parameters (get) or form data (post) in the context. Use this to simplify setting context variables for form data in particular. Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 13 +++---------- aurweb/templates.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 0839f64e..db23bc3a 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -9,7 +9,7 @@ from aurweb.auth import auth_required from aurweb.l10n import get_translator_for_request from aurweb.models.user import User from aurweb.scripts.notify import ResetKeyNotification -from aurweb.templates import make_context, render_template +from aurweb.templates import make_variable_context, render_template router = APIRouter() @@ -17,11 +17,7 @@ router = APIRouter() @router.get("/passreset", response_class=HTMLResponse) @auth_required(False) async def passreset(request: Request): - context = make_context(request, "Password Reset") - - for k, v in request.query_params.items(): - context[k] = v - + context = await make_variable_context(request, "Password Reset") return render_template(request, "passreset.html", context) @@ -34,10 +30,7 @@ async def passreset_post(request: Request, confirm: str = Form(default=None)): from aurweb.db import session - context = make_context(request, "Password Reset") - - for k, v in dict(await request.form()).items(): - context[k] = v + context = await make_variable_context(request, "Password Reset") # The user parameter being required, we can match against user = db.query(User, or_(User.Username == user, diff --git a/aurweb/templates.py b/aurweb/templates.py index 564f3149..4ea74a62 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -41,6 +41,20 @@ def make_context(request: Request, title: str, next: str = None): } +async def make_variable_context(request: Request, title: str, next: str = None): + """ Make a context with variables provided by the user + (query params via GET or form data via POST). """ + context = make_context(request, title, next) + to_copy = dict(request.query_params) \ + if request.method.lower() == "get" \ + else dict(await request.form()) + + for k, v in to_copy.items(): + context[k] = v + + return context + + def render_template(request: Request, path: str, context: dict, From 7a6a38592e63db1ca8a5a1748458afe659d5be3f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 27 Jan 2021 18:36:06 -0800 Subject: [PATCH 0206/1451] add python-email-validator dependency Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 1 + Dockerfile | 3 ++- INSTALL | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db7dec9b..f1fe5e6f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,7 @@ before_script: python-itsdangerous python-httpx python-jinja python-pytest-cov python-requests python-aiofiles python-python-multipart python-pytest-asyncio python-coverage python-bcrypt + python-email-validator - bash -c "echo '127.0.0.1' > /etc/hosts" - bash -c "echo '::1' >> /etc/hosts" diff --git a/Dockerfile b/Dockerfile index 6638f9a2..f65acc7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,8 @@ RUN pacman -Syu --noconfirm base-devel git gpgme protobuf pyalpm \ python-werkzeug python-pytest-tap python-fastapi nginx python-authlib \ python-itsdangerous python-httpx python-jinja python-pytest-cov \ python-requests python-aiofiles python-python-multipart \ - python-pytest-asyncio python-coverage hypercorn python-bcrypt + python-pytest-asyncio python-coverage hypercorn python-bcrypt \ + python-email-validator # Remove aurweb.sqlite3 if it was copied over via COPY. RUN rm -fv aurweb.sqlite3 diff --git a/INSTALL b/INSTALL index 6c43fec8..04ccd69e 100644 --- a/INSTALL +++ b/INSTALL @@ -51,7 +51,7 @@ read the instructions below. python-bleach python-markdown python-alembic hypercorn \ python-itsdangerous python-authlib python-httpx \ python-jinja python-aiofiles python-python-multipart \ - python-requests hypercorn python-bcrypt + python-requests hypercorn python-bcrypt python-email-validator # python3 setup.py install 5) Create a new MySQL database and a user and import the aurweb SQL schema: From df0a637d2b5da4a2fc6dae0c7f07bcd7f50e4828 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 28 Jan 2021 16:52:56 -0800 Subject: [PATCH 0207/1451] add aurweb.captcha, a CAPTCHA utility module This CAPTCHA workflow is the same workflow used by our current PHP implementation of account registration. Signed-off-by: Kevin Morris --- aurweb/captcha.py | 54 +++++++++++++++++++++++++++++++++++++++ aurweb/templates.py | 6 ++++- test/test_captcha.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 aurweb/captcha.py create mode 100644 test/test_captcha.py diff --git a/aurweb/captcha.py b/aurweb/captcha.py new file mode 100644 index 00000000..5475d85f --- /dev/null +++ b/aurweb/captcha.py @@ -0,0 +1,54 @@ +""" This module consists of aurweb's CAPTCHA utility functions and filters. """ +import hashlib + +import jinja2 + +from aurweb.db import query +from aurweb.models.user import User + + +def get_captcha_salts(): + """ Produce salts based on the current user count. """ + count = query(User).count() + salts = [] + for i in range(0, 6): + salts.append(f"aurweb-{count - i}") + return salts + + +def get_captcha_token(salt): + """ Produce a token for the CAPTCHA salt. """ + return hashlib.md5(salt.encode()).hexdigest()[:3] + + +def get_captcha_challenge(salt): + """ Get a CAPTCHA challenge string (shell command) for a salt. """ + token = get_captcha_token(salt) + return f"LC_ALL=C pacman -V|sed -r 's#[0-9]+#{token}#g'|md5sum|cut -c1-6" + + +def get_captcha_answer(token): + """ Compute the answer via md5 of the real template text, return the + first six digits of the hexadecimal hash. """ + text = r""" + .--. Pacman v%s.%s.%s - libalpm v%s.%s.%s +/ _.-' .-. .-. .-. Copyright (C) %s-%s Pacman Development Team +\ '-. '-' '-' '-' Copyright (C) %s-%s Judd Vinet + '--' + This program may be freely redistributed under + the terms of the GNU General Public License. +""" % tuple([token] * 10) + return hashlib.md5((text + "\n").encode()).hexdigest()[:6] + + +@jinja2.contextfilter +def captcha_salt_filter(context): + """ Returns the most recent CAPTCHA salt in the list of salts. """ + salts = get_captcha_salts() + return salts[0] + + +@jinja2.contextfilter +def captcha_cmdline_filter(context, salt): + """ Returns a CAPTCHA challenge for a given salt. """ + return get_captcha_challenge(salt) diff --git a/aurweb/templates.py b/aurweb/templates.py index 4ea74a62..d548e92b 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -12,7 +12,7 @@ from fastapi.responses import HTMLResponse import aurweb.config -from aurweb import l10n, time +from aurweb import captcha, l10n, time # Prepare jinja2 objects. loader = jinja2.FileSystemLoader(os.path.join( @@ -23,6 +23,10 @@ env = jinja2.Environment(loader=loader, autoescape=True, # Add tr translation filter. env.filters["tr"] = l10n.tr +# Add captcha filters. +env.filters["captcha_salt"] = captcha.captcha_salt_filter +env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter + def make_context(request: Request, title: str, next: str = None): """ Create a context for a jinja2 TemplateResponse. """ diff --git a/test/test_captcha.py b/test/test_captcha.py new file mode 100644 index 00000000..ec19dee9 --- /dev/null +++ b/test/test_captcha.py @@ -0,0 +1,60 @@ +import hashlib + +from aurweb import captcha + + +def test_captcha_salts(): + """ Make sure we can get some captcha salts. """ + salts = captcha.get_captcha_salts() + assert len(salts) == 6 + + +def test_captcha_token(): + """ Make sure getting a captcha salt's token matches up against + the first three digits of the md5 hash of the salt. """ + salts = captcha.get_captcha_salts() + salt = salts[0] + + token1 = captcha.get_captcha_token(salt) + token2 = hashlib.md5(salt.encode()).hexdigest()[:3] + + assert token1 == token2 + + +def test_captcha_challenge_answer(): + """ Make sure that executing the captcha challenge via shell + produces the correct result by comparing it against a straight + up token conversion. """ + salts = captcha.get_captcha_salts() + salt = salts[0] + + challenge = captcha.get_captcha_challenge(salt) + + token = captcha.get_captcha_token(salt) + challenge2 = f"LC_ALL=C pacman -V|sed -r 's#[0-9]+#{token}#g'|md5sum|cut -c1-6" + + assert challenge == challenge2 + + +def test_captcha_salt_filter(): + """ Make sure captcha_salt_filter returns the first salt from + get_captcha_salts(). + + Example usage: + + """ + salt = captcha.captcha_salt_filter(None) + assert salt == captcha.get_captcha_salts()[0] + + +def test_captcha_cmdline_filter(): + """ Make sure that the captcha_cmdline filter gives us the + same challenge that get_captcha_challenge does. + + Example usage: + {{ captcha_salt | captcha_cmdline }} + """ + salt = captcha.captcha_salt_filter(None) + display1 = captcha.captcha_cmdline_filter(None, salt) + display2 = captcha.get_captcha_challenge(salt) + assert display1 == display2 From 19b4a896f111c34fcf57a5d6fb0b40cd9ad43e51 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Jan 2021 02:08:59 -0800 Subject: [PATCH 0208/1451] add openssh to test dependencies Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 +- Dockerfile | 2 +- test/README.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f1fe5e6f..58fb9fed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ before_script: python-itsdangerous python-httpx python-jinja python-pytest-cov python-requests python-aiofiles python-python-multipart python-pytest-asyncio python-coverage python-bcrypt - python-email-validator + python-email-validator openssh - bash -c "echo '127.0.0.1' > /etc/hosts" - bash -c "echo '::1' >> /etc/hosts" diff --git a/Dockerfile b/Dockerfile index f65acc7c..cf54a13c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN pacman -Syu --noconfirm base-devel git gpgme protobuf pyalpm \ python-itsdangerous python-httpx python-jinja python-pytest-cov \ python-requests python-aiofiles python-python-multipart \ python-pytest-asyncio python-coverage hypercorn python-bcrypt \ - python-email-validator + python-email-validator openssh # Remove aurweb.sqlite3 if it was copied over via COPY. RUN rm -fv aurweb.sqlite3 diff --git a/test/README.md b/test/README.md index 872d980b..0f3f4cbd 100644 --- a/test/README.md +++ b/test/README.md @@ -28,6 +28,7 @@ For all the test to run, the following Arch packages should be installed: - python-pytest-cov - python-pytest-asyncio - postfix +- openssh Running tests ------------- From c94793b0b11b372a299fb6d23e39562066b7531b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 28 Jan 2021 20:26:34 -0800 Subject: [PATCH 0209/1451] add user registration routes * Added /register get and post routes. + Added default attributes to AnonymousUser, including a new AnonymousList which behaves like an sqlalchemy relationship list. + aurweb.util: Added validation functions for various user fields used throughout registration. + test_accounts_routes: Added get|post register route tests. Signed-off-by: Kevin Morris --- aurweb/auth.py | 10 + aurweb/routers/accounts.py | 320 +++++++++++++++++++++++- aurweb/util.py | 84 +++++++ templates/partials/account_form.html | 343 ++++++++++++++++++++++++++ templates/register.html | 30 +++ test/test_accounts_routes.py | 356 ++++++++++++++++++++++++++- 6 files changed, 1140 insertions(+), 3 deletions(-) create mode 100644 templates/partials/account_form.html create mode 100644 templates/register.html diff --git a/aurweb/auth.py b/aurweb/auth.py index 53c853de..a4ff2167 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -7,12 +7,22 @@ from fastapi.responses import RedirectResponse from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError from starlette.requests import HTTPConnection +import aurweb.config + from aurweb.models.session import Session from aurweb.models.user import User from aurweb.templates import make_context, render_template class AnonymousUser: + # Stub attributes used to mimic a real user. + ID = 0 + LangPreference = aurweb.config.get("options", "default_lang") + Timezone = aurweb.config.get("options", "default_timezone") + + # A stub ssh_pub_key relationship. + ssh_pub_key = None + @staticmethod def is_authenticated(): return False diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index db23bc3a..a43ba9f7 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -1,12 +1,20 @@ +import copy + from http import HTTPStatus from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse -from sqlalchemy import or_ +from sqlalchemy import and_, func, or_ -from aurweb import db +import aurweb.config + +from aurweb import db, l10n, time, util from aurweb.auth import auth_required +from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token from aurweb.l10n import get_translator_for_request +from aurweb.models.account_type import AccountType +from aurweb.models.ban import Ban +from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.user import User from aurweb.scripts.notify import ResetKeyNotification from aurweb.templates import make_variable_context, render_template @@ -93,3 +101,311 @@ async def passreset_post(request: Request, # Render ?step=confirm. return RedirectResponse(url="/passreset?step=confirm", status_code=int(HTTPStatus.SEE_OTHER)) + + +def process_account_form(request: Request, user: User, args: dict): + """ Process an account form. All fields are optional and only checks + requirements in the case they are present. + + ``` + context = await make_variable_context(request, "Accounts") + ok, errors = process_account_form(request, user, **kwargs) + if not ok: + context["errors"] = errors + return render_template(request, "some_account_template.html", context) + ``` + + :param request: An incoming FastAPI request + :param user: The user model of the account being processed + :param args: A dictionary of arguments generated via request.form() + :return: A (passed processing boolean, list of errors) tuple + """ + + # Get a local translator. + _ = get_translator_for_request(request) + + host = request.client.host + ban = db.query(Ban, Ban.IPAddress == host).first() + if ban: + return False, [ + "Account registration has been disabled for your " + + "IP address, probably due to sustained spam attacks. " + + "Sorry for the inconvenience." + ] + + if request.user.is_authenticated(): + if not request.user.valid_password(args.get("passwd", None)): + return False, ["Invalid password."] + + email = args.get("E", None) + username = args.get("U", None) + + if not email or not username: + return False, ["Missing a required field."] + + username_min_len = aurweb.config.getint("options", "username_min_len") + username_max_len = aurweb.config.getint("options", "username_max_len") + if not util.valid_username(args.get("U")): + return False, [ + "The username is invalid.", + [ + _("It must be between %s and %s characters long") % ( + username_min_len, username_max_len), + "Start and end with a letter or number", + "Can contain only one period, underscore or hyphen.", + ] + ] + + password = args.get("P", None) + if password: + confirmation = args.get("C", None) + if not util.valid_password(password): + return False, [ + _("Your password must be at least %s characters.") % ( + username_min_len) + ] + elif not confirmation: + return False, ["Please confirm your new password."] + elif password != confirmation: + return False, ["Password fields do not match."] + + backup_email = args.get("BE", None) + homepage = args.get("HP", None) + pgp_key = args.get("K", None) + ssh_pubkey = args.get("PK", None) + language = args.get("L", None) + timezone = args.get("TZ", None) + + def username_exists(username): + return and_(User.ID != user.ID, + func.lower(User.Username) == username.lower()) + + def email_exists(email): + return and_(User.ID != user.ID, + func.lower(User.Email) == email.lower()) + + if not util.valid_email(email): + return False, ["The email address is invalid."] + elif backup_email and not util.valid_email(backup_email): + return False, ["The backup email address is invalid."] + elif homepage and not util.valid_homepage(homepage): + return False, [ + "The home page is invalid, please specify the full HTTP(s) URL."] + elif pgp_key and not util.valid_pgp_fingerprint(pgp_key): + return False, ["The PGP key fingerprint is invalid."] + elif ssh_pubkey and not util.valid_ssh_pubkey(ssh_pubkey): + return False, ["The SSH public key is invalid."] + elif language and language not in l10n.SUPPORTED_LANGUAGES: + return False, ["Language is not currently supported."] + elif timezone and timezone not in time.SUPPORTED_TIMEZONES: + return False, ["Timezone is not currently supported."] + elif db.query(User, username_exists(username)).first(): + # If the username already exists... + return False, [ + _("The username, %s%s%s, is already in use.") % ( + "", username, "") + ] + elif db.query(User, email_exists(email)).first(): + # If the email already exists... + return False, [ + _("The address, %s%s%s, is already in use.") % ( + "", email, "") + ] + + def ssh_fingerprint_exists(fingerprint): + return and_(SSHPubKey.UserID != user.ID, + SSHPubKey.Fingerprint == fingerprint) + + if ssh_pubkey: + fingerprint = get_fingerprint(ssh_pubkey.strip().rstrip()) + if fingerprint is None: + return False, ["The SSH public key is invalid."] + + if db.query(SSHPubKey, ssh_fingerprint_exists(fingerprint)).first(): + return False, [ + _("The SSH public key, %s%s%s, is already in use.") % ( + "", fingerprint, "") + ] + + captcha_salt = args.get("captcha_salt", None) + if captcha_salt and captcha_salt not in get_captcha_salts(): + return False, ["This CAPTCHA has expired. Please try again."] + + captcha = args.get("captcha", None) + if captcha: + answer = get_captcha_answer(get_captcha_token(captcha_salt)) + if captcha != answer: + return False, ["The entered CAPTCHA answer is invalid."] + + return True, [] + + +def make_account_form_context(context: dict, + request: Request, + user: User, + args: dict): + """ Modify a FastAPI context and add attributes for the account form. + + :param context: FastAPI context + :param request: FastAPI request + :param user: Target user + :param args: Persistent arguments: request.form() + :return: FastAPI context adjusted for account form + """ + # Do not modify the original context. + context = copy.copy(context) + + context["account_types"] = [ + (1, "Normal User"), + (2, "Trusted User") + ] + + user_account_type_id = context.get("account_types")[0][0] + + if request.user.has_credential("CRED_ACCOUNT_EDIT_DEV"): + context["account_types"].append((3, "Developer")) + context["account_types"].append((4, "Trusted User & Developer")) + + if request.user.is_authenticated(): + context["username"] = args.get("U", user.Username) + context["account_type"] = args.get("T", user.AccountType.ID) + context["suspended"] = args.get("S", user.Suspended) + context["email"] = args.get("E", user.Email) + context["hide_email"] = args.get("H", user.HideEmail) + context["backup_email"] = args.get("BE", user.BackupEmail) + context["realname"] = args.get("R", user.RealName) + context["homepage"] = args.get("HP", user.Homepage or str()) + context["ircnick"] = args.get("I", user.IRCNick) + context["pgp"] = args.get("K", user.PGPKey or str()) + context["lang"] = args.get("L", user.LangPreference) + context["tz"] = args.get("TZ", user.Timezone) + ssh_pk = user.ssh_pub_key.PubKey if user.ssh_pub_key else str() + context["ssh_pk"] = args.get("PK", ssh_pk) + context["cn"] = args.get("CN", user.CommentNotify) + context["un"] = args.get("UN", user.UpdateNotify) + context["on"] = args.get("ON", user.OwnershipNotify) + else: + context["username"] = args.get("U", str()) + context["account_type"] = args.get("T", user_account_type_id) + context["suspended"] = args.get("S", False) + context["email"] = args.get("E", str()) + context["hide_email"] = args.get("H", False) + context["backup_email"] = args.get("BE", str()) + context["realname"] = args.get("R", str()) + context["homepage"] = args.get("HP", str()) + context["ircnick"] = args.get("I", str()) + context["pgp"] = args.get("K", str()) + context["lang"] = args.get("L", context.get("language")) + context["tz"] = args.get("TZ", context.get("timezone")) + context["ssh_pk"] = args.get("PK", str()) + context["cn"] = args.get("CN", True) + context["un"] = args.get("UN", False) + context["on"] = args.get("ON", True) + + context["password"] = args.get("P", str()) + context["confirm"] = args.get("C", str()) + + return context + + +@router.get("/register", response_class=HTMLResponse) +@auth_required(False) +async def account_register(request: Request, + U: str = Form(default=str()), # Username + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=None), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key FP + L: str = Form(default=aurweb.config.get( + "options", "default_lang")), + TZ: str = Form(default=aurweb.config.get( + "options", "default_timezone")), + PK: str = Form(default=None), + CN: bool = Form(default=False), # Comment Notify + CU: bool = Form(default=False), # Update Notify + CO: bool = Form(default=False), # Owner Notify + captcha: str = Form(default=str())): + context = await make_variable_context(request, "Register") + context["captcha_salt"] = get_captcha_salts()[0] + context = make_account_form_context(context, request, None, dict()) + return render_template(request, "register.html", context) + + +@router.post("/register", response_class=HTMLResponse) +@auth_required(False) +async def account_register_post(request: Request, + U: str = Form(default=str()), # Username + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=''), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key + L: str = Form(default=aurweb.config.get( + "options", "default_lang")), + TZ: str = Form(default=aurweb.config.get( + "options", "default_timezone")), + PK: str = Form(default=None), # SSH PubKey + CN: bool = Form(default=False), + UN: bool = Form(default=False), + ON: bool = Form(default=False), + captcha: str = Form(default=None), + captcha_salt: str = Form(...)): + from aurweb.db import session + + context = await make_variable_context(request, "Register") + + args = dict(await request.form()) + context = make_account_form_context(context, request, None, args) + + ok, errors = process_account_form(request, request.user, args) + + if not ok: + # If the field values given do not meet the requirements, + # return HTTP 400 with an error. + context["errors"] = errors + return render_template(request, "register.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + + if not captcha: + context["errors"] = ["The CAPTCHA is missing."] + return render_template(request, "register.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + + # Create a user with no password with a resetkey, then send + # an email off about it. + resetkey = db.make_random_value(User, User.ResetKey) + + # By default, we grab the User account type to associate with. + account_type = db.query(AccountType, + AccountType.AccountType == "User").first() + + # Create a user given all parameters available. + user = db.create(User, Username=U, Email=E, HideEmail=H, BackupEmail=BE, + RealName=R, Homepage=HP, IRCNick=I, PGPKey=K, + LangPreference=L, Timezone=TZ, CommentNotify=CN, + UpdateNotify=UN, OwnershipNotify=ON, ResetKey=resetkey, + AccountType=account_type) + + # If a PK was given and either one does not exist or the given + # PK mismatches the existing user's SSHPubKey.PubKey. + if PK: + # Get the second element in the PK, which is the actual key. + pubkey = PK.strip().rstrip() + fingerprint = get_fingerprint(pubkey) + user.ssh_pub_key = SSHPubKey(UserID=user.ID, + PubKey=pubkey, + Fingerprint=fingerprint) + session.commit() + + # Send a reset key notification to the new user. + executor = db.ConnectionExecutor(db.get_engine().raw_connection()) + ResetKeyNotification(executor, user.ID).send() + + context["complete"] = True + context["user"] = user + return render_template(request, "register.html", context) diff --git a/aurweb/util.py b/aurweb/util.py index 65f18a4c..5e1717bd 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,7 +1,91 @@ +import base64 import random +import re import string +from urllib.parse import urlparse + +import jinja2 + +from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email + +import aurweb.config + def make_random_string(length): return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) + + +def valid_username(username): + min_len = aurweb.config.getint("options", "username_min_len") + max_len = aurweb.config.getint("options", "username_max_len") + if not (min_len <= len(username) <= max_len): + return False + + # Check that username contains: one or more alphanumeric + # characters, an optional separator of '.', '-' or '_', followed + # by alphanumeric characters. + return re.match(r'^[a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$', username) + + +def valid_email(email): + try: + validate_email(email) + except EmailUndeliverableError: + return False + except EmailNotValidError: + return False + return True + + +def valid_homepage(homepage): + parts = urlparse(homepage) + return parts.scheme in ("http", "https") and bool(parts.netloc) + + +def valid_password(password): + min_len = aurweb.config.getint("options", "passwd_min_len") + return len(password) >= min_len + + +def valid_pgp_fingerprint(fp): + fp = fp.replace(" ", "") + try: + # Attempt to convert the fingerprint to an int via base16. + # If it can't, it's not a hex string. + int(fp, 16) + except ValueError: + return False + + # Check the length; must be 40 hexadecimal digits. + return len(fp) == 40 + + +def valid_ssh_pubkey(pk): + valid_prefixes = ("ssh-rsa", "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", + "ssh-ed25519") + + has_valid_prefix = False + for prefix in valid_prefixes: + if "%s " % prefix in pk: + has_valid_prefix = True + break + if not has_valid_prefix: + return False + + tokens = pk.strip().rstrip().split(" ") + if len(tokens) < 2: + return False + + return base64.b64encode(base64.b64decode(tokens[1])).decode() == tokens[1] + + +@jinja2.contextfilter +def account_url(context, user): + request = context.get("request") + base = f"{request.url.scheme}://{request.url.hostname}" + if request.url.scheme == "http" and request.url.port != 80: + base += f":{request.url.port}" + return f"{base}/account/{user.Username}" diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html new file mode 100644 index 00000000..3af13368 --- /dev/null +++ b/templates/partials/account_form.html @@ -0,0 +1,343 @@ + +
    +
    + +
    +
    + +

    + + + + ({% trans %}required{% endtrans %}) +

    +

    + {{ "Your user name is the name you will use to login. " + "It is visible to the general public, even if your " + "account is inactive." | tr }} +

    + + {% if request.user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") %} +

    + + +

    + +

    + + + +

    + {% endif %} + + +

    + + + + ({% trans %}required{% endtrans %}) +

    +

    + {{ "Please ensure you correctly entered your email " + "address, otherwise you will be locked out." | tr }} +

    + + +

    + + + +

    +

    + {{ "If you do not hide your email address, it is " + "visible to all registered AUR users. If you hide your " + "email address, it is visible to members of the Arch " + "Linux staff only." | tr }} +

    + + +

    + + + +

    +

    + + {{ "Optionally provide a secondary email address that " + "can be used to restore your account in case you lose " + "access to your primary email address." | tr }} + {{ "Password reset links are always sent to both your " + "primary and your backup email address." | tr }} + {{ "Your backup email address is always only visible to " + "members of the Arch Linux staff, independent of the %s " + "setting." | tr + | format("%s" | format("Hide Email Address" | tr)) + | safe }} + +

    + + +

    + + + +

    + + +

    + + + +

    + + +

    + + + +

    + + +

    + + + +

    + + +

    + + + +

    + + +

    + + + +

    + +
    + + {% if form_type == "UpdateAccount" %} +
    + + {{ + "If you want to change the password, enter a new password " + "and confirm the new password by entering it again." | tr + }} + +

    + + +

    + +

    + + + +

    +
    + {% endif %} + +
    + + {{ + "The following information is only required if you " + "want to submit packages to the Arch User Repository." | tr + }} + +

    + + + + +

    +
    + +
    + {% trans%}Notification settings{% endtrans %}: +

    + + + +

    +

    + + + +

    +

    + + + +

    +
    + +
    + {% if form_type == "UpdateAccount" %} + + {{ "To confirm the profile changes, please enter " + "your current password:" | tr }} + +

    + + +

    + {% else %} + + + {{ "To protect the AUR against automated account creation, " + "we kindly ask you to provide the output of the following " + "command:" | tr }} + + {{ captcha_salt | captcha_cmdline }} + + +

    + + + ({% trans %}required{% endtrans %}) + + +

    + {% endif %} +
    + +
    +

    + + {% if form_type == "UpdateAccount" %} +   + {% else %} +   + {% endif %} + +

    +
    +
    diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 00000000..a15971a1 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,30 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {% trans %}Register{% endtrans %}

    + + {% if complete %} + {{ + "The account, %s%s%s, has been successfully created." + | tr + | format("", "'" + user.Username + "'", "") + | safe + }} +

    + {% trans %}A password reset key has been sent to your e-mail address.{% endtrans %} +

    + {% else %} + {% if errors %} + {% include "partials/error.html" %} + {% else %} +

    + {% trans %}Use this form to create an account.{% endtrans %} +

    + {% endif %} + + {% set form_type = "NewAccount" %} + {% include "partials/account_form.html" %} + {% endif %} +
    +{% endblock %} diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 69896a0f..d79137bf 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -1,13 +1,21 @@ +import re +import tempfile + +from datetime import datetime from http import HTTPStatus +from subprocess import Popen import pytest from fastapi.testclient import TestClient +from aurweb import captcha from aurweb.asgi import app -from aurweb.db import query +from aurweb.db import create, delete, query from aurweb.models.account_type import AccountType +from aurweb.models.ban import Ban from aurweb.models.session import Session +from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.user import User from aurweb.testing import setup_test_db from aurweb.testing.models import make_user @@ -220,3 +228,349 @@ def test_post_passreset_error_password_requirements(): error = f"Your password must be at least {passwd_min_len} characters." assert error in response.content.decode("utf-8") + + +def test_get_register(): + with client as request: + response = request.get("/register") + assert response.status_code == int(HTTPStatus.OK) + + +def post_register(request, **kwargs): + """ A simple helper that allows overrides to test defaults. """ + salt = captcha.get_captcha_salts()[0] + token = captcha.get_captcha_token(salt) + answer = captcha.get_captcha_answer(token) + + data = { + "U": "newUser", + "E": "newUser@email.org", + "P": "newUserPassword", + "C": "newUserPassword", + "L": "en", + "TZ": "UTC", + "captcha": answer, + "captcha_salt": salt + } + + # For any kwargs given, override their k:v pairs in data. + args = dict(kwargs) + for k, v in args.items(): + data[k] = v + + return request.post("/register", data=data, allow_redirects=False) + + +def test_post_register(): + with client as request: + response = post_register(request) + assert response.status_code == int(HTTPStatus.OK) + + expected = "The account, 'newUser', " + expected += "has been successfully created." + assert expected in response.content.decode() + + +def test_post_register_rejects_case_insensitive_spoof(): + with client as request: + response = post_register(request, U="newUser", E="newUser@example.org") + assert response.status_code == int(HTTPStatus.OK) + + with client as request: + response = post_register(request, U="NEWUSER", E="BLAH@GMAIL.COM") + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + expected = "The username, NEWUSER, is already in use." + assert expected in response.content.decode() + + with client as request: + response = post_register(request, U="BLAH", E="NEWUSER@EXAMPLE.ORG") + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + expected = "The address, NEWUSER@EXAMPLE.ORG, " + expected += "is already in use." + assert expected in response.content.decode() + + +def test_post_register_error_expired_captcha(): + with client as request: + response = post_register(request, captcha_salt="invalid-salt") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "This CAPTCHA has expired. Please try again." in content + + +def test_post_register_error_missing_captcha(): + with client as request: + response = post_register(request, captcha=None) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The CAPTCHA is missing." in content + + +def test_post_register_error_invalid_captcha(): + with client as request: + response = post_register(request, captcha="invalid blah blah") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The entered CAPTCHA answer is invalid." in content + + +def test_post_register_error_ip_banned(): + # 'testclient' is used as request.client.host via FastAPI TestClient. + create(Ban, IPAddress="testclient", BanTS=datetime.utcnow()) + + with client as request: + response = post_register(request) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert ("Account registration has been disabled for your IP address, " + + "probably due to sustained spam attacks. Sorry for the " + + "inconvenience.") in content + + +def test_post_register_error_missing_username(): + with client as request: + response = post_register(request, U="") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Missing a required field." in content + + +def test_post_register_error_missing_email(): + with client as request: + response = post_register(request, E="") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Missing a required field." in content + + +def test_post_register_error_invalid_username(): + with client as request: + # Our test config requires at least three characters for a + # valid username, so test against two characters: 'ba'. + response = post_register(request, U="ba") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The username is invalid." in content + + +def test_post_register_invalid_password(): + with client as request: + response = post_register(request, P="abc", C="abc") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"Your password must be at least \d+ characters." + assert re.search(expected, content) + + +def test_post_register_error_missing_confirm(): + with client as request: + response = post_register(request, C=None) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Please confirm your new password." in content + + +def test_post_register_error_mismatched_confirm(): + with client as request: + response = post_register(request, C="mismatched") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Password fields do not match." in content + + +def test_post_register_error_invalid_email(): + with client as request: + response = post_register(request, E="bad@email") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The email address is invalid." in content + + +def test_post_register_error_undeliverable_email(): + with client as request: + # At the time of writing, webchat.freenode.net does not contain + # mx records; if it ever does, it'll break this test. + response = post_register(request, E="email@bad.c") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The email address is invalid." in content + + +def test_post_register_invalid_backup_email(): + with client as request: + response = post_register(request, BE="bad@email") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The backup email address is invalid." in content + + +def test_post_register_error_invalid_homepage(): + with client as request: + response = post_register(request, HP="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "The home page is invalid, please specify the full HTTP(s) URL." + assert expected in content + + +def test_post_register_error_invalid_pgp_fingerprints(): + with client as request: + response = post_register(request, K="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "The PGP key fingerprint is invalid." + assert expected in content + + pk = 'z' + ('a' * 39) + with client as request: + response = post_register(request, K=pk) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "The PGP key fingerprint is invalid." + assert expected in content + + +def test_post_register_error_invalid_ssh_pubkeys(): + with client as request: + response = post_register(request, PK="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The SSH public key is invalid." in content + + with client as request: + response = post_register(request, PK="ssh-rsa ") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The SSH public key is invalid." in content + + +def test_post_register_error_unsupported_language(): + with client as request: + response = post_register(request, L="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "Language is not currently supported." + assert expected in content + + +def test_post_register_error_unsupported_timezone(): + with client as request: + response = post_register(request, TZ="ABCDEFGH") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "Timezone is not currently supported." + assert expected in content + + +def test_post_register_error_username_taken(): + with client as request: + response = post_register(request, U="test") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"The username, .*, is already in use." + assert re.search(expected, content) + + +def test_post_register_error_email_taken(): + with client as request: + response = post_register(request, E="test@example.org") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"The address, .*, is already in use." + assert re.search(expected, content) + + +def test_post_register_error_ssh_pubkey_taken(): + pk = str() + + # Create a public key with ssh-keygen (this adds ssh-keygen as a + # dependency to passing this test). + with tempfile.TemporaryDirectory() as tmpdir: + with open("/dev/null", "w") as null: + proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, stderr=null) + proc.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + # Take the sha256 fingerprint of the ssh public key, create it. + fp = get_fingerprint(pk) + create(SSHPubKey, UserID=user.ID, PubKey=pk, Fingerprint=fp) + + with client as request: + response = post_register(request, PK=pk) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"The SSH public key, .*, is already in use." + assert re.search(expected, content) + + +def test_post_register_with_ssh_pubkey(): + pk = str() + + # Create a public key with ssh-keygen (this adds ssh-keygen as a + # dependency to passing this test). + with tempfile.TemporaryDirectory() as tmpdir: + with open("/dev/null", "w") as null: + proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, stderr=null) + proc.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + with client as request: + response = post_register(request, PK=pk) + + assert response.status_code == int(HTTPStatus.OK) From d323c1f95b666eb5d607919c14c719884b5e1457 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Jan 2021 02:09:34 -0800 Subject: [PATCH 0210/1451] add python-lxml to dependencies Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 +- Dockerfile | 2 +- INSTALL | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 58fb9fed..5bdf427c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ before_script: python-itsdangerous python-httpx python-jinja python-pytest-cov python-requests python-aiofiles python-python-multipart python-pytest-asyncio python-coverage python-bcrypt - python-email-validator openssh + python-email-validator openssh python-lxml - bash -c "echo '127.0.0.1' > /etc/hosts" - bash -c "echo '::1' >> /etc/hosts" diff --git a/Dockerfile b/Dockerfile index cf54a13c..c432f73f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN pacman -Syu --noconfirm base-devel git gpgme protobuf pyalpm \ python-itsdangerous python-httpx python-jinja python-pytest-cov \ python-requests python-aiofiles python-python-multipart \ python-pytest-asyncio python-coverage hypercorn python-bcrypt \ - python-email-validator openssh + python-email-validator openssh python-lxml # Remove aurweb.sqlite3 if it was copied over via COPY. RUN rm -fv aurweb.sqlite3 diff --git a/INSTALL b/INSTALL index 04ccd69e..3381daf5 100644 --- a/INSTALL +++ b/INSTALL @@ -51,7 +51,8 @@ read the instructions below. python-bleach python-markdown python-alembic hypercorn \ python-itsdangerous python-authlib python-httpx \ python-jinja python-aiofiles python-python-multipart \ - python-requests hypercorn python-bcrypt python-email-validator + python-requests hypercorn python-bcrypt python-email-validator \ + python-lxml # python3 setup.py install 5) Create a new MySQL database and a user and import the aurweb SQL schema: From 4e9ef6fb00211378ca7373b0e41ee29479c9aa44 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 28 Jan 2021 20:34:27 -0800 Subject: [PATCH 0211/1451] add account edit (settings) routes * Added account_url filter to jinja2 environment. This produces a path to the user's account url (/account/{username}). * Updated archdev-navbar to link to new edit route. + Added migrate_cookies(request, response) to aurweb.util, a function that simply migrates the request cookies to response and returns it. + Added account_edit tests to test_accounts_routes.py. Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 145 ++++++++++++ aurweb/templates.py | 5 +- aurweb/util.py | 6 + templates/account/edit.html | 46 ++++ templates/partials/account_form.html | 9 + templates/partials/archdev-navbar.html | 20 +- test/test_accounts_routes.py | 296 +++++++++++++++++++++++++ 7 files changed, 522 insertions(+), 5 deletions(-) create mode 100644 templates/account/edit.html diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index a43ba9f7..689f7f58 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -1,5 +1,6 @@ import copy +from datetime import datetime from http import HTTPStatus from fastapi import APIRouter, Form, Request @@ -284,6 +285,7 @@ def make_account_form_context(context: dict, context["cn"] = args.get("CN", user.CommentNotify) context["un"] = args.get("UN", user.UpdateNotify) context["on"] = args.get("ON", user.OwnershipNotify) + context["inactive"] = args.get("J", user.InactivityTS != 0) else: context["username"] = args.get("U", str()) context["account_type"] = args.get("T", user_account_type_id) @@ -301,6 +303,7 @@ def make_account_form_context(context: dict, context["cn"] = args.get("CN", True) context["un"] = args.get("UN", False) context["on"] = args.get("ON", True) + context["inactive"] = args.get("J", False) context["password"] = args.get("P", str()) context["confirm"] = args.get("C", str()) @@ -409,3 +412,145 @@ async def account_register_post(request: Request, context["complete"] = True context["user"] = user return render_template(request, "register.html", context) + + +def cannot_edit(request, user): + """ Return a 401 HTMLResponse if the request user doesn't + have authorization, otherwise None. """ + has_dev_cred = request.user.has_credential("CRED_ACCOUNT_EDIT_DEV", + approved=[user]) + if not has_dev_cred: + return HTMLResponse(status_code=int(HTTPStatus.UNAUTHORIZED)) + return None + + +@router.get("/account/{username}/edit", response_class=HTMLResponse) +@auth_required(True) +async def account_edit(request: Request, + username: str): + user = db.query(User, User.Username == username).first() + response = cannot_edit(request, user) + if response: + return response + + context = await make_variable_context(request, "Accounts") + context["user"] = user + + context = make_account_form_context(context, request, user, dict()) + return render_template(request, "account/edit.html", context) + + +@router.post("/account/{username}/edit", response_class=HTMLResponse) +@auth_required(True) +async def account_edit_post(request: Request, + username: str, + U: str = Form(default=str()), # Username + J: bool = Form(default=False), + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=None), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key + L: str = Form(aurweb.config.get( + "options", "default_lang")), + TZ: str = Form(aurweb.config.get( + "options", "default_timezone")), + P: str = Form(default=str()), # New Password + C: str = Form(default=None), # Password Confirm + PK: str = Form(default=None), # PubKey + CN: bool = Form(default=False), # Comment Notify + UN: bool = Form(default=False), # Update Notify + ON: bool = Form(default=False), # Owner Notify + passwd: str = Form(default=str())): + from aurweb.db import session + + user = session.query(User).filter(User.Username == username).first() + response = cannot_edit(request, user) + if response: + return response + + context = await make_variable_context(request, "Accounts") + context["user"] = user + + if not passwd: + context["errors"] = ["Invalid password."] + return render_template(request, "account/edit.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + + args = dict(await request.form()) + context = make_account_form_context(context, request, user, args) + ok, errors = process_account_form(request, user, args) + + if not ok: + context["errors"] = errors + return render_template(request, "account/edit.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + + # Set all updated fields as needed. + user.Username = U or user.Username + user.Email = E or user.Email + user.HideEmail = bool(H) + user.BackupEmail = BE or user.BackupEmail + user.RealName = R or user.RealName + user.Homepage = HP or user.Homepage + user.IRCNick = I or user.IRCNick + user.PGPKey = K or user.PGPKey + user.InactivityTS = datetime.utcnow().timestamp() if J else 0 + + # If we update the language, update the cookie as well. + if L and L != user.LangPreference: + request.cookies["AURLANG"] = L + user.LangPreference = L + context["language"] = L + + # If we update the timezone, also update the cookie. + if TZ and TZ != user.Timezone: + user.Timezone = TZ + request.cookies["AURTZ"] = TZ + context["timezone"] = TZ + + user.CommentNotify = bool(CN) + user.UpdateNotify = bool(UN) + user.OwnershipNotify = bool(ON) + + # If a PK is given, compare it against the target user's PK. + if PK: + # Get the second token in the public key, which is the actual key. + pubkey = PK.strip().rstrip() + fingerprint = get_fingerprint(pubkey) + if not user.ssh_pub_key: + # No public key exists, create one. + user.ssh_pub_key = SSHPubKey(UserID=user.ID, + PubKey=PK, + Fingerprint=fingerprint) + elif user.ssh_pub_key.Fingerprint != fingerprint: + # A public key already exists, update it. + user.ssh_pub_key.PubKey = PK + user.ssh_pub_key.Fingerprint = fingerprint + elif user.ssh_pub_key: + # Else, if the user has a public key already, delete it. + session.delete(user.ssh_pub_key) + + # Commit changes, if any. + session.commit() + + if P and not user.valid_password(P): + # Remove the fields we consumed for passwords. + context["P"] = context["C"] = str() + + # If a password was given and it doesn't match the user's, update it. + user.update_password(P) + if user == request.user: + # If the target user is the request user, login with + # the updated password and update AURSID. + request.cookies["AURSID"] = user.login(request, P) + + if not errors: + context["complete"] = True + + # Update cookies with requests, in case they were changed. + response = render_template(request, "account/edit.html", context) + return util.migrate_cookies(request, response) +>>>>>> > dddd1137... add account edit(settings) routes diff --git a/aurweb/templates.py b/aurweb/templates.py index d548e92b..c0472b2e 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -12,7 +12,7 @@ from fastapi.responses import HTMLResponse import aurweb.config -from aurweb import captcha, l10n, time +from aurweb import captcha, l10n, time, util # Prepare jinja2 objects. loader = jinja2.FileSystemLoader(os.path.join( @@ -27,6 +27,9 @@ env.filters["tr"] = l10n.tr env.filters["captcha_salt"] = captcha.captcha_salt_filter env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter +# Add account utility filters. +env.filters["account_url"] = util.account_url + def make_context(request: Request, title: str, next: str = None): """ Create a context for a jinja2 TemplateResponse. """ diff --git a/aurweb/util.py b/aurweb/util.py index 5e1717bd..8b6ddbe7 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -82,6 +82,12 @@ def valid_ssh_pubkey(pk): return base64.b64encode(base64.b64decode(tokens[1])).decode() == tokens[1] +def migrate_cookies(request, response): + for k, v in request.cookies.items(): + response.set_cookie(k, v) + return response + + @jinja2.contextfilter def account_url(context, user): request = context.get("request") diff --git a/templates/account/edit.html b/templates/account/edit.html new file mode 100644 index 00000000..f8895d92 --- /dev/null +++ b/templates/account/edit.html @@ -0,0 +1,46 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {% trans %}Accounts{% endtrans %}

    + + {% if complete %} + + {{ + "The account, %s%s%s, has been successfully modified." + | tr + | format("", user.Username, "") + | safe + }} + + {% else %} + {% if errors %} + {% include "partials/error.html" %} + {% else %} +

    + {{ "Click %shere%s if you want to permanently delete this account." + | tr + | format('' | format(user | account_url), + "") + | safe + }} + {{ "Click %shere%s for user details." + | tr + | format('' | format(user | account_url), + "") + | safe + }} + {{ "Click %shere%s to list the comments made by this account." + | tr + | format('' | format(user | account_url), + "") + | safe + }} +

    + {% endif %} + + {% set form_type = "UpdateAccount" %} + {% include "partials/account_form.html" %} + {% endif %} +
    +{% endblock %} diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 3af13368..5ae18131 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -42,6 +42,15 @@ "account is inactive." | tr }}

    +

    + + +

    + {% if request.user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") %}

  • AUR {% trans %}Home{% endtrans %}
  • {% endif %}
  • {% trans %}Packages{% endtrans %}
  • -
  • {% trans %}Register{% endtrans %}
  • -
  • - {% if request.user.is_authenticated() %} + {% if request.user.is_authenticated() %} +
  • + + {% trans %} My Account{% endtrans %} + +
  • +
  • {% trans %}Logout{% endtrans %} - {% else %} +
  • + {% else %} +
  • + + {% trans %}Register{% endtrans %} + +
  • +
  • {% trans %}Login{% endtrans %} +
  • {% endif %} diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index d79137bf..540adde7 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -5,6 +5,7 @@ from datetime import datetime from http import HTTPStatus from subprocess import Popen +import lxml.html import pytest from fastapi.testclient import TestClient @@ -574,3 +575,298 @@ def test_post_register_with_ssh_pubkey(): response = post_register(request, PK=pk) assert response.status_code == int(HTTPStatus.OK) + + +def test_get_account_edit(): + request = Request() + sid = user.login(request, "testPassword") + + with client as request: + response = request.get("/account/test/edit", cookies={ + "AURSID": sid + }, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_get_account_edit_unauthorized(): + request = Request() + sid = user.login(request, "testPassword") + + create(User, Username="test2", Email="test2@example.org", + Passwd="testPassword") + + with client as request: + # Try to edit `test2` while authenticated as `test`. + response = request.get("/account/test2/edit", cookies={ + "AURSID": sid + }, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.UNAUTHORIZED) + + +def test_post_account_edit(): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test666@example.org", + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + expected = "The account, test, " + expected += "has been successfully modified." + assert expected in response.content.decode() + + +def test_post_account_edit_dev(): + from aurweb.db import session + + # Modify our user to be a "Trusted User & Developer" + name = "Trusted User & Developer" + tu_or_dev = query(AccountType, AccountType.AccountType == name).first() + user.AccountType = tu_or_dev + session.commit() + + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test666@example.org", + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + expected = "The account, test, " + expected += "has been successfully modified." + assert expected in response.content.decode() + + +def test_post_account_edit_language(): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "L": "de", # German + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + # Parse the response content html into an lxml root, then make + # sure we see a 'de' option selected on the page. + content = response.content.decode() + root = lxml.html.fromstring(content) + lang_nodes = root.xpath('//option[@value="de"]/@selected') + assert lang_nodes and len(lang_nodes) != 0 + assert lang_nodes[0] == "selected" + + +def test_post_account_edit_timezone(): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "TZ": "CET", + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_post_account_edit_error_missing_password(): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "TZ": "CET", + "passwd": "" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Invalid password." in content + + +def test_post_account_edit_error_invalid_password(): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "TZ": "CET", + "passwd": "invalid" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Invalid password." in content + + +def test_post_account_edit_error_unauthorized(): + request = Request() + sid = user.login(request, "testPassword") + + test2 = create(User, Username="test2", Email="test2@example.org", + Passwd="testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "TZ": "CET", + "passwd": "testPassword" + } + + with client as request: + # Attempt to edit 'test2' while logged in as 'test'. + response = request.post("/account/test2/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.UNAUTHORIZED) + + +def test_post_account_edit_ssh_pub_key(): + pk = str() + + # Create a public key with ssh-keygen (this adds ssh-keygen as a + # dependency to passing this test). + with tempfile.TemporaryDirectory() as tmpdir: + with open("/dev/null", "w") as null: + proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, stderr=null) + proc.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "PK": pk, + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + # Now let's update what's already there to gain coverage over that path. + pk = str() + with tempfile.TemporaryDirectory() as tmpdir: + with open("/dev/null", "w") as null: + proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, stderr=null) + proc.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + post_data["PK"] = pk + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_post_account_edit_invalid_ssh_pubkey(): + pubkey = "ssh-rsa fake key" + + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "P": "newPassword", + "C": "newPassword", + "PK": pubkey, + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + +def test_post_account_edit_password(): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "P": "newPassword", + "C": "newPassword", + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + assert user.valid_password("newPassword") + + +>>>>>> > dddd1137... add account edit(settings) routes From 4f928b45770b3b8fd6013473b57feb223679f884 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Jan 2021 23:40:38 -0800 Subject: [PATCH 0212/1451] add account (view) route + Added get /account/{username} route. + Added account/show.html template which shows a single use Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 18 ++++++- templates/account/show.html | 96 ++++++++++++++++++++++++++++++++++++ test/test_accounts_routes.py | 31 +++++++++++- 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 templates/account/show.html diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 689f7f58..c7c96003 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -3,7 +3,7 @@ import copy from datetime import datetime from http import HTTPStatus -from fastapi import APIRouter, Form, Request +from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, func, or_ @@ -553,4 +553,18 @@ async def account_edit_post(request: Request, # Update cookies with requests, in case they were changed. response = render_template(request, "account/edit.html", context) return util.migrate_cookies(request, response) ->>>>>> > dddd1137... add account edit(settings) routes + + +@router.get("/account/{username}") +@auth_required(True, template=("account/show.html", "Accounts")) +async def account(request: Request, username: str): + user = db.query(User, User.Username == username).first() + + context = await make_variable_context(request, "Accounts") + + if not user: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + + context["user"] = user + + return render_template(request, "account/show.html", context) diff --git a/templates/account/show.html b/templates/account/show.html new file mode 100644 index 00000000..139ff1f5 --- /dev/null +++ b/templates/account/show.html @@ -0,0 +1,96 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {% trans %}Accounts{% endtrans %}

    + + {% if not request.user.is_authenticated() %} + {% trans %}You must log in to view user information.{% endtrans %} + {% else %} + + + + + +
    +

    {{ user.Username }}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {% trans %}Username{% endtrans %}:{{ user.Username }}
    {% trans %}Account Type{% endtrans %}:{{ user.AccountType }}
    {% trans %}Email Address{% endtrans %}: + {{ user.Email }} +
    {% trans %}Real Name{% endtrans %}:{{ user.RealName }}
    {% trans %}Homepage{% endtrans %}: + {% if user.Homepage %} + {{ user.Homepage }} + {% endif %} +
    {% trans %}IRC Nick{% endtrans %}:{{ user.IRCNick }}
    {% trans %}PGP Key Fingerprint{% endtrans %}:{{ user.PGPKey or '' }}
    {% trans %}Status{% endtrans %}:{{ "Active" if not user.Suspended else "Suspended" | tr }}
    {% trans %}Registration date{% endtrans %}: + {{ user.RegistrationTS.strftime("%Y-%m-%d") }} +
    {% trans %}Links{% endtrans %}: + +
    +
    + {% endif %} +
    +{% endblock %} diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 540adde7..c42736fa 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -869,4 +869,33 @@ def test_post_account_edit_password(): assert user.valid_password("newPassword") ->>>>>> > dddd1137... add account edit(settings) routes +def test_get_account(): + request = Request() + sid = user.login(request, "testPassword") + + with client as request: + response = request.get("/account/test", cookies={"AURSID": sid}, + allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_get_account_not_found(): + request = Request() + sid = user.login(request, "testPassword") + + with client as request: + response = request.get("/account/not_found", cookies={"AURSID": sid}, + allow_redirects=False) + + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_get_account_unauthenticated(): + with client as request: + response = request.get("/account/test", allow_redirects=False) + + assert response.status_code == int(HTTPStatus.UNAUTHORIZED) + + content = response.content.decode() + assert "You must log in to view user information." in content From 32abdbafaed74a0a9dbf3c75401dfa1002f62ba6 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 24 May 2021 12:42:57 +0100 Subject: [PATCH 0213/1451] fastapi: Jinja contextfilter renamed to pass_context Closes: #23 Signed-off-by: Leonidas Spyropoulos --- aurweb/captcha.py | 6 +++--- aurweb/l10n.py | 4 ++-- aurweb/util.py | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/aurweb/captcha.py b/aurweb/captcha.py index 5475d85f..9451f42c 100644 --- a/aurweb/captcha.py +++ b/aurweb/captcha.py @@ -1,7 +1,7 @@ """ This module consists of aurweb's CAPTCHA utility functions and filters. """ import hashlib -import jinja2 +from jinja2 import pass_context from aurweb.db import query from aurweb.models.user import User @@ -41,14 +41,14 @@ def get_captcha_answer(token): return hashlib.md5((text + "\n").encode()).hexdigest()[:6] -@jinja2.contextfilter +@pass_context def captcha_salt_filter(context): """ Returns the most recent CAPTCHA salt in the list of salts. """ salts = get_captcha_salts() return salts[0] -@jinja2.contextfilter +@pass_context def captcha_cmdline_filter(context, salt): """ Returns a CAPTCHA challenge for a given salt. """ return get_captcha_challenge(salt) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index 4a5c1a46..9270f3ce 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -4,7 +4,7 @@ import typing from collections import OrderedDict from fastapi import Request -from jinja2 import contextfilter +from jinja2 import pass_context import aurweb.config @@ -88,7 +88,7 @@ def get_translator_for_request(request: Request): return translate -@contextfilter +@pass_context def tr(context: typing.Any, value: str): """ A translation filter; example: {{ "Hello" | tr("de") }}. """ _ = get_translator_for_request(context.get("request")) diff --git a/aurweb/util.py b/aurweb/util.py index 8b6ddbe7..8e4b291d 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -5,9 +5,8 @@ import string from urllib.parse import urlparse -import jinja2 - from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email +from jinja2 import pass_context import aurweb.config @@ -88,7 +87,7 @@ def migrate_cookies(request, response): return response -@jinja2.contextfilter +@pass_context def account_url(context, user): request = context.get("request") base = f"{request.url.scheme}://{request.url.hostname}" From 822905be7d41fbd52790de58ba20f0d82ce69efc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 24 May 2021 05:19:57 -0700 Subject: [PATCH 0214/1451] bugfix: relax `next` verification AUR renders its own 404 Not Found page when a bad route is encountered. Introducing the previous verification caused an error in this case when setting a language while viewing the Not Found page. So, instead of checking through routes, just make sure that the next parameter starts with a '/' character, which removes the possibility of any cross attacks. + Removed aurweb.asgi.routes; no longer needed. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 9 --------- aurweb/routers/html.py | 8 +++----- test/test_routes.py | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 1a61b1f4..861f6056 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -12,8 +12,6 @@ from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine from aurweb.routers import accounts, auth, errors, html, sso -routes = set() - # Setup the FastAPI app. app = FastAPI(exception_handlers=errors.exceptions) @@ -47,13 +45,6 @@ async def app_startup(): # Initialize the database engine and ORM. get_engine() -# NOTE: Always keep this dictionary updated with all routes -# that the application contains. We use this to check for -# parameter value verification. -routes = {route.path for route in app.routes} -routes.update({route.path for route in sso.router.routes}) -routes.update({route.path for route in html.router.routes}) - @app.exception_handler(HTTPException) async def http_exception_handler(request, exc): diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index e947d213..8f89e05c 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -32,11 +32,9 @@ async def language(request: Request, parameters across the redirect. """ from aurweb.db import session - from aurweb.asgi import routes - if unquote(next) not in routes: - return HTMLResponse( - b"Invalid 'next' parameter.", - status_code=400) + + if next[0] != '/': + return HTMLResponse(b"Invalid 'next' parameter.", status_code=400) query_string = "?" + q if q else str() diff --git a/test/test_routes.py b/test/test_routes.py index d512a172..e4816231 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -61,7 +61,7 @@ def test_language_invalid_next(): """ Test an invalid next route at '/language'. """ post_data = { "set_lang": "de", - "next": "/BLAHBLAHFAKE" + "next": "https://evil.net" } with client as req: response = req.post("/language", data=post_data) From a7e5498197ebac1986b7b05c4acb6026d9c6f24d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 30 May 2021 16:38:16 -0700 Subject: [PATCH 0215/1451] add PackageBase SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/db.py | 7 +++++ aurweb/models/package_base.py | 39 ++++++++++++++++++++++++ test/test_package_base.py | 57 +++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 aurweb/models/package_base.py create mode 100644 test/test_package_base.py diff --git a/aurweb/db.py b/aurweb/db.py index 7dab6c4a..bb58c0c8 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,5 +1,7 @@ import math +from sqlalchemy.orm import backref, relationship + import aurweb.config import aurweb.util @@ -51,6 +53,11 @@ def make_random_value(table: str, column: str): return string +def make_relationship(model, foreign_key, backref_): + return relationship(model, foreign_keys=[foreign_key], + backref=backref(backref_, lazy="dynamic")) + + def query(model, *args, **kwargs): return session.query(model).filter(*args, **kwargs) diff --git a/aurweb/models/package_base.py b/aurweb/models/package_base.py new file mode 100644 index 00000000..57e5a46b --- /dev/null +++ b/aurweb/models/package_base.py @@ -0,0 +1,39 @@ +from datetime import datetime + +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.user import User +from aurweb.schema import PackageBases + + +class PackageBase: + def __init__(self, Name: str = None, Flagger: User = None, + Maintainer: User = None, Submitter: User = None, + Packager: User = None, **kwargs): + self.Name = Name + self.Flagger = Flagger + self.Maintainer = Maintainer + self.Submitter = Submitter + self.Packager = Packager + + self.NumVotes = kwargs.get("NumVotes") + self.Popularity = kwargs.get("Popularity") + self.OutOfDateTS = kwargs.get("OutOfDateTS") + self.FlaggerComment = kwargs.get("FlaggerComment", str()) + self.SubmittedTS = kwargs.get("SubmittedTS", + datetime.utcnow().timestamp()) + self.ModifiedTS = kwargs.get("ModifiedTS", + datetime.utcnow().timestamp()) + + +mapper(PackageBase, PackageBases, properties={ + "Flagger": make_relationship(User, PackageBases.c.FlaggerUID, + "flagged_bases"), + "Submitter": make_relationship(User, PackageBases.c.SubmitterUID, + "submitted_bases"), + "Maintainer": make_relationship(User, PackageBases.c.MaintainerUID, + "maintained_bases"), + "Packager": make_relationship(User, PackageBases.c.PackagerUID, + "package_bases") +}) diff --git a/test/test_package_base.py b/test/test_package_base.py new file mode 100644 index 00000000..dcb0eb9e --- /dev/null +++ b/test/test_package_base.py @@ -0,0 +1,57 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query +from aurweb.models.account_type import AccountType +from aurweb.models.package_base import PackageBase +from aurweb.testing import setup_test_db +from aurweb.testing.models import make_user + +user = None + + +@pytest.fixture(autouse=True) +def setup(): + global user + + setup_test_db("Users", "PackageBases") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = make_user(Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + yield user + + +def test_package_base(): + pkgbase = create(PackageBase, + Name="beautiful-package", + Maintainer=user) + assert pkgbase in user.maintained_bases + + assert not pkgbase.OutOfDateTS + assert pkgbase.SubmittedTS > 0 + assert pkgbase.ModifiedTS > 0 + + +def test_package_base_relationships(): + pkgbase = create(PackageBase, + Name="beautiful-package", + Flagger=user, + Maintainer=user, + Submitter=user, + Packager=user) + assert pkgbase in user.flagged_bases + assert pkgbase in user.maintained_bases + assert pkgbase in user.submitted_bases + assert pkgbase in user.package_bases + + +def test_package_base_null_name_raises_exception(): + from aurweb.db import session + + with pytest.raises(IntegrityError): + create(PackageBase) + session.rollback() From fb210158113307d2fb17cd6377eee8e27a2cec05 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 30 May 2021 18:12:46 -0700 Subject: [PATCH 0216/1451] add PackageKeyword SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_keyword.py | 20 ++++++++++++ test/test_package_keyword.py | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 aurweb/models/package_keyword.py create mode 100644 test/test_package_keyword.py diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py new file mode 100644 index 00000000..87d97558 --- /dev/null +++ b/aurweb/models/package_keyword.py @@ -0,0 +1,20 @@ +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.package_base import PackageBase +from aurweb.schema import PackageKeywords + + +class PackageKeyword: + def __init__(self, + PackageBase: PackageBase = None, + Keyword: str = None): + self.PackageBase = PackageBase + self.Keyword = Keyword + + +mapper(PackageKeyword, PackageKeywords, properties={ + "PackageBase": make_relationship(PackageBase, + PackageKeywords.c.PackageBaseID, + "keywords") +}) diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py new file mode 100644 index 00000000..6e2df344 --- /dev/null +++ b/test/test_package_keyword.py @@ -0,0 +1,54 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query +from aurweb.models.account_type import AccountType +from aurweb.models.package_base import PackageBase +from aurweb.models.package_keyword import PackageKeyword +from aurweb.testing import setup_test_db +from aurweb.testing.models import make_user + +user, pkgbase = None, None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase + + setup_test_db("Users", "PackageBases", "PackageKeywords") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = make_user(Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + pkgbase = create(PackageBase, + Name="beautiful-package", + Maintainer=user) + + yield pkgbase + + from aurweb.db import session + session.delete(pkgbase) + session.commit() + + +def test_package_keyword(): + from aurweb.db import session + pkg_keyword = create(PackageKeyword, + PackageBase=pkgbase, + Keyword="test") + assert pkg_keyword in pkgbase.keywords + assert pkgbase == pkg_keyword.PackageBase + session.delete(pkg_keyword) + session.commit() + + +def test_package_keyword_null_pkgbase_raises_exception(): + from aurweb.db import session + + with pytest.raises(IntegrityError): + create(PackageKeyword, + Keyword="test") + session.rollback() From 29db2ee5139baaa7cf4e9989e5916afca6f98bf1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 30 May 2021 22:28:43 -0700 Subject: [PATCH 0217/1451] add Term SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/term.py | 15 +++++++++++++++ test/test_term.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 aurweb/models/term.py create mode 100644 test/test_term.py diff --git a/aurweb/models/term.py b/aurweb/models/term.py new file mode 100644 index 00000000..1b4902f7 --- /dev/null +++ b/aurweb/models/term.py @@ -0,0 +1,15 @@ +from sqlalchemy.orm import mapper + +from aurweb.schema import Terms + + +class Term: + def __init__(self, + Description: str = None, URL: str = None, + Revision: int = None): + self.Description = Description + self.URL = URL + self.Revision = Revision + + +mapper(Term, Terms) diff --git a/test/test_term.py b/test/test_term.py new file mode 100644 index 00000000..4ae1e1cd --- /dev/null +++ b/test/test_term.py @@ -0,0 +1,30 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, delete +from aurweb.models.term import Term + + +def test_term_creation(): + term = create(Term, Description="Term description", + URL="https://fake_url.io") + assert bool(term.ID) + assert term.Description == "Term description" + assert term.URL == "https://fake_url.io" + assert term.Revision == 1 + delete(Term, Term.ID == term.ID) + + +def test_term_null_description_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(Term, URL="https://fake_url.io") + session.rollback() + + +def test_term_null_url_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(Term, Description="Term description") + session.rollback() From 718fa48a5cb0be18cea315bfb1742ef95f30da98 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 30 May 2021 22:29:01 -0700 Subject: [PATCH 0218/1451] add AcceptedTerm SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/accepted_term.py | 24 ++++++++++++++ test/test_accepted_term.py | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 aurweb/models/accepted_term.py create mode 100644 test/test_accepted_term.py diff --git a/aurweb/models/accepted_term.py b/aurweb/models/accepted_term.py new file mode 100644 index 00000000..6e8ffe99 --- /dev/null +++ b/aurweb/models/accepted_term.py @@ -0,0 +1,24 @@ +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.term import Term +from aurweb.models.user import User +from aurweb.schema import AcceptedTerms + + +class AcceptedTerm: + def __init__(self, + User: User = None, Term: Term = None, + Revision: int = None): + self.User = User + self.Term = Term + self.Revision = Revision + + +properties = { + "User": make_relationship(User, AcceptedTerms.c.UsersID, "accepted_terms"), + "Term": make_relationship(Term, AcceptedTerms.c.TermsID, "accepted") +} + +mapper(AcceptedTerm, AcceptedTerms, properties=properties, + primary_key=[AcceptedTerms.c.UsersID, AcceptedTerms.c.TermsID]) diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py new file mode 100644 index 00000000..4dd8a5ca --- /dev/null +++ b/test/test_accepted_term.py @@ -0,0 +1,57 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, delete, query +from aurweb.models.accepted_term import AcceptedTerm +from aurweb.models.account_type import AccountType +from aurweb.models.term import Term +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user, term, accepted_term = None, None, None + + +@pytest.fixture(autouse=True) +def setup(): + global user, term, accepted_term + + setup_test_db("Users", "AcceptedTerms", "Terms") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + account_type=account_type) + + term = create(Term, Description="Test term", URL="https://test.term") + + yield term + + delete(Term, Term.ID == term.ID) + delete(User, User.ID == user.ID) + + +def test_accepted_term(): + accepted_term = create(AcceptedTerm, User=user, Term=term) + + # Make sure our AcceptedTerm relationships got initialized properly. + assert accepted_term.User == user + assert accepted_term in user.accepted_terms + assert accepted_term in term.accepted + + delete(AcceptedTerm, AcceptedTerm.User == user, AcceptedTerm.Term == term) + + +def test_accepted_term_null_user_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(AcceptedTerm, Term=term) + session.rollback() + + +def test_accepted_term_null_term_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(AcceptedTerm, User=user) + session.rollback() From e1ab02c2bf88e2a2fcf2048da212f586c6e3a389 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 30 May 2021 23:06:33 -0700 Subject: [PATCH 0219/1451] Fix database initialization in test_term.py Signed-off-by: Kevin Morris --- test/test_term.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_term.py b/test/test_term.py index 4ae1e1cd..00397b33 100644 --- a/test/test_term.py +++ b/test/test_term.py @@ -2,10 +2,15 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, delete +from aurweb.db import create, delete, get_engine from aurweb.models.term import Term +@pytest.fixture(autouse=True) +def setup(): + get_engine() + + def test_term_creation(): term = create(Term, Description="Term description", URL="https://fake_url.io") From b692b11f62efffd8554fce95c7a0f2a2cdb9014b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 30 May 2021 23:05:16 -0700 Subject: [PATCH 0220/1451] add Group SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/group.py | 11 +++++++++++ test/test_group.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 aurweb/models/group.py create mode 100644 test/test_group.py diff --git a/aurweb/models/group.py b/aurweb/models/group.py new file mode 100644 index 00000000..5d4f3834 --- /dev/null +++ b/aurweb/models/group.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import mapper + +from aurweb.schema import Groups + + +class Group: + def __init__(self, Name: str = None): + self.Name = Name + + +mapper(Group, Groups) diff --git a/test/test_group.py b/test/test_group.py new file mode 100644 index 00000000..bbb774b9 --- /dev/null +++ b/test/test_group.py @@ -0,0 +1,21 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, delete, get_engine +from aurweb.models.group import Group + + +def test_group_creation(): + get_engine() + group = create(Group, Name="Test Group") + assert bool(group.ID) + assert group.Name == "Test Group" + delete(Group, Group.ID == group.ID) + + +def test_group_null_name_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(Group) + session.rollback() From 794868b20f700461fd8978be9c162c708db43c57 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 31 May 2021 22:41:06 -0700 Subject: [PATCH 0221/1451] aurweb.testing.setup_test_db: Expunge objects This is needed to avoid redundant objects in SQLAlchemy's IdentityMap, since we pass a direct .execute to delete the tables passed in. Additionally, remove our engine.connect() call in favor of relying on the already-established Session. Signed-off-by: Kevin Morris --- aurweb/testing/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 0a807b40..02c21a4c 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -21,10 +21,12 @@ def setup_test_db(*args): test_tables = ["Users", "Sessions"]; setup_test_db(*test_tables) """ - engine = aurweb.db.get_engine() - conn = engine.connect() + # Make sure that we've grabbed the engine before using the session. + aurweb.db.get_engine() tables = list(args) for table in tables: - conn.execute(f"DELETE FROM {table}") - conn.close() + aurweb.db.session.execute(f"DELETE FROM {table}") + + # Expunge all objects from SQLAlchemy's IdentityMap. + aurweb.db.session.expunge_all() From f8a6049de24a1b92b6d1c3456570c47f464aaf21 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 31 May 2021 22:43:28 -0700 Subject: [PATCH 0222/1451] aurweb.db.session: Use autoflush=True for Sessions We'd like SQLAlchemy to automatically maintain flushes for us. Signed-off-by: Kevin Morris --- aurweb/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/db.py b/aurweb/db.py index bb58c0c8..ca5ce412 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -135,7 +135,7 @@ def get_engine(): # https://fastapi.tiangolo.com/tutorial/sql-databases/#note connect_args["check_same_thread"] = False engine = create_engine(get_sqlalchemy_url(), connect_args=connect_args) - Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) + Session = sessionmaker(autocommit=False, autoflush=True, bind=engine) session = Session() return engine From 621e459dfbd3d6e8e0d7a790dae4e14b092078ad Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 31 May 2021 22:45:30 -0700 Subject: [PATCH 0223/1451] aurweb.models.user: Remove session.commit() from construction We don't want to do this on construction. We only want to do this when we want to actually add the user to the database (or modify it). Signed-off-by: Kevin Morris --- aurweb/models/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 3983e098..6c5c6e21 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -51,11 +51,9 @@ class User: self.update_password(passwd) def update_password(self, password, salt_rounds=12): - from aurweb.db import session self.Passwd = bcrypt.hashpw( password.encode(), bcrypt.gensalt(rounds=salt_rounds)).decode() - session.commit() @staticmethod def minimum_passwd_length(): From 15b1332656a7f2bb0aa2abe108af4ede0629b749 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 00:25:49 -0700 Subject: [PATCH 0224/1451] add Package SQLAlchemy ORM model Additionally, add an optional **kwargs passing via make_relationship. This allows us to use things like `uselist=False`, which was needed for test/test_package.py. Signed-off-by: Kevin Morris --- aurweb/db.py | 5 ++- aurweb/models/package.py | 24 ++++++++++++ test/test_package.py | 79 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 aurweb/models/package.py create mode 100644 test/test_package.py diff --git a/aurweb/db.py b/aurweb/db.py index ca5ce412..500cf95a 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -53,9 +53,10 @@ def make_random_value(table: str, column: str): return string -def make_relationship(model, foreign_key, backref_): +def make_relationship(model, foreign_key: str, backref_: str, **kwargs): return relationship(model, foreign_keys=[foreign_key], - backref=backref(backref_, lazy="dynamic")) + backref=backref(backref_, lazy="dynamic"), + **kwargs) def query(model, *args, **kwargs): diff --git a/aurweb/models/package.py b/aurweb/models/package.py new file mode 100644 index 00000000..fa82bb74 --- /dev/null +++ b/aurweb/models/package.py @@ -0,0 +1,24 @@ +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.package_base import PackageBase +from aurweb.schema import Packages + + +class Package: + def __init__(self, + PackageBase: PackageBase = None, + Name: str = None, Version: str = None, + Description: str = None, URL: str = None): + self.PackageBase = PackageBase + self.Name = Name + self.Version = Version + self.Description = Description + self.URL = URL + + +mapper(Package, Packages, properties={ + "PackageBase": make_relationship(PackageBase, + Packages.c.PackageBaseID, + "package", uselist=False) +}) diff --git a/test/test_package.py b/test/test_package.py new file mode 100644 index 00000000..1d670087 --- /dev/null +++ b/test/test_package.py @@ -0,0 +1,79 @@ +import pytest + +from sqlalchemy import and_ +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query +from aurweb.models.account_type import AccountType +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = package = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase, package + + setup_test_db("Users", "PackageBases", "Packages") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + pkgbase = create(PackageBase, + Name="beautiful-package", + Maintainer=user) + package = create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") + + yield package + + +def test_package(): + from aurweb.db import session + + assert pkgbase == package.PackageBase + assert package.Name == "beautiful-package" + assert package.Description == "Test description." + assert package.Version == str() # Default version. + assert package.URL == "https://test.package" + + # Update package Version. + package.Version = "1.2.3" + session.commit() + + # Make sure it got updated in the database. + record = query(Package, + and_(Package.ID == package.ID, + Package.Version == "1.2.3")).first() + assert record is not None + + +def test_package_null_pkgbase_raises_exception(): + from aurweb.db import session + + with pytest.raises(IntegrityError): + create(Package, + Name="some-package", + Description="Some description.", + URL="https://some.package") + session.rollback() + + +def test_package_null_name_raises_exception(): + from aurweb.db import session + + with pytest.raises(IntegrityError): + create(Package, + PackageBase=pkgbase, + Description="Some description.", + URL="https://some.package") + session.rollback() From f2121fb833d2279c5fb6b5863988209e45176fd0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 00:26:32 -0700 Subject: [PATCH 0225/1451] simplify test_package_keyword.py We no longer need to delete records like this; in fact, it causes errors now. Fix this by removing the deletions and allow setup_test_db to do it's job. We'll need to do this for other tests as well. Signed-off-by: Kevin Morris --- test/test_package_keyword.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py index 6e2df344..f110b123 100644 --- a/test/test_package_keyword.py +++ b/test/test_package_keyword.py @@ -29,20 +29,13 @@ def setup(): yield pkgbase - from aurweb.db import session - session.delete(pkgbase) - session.commit() - def test_package_keyword(): - from aurweb.db import session pkg_keyword = create(PackageKeyword, PackageBase=pkgbase, Keyword="test") assert pkg_keyword in pkgbase.keywords assert pkgbase == pkg_keyword.PackageBase - session.delete(pkg_keyword) - session.commit() def test_package_keyword_null_pkgbase_raises_exception(): From 38dc2bb99dcbab372c4c7fcd3716f7f532c22ee0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 03:26:14 -0700 Subject: [PATCH 0226/1451] Sanitize and modernize pytests Some of these tests were written before some of our convenient tooling existed. Additionally, some of the tests were not cooperating with PEP-8 guidelines or isorted. This commit does the following: - Replaces all calls to make_(user|session) with aurweb.db.create(Model, ...). - Replace calls to session.add(...) + session.commit() with aurweb.db.create. - Removes the majority of calls to (session|aurweb.db).delete(...). - Replaces session.query calls with aurweb.db.query. - Initializes all mutable globals in pytest fixture setup(). - Makes mutable global declarations more concise: `var1, var2 = None, None` -> `var1 = var2 = None` - Defines a warning exclusion for test/test_ssh_pub_key.py. - Removes the aurweb.testing.models module. - Removes some useless pytest.fixture yielding. As of this commit, developers should use the following guidelines when writing tests: - Always use aurweb.db.(create|delete|query) for database operations, where possible. - Always define mutable globals in the style: `var1 = var2 = None`. - `yield` the most dependent model in pytest setup fixture **iff** you must delete records after test runs to maintain database integrity. Example: test/test_account_type.py. This all makes the test code look and behave much cleaner. Previously, aurweb.testing.setup_test_db was buggy and leaving objects around in SQLAlchemy's IdentityMap. Signed-off-by: Kevin Morris --- aurweb/testing/models.py | 25 --------------- setup.cfg | 28 +++++++++++------ test/test_accepted_term.py | 11 ++----- test/test_account_type.py | 28 ++++++----------- test/test_accounts_routes.py | 13 ++++---- test/test_auth.py | 29 +++++++---------- test/test_auth_routes.py | 17 +++++----- test/test_ban.py | 16 ++++------ test/test_exceptions.py | 61 +++++++++++++++++------------------- test/test_group.py | 10 ++++-- test/test_initdb.py | 5 +-- test/test_package.py | 2 -- test/test_package_base.py | 9 +++--- test/test_package_keyword.py | 12 +++---- test/test_routes.py | 17 +++++----- test/test_session.py | 31 +++++++++--------- test/test_ssh_pub_key.py | 29 ++++++----------- test/test_term.py | 6 ++-- test/test_user.py | 50 +++++++++-------------------- 19 files changed, 160 insertions(+), 239 deletions(-) delete mode 100644 aurweb/testing/models.py diff --git a/aurweb/testing/models.py b/aurweb/testing/models.py deleted file mode 100644 index 8a27c409..00000000 --- a/aurweb/testing/models.py +++ /dev/null @@ -1,25 +0,0 @@ -import warnings - -from sqlalchemy import exc - -import aurweb.db - - -def make_user(**kwargs): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", exc.SAWarning) - from aurweb.models.user import User - user = User(**kwargs) - aurweb.db.session.add(user) - aurweb.db.session.commit() - return user - - -def make_session(**kwargs): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", exc.SAWarning) - from aurweb.models.session import Session - session = Session(**kwargs) - aurweb.db.session.add(session) - aurweb.db.session.commit() - return session diff --git a/setup.cfg b/setup.cfg index 98261651..31a0eb8a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,18 +2,26 @@ max-line-length = 127 max-complexity = 10 -# Ignore some unavoidable flake8 warnings; we know this is against -# pycodestyle, but some of the existing codebase uses `I` variables, -# so specifically silence warnings about it in pre-defined files. -# In E741, the 'I', 'O', 'l' are ambiguous variable names. -# Our current implementation uses these variables through HTTP -# and the FastAPI form specification wants them named as such. -# In C901's case, our process_account_form function is way too -# complex for PEP (too many if statements). However, we need to -# process these anyways, and making it any more complex would -# just add confusion to the implementation. +# aurweb/routers/accounts.py +# Ignore some unavoidable flake8 warnings; we know this is against +# pycodestyle, but some of the existing codebase uses `I` variables, +# so specifically silence warnings about it in pre-defined files. +# In E741, the 'I', 'O', 'l' are ambiguous variable names. +# Our current implementation uses these variables through HTTP +# and the FastAPI form specification wants them named as such. +# In C901's case, our process_account_form function is way too +# complex for PEP (too many if statements). However, we need to +# process these anyways, and making it any more complex would +# just add confusion to the implementation. +# +# test/test_ssh_pub_key.py +# E501 is detected due to our >127 width test constant. Ignore it. +# Due to this, line width should _always_ be looked at in code reviews. +# Anything like this should be questioned. +# per-file-ignores = aurweb/routers/accounts.py:E741,C901 + test/test_ssh_pub_key.py:E501 [isort] line_length = 127 diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py index 4dd8a5ca..4ddf1fc3 100644 --- a/test/test_accepted_term.py +++ b/test/test_accepted_term.py @@ -2,14 +2,14 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, delete, query +from aurweb.db import create, query from aurweb.models.accepted_term import AcceptedTerm from aurweb.models.account_type import AccountType from aurweb.models.term import Term from aurweb.models.user import User from aurweb.testing import setup_test_db -user, term, accepted_term = None, None, None +user = term = accepted_term = None @pytest.fixture(autouse=True) @@ -26,11 +26,6 @@ def setup(): term = create(Term, Description="Test term", URL="https://test.term") - yield term - - delete(Term, Term.ID == term.ID) - delete(User, User.ID == user.ID) - def test_accepted_term(): accepted_term = create(AcceptedTerm, User=user, Term=term) @@ -40,8 +35,6 @@ def test_accepted_term(): assert accepted_term in user.accepted_terms assert accepted_term in term.accepted - delete(AcceptedTerm, AcceptedTerm.User == user, AcceptedTerm.Term == term) - def test_accepted_term_null_user_raises_exception(): from aurweb.db import session diff --git a/test/test_account_type.py b/test/test_account_type.py index 9419970c..3bd76d1e 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -1,9 +1,9 @@ import pytest +from aurweb.db import create, delete, query from aurweb.models.account_type import AccountType from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_user account_type = None @@ -12,24 +12,17 @@ account_type = None def setup(): setup_test_db("Users") - from aurweb.db import session - global account_type - account_type = AccountType(AccountType="TestUser") - session.add(account_type) - session.commit() + account_type = create(AccountType, AccountType="TestUser") yield account_type - session.delete(account_type) - session.commit() + delete(AccountType, AccountType.ID == account_type.ID) def test_account_type(): """ Test creating an AccountType, and reading its columns. """ - from aurweb.db import session - # Make sure it got created and was given an ID. assert bool(account_type.ID) @@ -39,20 +32,17 @@ def test_account_type(): "" % ( account_type.ID) - record = session.query(AccountType).filter( - AccountType.AccountType == "TestUser").first() + record = query(AccountType, + AccountType.AccountType == "TestUser").first() assert account_type == record def test_user_account_type_relationship(): - from aurweb.db import session - - user = make_user(Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) assert user.AccountType == account_type assert account_type.users.filter(User.ID == user.ID).first() - session.delete(user) - session.commit() + delete(User, User.ID == user.ID) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index c42736fa..0f813823 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -12,14 +12,13 @@ from fastapi.testclient import TestClient from aurweb import captcha from aurweb.asgi import app -from aurweb.db import create, delete, query +from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.ban import Ban from aurweb.models.session import Session from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_user from aurweb.testing.requests import Request # Some test global constants. @@ -39,9 +38,9 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username=TEST_USERNAME, Email=TEST_EMAIL, - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, + RealName="Test User", Passwd="testPassword", + AccountType=account_type) def test_get_passreset_authed_redirects(): @@ -751,8 +750,8 @@ def test_post_account_edit_error_unauthorized(): request = Request() sid = user.login(request, "testPassword") - test2 = create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword") + create(User, Username="test2", + Email="test2@example.org", Passwd="testPassword") post_data = { "U": "test", diff --git a/test/test_auth.py b/test/test_auth.py index d43459cd..7837e7f7 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -5,16 +5,14 @@ import pytest from starlette.authentication import AuthenticationError from aurweb.auth import BasicAuthBackend, has_credential -from aurweb.db import query +from aurweb.db import create, query from aurweb.models.account_type import AccountType +from aurweb.models.session import Session +from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_session, make_user from aurweb.testing.requests import Request -# Persistent user object, initialized in our setup fixture. -user = None -backend = None -request = None +user = backend = request = None @pytest.fixture(autouse=True) @@ -23,16 +21,11 @@ def setup(): setup_test_db("Users", "Sessions") - from aurweb.db import session - account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.com", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - - session.add(user) - session.commit() + user = create(User, Username="test", Email="test@example.com", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) backend = BasicAuthBackend() request = Request() @@ -60,8 +53,8 @@ async def test_auth_backend_invalid_sid(): async def test_auth_backend_invalid_user_id(): # Create a new session with a fake user id. now_ts = datetime.utcnow().timestamp() - make_session(UsersID=666, SessionID="realSession", - LastUpdateTS=now_ts + 5) + create(Session, UsersID=666, SessionID="realSession", + LastUpdateTS=now_ts + 5) # Here, we specify a real SID; but it's user is not there. request.cookies["AURSID"] = "realSession" @@ -74,8 +67,8 @@ async def test_basic_auth_backend(): # This time, everything matches up. We expect the user to # equal the real_user. now_ts = datetime.utcnow().timestamp() - make_session(UsersID=user.ID, SessionID="realSession", - LastUpdateTS=now_ts + 5) + create(Session, UsersID=user.ID, SessionID="realSession", + LastUpdateTS=now_ts + 5) _, result = await backend.authenticate(request) assert result == user diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index ff8a08e9..360b48cc 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -8,33 +8,34 @@ from fastapi.testclient import TestClient import aurweb.config from aurweb.asgi import app -from aurweb.db import query +from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.session import Session +from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_user # Some test global constants. TEST_USERNAME = "test" TEST_EMAIL = "test@example.org" # Global mutables. -client = TestClient(app) -user = None +user = client = None @pytest.fixture(autouse=True) def setup(): - global user + global user, client setup_test_db("Users", "Sessions", "Bans") account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username=TEST_USERNAME, Email=TEST_EMAIL, - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + client = TestClient(app) def test_login_logout(): diff --git a/test/test_ban.py b/test/test_ban.py index de4f5b1b..a4fa5a28 100644 --- a/test/test_ban.py +++ b/test/test_ban.py @@ -6,27 +6,23 @@ import pytest from sqlalchemy import exc as sa_exc +from aurweb.db import create from aurweb.models.ban import Ban, is_banned from aurweb.testing import setup_test_db from aurweb.testing.requests import Request -ban = None - -request = Request() +ban = request = None @pytest.fixture(autouse=True) def setup(): - from aurweb.db import session - - global ban + global ban, request setup_test_db("Bans") - ban = Ban(IPAddress="127.0.0.1", - BanTS=datetime.utcnow() + timedelta(seconds=30)) - session.add(ban) - session.commit() + ts = datetime.utcnow() + timedelta(seconds=30) + ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) + request = Request() def test_ban(): diff --git a/test/test_exceptions.py b/test/test_exceptions.py index feac2656..7247106b 100644 --- a/test/test_exceptions.py +++ b/test/test_exceptions.py @@ -1,102 +1,99 @@ -from aurweb.exceptions import (AlreadyVotedException, AurwebException, BannedException, BrokenUpdateHookException, - InvalidArgumentsException, InvalidCommentException, InvalidPackageBaseException, - InvalidReasonException, InvalidRepositoryNameException, InvalidUserException, - MaintenanceException, NotVotedException, PackageBaseExistsException, PermissionDeniedException) +from aurweb import exceptions def test_aurweb_exception(): try: - raise AurwebException("test") - except AurwebException as exc: + raise exceptions.AurwebException("test") + except exceptions.AurwebException as exc: assert str(exc) == "test" def test_maintenance_exception(): try: - raise MaintenanceException("test") - except MaintenanceException as exc: + raise exceptions.MaintenanceException("test") + except exceptions.MaintenanceException as exc: assert str(exc) == "test" def test_banned_exception(): try: - raise BannedException("test") - except BannedException as exc: + raise exceptions.BannedException("test") + except exceptions.BannedException as exc: assert str(exc) == "test" def test_already_voted_exception(): try: - raise AlreadyVotedException("test") - except AlreadyVotedException as exc: + raise exceptions.AlreadyVotedException("test") + except exceptions.AlreadyVotedException as exc: assert str(exc) == "already voted for package base: test" def test_broken_update_hook_exception(): try: - raise BrokenUpdateHookException("test") - except BrokenUpdateHookException as exc: + raise exceptions.BrokenUpdateHookException("test") + except exceptions.BrokenUpdateHookException as exc: assert str(exc) == "broken update hook: test" def test_invalid_arguments_exception(): try: - raise InvalidArgumentsException("test") - except InvalidArgumentsException as exc: + raise exceptions.InvalidArgumentsException("test") + except exceptions.InvalidArgumentsException as exc: assert str(exc) == "test" def test_invalid_packagebase_exception(): try: - raise InvalidPackageBaseException("test") - except InvalidPackageBaseException as exc: + raise exceptions.InvalidPackageBaseException("test") + except exceptions.InvalidPackageBaseException as exc: assert str(exc) == "package base not found: test" def test_invalid_comment_exception(): try: - raise InvalidCommentException("test") - except InvalidCommentException as exc: + raise exceptions.InvalidCommentException("test") + except exceptions.InvalidCommentException as exc: assert str(exc) == "comment is too short: test" def test_invalid_reason_exception(): try: - raise InvalidReasonException("test") - except InvalidReasonException as exc: + raise exceptions.InvalidReasonException("test") + except exceptions.InvalidReasonException as exc: assert str(exc) == "invalid reason: test" def test_invalid_user_exception(): try: - raise InvalidUserException("test") - except InvalidUserException as exc: + raise exceptions.InvalidUserException("test") + except exceptions.InvalidUserException as exc: assert str(exc) == "unknown user: test" def test_not_voted_exception(): try: - raise NotVotedException("test") - except NotVotedException as exc: + raise exceptions.NotVotedException("test") + except exceptions.NotVotedException as exc: assert str(exc) == "missing vote for package base: test" def test_packagebase_exists_exception(): try: - raise PackageBaseExistsException("test") - except PackageBaseExistsException as exc: + raise exceptions.PackageBaseExistsException("test") + except exceptions.PackageBaseExistsException as exc: assert str(exc) == "package base already exists: test" def test_permission_denied_exception(): try: - raise PermissionDeniedException("test") - except PermissionDeniedException as exc: + raise exceptions.PermissionDeniedException("test") + except exceptions.PermissionDeniedException as exc: assert str(exc) == "permission denied: test" def test_repository_name_exception(): try: - raise InvalidRepositoryNameException("test") - except InvalidRepositoryNameException as exc: + raise exceptions.InvalidRepositoryNameException("test") + except exceptions.InvalidRepositoryNameException as exc: assert str(exc) == "invalid repository name: test" diff --git a/test/test_group.py b/test/test_group.py index bbb774b9..da017a96 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -2,16 +2,20 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, delete, get_engine +from aurweb.db import create from aurweb.models.group import Group +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db("Groups") def test_group_creation(): - get_engine() group = create(Group, Name="Test Group") assert bool(group.ID) assert group.Name == "Test Group" - delete(Group, Group.ID == group.ID) def test_group_null_name_raises_exception(): diff --git a/test/test_initdb.py b/test/test_initdb.py index ff089b63..eae33007 100644 --- a/test/test_initdb.py +++ b/test/test_initdb.py @@ -23,5 +23,6 @@ def test_run(): use_alembic = True verbose = False aurweb.initdb.run(Args()) - assert aurweb.db.session.query(AccountType).filter( - AccountType.AccountType == "User").first() is not None + record = aurweb.db.query(AccountType, + AccountType.AccountType == "User").first() + assert record is not None diff --git a/test/test_package.py b/test/test_package.py index 1d670087..66d557f3 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -34,8 +34,6 @@ def setup(): Description="Test description.", URL="https://test.package") - yield package - def test_package(): from aurweb.db import session diff --git a/test/test_package_base.py b/test/test_package_base.py index dcb0eb9e..e0359f4f 100644 --- a/test/test_package_base.py +++ b/test/test_package_base.py @@ -5,8 +5,8 @@ from sqlalchemy.exc import IntegrityError from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.package_base import PackageBase +from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_user user = None @@ -19,10 +19,9 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - yield user + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) def test_package_base(): diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py index f110b123..316e7ca8 100644 --- a/test/test_package_keyword.py +++ b/test/test_package_keyword.py @@ -6,10 +6,10 @@ from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.package_base import PackageBase from aurweb.models.package_keyword import PackageKeyword +from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_user -user, pkgbase = None, None +user = pkgbase = None @pytest.fixture(autouse=True) @@ -20,15 +20,13 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) pkgbase = create(PackageBase, Name="beautiful-package", Maintainer=user) - yield pkgbase - def test_package_keyword(): pkg_keyword = create(PackageKeyword, diff --git a/test/test_routes.py b/test/test_routes.py index e4816231..f4bb063f 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -7,27 +7,28 @@ import pytest from fastapi.testclient import TestClient from aurweb.asgi import app -from aurweb.db import query +from aurweb.db import create, query from aurweb.models.account_type import AccountType +from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_user from aurweb.testing.requests import Request -client = TestClient(app) -user = None +user = client = None @pytest.fixture(autouse=True) def setup(): - global user + global user, client setup_test_db("Users", "Sessions") account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + client = TestClient(app) def test_index(): diff --git a/test/test_session.py b/test/test_session.py index 560f628c..2877ea7f 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -4,39 +4,38 @@ from unittest import mock import pytest +from aurweb.db import create, query from aurweb.models.account_type import AccountType -from aurweb.models.session import generate_unique_sid +from aurweb.models.session import Session, generate_unique_sid +from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_session, make_user -user, _session = None, None +user = session = None @pytest.fixture(autouse=True) def setup(): - from aurweb.db import session - - global user, _session + global user, session setup_test_db("Users", "Sessions") - account_type = session.query(AccountType).filter( - AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.org", - ResetKey="testReset", Passwd="testPassword", - AccountType=account_type) - _session = make_session(UsersID=user.ID, SessionID="testSession", - LastUpdateTS=datetime.utcnow()) + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + ResetKey="testReset", Passwd="testPassword", + AccountType=account_type) + session = create(Session, UsersID=user.ID, SessionID="testSession", + LastUpdateTS=datetime.utcnow()) def test_session(): - assert _session.SessionID == "testSession" - assert _session.UsersID == user.ID + assert session.SessionID == "testSession" + assert session.UsersID == user.ID def test_session_user_association(): # Make sure that the Session user attribute is correct. - assert _session.User == user + assert session.User == user def test_generate_unique_sid(): diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index fe9df047..4072549e 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -1,46 +1,37 @@ import pytest -from aurweb.db import query +from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint +from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_user TEST_SSH_PUBKEY = """ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCycoCi5yGCvSclH2wmNBUuwsYEzRZZBJaQquRc4ysl+Tg+/jiDkR3Zn9fIznC4KnFoyrIHzkKuePZ3bNDYwkZxkJKoWBCh4hXKDXSm87FMN0+VDC+1QxF/z0XaAGr/P6f4XukabyddypBdnHcZiplbw+YOSqcAE2TCqOlSXwNMOcF9U89UsR/Q9i9I52hlvU0q8+fZVGhou1KCowFSnHYtrr5KYJ04CXkJ13DkVf3+pjQWyrByvBcf1hGEaczlgfobrrv/y96jDhgfXucxliNKLdufDPPkii3LhhsNcDmmI1VZ3v0irKvd9WZuauqloobY84zEFcDTyjn0hxGjVeYFejm4fBnvjga0yZXORuWksdNfXWLDxFk6MDDd1jF0ExRbP+OxDuU4IVyIuDL7S3cnbf2YjGhkms/8voYT2OBE7FwNlfv98Kr0NUp51zpf55Arxn9j0Rz9xTA7FiODQgCn6iQ0SDtzUNL0IKTCw26xJY5gzMxbfpvzPQGeulx/ioM= kevr@volcano """ -user, ssh_pub_key = None, None +user = ssh_pub_key = None @pytest.fixture(autouse=True) def setup(): - from aurweb.db import session - global user, ssh_pub_key setup_test_db("Users", "SSHPubKeys") account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) assert account_type == user.AccountType assert account_type.ID == user.AccountTypeID - ssh_pub_key = SSHPubKey(UserID=user.ID, - Fingerprint="testFingerprint", - PubKey="testPubKey") - - session.add(ssh_pub_key) - session.commit() - - yield ssh_pub_key - - session.delete(ssh_pub_key) - session.commit() + ssh_pub_key = create(SSHPubKey, + UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey") def test_ssh_pub_key(): diff --git a/test/test_term.py b/test/test_term.py index 00397b33..aa1dfcc6 100644 --- a/test/test_term.py +++ b/test/test_term.py @@ -2,13 +2,14 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, delete, get_engine +from aurweb.db import create from aurweb.models.term import Term +from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) def setup(): - get_engine() + setup_test_db("Terms") def test_term_creation(): @@ -18,7 +19,6 @@ def test_term_creation(): assert term.Description == "Term description" assert term.URL == "https://fake_url.io" assert term.Revision == 1 - delete(Term, Term.ID == term.ID) def test_term_null_description_raises_exception(): diff --git a/test/test_user.py b/test/test_user.py index e8056681..8b4da61e 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -8,23 +8,20 @@ import pytest import aurweb.auth import aurweb.config -from aurweb.db import query +from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.ban import Ban from aurweb.models.session import Session from aurweb.models.ssh_pub_key import SSHPubKey from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.models import make_session, make_user from aurweb.testing.requests import Request -account_type, user = None, None +account_type = user = None @pytest.fixture(autouse=True) def setup(): - from aurweb.db import session - global account_type, user setup_test_db("Users", "Sessions", "Bans", "SSHPubKeys") @@ -32,15 +29,13 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = make_user(Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) def test_user_login_logout(): """ Test creating a user and reading its columns. """ - from aurweb.db import session - # Assert that make_user created a valid user. assert bool(user.ID) @@ -61,8 +56,8 @@ def test_user_login_logout(): assert "AURSID" in request.cookies # Expect that User session relationships work right. - user_session = session.query(Session).filter( - Session.UsersID == user.ID).first() + user_session = query(Session, + Session.UsersID == user.ID).first() assert user_session == user.session assert user.session.SessionID == sid assert user.session.User == user @@ -103,13 +98,9 @@ def test_user_login_twice(): def test_user_login_banned(): - from aurweb.db import session - # Add ban for the next 30 seconds. banned_timestamp = datetime.utcnow() + timedelta(seconds=30) - ban = Ban(IPAddress="127.0.0.1", BanTS=banned_timestamp) - session.add(ban) - session.commit() + create(Ban, IPAddress="127.0.0.1", BanTS=banned_timestamp) request = Request() request.client.host = "127.0.0.1" @@ -138,19 +129,14 @@ def test_legacy_user_authentication(): def test_user_login_with_outdated_sid(): - from aurweb.db import session - # Make a session with a LastUpdateTS 5 seconds ago, causing # user.login to update it with a new sid. - _session = make_session(UsersID=user.ID, SessionID="stub", - LastUpdateTS=datetime.utcnow().timestamp() - 5) + create(Session, UsersID=user.ID, SessionID="stub", + LastUpdateTS=datetime.utcnow().timestamp() - 5) sid = user.login(Request(), "testPassword") assert sid and user.is_authenticated() assert sid != "stub" - session.delete(_session) - session.commit() - def test_user_update_password(): user.update_password("secondPassword") @@ -169,21 +155,14 @@ def test_user_has_credential(): def test_user_ssh_pub_key(): - from aurweb.db import session - assert user.ssh_pub_key is None - ssh_pub_key = SSHPubKey(UserID=user.ID, - Fingerprint="testFingerprint", - PubKey="testPubKey") - session.add(ssh_pub_key) - session.commit() + ssh_pub_key = create(SSHPubKey, UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey") assert user.ssh_pub_key == ssh_pub_key - session.delete(ssh_pub_key) - session.commit() - def test_user_credential_types(): from aurweb.db import session @@ -203,8 +182,7 @@ def test_user_credential_types(): assert aurweb.auth.trusted_user_or_dev(user) developer_type = query(AccountType, - AccountType.AccountType == "Developer")\ - .first() + AccountType.AccountType == "Developer").first() user.AccountType = developer_type session.commit() From 943d97efac1f6fca6c823e0edb416b3c300f4b3d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 04:48:49 -0700 Subject: [PATCH 0227/1451] add License SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/license.py | 11 +++++++++++ test/test_license.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 aurweb/models/license.py create mode 100644 test/test_license.py diff --git a/aurweb/models/license.py b/aurweb/models/license.py new file mode 100644 index 00000000..1c174925 --- /dev/null +++ b/aurweb/models/license.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import mapper + +from aurweb.schema import Licenses + + +class License: + def __init__(self, Name: str = None): + self.Name = Name + + +mapper(License, Licenses) diff --git a/test/test_license.py b/test/test_license.py new file mode 100644 index 00000000..feb7a396 --- /dev/null +++ b/test/test_license.py @@ -0,0 +1,25 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create +from aurweb.models.license import License +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db("Licenses") + + +def test_license_creation(): + license = create(License, Name="Test License") + assert bool(license.ID) + assert license.Name == "Test License" + + +def test_license_null_name_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(License) + session.rollback() From 75cc0be189271ed3583486c0b66463f8e43605f4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 05:06:38 -0700 Subject: [PATCH 0228/1451] add PackageLicense SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_license.py | 27 +++++++++++++++++ test/test_package_license.py | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 aurweb/models/package_license.py create mode 100644 test/test_package_license.py diff --git a/aurweb/models/package_license.py b/aurweb/models/package_license.py new file mode 100644 index 00000000..187b113e --- /dev/null +++ b/aurweb/models/package_license.py @@ -0,0 +1,27 @@ +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.license import License +from aurweb.models.package import Package +from aurweb.schema import PackageLicenses + + +class PackageLicense: + def __init__(self, Package: Package = None, License: License = None): + self.Package = Package + self.License = License + + +properties = { + "Package": make_relationship(Package, + PackageLicenses.c.PackageID, + "package_license", + uselist=False), + "License": make_relationship(License, + PackageLicenses.c.LicenseID, + "package_license", + uselist=False) +} + +mapper(PackageLicense, PackageLicenses, properties=properties, + primary_key=[PackageLicenses.c.PackageID, PackageLicenses.c.LicenseID]) diff --git a/test/test_package_license.py b/test/test_package_license.py new file mode 100644 index 00000000..72eb3681 --- /dev/null +++ b/test/test_package_license.py @@ -0,0 +1,52 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query +from aurweb.models.account_type import AccountType +from aurweb.models.license import License +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_license import PackageLicense +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = license = pkgbase = package = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, license, pkgbase, package + + setup_test_db("Users", "PackageBases", "Packages", + "Licenses", "PackageLicenses") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + account_type=account_type) + + license = create(License, Name="Test License") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + package = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + + +def test_package_license(): + package_license = create(PackageLicense, Package=package, License=license) + assert package_license.License == license + assert package_license.Package == package + + +def test_package_license_null_package_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(PackageLicense, License=license) + session.rollback() + + +def test_package_license_null_license_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(PackageLicense, Package=package) + session.rollback() From a8a9c28783d606863b57066103e15b64d75fdb69 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 05:21:01 -0700 Subject: [PATCH 0229/1451] Jinja bugfix: add xmlns + xml:lang to This was not brought over during the initial commit involving partisl/layout.html. Signed-off-by: Kevin Morris --- templates/partials/layout.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/partials/layout.html b/templates/partials/layout.html index d30208a9..019ebff7 100644 --- a/templates/partials/layout.html +++ b/templates/partials/layout.html @@ -1,5 +1,6 @@ - + {% include 'partials/head.html' %} From 4201348dea2b74bfc172573209561f76b1a36597 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 05:34:27 -0700 Subject: [PATCH 0230/1451] add PackageGroup SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_group.py | 27 ++++++++++++++++++ test/test_package_group.py | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 aurweb/models/package_group.py create mode 100644 test/test_package_group.py diff --git a/aurweb/models/package_group.py b/aurweb/models/package_group.py new file mode 100644 index 00000000..8a32c00b --- /dev/null +++ b/aurweb/models/package_group.py @@ -0,0 +1,27 @@ +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.group import Group +from aurweb.models.package import Package +from aurweb.schema import PackageGroups + + +class PackageGroup: + def __init__(self, Package: Package = None, Group: Group = None): + self.Package = Package + self.Group = Group + + +properties = { + "Package": make_relationship(Package, + PackageGroups.c.PackageID, + "package_group", + uselist=False), + "Group": make_relationship(Group, + PackageGroups.c.GroupID, + "package_group", + uselist=False) +} + +mapper(PackageGroup, PackageGroups, properties=properties, + primary_key=[PackageGroups.c.PackageID, PackageGroups.c.GroupID]) diff --git a/test/test_package_group.py b/test/test_package_group.py new file mode 100644 index 00000000..28047a7f --- /dev/null +++ b/test/test_package_group.py @@ -0,0 +1,52 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query +from aurweb.models.account_type import AccountType +from aurweb.models.group import Group +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_group import PackageGroup +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = group = pkgbase = package = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, group, pkgbase, package + + setup_test_db("Users", "PackageBases", "Packages", + "Groups", "PackageGroups") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + account_type=account_type) + + group = create(Group, Name="Test Group") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + package = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + + +def test_package_group(): + package_group = create(PackageGroup, Package=package, Group=group) + assert package_group.Group == group + assert package_group.Package == package + + +def test_package_group_null_package_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(PackageGroup, Group=group) + session.rollback() + + +def test_package_group_null_group_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(PackageGroup, Package=package) + session.rollback() From 068c8ba638dd032df917d82af8fe6ffe70264ab3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 06:44:24 -0700 Subject: [PATCH 0231/1451] add DependencyType SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/dependency_type.py | 11 +++++++++++ test/test_dependency_type.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 aurweb/models/dependency_type.py create mode 100644 test/test_dependency_type.py diff --git a/aurweb/models/dependency_type.py b/aurweb/models/dependency_type.py new file mode 100644 index 00000000..87b38069 --- /dev/null +++ b/aurweb/models/dependency_type.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import mapper + +from aurweb.schema import DependencyTypes + + +class DependencyType: + def __init__(self, Name: str = None): + self.Name = Name + + +mapper(DependencyType, DependencyTypes) diff --git a/test/test_dependency_type.py b/test/test_dependency_type.py new file mode 100644 index 00000000..6c37cc58 --- /dev/null +++ b/test/test_dependency_type.py @@ -0,0 +1,31 @@ +import pytest + +from aurweb.db import create, delete, query +from aurweb.models.dependency_type import DependencyType +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db() + + +def test_dependency_types(): + dep_types = ["depends", "makedepends", "checkdepends", "optdepends"] + for dep_type in dep_types: + dependency_type = query(DependencyType, + DependencyType.Name == dep_type).first() + assert dependency_type is not None + + +def test_dependency_type_creation(): + dependency_type = create(DependencyType, Name="Test Type") + assert bool(dependency_type.ID) + assert dependency_type.Name == "Test Type" + delete(DependencyType, DependencyType.ID == dependency_type.ID) + + +def test_dependency_type_null_name_uses_default(): + dependency_type = create(DependencyType) + assert dependency_type.Name == str() + delete(DependencyType, DependencyType.ID == dependency_type.ID) From e401b92acb82a52f62441f8decb209448ce457a1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 07:21:54 -0700 Subject: [PATCH 0232/1451] add PackageDependency (PackageDepends) ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_dependency.py | 31 ++++++++ test/test_package_dependency.py | 113 ++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 aurweb/models/package_dependency.py create mode 100644 test/test_package_dependency.py diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py new file mode 100644 index 00000000..ae6ae62a --- /dev/null +++ b/aurweb/models/package_dependency.py @@ -0,0 +1,31 @@ +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.dependency_type import DependencyType +from aurweb.models.package import Package +from aurweb.schema import PackageDepends + + +class PackageDependency: + def __init__(self, Package: Package = None, + DependencyType: DependencyType = None, + DepName: str = None, DepDesc: str = None, + DepCondition: str = None, DepArch: str = None): + self.Package = Package + self.DependencyType = DependencyType + self.DepName = DepName # nullable=False + self.DepDesc = DepDesc + self.DepCondition = DepCondition + self.DepArch = DepArch + + +properties = { + "Package": make_relationship(Package, PackageDepends.c.PackageID, + "package_dependencies"), + "DependencyType": make_relationship(DependencyType, + PackageDepends.c.DepTypeID, + "package_dependencies") +} + +mapper(PackageDependency, PackageDepends, properties=properties, + primary_key=[PackageDepends.c.PackageID, PackageDepends.c.DepTypeID]) diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py new file mode 100644 index 00000000..fc21a08c --- /dev/null +++ b/test/test_package_dependency.py @@ -0,0 +1,113 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query +from aurweb.models.account_type import AccountType +from aurweb.models.dependency_type import DependencyType +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_dependency import PackageDependency +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = package = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase, package + + setup_test_db("Users", "PackageBases", "Packages", "PackageDepends") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + pkgbase = create(PackageBase, + Name="test-package", + Maintainer=user) + package = create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") + + +def test_package_dependencies(): + depends = query(DependencyType, DependencyType.Name == "depends").first() + pkgdep = create(PackageDependency, Package=package, + DependencyType=depends, + DepName="test-dep") + assert pkgdep.DepName == "test-dep" + assert pkgdep.Package == package + assert pkgdep.DependencyType == depends + assert pkgdep in depends.package_dependencies + assert pkgdep in package.package_dependencies + + makedepends = query(DependencyType, + DependencyType.Name == "makedepends").first() + pkgdep = create(PackageDependency, Package=package, + DependencyType=makedepends, + DepName="test-dep") + assert pkgdep.DepName == "test-dep" + assert pkgdep.Package == package + assert pkgdep.DependencyType == makedepends + assert pkgdep in makedepends.package_dependencies + assert pkgdep in package.package_dependencies + + checkdepends = query(DependencyType, + DependencyType.Name == "checkdepends").first() + pkgdep = create(PackageDependency, Package=package, + DependencyType=checkdepends, + DepName="test-dep") + assert pkgdep.DepName == "test-dep" + assert pkgdep.Package == package + assert pkgdep.DependencyType == checkdepends + assert pkgdep in checkdepends.package_dependencies + assert pkgdep in package.package_dependencies + + optdepends = query(DependencyType, + DependencyType.Name == "optdepends").first() + pkgdep = create(PackageDependency, Package=package, + DependencyType=optdepends, + DepName="test-dep") + assert pkgdep.DepName == "test-dep" + assert pkgdep.Package == package + assert pkgdep.DependencyType == optdepends + assert pkgdep in optdepends.package_dependencies + assert pkgdep in package.package_dependencies + + +def test_package_dependencies_null_package_raises_exception(): + from aurweb.db import session + + depends = query(DependencyType, DependencyType.Name == "depends").first() + with pytest.raises(IntegrityError): + create(PackageDependency, + DependencyType=depends, + DepName="test-dep") + session.rollback() + + +def test_package_dependencies_null_dependency_type_raises_exception(): + from aurweb.db import session + + with pytest.raises(IntegrityError): + create(PackageDependency, + Package=package, + DepName="test-dep") + session.rollback() + + +def test_package_dependencies_null_depname_raises_exception(): + from aurweb.db import session + + depends = query(DependencyType, DependencyType.Name == "depends").first() + with pytest.raises(IntegrityError): + create(PackageDependency, + Package=package, + DependencyType=depends) + session.rollback() From a9cfbce11e3c16c22d168f8fc55238f17ea78273 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 07:37:05 -0700 Subject: [PATCH 0233/1451] add RelationType SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/relation_type.py | 11 +++++++++++ test/test_relation_type.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 aurweb/models/relation_type.py create mode 100644 test/test_relation_type.py diff --git a/aurweb/models/relation_type.py b/aurweb/models/relation_type.py new file mode 100644 index 00000000..b4d1efbc --- /dev/null +++ b/aurweb/models/relation_type.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import mapper + +from aurweb.schema import RelationTypes + + +class RelationType: + def __init__(self, Name: str = None): + self.Name = Name + + +mapper(RelationType, RelationTypes) diff --git a/test/test_relation_type.py b/test/test_relation_type.py new file mode 100644 index 00000000..bf23505c --- /dev/null +++ b/test/test_relation_type.py @@ -0,0 +1,32 @@ +import pytest + +from aurweb.db import create, delete, query +from aurweb.models.relation_type import RelationType +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db() + + +def test_relation_type_creation(): + relation_type = create(RelationType, Name="test-relation") + assert bool(relation_type.ID) + assert relation_type.Name == "test-relation" + + delete(RelationType, RelationType.ID == relation_type.ID) + + +def test_relation_types(): + conflicts = query(RelationType, RelationType.Name == "conflicts").first() + assert conflicts is not None + assert conflicts.Name == "conflicts" + + provides = query(RelationType, RelationType.Name == "provides").first() + assert provides is not None + assert provides.Name == "provides" + + replaces = query(RelationType, RelationType.Name == "replaces").first() + assert replaces is not None + assert replaces.Name == "replaces" From 2b83d2fb6bb8b5066053220b5929d5d67333f9dd Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 1 Jun 2021 07:52:22 -0700 Subject: [PATCH 0234/1451] add PackageRelation SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_relation.py | 33 ++++++++++ test/test_package_relation.py | 100 ++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 aurweb/models/package_relation.py create mode 100644 test/test_package_relation.py diff --git a/aurweb/models/package_relation.py b/aurweb/models/package_relation.py new file mode 100644 index 00000000..196f1dee --- /dev/null +++ b/aurweb/models/package_relation.py @@ -0,0 +1,33 @@ +from sqlalchemy.orm import mapper + +from aurweb.db import make_relationship +from aurweb.models.package import Package +from aurweb.models.relation_type import RelationType +from aurweb.schema import PackageRelations + + +class PackageRelation: + def __init__(self, Package: Package = None, + RelationType: RelationType = None, + RelName: str = None, RelCondition: str = None, + RelArch: str = None): + self.Package = Package + self.RelationType = RelationType + self.RelName = RelName # nullable=False + self.RelCondition = RelCondition + self.RelArch = RelArch + + +properties = { + "Package": make_relationship(Package, PackageRelations.c.PackageID, + "package_relations"), + "RelationType": make_relationship(RelationType, + PackageRelations.c.RelTypeID, + "package_relations") +} + +mapper(PackageRelation, PackageRelations, properties=properties, + primary_key=[ + PackageRelations.c.PackageID, + PackageRelations.c.RelTypeID + ]) diff --git a/test/test_package_relation.py b/test/test_package_relation.py new file mode 100644 index 00000000..dd0455cd --- /dev/null +++ b/test/test_package_relation.py @@ -0,0 +1,100 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query +from aurweb.models.account_type import AccountType +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_relation import PackageRelation +from aurweb.models.relation_type import RelationType +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = package = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase, package + + setup_test_db("Users", "PackageBases", "Packages", "PackageRelations") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + pkgbase = create(PackageBase, + Name="test-package", + Maintainer=user) + package = create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") + + +def test_package_dependencies(): + conflicts = query(RelationType, RelationType.Name == "conflicts").first() + pkgrel = create(PackageRelation, Package=package, + RelationType=conflicts, + RelName="test-relation") + assert pkgrel.RelName == "test-relation" + assert pkgrel.Package == package + assert pkgrel.RelationType == conflicts + assert pkgrel in conflicts.package_relations + assert pkgrel in package.package_relations + + provides = query(RelationType, RelationType.Name == "provides").first() + pkgrel = create(PackageRelation, Package=package, + RelationType=provides, + RelName="test-relation") + assert pkgrel.RelName == "test-relation" + assert pkgrel.Package == package + assert pkgrel.RelationType == provides + assert pkgrel in provides.package_relations + assert pkgrel in package.package_relations + + replaces = query(RelationType, RelationType.Name == "replaces").first() + pkgrel = create(PackageRelation, Package=package, + RelationType=replaces, + RelName="test-relation") + assert pkgrel.RelName == "test-relation" + assert pkgrel.Package == package + assert pkgrel.RelationType == replaces + assert pkgrel in replaces.package_relations + assert pkgrel in package.package_relations + + +def test_package_dependencies_null_package_raises_exception(): + from aurweb.db import session + + conflicts = query(RelationType, RelationType.Name == "conflicts").first() + with pytest.raises(IntegrityError): + create(PackageRelation, + RelationType=conflicts, + RelName="test-relation") + session.rollback() + + +def test_package_dependencies_null_dependency_type_raises_exception(): + from aurweb.db import session + + with pytest.raises(IntegrityError): + create(PackageRelation, + Package=package, + RelName="test-relation") + session.rollback() + + +def test_package_dependencies_null_depname_raises_exception(): + from aurweb.db import session + + depends = query(RelationType, RelationType.Name == "depends").first() + with pytest.raises(IntegrityError): + create(PackageRelation, + Package=package, + RelationType=depends) + session.rollback() From a65a60604ab09e83f18cec58afb7a807b1eb2b30 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 3 Jun 2021 10:51:46 -0700 Subject: [PATCH 0235/1451] add ApiRateLimit SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/api_rate_limit.py | 15 +++++++++++++ test/test_api_rate_limit.py | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 aurweb/models/api_rate_limit.py create mode 100644 test/test_api_rate_limit.py diff --git a/aurweb/models/api_rate_limit.py b/aurweb/models/api_rate_limit.py new file mode 100644 index 00000000..44e7a463 --- /dev/null +++ b/aurweb/models/api_rate_limit.py @@ -0,0 +1,15 @@ +from sqlalchemy.orm import mapper + +from aurweb.schema import ApiRateLimit as _ApiRateLimit + + +class ApiRateLimit: + def __init__(self, IP: str = None, + Requests: int = None, + WindowStart: int = None): + self.IP = IP + self.Requests = Requests + self.WindowStart = WindowStart + + +mapper(ApiRateLimit, _ApiRateLimit, primary_key=[_ApiRateLimit.c.IP]) diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py new file mode 100644 index 00000000..91ab5854 --- /dev/null +++ b/test/test_api_rate_limit.py @@ -0,0 +1,40 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create +from aurweb.models.api_rate_limit import ApiRateLimit +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db("ApiRateLimit") + + +def test_api_rate_key_creation(): + rate = create(ApiRateLimit, IP="127.0.0.1", Requests=10, WindowStart=1) + assert rate.IP == "127.0.0.1" + assert rate.Requests == 10 + assert rate.WindowStart == 1 + + +def test_api_rate_key_null_ip_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(ApiRateLimit, Requests=10, WindowStart=1) + session.rollback() + + +def test_api_rate_key_null_requests_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(ApiRateLimit, IP="127.0.0.1", WindowStart=1) + session.rollback() + + +def test_api_rate_key_null_window_start_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(ApiRateLimit, IP="127.0.0.1", WindowStart=1) + session.rollback() From e5df083d4553440af309be89c5dcdd11d68dc7d5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 3 Jun 2021 22:56:47 -0700 Subject: [PATCH 0236/1451] use String(max_len) for DECIMAL types with sqlite This solves an issue where DECIMAL is not native to sqlite by using a string to store values and converting them to float in user code. Signed-off-by: Kevin Morris --- aurweb/schema.py | 10 ++++++++-- web/html/addvote.php | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/aurweb/schema.py b/aurweb/schema.py index f0162045..0d40e272 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -107,7 +107,10 @@ PackageBases = Table( Column('ID', INTEGER(unsigned=True), primary_key=True), Column('Name', String(255), nullable=False, unique=True), Column('NumVotes', INTEGER(unsigned=True), nullable=False, server_default=text("0")), - Column('Popularity', DECIMAL(10, 6, unsigned=True), nullable=False, server_default=text("0")), + Column('Popularity', + DECIMAL(10, 6, unsigned=True) + if db_backend == "mysql" else String(16), # Stubbed out to test. + nullable=False, server_default=text("0")), Column('OutOfDateTS', BIGINT(unsigned=True)), Column('FlaggerComment', Text, nullable=False), Column('SubmittedTS', BIGINT(unsigned=True), nullable=False), @@ -383,7 +386,10 @@ TU_VoteInfo = Table( Column('User', String(32), nullable=False), Column('Submitted', BIGINT(unsigned=True), nullable=False), Column('End', BIGINT(unsigned=True), nullable=False), - Column('Quorum', DECIMAL(2, 2, unsigned=True), nullable=False), + Column('Quorum', + DECIMAL(2, 2, unsigned=True) + if db_backend == "mysql" else String(4), + nullable=False), Column('SubmitterID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), Column('Yes', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), Column('No', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), diff --git a/web/html/addvote.php b/web/html/addvote.php index 3672c031..70280cfd 100644 --- a/web/html/addvote.php +++ b/web/html/addvote.php @@ -67,8 +67,11 @@ if (has_credential(CRED_TU_ADD_VOTE)) { } } - if (!empty($_POST['addVote']) && empty($error)) { - add_tu_proposal($_POST['agenda'], $_POST['user'], $len, $quorum, $uid); + if (!empty($_POST['addVote']) && empty($error)) { + // Convert $quorum to a String of maximum length "12.34" (5). + $quorum_str = substr(strval($quorum), min(5, strlen($quorum)); + add_tu_proposal($_POST['agenda'], $_POST['user'], + $len, $quorum_str, $uid); print "

    " . __("New proposal submitted.") . "

    \n"; } else { From d7481b96499f06be0d9ca983bb9efc578b38eb97 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 4 Jun 2021 00:31:15 -0700 Subject: [PATCH 0237/1451] modify schema primary keys to be nullable+defaulted This fixes SQLAlchemy warnings related to primary keys not having an auto_increment or nullable. We've done this by making all foreign primary keys nullable. In ApiRateLimit's case, we can set a default str to act as a null, which seems a bit more sensible. Signed-off-by: Kevin Morris --- aurweb/models/package_group.py | 12 ++++++++++++ aurweb/models/package_keyword.py | 7 +++++++ aurweb/models/package_license.py | 14 ++++++++++++++ aurweb/schema.py | 12 ++++++------ test/test_api_rate_limit.py | 8 +++----- 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/aurweb/models/package_group.py b/aurweb/models/package_group.py index 8a32c00b..c155fe00 100644 --- a/aurweb/models/package_group.py +++ b/aurweb/models/package_group.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import mapper +from sqlalchemy.exc import IntegrityError from aurweb.db import make_relationship from aurweb.models.group import Group @@ -9,7 +10,18 @@ from aurweb.schema import PackageGroups class PackageGroup: def __init__(self, Package: Package = None, Group: Group = None): self.Package = Package + if not self.Package: + raise IntegrityError( + statement="Primary key PackageID cannot be null.", + orig="PackageGroups.PackageID", + params=("NULL")) + self.Group = Group + if not self.Group: + raise IntegrityError( + statement="Primary key GroupID cannot be null.", + orig="PackageGroups.GroupID", + params=("NULL")) properties = { diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py index 87d97558..4a66f38e 100644 --- a/aurweb/models/package_keyword.py +++ b/aurweb/models/package_keyword.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import mapper +from sqlalchemy.exc import IntegrityError from aurweb.db import make_relationship from aurweb.models.package_base import PackageBase @@ -10,6 +11,12 @@ class PackageKeyword: PackageBase: PackageBase = None, Keyword: str = None): self.PackageBase = PackageBase + if not self.PackageBase: + raise IntegrityError( + statement="Primary key PackageBaseID cannot be null.", + orig="PackageKeywords.PackageBaseID", + params=("NULL")) + self.Keyword = Keyword diff --git a/aurweb/models/package_license.py b/aurweb/models/package_license.py index 187b113e..6f23f84a 100644 --- a/aurweb/models/package_license.py +++ b/aurweb/models/package_license.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import mapper +from sqlalchemy.exc import IntegrityError from aurweb.db import make_relationship from aurweb.models.license import License @@ -9,7 +10,18 @@ from aurweb.schema import PackageLicenses class PackageLicense: def __init__(self, Package: Package = None, License: License = None): self.Package = Package + if not self.Package: + raise IntegrityError( + statement="Primary key PackageID cannot be null.", + orig="PackageLicenses.PackageID", + params=("NULL")) + self.License = License + if not self.License: + raise IntegrityError( + statement="Primary key LicenseID cannot be null.", + orig="PackageLicenses.LicenseID", + params=("NULL")) properties = { @@ -21,6 +33,8 @@ properties = { PackageLicenses.c.LicenseID, "package_license", uselist=False) + + } mapper(PackageLicense, PackageLicenses, properties=properties, diff --git a/aurweb/schema.py b/aurweb/schema.py index 0d40e272..fa8923a3 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -133,7 +133,7 @@ PackageBases = Table( # Keywords of package bases PackageKeywords = Table( 'PackageKeywords', metadata, - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), primary_key=True, nullable=True), Column('Keyword', String(255), primary_key=True, nullable=False, server_default=text("''")), mysql_engine='InnoDB', mysql_charset='utf8mb4', @@ -170,8 +170,8 @@ Licenses = Table( # Information about package-license-relations PackageLicenses = Table( 'PackageLicenses', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=False), - Column('LicenseID', ForeignKey('Licenses.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=True), + Column('LicenseID', ForeignKey('Licenses.ID', ondelete='CASCADE'), primary_key=True, nullable=True), mysql_engine='InnoDB', ) @@ -190,8 +190,8 @@ Groups = Table( # Information about package-group-relations PackageGroups = Table( 'PackageGroups', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=False), - Column('GroupID', ForeignKey('Groups.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=True), + Column('GroupID', ForeignKey('Groups.ID', ondelete='CASCADE'), primary_key=True, nullable=True), mysql_engine='InnoDB', ) @@ -445,7 +445,7 @@ AcceptedTerms = Table( # Rate limits for API ApiRateLimit = Table( 'ApiRateLimit', metadata, - Column('IP', String(45), primary_key=True), + Column('IP', String(45), primary_key=True, unique=True, default=str()), Column('Requests', INTEGER(11), nullable=False), Column('WindowStart', BIGINT(20), nullable=False), Index('ApiRateLimitWindowStart', 'WindowStart'), diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py index 91ab5854..c599ddcf 100644 --- a/test/test_api_rate_limit.py +++ b/test/test_api_rate_limit.py @@ -19,11 +19,9 @@ def test_api_rate_key_creation(): assert rate.WindowStart == 1 -def test_api_rate_key_null_ip_raises_exception(): - from aurweb.db import session - with pytest.raises(IntegrityError): - create(ApiRateLimit, Requests=10, WindowStart=1) - session.rollback() +def test_api_rate_key_ip_default(): + api_rate_limit = create(ApiRateLimit, Requests=10, WindowStart=1) + assert api_rate_limit.IP == str() def test_api_rate_key_null_requests_raises_exception(): From aecb64947354283a9b2dd357a67e997c78f4adac Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 4 Jun 2021 00:43:57 -0700 Subject: [PATCH 0238/1451] use mysql backend in config.dev First off: This commit changes the default development database backend to mysql. sqlite, however, is still completely supported with the caveat that a user must now modify config.dev to use the sqlite backend. While looking into this, it was discovered that our SQLAlchemy backend for mysql (mysql-connector) completely broke model attributes when we switched to utf8mb4_bin (binary) -- it does not correct the correct conversion to and from binary utf8mb4. The new, replacement dependency mysqlclient does. mysqlclient is also recommended in SQLAlchemy documentation as the "best" one available. The mysqlclient backend uses a different exception flow then sqlite, and so tests expecting IntegrityError has to be modified to expect OperationalError from sqlalchemy.exc. So, for each model that we define, check keys that can't be NULL and raise sqlalchemy.exc.IntegrityError if we have to. This way we keep our exceptions uniform. Signed-off-by: Kevin Morris --- aurweb/db.py | 39 ++++-- aurweb/initdb.py | 6 +- aurweb/models/accepted_term.py | 13 ++ aurweb/models/api_rate_limit.py | 13 ++ aurweb/models/group.py | 6 + aurweb/models/license.py | 6 + aurweb/models/package.py | 13 ++ aurweb/models/package_base.py | 7 + aurweb/models/package_dependency.py | 21 ++- aurweb/models/package_group.py | 2 +- aurweb/models/package_keyword.py | 2 +- aurweb/models/package_license.py | 2 +- aurweb/models/package_relation.py | 19 +++ aurweb/models/session.py | 12 +- aurweb/models/term.py | 13 ++ conf/config.dev | 20 +-- test/Makefile | 2 +- test/test_accounts_routes.py | 42 ++++-- test/test_api_rate_limit.py | 2 +- test/test_auth.py | 13 +- test/test_ban.py | 3 +- test/test_db.py | 202 ++++++++++++++++++++-------- test/test_initdb.py | 20 +-- test/test_package_relation.py | 18 ++- test/test_session.py | 2 +- 25 files changed, 363 insertions(+), 135 deletions(-) diff --git a/aurweb/db.py b/aurweb/db.py index 500cf95a..590712e0 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -98,9 +98,11 @@ def get_sqlalchemy_url(): param_query = None else: port = None - param_query = {'unix_socket': aurweb.config.get('database', 'socket')} + param_query = { + 'unix_socket': aurweb.config.get('database', 'socket') + } return constructor( - 'mysql+mysqlconnector', + 'mysql+mysqldb', username=aurweb.config.get('database', 'user'), password=aurweb.config.get('database', 'password'), host=aurweb.config.get('database', 'host'), @@ -117,7 +119,7 @@ def get_sqlalchemy_url(): raise ValueError('unsupported database backend') -def get_engine(): +def get_engine(echo: bool = False): """ Return the global SQLAlchemy engine. @@ -135,13 +137,24 @@ def get_engine(): # check_same_thread is for a SQLite technicality # https://fastapi.tiangolo.com/tutorial/sql-databases/#note connect_args["check_same_thread"] = False - engine = create_engine(get_sqlalchemy_url(), connect_args=connect_args) + + engine = create_engine(get_sqlalchemy_url(), + connect_args=connect_args, + echo=echo) Session = sessionmaker(autocommit=False, autoflush=True, bind=engine) session = Session() return engine +def kill_engine(): + global engine, Session, session + if engine: + session.close() + engine.dispose() + engine = Session = session = None + + def connect(): """ Return an SQLAlchemy connection. Connections are usually pooled. See @@ -160,8 +173,7 @@ class ConnectionExecutor: def __init__(self, conn, backend=aurweb.config.get("database", "backend")): self._conn = conn if backend == "mysql": - import mysql.connector - self._paramstyle = mysql.connector.paramstyle + self._paramstyle = "format" elif backend == "sqlite": import sqlite3 self._paramstyle = sqlite3.paramstyle @@ -197,18 +209,17 @@ class Connection: aur_db_backend = aurweb.config.get('database', 'backend') if aur_db_backend == 'mysql': - import mysql.connector + import MySQLdb aur_db_host = aurweb.config.get('database', 'host') aur_db_name = aurweb.config.get('database', 'name') aur_db_user = aurweb.config.get('database', 'user') aur_db_pass = aurweb.config.get('database', 'password') aur_db_socket = aurweb.config.get('database', 'socket') - self._conn = mysql.connector.connect(host=aur_db_host, - user=aur_db_user, - passwd=aur_db_pass, - db=aur_db_name, - unix_socket=aur_db_socket, - buffered=True) + self._conn = MySQLdb.connect(host=aur_db_host, + user=aur_db_user, + passwd=aur_db_pass, + db=aur_db_name, + unix_socket=aur_db_socket) elif aur_db_backend == 'sqlite': import sqlite3 aur_db_name = aurweb.config.get('database', 'name') @@ -217,7 +228,7 @@ class Connection: else: raise ValueError('unsupported database backend') - self._conn = ConnectionExecutor(self._conn) + self._conn = ConnectionExecutor(self._conn, aur_db_backend) def execute(self, query, params=()): return self._conn.execute(query, params) diff --git a/aurweb/initdb.py b/aurweb/initdb.py index 5f55bfc9..46f079c0 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -2,7 +2,6 @@ import argparse import alembic.command import alembic.config -import sqlalchemy import aurweb.db import aurweb.schema @@ -34,6 +33,8 @@ def feed_initial_data(conn): def run(args): + aurweb.config.rehash() + # Ensure Alembic is fine before we do the real work, in order not to fail at # the last step and leave the database in an inconsistent state. The # configuration is loaded lazily, so we query it to force its loading. @@ -42,8 +43,7 @@ def run(args): alembic_config.get_main_option('script_location') alembic_config.attributes["configure_logger"] = False - engine = sqlalchemy.create_engine(aurweb.db.get_sqlalchemy_url(), - echo=(args.verbose >= 1)) + engine = aurweb.db.get_engine(echo=(args.verbose >= 1)) aurweb.schema.metadata.create_all(engine) feed_initial_data(engine.connect()) diff --git a/aurweb/models/accepted_term.py b/aurweb/models/accepted_term.py index 6e8ffe99..483109f1 100644 --- a/aurweb/models/accepted_term.py +++ b/aurweb/models/accepted_term.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.db import make_relationship @@ -11,7 +12,19 @@ class AcceptedTerm: User: User = None, Term: Term = None, Revision: int = None): self.User = User + if not self.User: + raise IntegrityError( + statement="Foreign key UserID cannot be null.", + orig="AcceptedTerms.UserID", + params=("NULL")) + self.Term = Term + if not self.Term: + raise IntegrityError( + statement="Foreign key TermID cannot be null.", + orig="AcceptedTerms.TermID", + params=("NULL")) + self.Revision = Revision diff --git a/aurweb/models/api_rate_limit.py b/aurweb/models/api_rate_limit.py index 44e7a463..8b945b6a 100644 --- a/aurweb/models/api_rate_limit.py +++ b/aurweb/models/api_rate_limit.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.schema import ApiRateLimit as _ApiRateLimit @@ -8,8 +9,20 @@ class ApiRateLimit: Requests: int = None, WindowStart: int = None): self.IP = IP + self.Requests = Requests + if self.Requests is None: + raise IntegrityError( + statement="Column Requests cannot be null.", + orig="ApiRateLimit.Requests", + params=("NULL")) + self.WindowStart = WindowStart + if self.WindowStart is None: + raise IntegrityError( + statement="Column WindowStart cannot be null.", + orig="ApiRateLimit.WindowStart", + params=("NULL")) mapper(ApiRateLimit, _ApiRateLimit, primary_key=[_ApiRateLimit.c.IP]) diff --git a/aurweb/models/group.py b/aurweb/models/group.py index 5d4f3834..c5583eb4 100644 --- a/aurweb/models/group.py +++ b/aurweb/models/group.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.schema import Groups @@ -6,6 +7,11 @@ from aurweb.schema import Groups class Group: def __init__(self, Name: str = None): self.Name = Name + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="Groups.Name", + params=("NULL")) mapper(Group, Groups) diff --git a/aurweb/models/license.py b/aurweb/models/license.py index 1c174925..bcc02713 100644 --- a/aurweb/models/license.py +++ b/aurweb/models/license.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.schema import Licenses @@ -6,6 +7,11 @@ from aurweb.schema import Licenses class License: def __init__(self, Name: str = None): self.Name = Name + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="Licenses.Name", + params=("NULL")) mapper(License, Licenses) diff --git a/aurweb/models/package.py b/aurweb/models/package.py index fa82bb74..28a13791 100644 --- a/aurweb/models/package.py +++ b/aurweb/models/package.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.db import make_relationship @@ -11,7 +12,19 @@ class Package: Name: str = None, Version: str = None, Description: str = None, URL: str = None): self.PackageBase = PackageBase + if not self.PackageBase: + raise IntegrityError( + statement="Foreign key UserID cannot be null.", + orig="Packages.PackageBaseID", + params=("NULL")) + self.Name = Name + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="Packages.Name", + params=("NULL")) + self.Version = Version self.Description = Description self.URL = URL diff --git a/aurweb/models/package_base.py b/aurweb/models/package_base.py index 57e5a46b..699559d5 100644 --- a/aurweb/models/package_base.py +++ b/aurweb/models/package_base.py @@ -1,5 +1,6 @@ from datetime import datetime +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.db import make_relationship @@ -12,6 +13,12 @@ class PackageBase: Maintainer: User = None, Submitter: User = None, Packager: User = None, **kwargs): self.Name = Name + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="PackageBases.Name", + params=("NULL")) + self.Flagger = Flagger self.Maintainer = Maintainer self.Submitter = Submitter diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index ae6ae62a..21801802 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.db import make_relationship @@ -12,8 +13,26 @@ class PackageDependency: DepName: str = None, DepDesc: str = None, DepCondition: str = None, DepArch: str = None): self.Package = Package + if not self.Package: + raise IntegrityError( + statement="Foreign key PackageID cannot be null.", + orig="PackageDependencies.PackageID", + params=("NULL")) + self.DependencyType = DependencyType - self.DepName = DepName # nullable=False + if not self.DependencyType: + raise IntegrityError( + statement="Foreign key DepTypeID cannot be null.", + orig="PackageDependencies.DepTypeID", + params=("NULL")) + + self.DepName = DepName + if not self.DepName: + raise IntegrityError( + statement="Column DepName cannot be null.", + orig="PackageDependencies.DepName", + params=("NULL")) + self.DepDesc = DepDesc self.DepCondition = DepCondition self.DepArch = DepArch diff --git a/aurweb/models/package_group.py b/aurweb/models/package_group.py index c155fe00..19a11c80 100644 --- a/aurweb/models/package_group.py +++ b/aurweb/models/package_group.py @@ -1,5 +1,5 @@ -from sqlalchemy.orm import mapper from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import mapper from aurweb.db import make_relationship from aurweb.models.group import Group diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py index 4a66f38e..2bae223c 100644 --- a/aurweb/models/package_keyword.py +++ b/aurweb/models/package_keyword.py @@ -1,5 +1,5 @@ -from sqlalchemy.orm import mapper from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import mapper from aurweb.db import make_relationship from aurweb.models.package_base import PackageBase diff --git a/aurweb/models/package_license.py b/aurweb/models/package_license.py index 6f23f84a..491874a4 100644 --- a/aurweb/models/package_license.py +++ b/aurweb/models/package_license.py @@ -1,5 +1,5 @@ -from sqlalchemy.orm import mapper from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import mapper from aurweb.db import make_relationship from aurweb.models.license import License diff --git a/aurweb/models/package_relation.py b/aurweb/models/package_relation.py index 196f1dee..d9ade727 100644 --- a/aurweb/models/package_relation.py +++ b/aurweb/models/package_relation.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.db import make_relationship @@ -12,8 +13,26 @@ class PackageRelation: RelName: str = None, RelCondition: str = None, RelArch: str = None): self.Package = Package + if not self.Package: + raise IntegrityError( + statement="Foreign key PackageID cannot be null.", + orig="PackageRelations.PackageID", + params=("NULL")) + self.RelationType = RelationType + if not self.RelationType: + raise IntegrityError( + statement="Foreign key RelTypeID cannot be null.", + orig="PackageRelations.RelTypeID", + params=("NULL")) + self.RelName = RelName # nullable=False + if not self.RelName: + raise IntegrityError( + statement="Column RelName cannot be null.", + orig="PackageRelations.RelName", + params=("NULL")) + self.RelCondition = RelCondition self.RelArch = RelArch diff --git a/aurweb/models/session.py b/aurweb/models/session.py index 60749303..f1e0fff5 100644 --- a/aurweb/models/session.py +++ b/aurweb/models/session.py @@ -1,16 +1,20 @@ -from sqlalchemy import Column, Integer +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, mapper, relationship -from aurweb.db import make_random_value +from aurweb.db import make_random_value, query from aurweb.models.user import User from aurweb.schema import Sessions class Session: - UsersID = Column(Integer, nullable=True) - def __init__(self, **kwargs): self.UsersID = kwargs.get("UsersID") + if not query(User, User.ID == self.UsersID).first(): + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="Sessions.UsersID", + params=("NULL")) + self.SessionID = kwargs.get("SessionID") self.LastUpdateTS = kwargs.get("LastUpdateTS") diff --git a/aurweb/models/term.py b/aurweb/models/term.py index 1b4902f7..1a0780df 100644 --- a/aurweb/models/term.py +++ b/aurweb/models/term.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import mapper from aurweb.schema import Terms @@ -8,7 +9,19 @@ class Term: Description: str = None, URL: str = None, Revision: int = None): self.Description = Description + if not self.Description: + raise IntegrityError( + statement="Column Description cannot be null.", + orig="Terms.Description", + params=("NULL")) + self.URL = URL + if not self.URL: + raise IntegrityError( + statement="Column URL cannot be null.", + orig="Terms.URL", + params=("NULL")) + self.Revision = Revision diff --git a/conf/config.dev b/conf/config.dev index 94775a92..45d940e6 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -6,17 +6,19 @@ ; development-specific options too. [database] -backend = sqlite -name = YOUR_AUR_ROOT/aurweb.sqlite3 +; Options: mysql, sqlite. +backend = mysql -; Alternative MySQL configuration (Use either port of socket, if both defined port takes priority) -;backend = mysql -;name = aurweb -;user = aur -;password = aur -;host = localhost +; If using sqlite, set name to the database file path. +name = aurweb + +; MySQL database information. User defaults to root for containerized +; testing with mysqldb. This should be set to a non-root user. +user = root +;password = non-root-user-password +host = localhost ;port = 3306 -;socket = /var/run/mysqld/mysqld.sock +socket = /var/run/mysqld/mysqld.sock [options] aurwebdir = YOUR_AUR_ROOT diff --git a/test/Makefile b/test/Makefile index 060e57c2..920c7113 100644 --- a/test/Makefile +++ b/test/Makefile @@ -8,7 +8,7 @@ MAKEFLAGS = -j1 check: sh pytest pytest: - cd .. && AUR_CONFIG=conf/config coverage run --append /usr/bin/pytest test + cd .. && coverage run --append /usr/bin/pytest test ifdef PROVE sh: diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 0f813823..3080a505 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -802,18 +802,40 @@ def test_post_account_edit_ssh_pub_key(): assert response.status_code == int(HTTPStatus.OK) # Now let's update what's already there to gain coverage over that path. - pk = str() - with tempfile.TemporaryDirectory() as tmpdir: - with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) - proc.wait() - assert proc.returncode == 0 + post_data["PK"] = make_ssh_pubkey() - # Read in the public key, then delete the temp dir we made. - pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) - post_data["PK"] = pk + assert response.status_code == int(HTTPStatus.OK) + + +def test_post_account_edit_missing_ssh_pubkey(): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": user.Username, + "E": user.Email, + "PK": make_ssh_pubkey(), + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + post_data = { + "U": user.Username, + "E": user.Email, + "PK": str(), # Pass an empty string now to walk the delete path. + "passwd": "testPassword" + } with client as request: response = request.post("/account/test/edit", cookies={ diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py index c599ddcf..536e3841 100644 --- a/test/test_api_rate_limit.py +++ b/test/test_api_rate_limit.py @@ -34,5 +34,5 @@ def test_api_rate_key_null_requests_raises_exception(): def test_api_rate_key_null_window_start_raises_exception(): from aurweb.db import session with pytest.raises(IntegrityError): - create(ApiRateLimit, IP="127.0.0.1", WindowStart=1) + create(ApiRateLimit, IP="127.0.0.1", Requests=1) session.rollback() diff --git a/test/test_auth.py b/test/test_auth.py index 7837e7f7..42eac040 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -4,6 +4,8 @@ import pytest from starlette.authentication import AuthenticationError +import aurweb.config + from aurweb.auth import BasicAuthBackend, has_credential from aurweb.db import create, query from aurweb.models.account_type import AccountType @@ -53,13 +55,12 @@ async def test_auth_backend_invalid_sid(): async def test_auth_backend_invalid_user_id(): # Create a new session with a fake user id. now_ts = datetime.utcnow().timestamp() - create(Session, UsersID=666, SessionID="realSession", - LastUpdateTS=now_ts + 5) + db_backend = aurweb.config.get("database", "backend") + with pytest.raises(IntegrityError): + create(Session, UsersID=666, SessionID="realSession", + LastUpdateTS=now_ts + 5) - # Here, we specify a real SID; but it's user is not there. - request.cookies["AURSID"] = "realSession" - with pytest.raises(AuthenticationError, match="Invalid User ID: 666"): - await backend.authenticate(request) + session.rollback() @pytest.mark.asyncio diff --git a/test/test_ban.py b/test/test_ban.py index a4fa5a28..b728644b 100644 --- a/test/test_ban.py +++ b/test/test_ban.py @@ -33,8 +33,7 @@ def test_ban(): def test_invalid_ban(): from aurweb.db import session - with pytest.raises(sa_exc.IntegrityError, - match="NOT NULL constraint failed: Bans.IPAddress"): + with pytest.raises(sa_exc.IntegrityError): bad_ban = Ban(BanTS=datetime.utcnow()) session.add(bad_ban) diff --git a/test/test_db.py b/test/test_db.py index e0946ed5..3911134f 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -5,16 +5,22 @@ import tempfile from unittest import mock -import mysql.connector import pytest import aurweb.config +import aurweb.initdb from aurweb import db from aurweb.models.account_type import AccountType from aurweb.testing import setup_test_db +class Args: + """ Stub arguments used for running aurweb.initdb. """ + use_alembic = True + verbose = True + + class DBCursor: """ A fake database cursor object used in tests. """ items = [] @@ -38,27 +44,73 @@ class DBConnection: pass +def make_temp_config(config_file, *replacements): + """ Generate a temporary config file with a set of replacements. + + :param *replacements: A variable number of tuple regex replacement pairs + :return: A tuple containing (temp directory, temp config file) + """ + tmpdir = tempfile.TemporaryDirectory() + tmp = os.path.join(tmpdir.name, "config.tmp") + with open(config_file) as f: + config = f.read() + for repl in list(replacements): + config = re.sub(repl[0], repl[1], config) + with open(tmp, "w") as o: + o.write(config) + aurwebdir = aurweb.config.get("options", "aurwebdir") + defaults = os.path.join(aurwebdir, "conf/config.defaults") + with open(defaults) as i: + with open(f"{tmp}.defaults", "w") as o: + o.write(i.read()) + return tmpdir, tmp + + +def make_temp_sqlite_config(config_file): + return make_temp_config(config_file, + (r"backend = .*", "backend = sqlite"), + (r"name = .*", "name = /tmp/aurweb.sqlite3")) + + +def make_temp_mysql_config(config_file): + return make_temp_config(config_file, + (r"backend = .*", "backend = mysql"), + (r"name = .*", "name = aurweb")) + + @pytest.fixture(autouse=True) def setup_db(): - setup_test_db("Bans") + if os.path.exists("/tmp/aurweb.sqlite3"): + os.remove("/tmp/aurweb.sqlite3") + + # In various places in this test, we reinitialize the engine. + # Make sure we kill the previous engine before initializing + # it via setup_test_db(). + aurweb.db.kill_engine() + setup_test_db() def test_sqlalchemy_sqlite_url(): - with mock.patch.dict(os.environ, {"AUR_CONFIG": "conf/config.dev"}): - aurweb.config.rehash() - assert db.get_sqlalchemy_url() + tmpctx, tmp = make_temp_sqlite_config("conf/config") + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() aurweb.config.rehash() def test_sqlalchemy_mysql_url(): - with mock.patch.dict(os.environ, {"AUR_CONFIG": "conf/config.defaults"}): - aurweb.config.rehash() - assert db.get_sqlalchemy_url() + tmpctx, tmp = make_temp_mysql_config("conf/config") + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() aurweb.config.rehash() def test_sqlalchemy_mysql_port_url(): - tmpctx, tmp = make_temp_config("conf/config.defaults", ";port = 3306", "port = 3306") + tmpctx, tmp = make_temp_config("conf/config", + (r";port = 3306", "port = 3306")) with tmpctx: with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): @@ -67,18 +119,9 @@ def test_sqlalchemy_mysql_port_url(): aurweb.config.rehash() -def make_temp_config(config_file, src_str, replace_with): - tmpdir = tempfile.TemporaryDirectory() - tmp = os.path.join(tmpdir.name, "config.tmp") - with open(config_file) as f: - config = re.sub(src_str, f'{replace_with}', f.read()) - with open(tmp, "w") as o: - o.write(config) - return tmpdir, tmp - - def test_sqlalchemy_unknown_backend(): - tmpctx, tmp = make_temp_config("conf/config", "backend = sqlite", "backend = blah") + tmpctx, tmp = make_temp_config("conf/config", + (r"backend = mysql", "backend = blah")) with tmpctx: with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): @@ -89,22 +132,31 @@ def test_sqlalchemy_unknown_backend(): def test_db_connects_without_fail(): + """ This only tests the actual config supplied to pytest. """ db.connect() assert db.engine is not None -def test_connection_class_without_fail(): - conn = db.Connection() +def test_connection_class_sqlite_without_fail(): + tmpctx, tmp = make_temp_sqlite_config("conf/config") + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() - cur = conn.execute( - "SELECT AccountType FROM AccountTypes WHERE ID = ?", (1,)) - account_type = cur.fetchone()[0] + aurweb.db.kill_engine() + aurweb.initdb.run(Args()) - assert account_type == "User" + conn = db.Connection() + cur = conn.execute( + "SELECT AccountType FROM AccountTypes WHERE ID = ?", (1,)) + account_type = cur.fetchone()[0] + assert account_type == "User" + aurweb.config.rehash() def test_connection_class_unsupported_backend(): - tmpctx, tmp = make_temp_config("conf/config", "backend = sqlite", "backend = blah") + tmpctx, tmp = make_temp_config("conf/config", + (r"backend = mysql", "backend = blah")) with tmpctx: with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): @@ -114,10 +166,9 @@ def test_connection_class_unsupported_backend(): aurweb.config.rehash() -@mock.patch("mysql.connector.connect", mock.MagicMock(return_value=True)) -@mock.patch.object(mysql.connector, "paramstyle", "qmark") +@mock.patch("MySQLdb.connect", mock.MagicMock(return_value=True)) def test_connection_mysql(): - tmpctx, tmp = make_temp_config("conf/config", "backend = sqlite", "backend = mysql") + tmpctx, tmp = make_temp_mysql_config("conf/config") with tmpctx: with mock.patch.dict(os.environ, { "AUR_CONFIG": tmp, @@ -137,44 +188,78 @@ def test_connection_sqlite(): @mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) @mock.patch.object(sqlite3, "paramstyle", "format") def test_connection_execute_paramstyle_format(): - conn = db.Connection() + tmpctx, tmp = make_temp_sqlite_config("conf/config") - # First, test ? to %s format replacement. - account_types = conn\ - .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"])\ - .fetchall() - assert account_types == \ - ["SELECT * FROM AccountTypes WHERE AccountType = %s", ["User"]] + with tmpctx: + with mock.patch.dict(os.environ, { + "AUR_CONFIG": tmp, + "AUR_CONFIG_DEFAULTS": "conf/config.defaults" + }): + aurweb.config.rehash() - # Test other format replacement. - account_types = conn\ - .execute("SELECT * FROM AccountTypes WHERE AccountType = %", ["User"])\ - .fetchall() - assert account_types == \ - ["SELECT * FROM AccountTypes WHERE AccountType = %%", ["User"]] + aurweb.db.kill_engine() + aurweb.initdb.run(Args()) + + conn = db.Connection() + + # First, test ? to %s format replacement. + account_types = conn\ + .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", + ["User"]).fetchall() + assert account_types == \ + ["SELECT * FROM AccountTypes WHERE AccountType = %s", ["User"]] + + # Test other format replacement. + account_types = conn\ + .execute("SELECT * FROM AccountTypes WHERE AccountType = %", + ["User"]).fetchall() + assert account_types == \ + ["SELECT * FROM AccountTypes WHERE AccountType = %%", ["User"]] + aurweb.config.rehash() @mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) @mock.patch.object(sqlite3, "paramstyle", "qmark") def test_connection_execute_paramstyle_qmark(): - conn = db.Connection() - # We don't modify anything when using qmark, so test equality. - account_types = conn\ - .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"])\ - .fetchall() - assert account_types == \ - ["SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"]] + tmpctx, tmp = make_temp_sqlite_config("conf/config") + + with tmpctx: + with mock.patch.dict(os.environ, { + "AUR_CONFIG": tmp, + "AUR_CONFIG_DEFAULTS": "conf/config.defaults" + }): + aurweb.config.rehash() + + aurweb.db.kill_engine() + aurweb.initdb.run(Args()) + + conn = db.Connection() + # We don't modify anything when using qmark, so test equality. + account_types = conn\ + .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", + ["User"]).fetchall() + assert account_types == \ + ["SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"]] + aurweb.config.rehash() @mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) @mock.patch.object(sqlite3, "paramstyle", "unsupported") def test_connection_execute_paramstyle_unsupported(): - conn = db.Connection() - with pytest.raises(ValueError, match="unsupported paramstyle"): - conn.execute( - "SELECT * FROM AccountTypes WHERE AccountType = ?", - ["User"] - ).fetchall() + tmpctx, tmp = make_temp_sqlite_config("conf/config") + with tmpctx: + with mock.patch.dict(os.environ, { + "AUR_CONFIG": tmp, + "AUR_CONFIG_DEFAULTS": "conf/config.defaults" + }): + aurweb.config.rehash() + conn = db.Connection() + with pytest.raises(ValueError, match="unsupported paramstyle"): + conn.execute( + "SELECT * FROM AccountTypes WHERE AccountType = ?", + ["User"] + ).fetchall() + aurweb.config.rehash() def test_create_delete(): @@ -186,13 +271,12 @@ def test_create_delete(): assert record is None -@mock.patch("mysql.connector.paramstyle", "qmark") def test_connection_executor_mysql_paramstyle(): executor = db.ConnectionExecutor(None, backend="mysql") - assert executor.paramstyle() == "qmark" + assert executor.paramstyle() == "format" @mock.patch("sqlite3.paramstyle", "pyformat") def test_connection_executor_sqlite_paramstyle(): executor = db.ConnectionExecutor(None, backend="sqlite") - assert executor.paramstyle() == "pyformat" + assert executor.paramstyle() == sqlite3.paramstyle diff --git a/test/test_initdb.py b/test/test_initdb.py index eae33007..c7d29ee2 100644 --- a/test/test_initdb.py +++ b/test/test_initdb.py @@ -1,27 +1,19 @@ -import pytest - import aurweb.config import aurweb.db import aurweb.initdb from aurweb.models.account_type import AccountType -from aurweb.schema import metadata -from aurweb.testing import setup_test_db -@pytest.fixture(autouse=True) -def setup(): - setup_test_db() - - tables = metadata.tables.keys() - for table in tables: - aurweb.db.session.execute(f"DROP TABLE IF EXISTS {table}") +class Args: + use_alembic = True + verbose = True def test_run(): - class Args: - use_alembic = True - verbose = False + from aurweb.schema import metadata + aurweb.db.kill_engine() + metadata.drop_all(aurweb.db.get_engine()) aurweb.initdb.run(Args()) record = aurweb.db.query(AccountType, AccountType.AccountType == "User").first() diff --git a/test/test_package_relation.py b/test/test_package_relation.py index dd0455cd..96932f40 100644 --- a/test/test_package_relation.py +++ b/test/test_package_relation.py @@ -1,6 +1,6 @@ import pytest -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError, OperationalError from aurweb.db import create, query from aurweb.models.account_type import AccountType @@ -36,7 +36,7 @@ def setup(): URL="https://test.package") -def test_package_dependencies(): +def test_package_relation(): conflicts = query(RelationType, RelationType.Name == "conflicts").first() pkgrel = create(PackageRelation, Package=package, RelationType=conflicts, @@ -68,10 +68,12 @@ def test_package_dependencies(): assert pkgrel in package.package_relations -def test_package_dependencies_null_package_raises_exception(): +def test_package_relation_null_package_raises_exception(): from aurweb.db import session conflicts = query(RelationType, RelationType.Name == "conflicts").first() + assert conflicts is not None + with pytest.raises(IntegrityError): create(PackageRelation, RelationType=conflicts, @@ -79,7 +81,7 @@ def test_package_dependencies_null_package_raises_exception(): session.rollback() -def test_package_dependencies_null_dependency_type_raises_exception(): +def test_package_relation_null_relation_type_raises_exception(): from aurweb.db import session with pytest.raises(IntegrityError): @@ -89,11 +91,13 @@ def test_package_dependencies_null_dependency_type_raises_exception(): session.rollback() -def test_package_dependencies_null_depname_raises_exception(): +def test_package_relation_null_relname_raises_exception(): from aurweb.db import session - depends = query(RelationType, RelationType.Name == "depends").first() - with pytest.raises(IntegrityError): + depends = query(RelationType, RelationType.Name == "conflicts").first() + assert depends is not None + + with pytest.raises((OperationalError, IntegrityError)): create(PackageRelation, Package=package, RelationType=depends) diff --git a/test/test_session.py b/test/test_session.py index 2877ea7f..c324a739 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -25,7 +25,7 @@ def setup(): ResetKey="testReset", Passwd="testPassword", AccountType=account_type) session = create(Session, UsersID=user.ID, SessionID="testSession", - LastUpdateTS=datetime.utcnow()) + LastUpdateTS=datetime.utcnow().timestamp()) def test_session(): From 228bc8fe7c3ea7cef66f00f1608b699d00838c43 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 4 Jun 2021 23:09:38 -0700 Subject: [PATCH 0239/1451] fix aurweb.auth test coverage With mysqlclient, we no longer need to account for a user not existing when an ssh key is found. Signed-off-by: Kevin Morris --- aurweb/auth.py | 14 +++++++++----- test/test_auth.py | 7 ++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index a4ff2167..401ed6ae 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -4,7 +4,8 @@ from datetime import datetime from http import HTTPStatus from fastapi.responses import RedirectResponse -from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError +from sqlalchemy import and_ +from starlette.authentication import AuthCredentials, AuthenticationBackend from starlette.requests import HTTPConnection import aurweb.config @@ -42,14 +43,17 @@ class BasicAuthBackend(AuthenticationBackend): now_ts = datetime.utcnow().timestamp() record = session.query(Session).filter( - Session.SessionID == sid, Session.LastUpdateTS >= now_ts).first() + and_(Session.SessionID == sid, + Session.LastUpdateTS >= now_ts)).first() + + # If no session with sid and a LastUpdateTS now or later exists. if not record: return None, AnonymousUser() + # At this point, we cannot have an invalid user if the record + # exists, due to ForeignKey constraints in the schema upheld + # by mysqlclient. user = session.query(User).filter(User.ID == record.UsersID).first() - if not user: - raise AuthenticationError(f"Invalid User ID: {record.UsersID}") - user.authenticated = True return AuthCredentials(["authenticated"]), user diff --git a/test/test_auth.py b/test/test_auth.py index 42eac040..05dd2020 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from starlette.authentication import AuthenticationError +from sqlalchemy.exc import IntegrityError import aurweb.config @@ -53,13 +53,13 @@ async def test_auth_backend_invalid_sid(): @pytest.mark.asyncio async def test_auth_backend_invalid_user_id(): + from aurweb.db import session + # Create a new session with a fake user id. now_ts = datetime.utcnow().timestamp() - db_backend = aurweb.config.get("database", "backend") with pytest.raises(IntegrityError): create(Session, UsersID=666, SessionID="realSession", LastUpdateTS=now_ts + 5) - session.rollback() @@ -70,6 +70,7 @@ async def test_basic_auth_backend(): now_ts = datetime.utcnow().timestamp() create(Session, UsersID=user.ID, SessionID="realSession", LastUpdateTS=now_ts + 5) + request.cookies["AURSID"] = "realSession" _, result = await backend.authenticate(request) assert result == user From 62e58b122f905b4e5462df6b0bbebd278bb56419 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 4 Jun 2021 23:13:05 -0700 Subject: [PATCH 0240/1451] fix test_accounts_routes test coverage Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 +- test/test_accounts_routes.py | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5bdf427c..8e14f77f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,7 @@ cache: before_script: - pacman -Syu --noconfirm --noprogressbar --needed --cachedir .pkg-cache - base-devel git gpgme protobuf pyalpm python-mysql-connector + base-devel git gpgme protobuf pyalpm python-mysqlclient python-pygit2 python-srcinfo python-bleach python-markdown python-sqlalchemy python-alembic python-pytest python-werkzeug python-pytest-tap python-fastapi hypercorn nginx python-authlib diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 3080a505..d5fd089e 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -30,6 +30,20 @@ client = TestClient(app) user = None +def make_ssh_pubkey(): + # Create a public key with ssh-keygen (this adds ssh-keygen as a + # dependency to passing this test). + with tempfile.TemporaryDirectory() as tmpdir: + with open("/dev/null", "w") as null: + proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, stderr=null) + proc.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + return open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + @pytest.fixture(autouse=True) def setup(): global user @@ -770,27 +784,13 @@ def test_post_account_edit_error_unauthorized(): def test_post_account_edit_ssh_pub_key(): - pk = str() - - # Create a public key with ssh-keygen (this adds ssh-keygen as a - # dependency to passing this test). - with tempfile.TemporaryDirectory() as tmpdir: - with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) - proc.wait() - assert proc.returncode == 0 - - # Read in the public key, then delete the temp dir we made. - pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() - request = Request() sid = user.login(request, "testPassword") post_data = { "U": "test", "E": "test@example.org", - "PK": pk, + "PK": make_ssh_pubkey(), "passwd": "testPassword" } From 4d1faca4477bc510e81513b659f199f0d8c41bb1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 5 Jun 2021 01:07:56 -0700 Subject: [PATCH 0241/1451] test both mysql and sqlite in .gitlab-ci.yml Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e14f77f..e65b4343 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,9 @@ cache: # For some reason Gitlab CI only supports storing cache/artifacts in a path relative to the build directory - .pkg-cache +variables: + AUR_CONFIG: conf/config + before_script: - pacman -Syu --noconfirm --noprogressbar --needed --cachedir .pkg-cache base-devel git gpgme protobuf pyalpm python-mysqlclient @@ -15,17 +18,31 @@ before_script: python-itsdangerous python-httpx python-jinja python-pytest-cov python-requests python-aiofiles python-python-multipart python-pytest-asyncio python-coverage python-bcrypt - python-email-validator openssh python-lxml + python-email-validator openssh python-lxml mariadb - bash -c "echo '127.0.0.1' > /etc/hosts" - bash -c "echo '::1' >> /etc/hosts" + - mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql + - (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & + - 'until : > /dev/tcp/127.0.0.1/3306; do sleep 1s; done' + - mysql -u root -e "CREATE USER 'aur'@'localhost' IDENTIFIED BY 'aur';" + - mysql -u root -e "CREATE DATABASE aurweb;" + - mysql -u root -e "GRANT ALL PRIVILEGES ON aurweb.* TO 'aur'@'localhost';" + - mysql -u root -e "FLUSH PRIVILEGES;" + - sed -r "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.dev > conf/config + - cp conf/config conf/config.sqlite + - cp conf/config.defaults conf/config.sqlite.defaults + - sed -i -r 's;backend = .*;backend = sqlite;' conf/config.sqlite + - sed -i -r "s;name = .*;name = $(pwd)/aurweb.sqlite3;" conf/config.sqlite + - AUR_CONFIG=conf/config.sqlite python -m aurweb.initdb test: script: - python setup.py install - - sed -r "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.dev > conf/config - - AUR_CONFIG=conf/config make -C po all install - - AUR_CONFIG=conf/config python -m aurweb.initdb - - make -C test + - make -C po all install + - python -m aurweb.initdb + - make -C test sh # sharness tests use sqlite. + - make -C test pytest # pytest with mysql. + - AUR_CONFIG=conf/config.sqlite make -C test pytest # pytest with sqlite. - coverage report --include='aurweb/*' - coverage xml --include='aurweb/*' artifacts: From 5ceeb88bee1575427008c119d2c7cd37274f0919 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 5 Jun 2021 21:19:20 -0700 Subject: [PATCH 0242/1451] remove unused imports, rectify isort violations Files got into the branch that violate both PEP-8 guidelines and isorts. This fixes them. Signed-off-by: Kevin Morris --- aurweb/git/update.py | 4 ++-- aurweb/models/user.py | 2 -- aurweb/routers/html.py | 3 +-- ...56e2ce8e2ffa_utf8mb4_charset_and_collation.py | 16 ++++++++-------- ...fcd6e1cd_add_sso_account_id_in_table_users.py | 1 + .../versions/f47cad5d6d03_initial_revision.py | 5 ----- test/test_auth.py | 2 -- 7 files changed, 12 insertions(+), 21 deletions(-) diff --git a/aurweb/git/update.py b/aurweb/git/update.py index 3c9c3785..2424bf6c 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -305,9 +305,9 @@ def main(): # noqa: C901 try: metadata_pkgbase = metadata['pkgbase'] - except KeyError as e: + except KeyError: die_commit('invalid .SRCINFO, does not contain a pkgbase (is the file empty?)', - str(commit.id)) + str(commit.id)) if not re.match(repo_regex, metadata_pkgbase): die_commit('invalid pkgbase: {:s}'.format(metadata_pkgbase), str(commit.id)) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 6c5c6e21..1961228e 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -65,8 +65,6 @@ class User: def valid_password(self, password: str): """ Check authentication against a given password. """ - from aurweb.db import session - if password is None: return False diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 8f89e05c..890aff88 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -2,9 +2,8 @@ decorators in some way; more complex routes should be defined in their own modules and imported here. """ from http import HTTPStatus -from urllib.parse import unquote -from fastapi import APIRouter, Form, Request, HTTPException +from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from aurweb.templates import make_context, render_template diff --git a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py index e198c34c..67f0c065 100644 --- a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py +++ b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py @@ -56,14 +56,14 @@ db_backend = aurweb.config.get("database", "backend") def rebuild_unique_indexes_with_str_cols(): for idx_name in indexes: sql = f""" -DROP INDEX IF EXISTS {idx_name} +DROP INDEX IF EXISTS {idx_name} ON {indexes.get(idx_name)[0]} """ op.execute(sql) sql = f""" -CREATE UNIQUE INDEX {idx_name} -ON {indexes.get(idx_name)[0]} -({indexes.get(idx_name)[1]}, {indexes.get(idx_name)[2]}) +CREATE UNIQUE INDEX {idx_name} +ON {indexes.get(idx_name)[0]} +({indexes.get(idx_name)[1]}, {indexes.get(idx_name)[2]}) """ op.execute(sql) @@ -77,8 +77,8 @@ def upgrade(): def op_execute(table_meta): table, charset, collate = table_meta sql = f""" -ALTER TABLE {table} -CONVERT TO CHARACTER SET {charset} +ALTER TABLE {table} +CONVERT TO CHARACTER SET {charset} COLLATE {collate} """ op.execute(sql) @@ -94,8 +94,8 @@ def downgrade(): def op_execute(table_meta): table, charset, collate = table_meta sql = f""" -ALTER TABLE {table} -CONVERT TO CHARACTER SET {src_charset} +ALTER TABLE {table} +CONVERT TO CHARACTER SET {src_charset} COLLATE {src_collate} """ op.execute(sql) diff --git a/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py index 2b257e9d..49bf055a 100644 --- a/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py +++ b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py @@ -6,6 +6,7 @@ Create Date: 2020-06-08 10:04:13.898617 """ import sqlalchemy as sa + from alembic import op from sqlalchemy.engine.reflection import Inspector diff --git a/migrations/versions/f47cad5d6d03_initial_revision.py b/migrations/versions/f47cad5d6d03_initial_revision.py index 9e99490f..b214beea 100644 --- a/migrations/versions/f47cad5d6d03_initial_revision.py +++ b/migrations/versions/f47cad5d6d03_initial_revision.py @@ -1,14 +1,9 @@ """initial revision Revision ID: f47cad5d6d03 -Revises: Create Date: 2020-02-23 13:23:32.331396 """ -from alembic import op -import sqlalchemy as sa - - # revision identifiers, used by Alembic. revision = 'f47cad5d6d03' down_revision = None diff --git a/test/test_auth.py b/test/test_auth.py index 05dd2020..e5e1de11 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -4,8 +4,6 @@ import pytest from sqlalchemy.exc import IntegrityError -import aurweb.config - from aurweb.auth import BasicAuthBackend, has_credential from aurweb.db import create, query from aurweb.models.account_type import AccountType From e865a6347f16edba73e405a4dabc1f76d3ca6509 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 5 Jun 2021 21:28:26 -0700 Subject: [PATCH 0243/1451] .gitlab-ci.yml: enforce isort and flake8 compliance Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e65b4343..a9947dfe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,6 +19,7 @@ before_script: python-requests python-aiofiles python-python-multipart python-pytest-asyncio python-coverage python-bcrypt python-email-validator openssh python-lxml mariadb + python-isort flake8 - bash -c "echo '127.0.0.1' > /etc/hosts" - bash -c "echo '::1' >> /etc/hosts" - mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql @@ -45,6 +46,12 @@ test: - AUR_CONFIG=conf/config.sqlite make -C test pytest # pytest with sqlite. - coverage report --include='aurweb/*' - coverage xml --include='aurweb/*' + - flake8 --count aurweb # Assert no flake8 violations in aurweb. + - flake8 --count test # Assert no flake8 violations in test. + - flake8 --count migrations # Assert no flake8 violations in migrations. + - isort --check-only aurweb # Assert no isort violations in aurweb. + - isort --check-only test # Assert no flake8 violations in test. + - isort --check-only migrations # Assert no flake8 violations in migrations. artifacts: reports: cobertura: coverage.xml From 1874e821f5883c2338b07c4803b67c6802621c38 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 5 Jun 2021 18:13:10 -0700 Subject: [PATCH 0244/1451] add case [in]sensitivity tests + add OfficialProvider model `ci` in this context means "Case Insensitive". `cs` in this context means "Case Sensitive". New models created: - OfficialProvider This was required to write a test for checking that OfficialProviders behaves as we expect, which was the starter for the original aurblup bug. New tests created: - test_official_provider Modified tests: - test_package_base: add ci test - test_package: add ci test - test_session: add cs test - test_ssh_pub_key: add cs test Signed-off-by: Kevin Morris --- aurweb/models/official_provider.py | 34 ++++++++++++++ test/test_official_provider.py | 75 ++++++++++++++++++++++++++++++ test/test_package.py | 16 +++++++ test/test_package_base.py | 21 +++++++++ test/test_session.py | 9 ++++ test/test_ssh_pub_key.py | 12 +++++ 6 files changed, 167 insertions(+) create mode 100644 aurweb/models/official_provider.py create mode 100644 test/test_official_provider.py diff --git a/aurweb/models/official_provider.py b/aurweb/models/official_provider.py new file mode 100644 index 00000000..073eb435 --- /dev/null +++ b/aurweb/models/official_provider.py @@ -0,0 +1,34 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import mapper + +from aurweb.schema import OfficialProviders + + +class OfficialProvider: + def __init__(self, + Name: str = None, + Repo: str = None, + Provides: str = None): + self.Name = Name + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="OfficialProviders.Name", + params=("NULL")) + + self.Repo = Repo + if not self.Repo: + raise IntegrityError( + statement="Column Repo cannot be null.", + orig="OfficialProviders.Repo", + params=("NULL")) + + self.Provides = Provides + if not self.Provides: + raise IntegrityError( + statement="Column Provides cannot be null.", + orig="OfficialProviders.Provides", + params=("NULL")) + + +mapper(OfficialProvider, OfficialProviders) diff --git a/test/test_official_provider.py b/test/test_official_provider.py new file mode 100644 index 00000000..a1d3d54a --- /dev/null +++ b/test/test_official_provider.py @@ -0,0 +1,75 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create +from aurweb.models.official_provider import OfficialProvider +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db("OfficialProviders") + + +def test_official_provider_creation(): + oprovider = create(OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides") + assert bool(oprovider.ID) + assert oprovider.Name == "some-name" + assert oprovider.Repo == "some-repo" + assert oprovider.Provides == "some-provides" + + +def test_official_provider_cs(): + """ Test case sensitivity of the database table. """ + oprovider = create(OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides") + assert bool(oprovider.ID) + + oprovider_cs = create(OfficialProvider, + Name="SOME-NAME", + Repo="SOME-REPO", + Provides="SOME-PROVIDES") + assert bool(oprovider_cs.ID) + + assert oprovider.ID != oprovider_cs.ID + + assert oprovider.Name == "some-name" + assert oprovider.Repo == "some-repo" + assert oprovider.Provides == "some-provides" + + assert oprovider_cs.Name == "SOME-NAME" + assert oprovider_cs.Repo == "SOME-REPO" + assert oprovider_cs.Provides == "SOME-PROVIDES" + + +def test_official_provider_null_name_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(OfficialProvider, + Repo="some-repo", + Provides="some-provides") + session.rollback() + + +def test_official_provider_null_repo_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(OfficialProvider, + Name="some-name", + Provides="some-provides") + session.rollback() + + +def test_official_provider_null_provides_raises_exception(): + from aurweb.db import session + with pytest.raises(IntegrityError): + create(OfficialProvider, + Name="some-name", + Repo="some-repo") + session.rollback() diff --git a/test/test_package.py b/test/test_package.py index 66d557f3..a994f096 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -3,6 +3,8 @@ import pytest from sqlalchemy import and_ from sqlalchemy.exc import IntegrityError +import aurweb.config + from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.package import Package @@ -55,6 +57,20 @@ def test_package(): assert record is not None +def test_package_ci(): + """ Test case insensitivity of the database table. """ + if aurweb.config.get("database", "backend") == "sqlite": + return None # SQLite doesn't seem handle this. + + from aurweb.db import session + + with pytest.raises(IntegrityError): + create(Package, + PackageBase=pkgbase, + Name="Beautiful-Package") + session.rollback() + + def test_package_null_pkgbase_raises_exception(): from aurweb.db import session diff --git a/test/test_package_base.py b/test/test_package_base.py index e0359f4f..7f608c2c 100644 --- a/test/test_package_base.py +++ b/test/test_package_base.py @@ -2,6 +2,8 @@ import pytest from sqlalchemy.exc import IntegrityError +import aurweb.config + from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.package_base import PackageBase @@ -35,6 +37,25 @@ def test_package_base(): assert pkgbase.ModifiedTS > 0 +def test_package_base_ci(): + """ Test case insensitivity of the database table. """ + if aurweb.config.get("database", "backend") == "sqlite": + return None # SQLite doesn't seem handle this. + + from aurweb.db import session + + pkgbase = create(PackageBase, + Name="beautiful-package", + Maintainer=user) + assert bool(pkgbase.ID) + + with pytest.raises(IntegrityError): + create(PackageBase, + Name="Beautiful-Package", + Maintainer=user) + session.rollback() + + def test_package_base_relationships(): pkgbase = create(PackageBase, Name="beautiful-package", diff --git a/test/test_session.py b/test/test_session.py index c324a739..1dd82db1 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -33,6 +33,15 @@ def test_session(): assert session.UsersID == user.ID +def test_session_cs(): + """ Test case sensitivity of the database table. """ + session_cs = create(Session, UsersID=user.ID, + SessionID="TESTSESSION", + LastUpdateTS=datetime.utcnow().timestamp()) + assert session_cs.SessionID == "TESTSESSION" + assert session.SessionID == "testSession" + + def test_session_user_association(): # Make sure that the Session user attribute is correct. assert session.User == user diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index 4072549e..0793199a 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -41,6 +41,18 @@ def test_ssh_pub_key(): assert ssh_pub_key.PubKey == "testPubKey" +def test_ssh_pub_key_cs(): + """ Test case sensitivity of the database table. """ + ssh_pub_key_cs = create(SSHPubKey, UserID=user.ID, + Fingerprint="TESTFINGERPRINT", + PubKey="TESTPUBKEY") + + assert ssh_pub_key_cs.Fingerprint == "TESTFINGERPRINT" + assert ssh_pub_key_cs.PubKey == "TESTPUBKEY" + assert ssh_pub_key.Fingerprint == "testFingerprint" + assert ssh_pub_key.PubKey == "testPubKey" + + def test_ssh_pub_key_fingerprint(): assert get_fingerprint(TEST_SSH_PUBKEY) is not None From 889d358a6daff60d5aac3df62b54a7f939b9bc8a Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sun, 6 Jun 2021 21:49:27 +0200 Subject: [PATCH 0245/1451] Add missing ) for addvote.php --- web/html/addvote.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/html/addvote.php b/web/html/addvote.php index 70280cfd..3e4def44 100644 --- a/web/html/addvote.php +++ b/web/html/addvote.php @@ -69,7 +69,7 @@ if (has_credential(CRED_TU_ADD_VOTE)) { if (!empty($_POST['addVote']) && empty($error)) { // Convert $quorum to a String of maximum length "12.34" (5). - $quorum_str = substr(strval($quorum), min(5, strlen($quorum)); + $quorum_str = substr(strval($quorum), min(5, strlen($quorum))); add_tu_proposal($_POST['agenda'], $_POST['user'], $len, $quorum_str, $uid); From f9f41dc99beb9fd75d3f772bf4023168b53c9180 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 6 Jun 2021 16:30:16 -0700 Subject: [PATCH 0246/1451] restore TU_VoteInfo -> utf8mb4_general_ci Signed-off-by: Kevin Morris --- aurweb/schema.py | 4 +++- .../versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/aurweb/schema.py b/aurweb/schema.py index f0162045..a0bb7e09 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -389,7 +389,9 @@ TU_VoteInfo = Table( Column('No', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), Column('Abstain', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), Column('ActiveTUs', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), - mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', + mysql_engine='InnoDB', + mysql_charset='utf8mb4', + mysql_collate='utf8mb4_general_ci', ) diff --git a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py index e198c34c..03982676 100644 --- a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py +++ b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py @@ -37,7 +37,7 @@ tables = [ ('RequestTypes', 'utf8mb4', 'utf8mb4_general_ci'), ('SSHPubKeys', 'utf8mb4', 'utf8mb4_bin'), ('Sessions', 'utf8mb4', 'utf8mb4_bin'), - ('TU_VoteInfo', 'utf8mb4', 'utf8mb4_bin'), + ('TU_VoteInfo', 'utf8mb4', 'utf8mb4_general_ci'), ('Terms', 'utf8mb4', 'utf8mb4_general_ci'), ('Users', 'utf8mb4', 'utf8mb4_general_ci') ] From 4f09e939ae1d605f6568a180d8ab86033e847a0f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 6 Jun 2021 21:34:42 -0700 Subject: [PATCH 0247/1451] bugfix: gendummydata.py was producing invalid usernames As per our regex and policies, usernames should consist of ascii alphanumeric characters and possibly (-, _ or .). gendummydata.py was creating unicode versions of some usernames and adding them into the DB. With our newfound collations, this becomes a problem as it treats them as the same. This should have never been the case here, and so, gendummydata.py has been patched to normalize all of its usernames and package names. Signed-off-by: Kevin Morris --- schema/gendummydata.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index c7b3a06d..35805d6c 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -98,11 +98,19 @@ if MAX_USERS > len(contents): MAX_USERS = len(contents) if MAX_PKGS > len(contents): MAX_PKGS = len(contents) -if len(contents) - MAX_USERS > MAX_PKGS: - need_dupes = 0 -else: + +need_dupes = 0 +if not len(contents) - MAX_USERS > MAX_PKGS: need_dupes = 1 + +def normalize(unicode_data): + """ We only accept ascii for usernames. Also use this to normalize + package names; our database utf8mb4 collations compare with Unicode + Equivalence. """ + return unicode_data.encode('ascii', 'ignore').decode('ascii') + + # select random usernames # log.debug("Generating random user names...") @@ -110,12 +118,13 @@ user_id = USER_ID while len(seen_users) < MAX_USERS: user = random.randrange(0, len(contents)) word = contents[user].replace("'", "").replace(".", "").replace(" ", "_") - word = word.strip().lower() + word = normalize(word.strip().lower()) if word not in seen_users: seen_users[word] = user_id user_id += 1 user_keys = list(seen_users.keys()) + # select random package names # log.debug("Generating random package names...") @@ -123,7 +132,7 @@ num_pkgs = PKG_ID while len(seen_pkgs) < MAX_PKGS: pkg = random.randrange(0, len(contents)) word = contents[pkg].replace("'", "").replace(".", "").replace(" ", "_") - word = word.strip().lower() + word = normalize(word.strip().lower()) if not need_dupes: if word not in seen_pkgs and word not in seen_users: seen_pkgs[word] = num_pkgs @@ -285,10 +294,10 @@ for p in seen_pkgs_keys: for i in range(num_sources): src_file = user_keys[random.randrange(0, len(user_keys))] src = "%s%s.%s/%s/%s-%s.tar.gz" % ( - RANDOM_URL[random.randrange(0, len(RANDOM_URL))], - p, RANDOM_TLDS[random.randrange(0, len(RANDOM_TLDS))], - RANDOM_LOCS[random.randrange(0, len(RANDOM_LOCS))], - src_file, genVersion()) + RANDOM_URL[random.randrange(0, len(RANDOM_URL))], + p, RANDOM_TLDS[random.randrange(0, len(RANDOM_TLDS))], + RANDOM_LOCS[random.randrange(0, len(RANDOM_LOCS))], + src_file, genVersion()) s = "INSERT INTO PackageSources(PackageID, Source) VALUES (%d, '%s');\n" s = s % (seen_pkgs[p], src) out.write(s) From 7f7a975614f3c02517b4cbb94537fc4c60b26603 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 10 Jun 2021 01:11:41 -0700 Subject: [PATCH 0248/1451] remove autoflush from aurweb.db.Session This causes issues with the declarative API. Signed-off-by: Kevin Morris --- aurweb/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/db.py b/aurweb/db.py index 590712e0..1f6f50d8 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -141,7 +141,7 @@ def get_engine(echo: bool = False): engine = create_engine(get_sqlalchemy_url(), connect_args=connect_args, echo=echo) - Session = sessionmaker(autocommit=False, autoflush=True, bind=engine) + Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) session = Session() return engine From a625df07e294866398385548501656eb8645e610 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Thu, 10 Jun 2021 14:46:24 -0400 Subject: [PATCH 0249/1451] Source valid ssh prefixes from config Signed-off-by: Eli Schwartz --- web/lib/acctfuncs.inc.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index df016c6d..0d021f99 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -875,10 +875,7 @@ function valid_pgp_fingerprint($fingerprint) { * @return bool True if the SSH public key is valid, otherwise false */ function valid_ssh_pubkey($pubkey) { - $valid_prefixes = array( - "ssh-rsa", "ssh-dss", "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "ssh-ed25519" - ); + $valid_prefixes = explode(' ', config_get('auth', 'valid-keytypes')); $has_valid_prefix = false; foreach ($valid_prefixes as $prefix) { From b32022a176ede116068a405c928cf25e23ffb691 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Thu, 10 Jun 2021 14:35:13 -0400 Subject: [PATCH 0250/1451] Add FIDO/U2F ssh keytypes to default config Signed-off-by: Eli Schwartz --- conf/config.defaults | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/config.defaults b/conf/config.defaults index 98e033b7..e6961520 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -62,7 +62,7 @@ ECDSA = SHA256:L71Q91yHwmHPYYkJMDgj0xmUuw16qFOhJbBr1mzsiOI RSA = SHA256:Ju+yWiMb/2O+gKQ9RJCDqvRg7l+Q95KFAeqM5sr6l2s [auth] -valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 +valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ssh-ecdsa@openssh.com sk-ssh-ed25519@openssh.com username-regex = [a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$ git-serve-cmd = /usr/local/bin/aurweb-git-serve ssh-options = restrict From 888cf5118aa1f794f9c87413aa29b5db54adc84a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 6 Jun 2021 22:45:40 -0700 Subject: [PATCH 0251/1451] use declarative_base for all ORM models This rewrites the entire model base as declarative models. This allows us to more easily customize overlay fields in tables and is more common. This effort also brought some DB violations to light which this commit addresses. Signed-off-by: Kevin Morris --- aurweb/db.py | 13 ++---- aurweb/models/__init__.py | 1 + aurweb/models/accepted_term.py | 41 ++++++++++------- aurweb/models/account_type.py | 14 +++--- aurweb/models/api_rate_limit.py | 18 +++++--- aurweb/models/ban.py | 15 ++++--- aurweb/models/declarative.py | 10 +++++ aurweb/models/dependency_type.py | 15 ++++--- aurweb/models/group.py | 15 ++++--- aurweb/models/license.py | 15 ++++--- aurweb/models/official_provider.py | 15 ++++--- aurweb/models/package.py | 42 +++++++++++------- aurweb/models/package_base.py | 58 +++++++++++++++--------- aurweb/models/package_dependency.py | 55 ++++++++++++++--------- aurweb/models/package_group.py | 48 +++++++++++--------- aurweb/models/package_keyword.py | 32 +++++++------ aurweb/models/package_license.py | 52 ++++++++++++---------- aurweb/models/package_relation.py | 52 ++++++++++++---------- aurweb/models/relation_type.py | 15 ++++--- aurweb/models/session.py | 24 ++++++---- aurweb/models/ssh_pub_key.py | 26 +++++++---- aurweb/models/term.py | 15 ++++--- aurweb/models/user.py | 69 ++++++++++++----------------- test/test_accepted_term.py | 4 +- test/test_account_type.py | 3 +- test/test_package.py | 10 ++--- test/test_package_group.py | 2 +- test/test_package_license.py | 2 +- test/test_session.py | 9 ++-- 29 files changed, 398 insertions(+), 292 deletions(-) create mode 100644 aurweb/models/declarative.py diff --git a/aurweb/db.py b/aurweb/db.py index 1f6f50d8..9837c746 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,7 +1,5 @@ import math -from sqlalchemy.orm import backref, relationship - import aurweb.config import aurweb.util @@ -53,12 +51,6 @@ def make_random_value(table: str, column: str): return string -def make_relationship(model, foreign_key: str, backref_: str, **kwargs): - return relationship(model, foreign_keys=[foreign_key], - backref=backref(backref_, lazy="dynamic"), - **kwargs) - - def query(model, *args, **kwargs): return session.query(model).filter(*args, **kwargs) @@ -77,6 +69,10 @@ def delete(model, *args, **kwargs): session.commit() +def rollback(): + session.rollback() + + def get_sqlalchemy_url(): """ Build an SQLAlchemy for use with create_engine based on the aurweb configuration. @@ -137,7 +133,6 @@ def get_engine(echo: bool = False): # check_same_thread is for a SQLite technicality # https://fastapi.tiangolo.com/tutorial/sql-databases/#note connect_args["check_same_thread"] = False - engine = create_engine(get_sqlalchemy_url(), connect_args=connect_args, echo=echo) diff --git a/aurweb/models/__init__.py b/aurweb/models/__init__.py index e69de29b..ed0532c6 100644 --- a/aurweb/models/__init__.py +++ b/aurweb/models/__init__.py @@ -0,0 +1 @@ +# aurweb SQLAlchemy ORM model collection. diff --git a/aurweb/models/accepted_term.py b/aurweb/models/accepted_term.py index 483109f1..b46d086b 100644 --- a/aurweb/models/accepted_term.py +++ b/aurweb/models/accepted_term.py @@ -1,15 +1,33 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.term import Term -from aurweb.models.user import User -from aurweb.schema import AcceptedTerms +import aurweb.models.term +import aurweb.models.user + +from aurweb.models.declarative import Base -class AcceptedTerm: +class AcceptedTerm(Base): + __tablename__ = "AcceptedTerms" + + UsersID = Column(Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + User = relationship( + "User", backref=backref("accepted_terms", lazy="dynamic"), + foreign_keys=[UsersID]) + + TermsID = Column(Integer, ForeignKey("Terms.ID", ondelete="CASCADE"), + nullable=False) + Term = relationship( + "Term", backref=backref("accepted_terms", lazy="dynamic"), + foreign_keys=[TermsID]) + + __mapper_args__ = {"primary_key": [TermsID]} + def __init__(self, - User: User = None, Term: Term = None, + User: aurweb.models.user.User = None, + Term: aurweb.models.term.Term = None, Revision: int = None): self.User = User if not self.User: @@ -26,12 +44,3 @@ class AcceptedTerm: params=("NULL")) self.Revision = Revision - - -properties = { - "User": make_relationship(User, AcceptedTerms.c.UsersID, "accepted_terms"), - "Term": make_relationship(Term, AcceptedTerms.c.TermsID, "accepted") -} - -mapper(AcceptedTerm, AcceptedTerms, properties=properties, - primary_key=[AcceptedTerms.c.UsersID, AcceptedTerms.c.TermsID]) diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py index 44225e35..502a86b1 100644 --- a/aurweb/models/account_type.py +++ b/aurweb/models/account_type.py @@ -1,10 +1,15 @@ -from sqlalchemy.orm import mapper +from sqlalchemy import Column, Integer -from aurweb.schema import AccountTypes +from aurweb.models.declarative import Base -class AccountType: +class AccountType(Base): """ An ORM model of a single AccountTypes record. """ + __tablename__ = "AccountTypes" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} def __init__(self, **kwargs): self.AccountType = kwargs.pop("AccountType") @@ -15,6 +20,3 @@ class AccountType: def __repr__(self): return "" % ( self.ID, str(self)) - - -mapper(AccountType, AccountTypes, confirm_deleted_rows=False) diff --git a/aurweb/models/api_rate_limit.py b/aurweb/models/api_rate_limit.py index 8b945b6a..f4590553 100644 --- a/aurweb/models/api_rate_limit.py +++ b/aurweb/models/api_rate_limit.py @@ -1,11 +1,18 @@ +from sqlalchemy import Column, String from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper -from aurweb.schema import ApiRateLimit as _ApiRateLimit +from aurweb.models.declarative import Base -class ApiRateLimit: - def __init__(self, IP: str = None, +class ApiRateLimit(Base): + __tablename__ = "ApiRateLimit" + + IP = Column(String(45), primary_key=True, unique=True, default=str()) + + __mapper_args__ = {"primary_key": [IP]} + + def __init__(self, + IP: str = None, Requests: int = None, WindowStart: int = None): self.IP = IP @@ -23,6 +30,3 @@ class ApiRateLimit: statement="Column WindowStart cannot be null.", orig="ApiRateLimit.WindowStart", params=("NULL")) - - -mapper(ApiRateLimit, _ApiRateLimit, primary_key=[_ApiRateLimit.c.IP]) diff --git a/aurweb/models/ban.py b/aurweb/models/ban.py index be030380..e10087b0 100644 --- a/aurweb/models/ban.py +++ b/aurweb/models/ban.py @@ -1,10 +1,16 @@ from fastapi import Request -from sqlalchemy.orm import mapper +from sqlalchemy import Column, String -from aurweb.schema import Bans +from aurweb.models.declarative import Base -class Ban: +class Ban(Base): + __tablename__ = "Bans" + + IPAddress = Column(String(45), primary_key=True) + + __mapper_args__ = {"primary_key": [IPAddress]} + def __init__(self, **kwargs): self.IPAddress = kwargs.get("IPAddress") self.BanTS = kwargs.get("BanTS") @@ -14,6 +20,3 @@ def is_banned(request: Request): from aurweb.db import session ip = request.client.host return session.query(Ban).filter(Ban.IPAddress == ip).first() is not None - - -mapper(Ban, Bans) diff --git a/aurweb/models/declarative.py b/aurweb/models/declarative.py new file mode 100644 index 00000000..45a629ce --- /dev/null +++ b/aurweb/models/declarative.py @@ -0,0 +1,10 @@ +from sqlalchemy.ext.declarative import declarative_base + +import aurweb.db + +Base = declarative_base() +Base.__table_args__ = { + "autoload": True, + "autoload_with": aurweb.db.get_engine(), + "extend_existing": True +} diff --git a/aurweb/models/dependency_type.py b/aurweb/models/dependency_type.py index 87b38069..71acf368 100644 --- a/aurweb/models/dependency_type.py +++ b/aurweb/models/dependency_type.py @@ -1,11 +1,14 @@ -from sqlalchemy.orm import mapper +from sqlalchemy import Column, Integer -from aurweb.schema import DependencyTypes +from aurweb.models.declarative import Base -class DependencyType: +class DependencyType(Base): + __tablename__ = "DependencyTypes" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} + def __init__(self, Name: str = None): self.Name = Name - - -mapper(DependencyType, DependencyTypes) diff --git a/aurweb/models/group.py b/aurweb/models/group.py index c5583eb4..1bd3a402 100644 --- a/aurweb/models/group.py +++ b/aurweb/models/group.py @@ -1,10 +1,16 @@ +from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper -from aurweb.schema import Groups +from aurweb.models.declarative import Base -class Group: +class Group(Base): + __tablename__ = "Groups" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} + def __init__(self, Name: str = None): self.Name = Name if not self.Name: @@ -12,6 +18,3 @@ class Group: statement="Column Name cannot be null.", orig="Groups.Name", params=("NULL")) - - -mapper(Group, Groups) diff --git a/aurweb/models/license.py b/aurweb/models/license.py index bcc02713..aef6a619 100644 --- a/aurweb/models/license.py +++ b/aurweb/models/license.py @@ -1,10 +1,16 @@ +from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper -from aurweb.schema import Licenses +from aurweb.models.declarative import Base -class License: +class License(Base): + __tablename__ = "Licenses" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} + def __init__(self, Name: str = None): self.Name = Name if not self.Name: @@ -12,6 +18,3 @@ class License: statement="Column Name cannot be null.", orig="Licenses.Name", params=("NULL")) - - -mapper(License, Licenses) diff --git a/aurweb/models/official_provider.py b/aurweb/models/official_provider.py index 073eb435..756be843 100644 --- a/aurweb/models/official_provider.py +++ b/aurweb/models/official_provider.py @@ -1,10 +1,16 @@ +from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper -from aurweb.schema import OfficialProviders +from aurweb.models.declarative import Base -class OfficialProvider: +class OfficialProvider(Base): + __tablename__ = "OfficialProviders" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} + def __init__(self, Name: str = None, Repo: str = None, @@ -29,6 +35,3 @@ class OfficialProvider: statement="Column Provides cannot be null.", orig="OfficialProviders.Provides", params=("NULL")) - - -mapper(OfficialProvider, OfficialProviders) diff --git a/aurweb/models/package.py b/aurweb/models/package.py index 28a13791..ff518f20 100644 --- a/aurweb/models/package.py +++ b/aurweb/models/package.py @@ -1,20 +1,37 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.package_base import PackageBase -from aurweb.schema import Packages +import aurweb.db +import aurweb.models.package_base + +from aurweb.models.declarative import Base -class Package: +class Package(Base): + __tablename__ = "Packages" + + ID = Column(Integer, primary_key=True) + + PackageBaseID = Column( + Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False) + PackageBase = relationship( + "PackageBase", backref=backref("package", uselist=False), + foreign_keys=[PackageBaseID]) + + __mapper_args__ = {"primary_key": [ID]} + def __init__(self, - PackageBase: PackageBase = None, - Name: str = None, Version: str = None, - Description: str = None, URL: str = None): + PackageBase: aurweb.models.package_base.PackageBase = None, + Name: str = None, + Version: str = None, + Description: str = None, + URL: str = None): self.PackageBase = PackageBase if not self.PackageBase: raise IntegrityError( - statement="Foreign key UserID cannot be null.", + statement="Foreign key PackageBaseID cannot be null.", orig="Packages.PackageBaseID", params=("NULL")) @@ -28,10 +45,3 @@ class Package: self.Version = Version self.Description = Description self.URL = URL - - -mapper(Package, Packages, properties={ - "PackageBase": make_relationship(PackageBase, - Packages.c.PackageBaseID, - "package", uselist=False) -}) diff --git a/aurweb/models/package_base.py b/aurweb/models/package_base.py index 699559d5..261c30f3 100644 --- a/aurweb/models/package_base.py +++ b/aurweb/models/package_base.py @@ -1,17 +1,47 @@ from datetime import datetime +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.user import User -from aurweb.schema import PackageBases +import aurweb.models.user + +from aurweb.models.declarative import Base -class PackageBase: - def __init__(self, Name: str = None, Flagger: User = None, - Maintainer: User = None, Submitter: User = None, - Packager: User = None, **kwargs): +class PackageBase(Base): + __tablename__ = "PackageBases" + + FlaggerUID = Column(Integer, + ForeignKey("Users.ID", ondelete="SET NULL")) + Flagger = relationship( + "User", backref=backref("flagged_bases", lazy="dynamic"), + foreign_keys=[FlaggerUID]) + + SubmitterUID = Column(Integer, + ForeignKey("Users.ID", ondelete="SET NULL")) + Submitter = relationship( + "User", backref=backref("submitted_bases", lazy="dynamic"), + foreign_keys=[SubmitterUID]) + + MaintainerUID = Column(Integer, + ForeignKey("Users.ID", ondelete="SET NULL")) + Maintainer = relationship( + "User", backref=backref("maintained_bases", lazy="dynamic"), + foreign_keys=[MaintainerUID]) + + PackagerUID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) + Packager = relationship( + "User", backref=backref("package_bases", lazy="dynamic"), + foreign_keys=[PackagerUID]) + + def __init__(self, Name: str = None, + Flagger: aurweb.models.user.User = None, + Maintainer: aurweb.models.user.User = None, + Submitter: aurweb.models.user.User = None, + Packager: aurweb.models.user.User = None, + **kwargs): + super().__init__(**kwargs) self.Name = Name if not self.Name: raise IntegrityError( @@ -32,15 +62,3 @@ class PackageBase: datetime.utcnow().timestamp()) self.ModifiedTS = kwargs.get("ModifiedTS", datetime.utcnow().timestamp()) - - -mapper(PackageBase, PackageBases, properties={ - "Flagger": make_relationship(User, PackageBases.c.FlaggerUID, - "flagged_bases"), - "Submitter": make_relationship(User, PackageBases.c.SubmitterUID, - "submitted_bases"), - "Maintainer": make_relationship(User, PackageBases.c.MaintainerUID, - "maintained_bases"), - "Packager": make_relationship(User, PackageBases.c.PackagerUID, - "package_bases") -}) diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index 21801802..0bd84073 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -1,17 +1,40 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.dependency_type import DependencyType -from aurweb.models.package import Package -from aurweb.schema import PackageDepends +import aurweb.models.package + +from aurweb.models import dependency_type +from aurweb.models.declarative import Base -class PackageDependency: - def __init__(self, Package: Package = None, - DependencyType: DependencyType = None, - DepName: str = None, DepDesc: str = None, - DepCondition: str = None, DepArch: str = None): +class PackageDependency(Base): + __tablename__ = "PackageDepends" + + PackageID = Column( + Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), + nullable=False) + Package = relationship( + "Package", backref=backref("package_dependencies", lazy="dynamic"), + foreign_keys=[PackageID], lazy="select") + + DepTypeID = Column( + Integer, ForeignKey("DependencyTypes.ID", ondelete="NO ACTION"), + nullable=False) + DependencyType = relationship( + "DependencyType", + backref=backref("package_dependencies", lazy="dynamic"), + foreign_keys=[DepTypeID], lazy="select") + + __mapper_args__ = {"primary_key": [PackageID, DepTypeID]} + + def __init__(self, + Package: aurweb.models.package.Package = None, + DependencyType: dependency_type.DependencyType = None, + DepName: str = None, + DepDesc: str = None, + DepCondition: str = None, + DepArch: str = None): self.Package = Package if not self.Package: raise IntegrityError( @@ -36,15 +59,3 @@ class PackageDependency: self.DepDesc = DepDesc self.DepCondition = DepCondition self.DepArch = DepArch - - -properties = { - "Package": make_relationship(Package, PackageDepends.c.PackageID, - "package_dependencies"), - "DependencyType": make_relationship(DependencyType, - PackageDepends.c.DepTypeID, - "package_dependencies") -} - -mapper(PackageDependency, PackageDepends, properties=properties, - primary_key=[PackageDepends.c.PackageID, PackageDepends.c.DepTypeID]) diff --git a/aurweb/models/package_group.py b/aurweb/models/package_group.py index 19a11c80..a8031e0d 100644 --- a/aurweb/models/package_group.py +++ b/aurweb/models/package_group.py @@ -1,14 +1,33 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.group import Group -from aurweb.models.package import Package -from aurweb.schema import PackageGroups +import aurweb.models.group +import aurweb.models.package + +from aurweb.models.declarative import Base -class PackageGroup: - def __init__(self, Package: Package = None, Group: Group = None): +class PackageGroup(Base): + __tablename__ = "PackageGroups" + + PackageID = Column(Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), + primary_key=True, nullable=True) + Package = relationship( + "Package", backref=backref("package_groups", lazy="dynamic"), + foreign_keys=[PackageID]) + + GroupID = Column(Integer, ForeignKey("Groups.ID", ondelete="CASCADE"), + primary_key=True, nullable=True) + Group = relationship( + "Group", backref=backref("package_groups", lazy="dynamic"), + foreign_keys=[GroupID]) + + __mapper_args__ = {"primary_key": [PackageID, GroupID]} + + def __init__(self, + Package: aurweb.models.package.Package = None, + Group: aurweb.models.group.Group = None): self.Package = Package if not self.Package: raise IntegrityError( @@ -22,18 +41,3 @@ class PackageGroup: statement="Primary key GroupID cannot be null.", orig="PackageGroups.GroupID", params=("NULL")) - - -properties = { - "Package": make_relationship(Package, - PackageGroups.c.PackageID, - "package_group", - uselist=False), - "Group": make_relationship(Group, - PackageGroups.c.GroupID, - "package_group", - uselist=False) -} - -mapper(PackageGroup, PackageGroups, properties=properties, - primary_key=[PackageGroups.c.PackageID, PackageGroups.c.GroupID]) diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py index 2bae223c..2926740d 100644 --- a/aurweb/models/package_keyword.py +++ b/aurweb/models/package_keyword.py @@ -1,14 +1,27 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.package_base import PackageBase -from aurweb.schema import PackageKeywords +import aurweb.db +import aurweb.models.package_base + +from aurweb.models.declarative import Base -class PackageKeyword: +class PackageKeyword(Base): + __tablename__ = "PackageKeywords" + + PackageBaseID = Column( + Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), + primary_key=True, nullable=True) + PackageBase = relationship( + "PackageBase", backref=backref("keywords", lazy="dynamic"), + foreign_keys=[PackageBaseID]) + + __mapper_args__ = {"primary_key": [PackageBaseID]} + def __init__(self, - PackageBase: PackageBase = None, + PackageBase: aurweb.models.package_base.PackageBase = None, Keyword: str = None): self.PackageBase = PackageBase if not self.PackageBase: @@ -18,10 +31,3 @@ class PackageKeyword: params=("NULL")) self.Keyword = Keyword - - -mapper(PackageKeyword, PackageKeywords, properties={ - "PackageBase": make_relationship(PackageBase, - PackageKeywords.c.PackageBaseID, - "keywords") -}) diff --git a/aurweb/models/package_license.py b/aurweb/models/package_license.py index 491874a4..0689562f 100644 --- a/aurweb/models/package_license.py +++ b/aurweb/models/package_license.py @@ -1,14 +1,35 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.license import License -from aurweb.models.package import Package -from aurweb.schema import PackageLicenses +import aurweb.models.license +import aurweb.models.package + +from aurweb.models.declarative import Base -class PackageLicense: - def __init__(self, Package: Package = None, License: License = None): +class PackageLicense(Base): + __tablename__ = "PackageLicenses" + + PackageID = Column( + Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), + primary_key=True, nullable=True) + Package = relationship( + "Package", backref=backref("package_license", uselist=False), + foreign_keys=[PackageID]) + + LicenseID = Column( + Integer, ForeignKey("Licenses.ID", ondelete="CASCADE"), + primary_key=True, nullable=True) + License = relationship( + "License", backref=backref("package_license", uselist=False), + foreign_keys=[LicenseID]) + + __mapper_args__ = {"primary_key": [PackageID, LicenseID]} + + def __init__(self, + Package: aurweb.models.package.Package = None, + License: aurweb.models.license.License = None): self.Package = Package if not self.Package: raise IntegrityError( @@ -22,20 +43,3 @@ class PackageLicense: statement="Primary key LicenseID cannot be null.", orig="PackageLicenses.LicenseID", params=("NULL")) - - -properties = { - "Package": make_relationship(Package, - PackageLicenses.c.PackageID, - "package_license", - uselist=False), - "License": make_relationship(License, - PackageLicenses.c.LicenseID, - "package_license", - uselist=False) - - -} - -mapper(PackageLicense, PackageLicenses, properties=properties, - primary_key=[PackageLicenses.c.PackageID, PackageLicenses.c.LicenseID]) diff --git a/aurweb/models/package_relation.py b/aurweb/models/package_relation.py index d9ade727..9204af59 100644 --- a/aurweb/models/package_relation.py +++ b/aurweb/models/package_relation.py @@ -1,15 +1,36 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper +from sqlalchemy.orm import backref, relationship -from aurweb.db import make_relationship -from aurweb.models.package import Package -from aurweb.models.relation_type import RelationType -from aurweb.schema import PackageRelations +import aurweb.db +import aurweb.models.package +import aurweb.models.relation_type + +from aurweb.models.declarative import Base -class PackageRelation: - def __init__(self, Package: Package = None, - RelationType: RelationType = None, +class PackageRelation(Base): + __tablename__ = "PackageRelations" + + PackageID = Column( + Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), + nullable=False) + Package = relationship( + "Package", backref=backref("package_relations", lazy="dynamic"), + foreign_keys=[PackageID]) + + RelTypeID = Column( + Integer, ForeignKey("RelationTypes.ID", ondelete="CASCADE"), + nullable=False) + RelationType = relationship( + "RelationType", backref=backref("package_relations", lazy="dynamic"), + foreign_keys=[RelTypeID]) + + __mapper_args__ = {"primary_key": [PackageID, RelTypeID]} + + def __init__(self, + Package: aurweb.models.package.Package = None, + RelationType: aurweb.models.relation_type.RelationType = None, RelName: str = None, RelCondition: str = None, RelArch: str = None): self.Package = Package @@ -35,18 +56,3 @@ class PackageRelation: self.RelCondition = RelCondition self.RelArch = RelArch - - -properties = { - "Package": make_relationship(Package, PackageRelations.c.PackageID, - "package_relations"), - "RelationType": make_relationship(RelationType, - PackageRelations.c.RelTypeID, - "package_relations") -} - -mapper(PackageRelation, PackageRelations, properties=properties, - primary_key=[ - PackageRelations.c.PackageID, - PackageRelations.c.RelTypeID - ]) diff --git a/aurweb/models/relation_type.py b/aurweb/models/relation_type.py index b4d1efbc..319fb7f4 100644 --- a/aurweb/models/relation_type.py +++ b/aurweb/models/relation_type.py @@ -1,11 +1,14 @@ -from sqlalchemy.orm import mapper +from sqlalchemy import Column, Integer -from aurweb.schema import RelationTypes +from aurweb.models.declarative import Base -class RelationType: +class RelationType(Base): + __tablename__ = "RelationTypes" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} + def __init__(self, Name: str = None): self.Name = Name - - -mapper(RelationType, RelationTypes) diff --git a/aurweb/models/session.py b/aurweb/models/session.py index f1e0fff5..9154178e 100644 --- a/aurweb/models/session.py +++ b/aurweb/models/session.py @@ -1,12 +1,24 @@ +from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import backref, mapper, relationship +from sqlalchemy.orm import backref, relationship from aurweb.db import make_random_value, query +from aurweb.models.declarative import Base from aurweb.models.user import User -from aurweb.schema import Sessions -class Session: +class Session(Base): + __tablename__ = "Sessions" + + UsersID = Column( + Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + User = relationship( + "User", backref=backref("session", uselist=False), + foreign_keys=[UsersID]) + + __mapper_args__ = {"primary_key": [UsersID]} + def __init__(self, **kwargs): self.UsersID = kwargs.get("UsersID") if not query(User, User.ID == self.UsersID).first(): @@ -19,11 +31,5 @@ class Session: self.LastUpdateTS = kwargs.get("LastUpdateTS") -mapper(Session, Sessions, primary_key=[Sessions.c.SessionID], properties={ - "User": relationship(User, backref=backref("session", - uselist=False)) -}) - - def generate_unique_sid(): return make_random_value(Session, Session.SessionID) diff --git a/aurweb/models/ssh_pub_key.py b/aurweb/models/ssh_pub_key.py index 01ff558e..268a585b 100644 --- a/aurweb/models/ssh_pub_key.py +++ b/aurweb/models/ssh_pub_key.py @@ -3,13 +3,26 @@ import tempfile from subprocess import PIPE, Popen -from sqlalchemy.orm import backref, mapper, relationship +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import backref, relationship -from aurweb.models.user import User -from aurweb.schema import SSHPubKeys +from aurweb.models.declarative import Base -class SSHPubKey: +class SSHPubKey(Base): + __tablename__ = "SSHPubKeys" + + UserID = Column( + Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + User = relationship( + "User", backref=backref("ssh_pub_key", uselist=False), + foreign_keys=[UserID]) + + Fingerprint = Column(String(44), primary_key=True) + + __mapper_args__ = {"primary_key": Fingerprint} + def __init__(self, **kwargs): self.UserID = kwargs.get("UserID") self.Fingerprint = kwargs.get("Fingerprint") @@ -34,8 +47,3 @@ def get_fingerprint(pubkey): fp = parts[1].replace("SHA256:", "") return fp - - -mapper(SSHPubKey, SSHPubKeys, properties={ - "User": relationship(User, backref=backref("ssh_pub_key", uselist=False)) -}) diff --git a/aurweb/models/term.py b/aurweb/models/term.py index 1a0780df..b0da71f7 100644 --- a/aurweb/models/term.py +++ b/aurweb/models/term.py @@ -1,10 +1,16 @@ +from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapper -from aurweb.schema import Terms +from aurweb.models.declarative import Base -class Term: +class Term(Base): + __tablename__ = "Terms" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} + def __init__(self, Description: str = None, URL: str = None, Revision: int = None): @@ -23,6 +29,3 @@ class Term: params=("NULL")) self.Revision = Revision - - -mapper(Term, Terms) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 1961228e..83cde5f1 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -5,50 +5,44 @@ from datetime import datetime import bcrypt from fastapi import Request -from sqlalchemy.orm import backref, mapper, relationship +from sqlalchemy import Column, ForeignKey, Integer, String, text +from sqlalchemy.orm import backref, relationship import aurweb.config +import aurweb.models.account_type +import aurweb.schema -from aurweb.models.account_type import AccountType from aurweb.models.ban import is_banned -from aurweb.schema import Users +from aurweb.models.declarative import Base -class User: +class User(Base): """ An ORM model of a single Users record. """ + __tablename__ = "Users" + + ID = Column(Integer, primary_key=True) + + AccountTypeID = Column( + Integer, ForeignKey("AccountTypes.ID", ondelete="NO ACTION"), + nullable=False, server_default=text("1")) + AccountType = relationship( + "AccountType", + backref=backref("users", lazy="dynamic"), + foreign_keys=[AccountTypeID], + uselist=False) + + Passwd = Column(String(255), default=str()) + + __mapper_args__ = {"primary_key": [ID]} + + # High-level variables used to track authentication (not in DB). authenticated = False - def __init__(self, **kwargs): - # Set AccountTypeID if it was passed. - self.AccountTypeID = kwargs.get("AccountTypeID") + def __init__(self, Passwd: str = str(), **kwargs): + super().__init__(**kwargs) - account_type = kwargs.get("AccountType") - if account_type: - self.AccountType = account_type - - self.Username = kwargs.get("Username") - - self.ResetKey = kwargs.get("ResetKey") - self.Email = kwargs.get("Email") - self.BackupEmail = kwargs.get("BackupEmail") - self.RealName = kwargs.get("RealName") - self.LangPreference = kwargs.get("LangPreference") - self.Timezone = kwargs.get("Timezone") - self.Homepage = kwargs.get("Homepage") - self.IRCNick = kwargs.get("IRCNick") - self.PGPKey = kwargs.get("PGPKey") - self.RegistrationTS = datetime.utcnow() - self.CommentNotify = kwargs.get("CommentNotify") - self.UpdateNotify = kwargs.get("UpdateNotify") - self.OwnershipNotify = kwargs.get("OwnershipNotify") - self.SSOAccountID = kwargs.get("SSOAccountID") - - self.Salt = None - self.Passwd = str() - - passwd = kwargs.get("Passwd") - if passwd: - self.update_password(passwd) + if Passwd: + self.update_password(Passwd) def update_password(self, password, salt_rounds=12): self.Passwd = bcrypt.hashpw( @@ -154,10 +148,3 @@ class User: def __repr__(self): return "" % ( self.ID, str(self.AccountType), self.Username) - - -# Map schema.Users to User and give it some relationships. -mapper(User, Users, properties={ - "AccountType": relationship(AccountType, - backref=backref("users", lazy="dynamic")) -}) diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py index 4ddf1fc3..8569b021 100644 --- a/test/test_accepted_term.py +++ b/test/test_accepted_term.py @@ -22,7 +22,7 @@ def setup(): AccountType.AccountType == "User").first() user = create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - account_type=account_type) + AccountType=account_type) term = create(Term, Description="Test term", URL="https://test.term") @@ -33,7 +33,7 @@ def test_accepted_term(): # Make sure our AcceptedTerm relationships got initialized properly. assert accepted_term.User == user assert accepted_term in user.accepted_terms - assert accepted_term in term.accepted + assert accepted_term in term.accepted_terms def test_accepted_term_null_user_raises_exception(): diff --git a/test/test_account_type.py b/test/test_account_type.py index 3bd76d1e..fa4bc5ad 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -43,6 +43,7 @@ def test_user_account_type_relationship(): AccountType=account_type) assert user.AccountType == account_type - assert account_type.users.filter(User.ID == user.ID).first() + # This must be deleted here to avoid foreign key issues when + # deleting the temporary AccountType in the fixture. delete(User, User.ID == user.ID) diff --git a/test/test_package.py b/test/test_package.py index a994f096..9532823d 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -1,7 +1,7 @@ import pytest from sqlalchemy import and_ -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError, OperationalError import aurweb.config @@ -19,7 +19,7 @@ user = pkgbase = package = None def setup(): global user, pkgbase, package - setup_test_db("Users", "PackageBases", "Packages") + setup_test_db("Packages", "PackageBases", "Users") account_type = query(AccountType, AccountType.AccountType == "User").first() @@ -57,17 +57,17 @@ def test_package(): assert record is not None -def test_package_ci(): +def test_package_package_base_cant_change(): """ Test case insensitivity of the database table. """ if aurweb.config.get("database", "backend") == "sqlite": return None # SQLite doesn't seem handle this. from aurweb.db import session - with pytest.raises(IntegrityError): + with pytest.raises(OperationalError): create(Package, PackageBase=pkgbase, - Name="Beautiful-Package") + Name="invalidates-old-package-packagebase-relationship") session.rollback() diff --git a/test/test_package_group.py b/test/test_package_group.py index 28047a7f..0e6e41e3 100644 --- a/test/test_package_group.py +++ b/test/test_package_group.py @@ -25,7 +25,7 @@ def setup(): AccountType.AccountType == "User").first() user = create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - account_type=account_type) + AccountType=account_type) group = create(Group, Name="Test Group") pkgbase = create(PackageBase, Name="test-package", Maintainer=user) diff --git a/test/test_package_license.py b/test/test_package_license.py index 72eb3681..f7654dee 100644 --- a/test/test_package_license.py +++ b/test/test_package_license.py @@ -25,7 +25,7 @@ def setup(): AccountType.AccountType == "User").first() user = create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - account_type=account_type) + AccountType=account_type) license = create(License, Name="Test License") pkgbase = create(PackageBase, Name="test-package", Maintainer=user) diff --git a/test/test_session.py b/test/test_session.py index 1dd82db1..1ba11556 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -10,12 +10,12 @@ from aurweb.models.session import Session, generate_unique_sid from aurweb.models.user import User from aurweb.testing import setup_test_db -user = session = None +account_type = user = session = None @pytest.fixture(autouse=True) def setup(): - global user, session + global account_type, user, session setup_test_db("Users", "Sessions") @@ -35,7 +35,10 @@ def test_session(): def test_session_cs(): """ Test case sensitivity of the database table. """ - session_cs = create(Session, UsersID=user.ID, + user2 = create(User, Username="test2", Email="test2@example.org", + ResetKey="testReset2", Passwd="testPassword", + AccountType=account_type) + session_cs = create(Session, UsersID=user2.ID, SessionID="TESTSESSION", LastUpdateTS=datetime.utcnow().timestamp()) assert session_cs.SessionID == "TESTSESSION" From 5de7ff64df0dc33a20999ae7042ff04a77451819 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 10 Jun 2021 13:55:07 -0700 Subject: [PATCH 0252/1451] add PackageVote SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_vote.py | 53 ++++++++++++++++++++++++++++++++ test/test_package_vote.py | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 aurweb/models/package_vote.py create mode 100644 test/test_package_vote.py diff --git a/aurweb/models/package_vote.py b/aurweb/models/package_vote.py new file mode 100644 index 00000000..55a9ecbb --- /dev/null +++ b/aurweb/models/package_vote.py @@ -0,0 +1,53 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.package_base +import aurweb.models.user + +from aurweb.models.declarative import Base + + +class PackageVote(Base): + __tablename__ = "PackageVotes" + + UsersID = Column( + Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + User = relationship( + "User", backref=backref("package_votes", lazy="dynamic"), + foreign_keys=[UsersID]) + + PackageBaseID = Column( + Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False) + PackageBase = relationship( + "PackageBase", backref=backref("package_votes", lazy="dynamic"), + foreign_keys=[PackageBaseID]) + + __mapper_args__ = {"primary_key": [UsersID, PackageBaseID]} + + def __init__(self, + User: aurweb.models.user.User = None, + PackageBase: aurweb.models.package_base.PackageBase = None, + VoteTS: int = None): + self.User = User + if not self.User: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageVotes.UsersID", + params=("NULL")) + + self.PackageBase = PackageBase + if not self.PackageBase: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageVotes.PackageBaseID", + params=("NULL")) + + self.VoteTS = VoteTS + if not self.VoteTS: + raise IntegrityError( + statement="Column VoteTS cannot be null.", + orig="PackageVotes.VoteTS", + params=("NULL")) diff --git a/test/test_package_vote.py b/test/test_package_vote.py new file mode 100644 index 00000000..b352bf11 --- /dev/null +++ b/test/test_package_vote.py @@ -0,0 +1,58 @@ +from datetime import datetime + +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query, rollback +from aurweb.models.account_type import AccountType +from aurweb.models.package_base import PackageBase +from aurweb.models.package_vote import PackageVote +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase + + setup_test_db("Users", "PackageBases", "PackageVotes") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + print(account_type.ID) + print(account_type.AccountType) + + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + + +def test_package_vote_creation(): + ts = int(datetime.utcnow().timestamp()) + package_vote = create(PackageVote, User=user, PackageBase=pkgbase, + VoteTS=ts) + assert bool(package_vote) + assert package_vote.User == user + assert package_vote.PackageBase == pkgbase + assert package_vote.VoteTS == ts + + +def test_package_vote_null_user_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageVote, PackageBase=pkgbase, VoteTS=1) + rollback() + + +def test_package_vote_null_pkgbase_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageVote, User=user, VoteTS=1) + rollback() + + +def test_package_vote_null_votets_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageVote, User=user, PackageBase=pkgbase) + rollback() From d18cfad63eeaab54fd21d386a769d85067153f8f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 10 Jun 2021 14:18:39 -0700 Subject: [PATCH 0253/1451] use djangos method of wiping sqlite3 tables Django uses a reference graph to determine the order in table deletions that occur. Do the same here. This commit also adds in the `REGEXP` sqlite function, exactly how Django uses it in its reference graphing. Signed-off-by: Kevin Morris --- aurweb/db.py | 24 +++++++++++++++++++++++- aurweb/testing/__init__.py | 36 ++++++++++++++++++++++++++++++++++++ test/test_db.py | 3 +++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/aurweb/db.py b/aurweb/db.py index 9837c746..04c8653a 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,4 +1,8 @@ +import functools import math +import re + +from sqlalchemy import event import aurweb.config import aurweb.util @@ -129,13 +133,31 @@ def get_engine(echo: bool = False): if engine is None: connect_args = dict() - if aurweb.config.get("database", "backend") == "sqlite": + + db_backend = aurweb.config.get("database", "backend") + if db_backend == "sqlite": # check_same_thread is for a SQLite technicality # https://fastapi.tiangolo.com/tutorial/sql-databases/#note connect_args["check_same_thread"] = False + engine = create_engine(get_sqlalchemy_url(), connect_args=connect_args, echo=echo) + + if db_backend == "sqlite": + # For SQLite, we need to add some custom functions as + # they are used in the reference graph method. + def regexp(regex, item): + return bool(re.search(regex, str(item))) + + @event.listens_for(engine, "begin") + def do_begin(conn): + create_deterministic_function = functools.partial( + conn.connection.create_function, + deterministic=True + ) + create_deterministic_function("REGEXP", 2, regexp) + Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) session = Session() diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 02c21a4c..90d46720 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -1,6 +1,28 @@ +from itertools import chain + import aurweb.db +def references_graph(table): + """ Taken from Django's sqlite3/operations.py. """ + query = """ + WITH tables AS ( + SELECT :table name + UNION + SELECT sqlite_master.name + FROM sqlite_master + JOIN tables ON (sql REGEXP :regexp_1 || tables.name || :regexp_2) + ) SELECT name FROM tables; + """ + params = { + "table": table, + "regexp_1": r'(?i)\s+references\s+("|\')?', + "regexp_2": r'("|\')?\s*\(', + } + cursor = aurweb.db.session.execute(query, params=params) + return [row[0] for row in cursor.fetchall()] + + def setup_test_db(*args): """ This function is to be used to setup a test database before using it. It takes a variable number of table strings, and for @@ -25,8 +47,22 @@ def setup_test_db(*args): aurweb.db.get_engine() tables = list(args) + + db_backend = aurweb.config.get("database", "backend") + + if db_backend != "sqlite": + aurweb.db.session.execute("SET FOREIGN_KEY_CHECKS = 0") + else: + # We're using sqlite, setup tables to be deleted without violating + # foreign key constraints by graphing references. + tables = set(chain.from_iterable( + references_graph(table) for table in tables)) + for table in tables: aurweb.db.session.execute(f"DELETE FROM {table}") + if db_backend != "sqlite": + aurweb.db.session.execute("SET FOREIGN_KEY_CHECKS = 1") + # Expunge all objects from SQLAlchemy's IdentityMap. aurweb.db.session.expunge_all() diff --git a/test/test_db.py b/test/test_db.py index 3911134f..9298c53d 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -200,6 +200,9 @@ def test_connection_execute_paramstyle_format(): aurweb.db.kill_engine() aurweb.initdb.run(Args()) + # Test SQLite route of clearing tables. + setup_test_db("Users", "Bans") + conn = db.Connection() # First, test ? to %s format replacement. From 11c4926502e0767ee2435a79dd1c8ecaee727086 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 10 Jun 2021 17:46:29 -0700 Subject: [PATCH 0254/1451] add PackageSource SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_source.py | 31 +++++++++++++++++++++++ test/test_package_source.py | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 aurweb/models/package_source.py create mode 100644 test/test_package_source.py diff --git a/aurweb/models/package_source.py b/aurweb/models/package_source.py new file mode 100644 index 00000000..4ffa23df --- /dev/null +++ b/aurweb/models/package_source.py @@ -0,0 +1,31 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.package + +from aurweb.models.declarative import Base + + +class PackageSource(Base): + __tablename__ = "PackageSources" + + PackageID = Column(Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), + nullable=False) + Package = relationship( + "Package", backref=backref("package_sources", lazy="dynamic"), + foreign_keys=[PackageID]) + + __mapper_args__ = {"primary_key": [PackageID]} + + def __init__(self, + Package: aurweb.models.package.Package = None, + **kwargs): + super().__init__(**kwargs) + + self.Package = Package + if not self.Package: + raise IntegrityError( + statement="Foreign key PackageID cannot be null.", + orig="PackageSources.PackageID", + params=("NULL")) diff --git a/test/test_package_source.py b/test/test_package_source.py new file mode 100644 index 00000000..7453f756 --- /dev/null +++ b/test/test_package_source.py @@ -0,0 +1,44 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query, rollback +from aurweb.models.account_type import AccountType +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_source import PackageSource +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = package = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase, package + + setup_test_db("PackageSources", "Packages", "PackageBases", "Users") + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + pkgbase = create(PackageBase, + Name="test-package", + Maintainer=user) + package = create(Package, PackageBase=pkgbase, Name="test-package") + + +def test_package_source(): + pkgsource = create(PackageSource, Package=package) + assert pkgsource.Package == package + # By default, PackageSources.Source assigns the string '/dev/null'. + assert pkgsource.Source == "/dev/null" + assert pkgsource.SourceArch is None + + +def test_package_source_null_package_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageSource) + rollback() From fc28c1e5fd137ab36786eae864a19617bdce90e6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 00:35:18 -0700 Subject: [PATCH 0255/1451] add PackageComment SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_comment.py | 72 ++++++++++++++++++++++++++++++++ test/test_package_comment.py | 63 ++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 aurweb/models/package_comment.py create mode 100644 test/test_package_comment.py diff --git a/aurweb/models/package_comment.py b/aurweb/models/package_comment.py new file mode 100644 index 00000000..42a0661d --- /dev/null +++ b/aurweb/models/package_comment.py @@ -0,0 +1,72 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.package_base +import aurweb.models.user + +from aurweb.models.declarative import Base + + +class PackageComment(Base): + __tablename__ = "PackageComments" + + ID = Column(Integer, primary_key=True) + + PackageBaseID = Column( + Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False) + PackageBase = relationship( + "PackageBase", backref=backref("comments", lazy="dynamic"), + foreign_keys=[PackageBaseID]) + + UsersID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) + User = relationship( + "User", backref=backref("package_comments", lazy="dynamic"), + foreign_keys=[UsersID]) + + EditedUsersID = Column( + Integer, ForeignKey("Users.ID", ondelete="SET NULL")) + Editor = relationship( + "User", backref=backref("edited_comments", lazy="dynamic"), + foreign_keys=[EditedUsersID]) + + DelUsersID = Column( + Integer, ForeignKey("Users.ID", ondelete="SET NULL")) + Deleter = relationship( + "User", backref=backref("deleted_comments", lazy="dynamic"), + foreign_keys=[DelUsersID]) + + __mapper_args__ = {"primary_key": [ID]} + + def __init__(self, + PackageBase: aurweb.models.package_base.PackageBase = None, + User: aurweb.models.user.User = None, + **kwargs): + super().__init__(**kwargs) + + self.PackageBase = PackageBase + if not self.PackageBase: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageComments.PackageBaseID", + params=("NULL")) + + self.User = User + if not self.User: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageComments.UsersID", + params=("NULL")) + + if self.Comments is None: + raise IntegrityError( + statement="Column Comments cannot be null.", + orig="PackageComments.Comments", + params=("NULL")) + + if self.RenderedComment is None: + raise IntegrityError( + statement="Column RenderedComment cannot be null.", + orig="PackageComments.RenderedComment", + params=("NULL")) diff --git a/test/test_package_comment.py b/test/test_package_comment.py new file mode 100644 index 00000000..fb734071 --- /dev/null +++ b/test/test_package_comment.py @@ -0,0 +1,63 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query, rollback +from aurweb.models.account_type import AccountType +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comment import PackageComment +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = None + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db("PackageBases", "PackageComments", "Users") + + global user, pkgbase + + account_type = query(AccountType, + AccountType.AccountType == "User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + + +def test_package_comment_creation(): + package_comment = create(PackageComment, + PackageBase=pkgbase, + User=user, + Comments="Test comment.", + RenderedComment="Test rendered comment.") + assert bool(package_comment.ID) + + +def test_package_comment_null_package_base_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageComment, User=user, Comments="Test comment.", + RenderedComment="Test rendered comment.") + rollback() + + +def test_package_comment_null_user_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageComment, PackageBase=pkgbase, Comments="Test comment.", + RenderedComment="Test rendered comment.") + rollback() + + +def test_package_comment_null_comments_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageComment, PackageBase=pkgbase, User=user, + RenderedComment="Test rendered comment.") + rollback() + + +def test_package_comment_null_renderedcomment_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageComment, PackageBase=pkgbase, User=user, + Comments="Test comment.") + rollback() From ebd216edfd4f78db864f044fdc10d13cdc7b20b9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 16:52:45 -0700 Subject: [PATCH 0256/1451] add PackageComaintainer SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_comaintainer.py | 53 +++++++++++++++++++++++++++ test/test_package_comaintainer.py | 49 +++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 aurweb/models/package_comaintainer.py create mode 100644 test/test_package_comaintainer.py diff --git a/aurweb/models/package_comaintainer.py b/aurweb/models/package_comaintainer.py new file mode 100644 index 00000000..88fd58ae --- /dev/null +++ b/aurweb/models/package_comaintainer.py @@ -0,0 +1,53 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.package_base +import aurweb.models.user + +from aurweb.models.declarative import Base + + +class PackageComaintainer(Base): + __tablename__ = "PackageComaintainers" + + UsersID = Column( + Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + User = relationship( + "User", backref=backref("comaintained", lazy="dynamic"), + foreign_keys=[UsersID]) + + PackageBaseID = Column( + Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False) + PackageBase = relationship( + "PackageBase", backref=backref("comaintainers", lazy="dynamic"), + foreign_keys=[PackageBaseID]) + + __mapper_args__ = {"primary_key": [UsersID, PackageBaseID]} + + def __init__(self, + User: aurweb.models.user.User = None, + PackageBase: aurweb.models.package_base.PackageBase = None, + Priority: int = None): + self.User = User + if not self.User: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageComaintainers.UsersID", + params=("NULL")) + + self.PackageBase = PackageBase + if not self.PackageBase: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageComaintainers.PackageBaseID", + params=("NULL")) + + self.Priority = Priority + if not self.Priority: + raise IntegrityError( + statement="Column Priority cannot be null.", + orig="PackageComaintainers.Priority", + params=("NULL")) diff --git a/test/test_package_comaintainer.py b/test/test_package_comaintainer.py new file mode 100644 index 00000000..ac94a9ba --- /dev/null +++ b/test/test_package_comaintainer.py @@ -0,0 +1,49 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, rollback +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase + + setup_test_db("Users", "PackageBases", "PackageComaintainers") + + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + + +def test_package_comaintainer_creation(): + package_comaintainer = create(PackageComaintainer, User=user, PackageBase=pkgbase, + Priority=5) + assert bool(package_comaintainer) + assert package_comaintainer.User == user + assert package_comaintainer.PackageBase == pkgbase + assert package_comaintainer.Priority == 5 + + +def test_package_comaintainer_null_user_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageComaintainer, PackageBase=pkgbase, Priority=1) + rollback() + + +def test_package_comaintainer_null_pkgbase_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageComaintainer, User=user, Priority=1) + rollback() + + +def test_package_comaintainer_null_priority_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageComaintainer, User=user, PackageBase=pkgbase) + rollback() From 229df1adefb709959e70754845ed1bc65501064c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 16:56:15 -0700 Subject: [PATCH 0257/1451] test_package_vote: remove useless stuff Signed-off-by: Kevin Morris --- test/test_package_vote.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/test_package_vote.py b/test/test_package_vote.py index b352bf11..cb15e217 100644 --- a/test/test_package_vote.py +++ b/test/test_package_vote.py @@ -4,8 +4,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query, rollback -from aurweb.models.account_type import AccountType +from aurweb.db import create, rollback from aurweb.models.package_base import PackageBase from aurweb.models.package_vote import PackageVote from aurweb.models.user import User @@ -20,11 +19,6 @@ def setup(): setup_test_db("Users", "PackageBases", "PackageVotes") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - print(account_type.ID) - print(account_type.AccountType) - user = create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword") pkgbase = create(PackageBase, Name="test-package", Maintainer=user) From 5b856c7af2e023d24ca7ad0208f537aff45239db Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 17:14:28 -0700 Subject: [PATCH 0258/1451] add PackageNotification SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_notification.py | 47 +++++++++++++++++++++++++++ test/test_package_notification.py | 42 ++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 aurweb/models/package_notification.py create mode 100644 test/test_package_notification.py diff --git a/aurweb/models/package_notification.py b/aurweb/models/package_notification.py new file mode 100644 index 00000000..ab23a212 --- /dev/null +++ b/aurweb/models/package_notification.py @@ -0,0 +1,47 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.package_base +import aurweb.models.user + +from aurweb.models.declarative import Base + + +class PackageNotification(Base): + __tablename__ = "PackageNotifications" + + UserID = Column( + Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + User = relationship( + "User", backref=backref("package_notifications", lazy="dynamic"), + foreign_keys=[UserID]) + + PackageBaseID = Column( + Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False) + PackageBase = relationship( + "PackageBase", + backref=backref("package_notifications", lazy="dynamic"), + foreign_keys=[PackageBaseID]) + + __mapper_args__ = {"primary_key": [UserID, PackageBaseID]} + + def __init__(self, + User: aurweb.models.user.User = None, + PackageBase: aurweb.models.package_base.PackageBase = None, + NotificationTS: int = None): + self.User = User + if not self.User: + raise IntegrityError( + statement="Foreign key UserID cannot be null.", + orig="PackageNotifications.UserID", + params=("NULL")) + + self.PackageBase = PackageBase + if not self.PackageBase: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageNotifications.PackageBaseID", + params=("NULL")) diff --git a/test/test_package_notification.py b/test/test_package_notification.py new file mode 100644 index 00000000..2898a904 --- /dev/null +++ b/test/test_package_notification.py @@ -0,0 +1,42 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, rollback +from aurweb.models.package_base import PackageBase +from aurweb.models.package_notification import PackageNotification +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase + + setup_test_db("Users", "PackageBases", "PackageNotifications") + + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + + +def test_package_notification_creation(): + package_notification = create(PackageNotification, User=user, + PackageBase=pkgbase) + assert bool(package_notification) + assert package_notification.User == user + assert package_notification.PackageBase == pkgbase + + +def test_package_notification_null_user_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageNotification, PackageBase=pkgbase) + rollback() + + +def test_package_notification_null_pkgbase_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageNotification, User=user) + rollback() From 163e4d738999932fb7e109aba48170c8f2526ca6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 17:15:18 -0700 Subject: [PATCH 0259/1451] test_package_comaintainer: sanitize newlines Signed-off-by: Kevin Morris --- test/test_package_comaintainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_package_comaintainer.py b/test/test_package_comaintainer.py index ac94a9ba..cba99ba0 100644 --- a/test/test_package_comaintainer.py +++ b/test/test_package_comaintainer.py @@ -23,8 +23,8 @@ def setup(): def test_package_comaintainer_creation(): - package_comaintainer = create(PackageComaintainer, User=user, PackageBase=pkgbase, - Priority=5) + package_comaintainer = create(PackageComaintainer, User=user, + PackageBase=pkgbase, Priority=5) assert bool(package_comaintainer) assert package_comaintainer.User == user assert package_comaintainer.PackageBase == pkgbase From 511f174c8ba704b3cf53ae47fcf56e55c74026f8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 17:28:08 -0700 Subject: [PATCH 0260/1451] add PackageBlacklist SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_blacklist.py | 20 ++++++++++++++++++ test/test_package_blacklist.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 aurweb/models/package_blacklist.py create mode 100644 test/test_package_blacklist.py diff --git a/aurweb/models/package_blacklist.py b/aurweb/models/package_blacklist.py new file mode 100644 index 00000000..7702c877 --- /dev/null +++ b/aurweb/models/package_blacklist.py @@ -0,0 +1,20 @@ +from sqlalchemy import Column, Integer +from sqlalchemy.exc import IntegrityError + +from aurweb.models.declarative import Base + + +class PackageBlacklist(Base): + __tablename__ = "PackageBlacklist" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} + + def __init__(self, Name: str = None): + self.Name = Name + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="PackageBlacklist.Name", + params=("NULL")) diff --git a/test/test_package_blacklist.py b/test/test_package_blacklist.py new file mode 100644 index 00000000..3c64cc21 --- /dev/null +++ b/test/test_package_blacklist.py @@ -0,0 +1,34 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, rollback +from aurweb.models.package_base import PackageBase +from aurweb.models.package_blacklist import PackageBlacklist +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase + + setup_test_db("PackageBlacklist", "PackageBases", "Users") + + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + + +def test_package_blacklist_creation(): + package_blacklist = create(PackageBlacklist, Name="evil-package") + assert bool(package_blacklist.ID) + assert package_blacklist.Name == "evil-package" + + +def test_package_blacklist_null_name_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageBlacklist) + rollback() From 3bf4b3717a6baed1326168053daf5f374a6e5003 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 17:37:51 -0700 Subject: [PATCH 0261/1451] add RequestType SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/request_type.py | 11 +++++++++++ test/test_request_type.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 aurweb/models/request_type.py create mode 100644 test/test_request_type.py diff --git a/aurweb/models/request_type.py b/aurweb/models/request_type.py new file mode 100644 index 00000000..2c8276e8 --- /dev/null +++ b/aurweb/models/request_type.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, Integer + +from aurweb.models.declarative import Base + + +class RequestType(Base): + __tablename__ = "RequestTypes" + + ID = Column(Integer, primary_key=True) + + __mapper_args__ = {"primary_key": [ID]} diff --git a/test/test_request_type.py b/test/test_request_type.py new file mode 100644 index 00000000..a470a60b --- /dev/null +++ b/test/test_request_type.py @@ -0,0 +1,24 @@ +import pytest + +from aurweb.db import create, delete +from aurweb.models.request_type import RequestType +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db() + + +def test_request_type_creation(): + request_type = create(RequestType, Name="Test Request") + assert bool(request_type.ID) + assert request_type.Name == "Test Request" + delete(RequestType, RequestType.ID == request_type.ID) + + +def test_request_type_null_name_returns_empty_string(): + request_type = create(RequestType) + assert bool(request_type.ID) + assert request_type.Name == str() + delete(RequestType, RequestType.ID == request_type.ID) From 65ff0e76da9ea6cf7ba382e38414424148d7c487 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 19:57:52 -0700 Subject: [PATCH 0262/1451] aurweb.schema: Fix off-by-one String impls of DECIMAL Signed-off-by: Kevin Morris --- aurweb/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/schema.py b/aurweb/schema.py index 9caf6374..fb8f0dee 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -109,7 +109,7 @@ PackageBases = Table( Column('NumVotes', INTEGER(unsigned=True), nullable=False, server_default=text("0")), Column('Popularity', DECIMAL(10, 6, unsigned=True) - if db_backend == "mysql" else String(16), # Stubbed out to test. + if db_backend == "mysql" else String(17), nullable=False, server_default=text("0")), Column('OutOfDateTS', BIGINT(unsigned=True)), Column('FlaggerComment', Text, nullable=False), @@ -388,7 +388,7 @@ TU_VoteInfo = Table( Column('End', BIGINT(unsigned=True), nullable=False), Column('Quorum', DECIMAL(2, 2, unsigned=True) - if db_backend == "mysql" else String(4), + if db_backend == "mysql" else String(5), nullable=False), Column('SubmitterID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), Column('Yes', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), From 809939ab03c5192d9f06d2ffa138d4e064fd0b35 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 20:36:32 -0700 Subject: [PATCH 0263/1451] add TUVoteInfo SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/tu_voteinfo.py | 74 +++++++++++++++++++++++ test/test_tu_voteinfo.py | 111 +++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 aurweb/models/tu_voteinfo.py create mode 100644 test/test_tu_voteinfo.py diff --git a/aurweb/models/tu_voteinfo.py b/aurweb/models/tu_voteinfo.py new file mode 100644 index 00000000..2225b4d7 --- /dev/null +++ b/aurweb/models/tu_voteinfo.py @@ -0,0 +1,74 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.user + +from aurweb.models.declarative import Base + + +class TUVoteInfo(Base): + __tablename__ = "TU_VoteInfo" + + ID = Column(Integer, primary_key=True) + + SubmitterID = Column( + Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + Submitter = relationship( + "User", backref=backref("tu_voteinfo_set", lazy="dynamic"), + foreign_keys=[SubmitterID]) + + __mapper_args__ = {"primary_key": [ID]} + + def __init__(self, + Agenda: str = None, + User: str = None, + Submitted: int = None, + End: int = None, + Quorum: float = None, + Submitter: aurweb.models.user.User = None, + **kwargs): + super().__init__(**kwargs) + + self.Agenda = Agenda + if self.Agenda is None: + raise IntegrityError( + statement="Column Agenda cannot be null.", + orig="TU_VoteInfo.Agenda", + params=("NULL")) + + self.User = User + if self.User is None: + raise IntegrityError( + statement="Column User cannot be null.", + orig="TU_VoteInfo.User", + params=("NULL")) + + self.Submitted = Submitted + if self.Submitted is None: + raise IntegrityError( + statement="Column Submitted cannot be null.", + orig="TU_VoteInfo.Submitted", + params=("NULL")) + + self.End = End + if self.End is None: + raise IntegrityError( + statement="Column End cannot be null.", + orig="TU_VoteInfo.End", + params=("NULL")) + + if Quorum is None: + raise IntegrityError( + statement="Column Quorum cannot be null.", + orig="TU_VoteInfo.Quorum", + params=("NULL")) + self.Quorum = str(Quorum) + + self.Submitter = Submitter + if not self.Submitter: + raise IntegrityError( + statement="Foreign key SubmitterID cannot be null.", + orig="TU_VoteInfo.SubmitterID", + params=("NULL")) diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py new file mode 100644 index 00000000..e95f174b --- /dev/null +++ b/test/test_tu_voteinfo.py @@ -0,0 +1,111 @@ +from datetime import datetime + +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query, rollback +from aurweb.models.account_type import AccountType +from aurweb.models.tu_voteinfo import TUVoteInfo +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = None + + +@pytest.fixture(autouse=True) +def setup(): + global user + + setup_test_db("Users", "PackageBases", "TU_VoteInfo") + + tu_type = query(AccountType, + AccountType.AccountType == "Trusted User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=tu_type) + + +def test_tu_voteinfo_creation(): + ts = int(datetime.utcnow().timestamp()) + tu_voteinfo = create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 5, + Quorum=0.5, + Submitter=user) + assert bool(tu_voteinfo.ID) + assert tu_voteinfo.Agenda == "Blah blah." + assert tu_voteinfo.User == user.Username + assert tu_voteinfo.Submitted == ts + assert tu_voteinfo.End == ts + 5 + assert float(tu_voteinfo.Quorum) == 0.5 + assert tu_voteinfo.Submitter == user + assert tu_voteinfo.Yes == 0 + assert tu_voteinfo.No == 0 + assert tu_voteinfo.Abstain == 0 + assert tu_voteinfo.ActiveTUs == 0 + + assert tu_voteinfo in user.tu_voteinfo_set + + +def test_tu_voteinfo_null_submitter_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, End=0, + Quorum=0.50) + rollback() + + +def test_tu_voteinfo_null_agenda_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVoteInfo, + User=user.Username, + Submitted=0, End=0, + Quorum=0.50, + Submitter=user) + rollback() + + +def test_tu_voteinfo_null_user_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVoteInfo, + Agenda="Blah blah.", + Submitted=0, End=0, + Quorum=0.50, + Submitter=user) + rollback() + + +def test_tu_voteinfo_null_submitted_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + End=0, + Quorum=0.50, + Submitter=user) + rollback() + + +def test_tu_voteinfo_null_end_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + Quorum=0.50, + Submitter=user) + rollback() + + +def test_tu_voteinfo_null_quorum_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, End=0, + Submitter=user) + rollback() From 541c978ac4a7bdec79e1ce7359c23d0ff42723fd Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 19:14:33 -0700 Subject: [PATCH 0264/1451] add PackageRequest SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/package_request.py | 93 ++++++++++++++++++++++++ test/test_package_request.py | 119 +++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 aurweb/models/package_request.py create mode 100644 test/test_package_request.py diff --git a/aurweb/models/package_request.py b/aurweb/models/package_request.py new file mode 100644 index 00000000..00f46ce2 --- /dev/null +++ b/aurweb/models/package_request.py @@ -0,0 +1,93 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.package_base +import aurweb.models.request_type +import aurweb.models.user + +from aurweb.models.declarative import Base + + +class PackageRequest(Base): + __tablename__ = "PackageRequests" + + ID = Column(Integer, primary_key=True) + + ReqTypeID = Column( + Integer, ForeignKey("RequestTypes.ID", ondelete="NO ACTION"), + nullable=False) + RequestType = relationship( + "RequestType", backref=backref("package_requests", lazy="dynamic"), + foreign_keys=[ReqTypeID]) + + UsersID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) + User = relationship( + "User", backref=backref("package_requests", lazy="dynamic"), + foreign_keys=[UsersID]) + + PackageBaseID = Column( + Integer, ForeignKey("PackageBases.ID", ondelete="SET NULL"), + nullable=False) + PackageBase = relationship( + "PackageBase", backref=backref("requests", lazy="dynamic"), + foreign_keys=[PackageBaseID]) + + ClosedUID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) + Closer = relationship( + "User", backref=backref("closed_requests", lazy="dynamic"), + foreign_keys=[ClosedUID]) + + __mapper_args__ = {"primary_key": [ID]} + + def __init__(self, + RequestType: aurweb.models.request_type.RequestType = None, + PackageBase: aurweb.models.package_base.PackageBase = None, + PackageBaseName: str = None, + User: aurweb.models.user.User = None, + Comments: str = None, + ClosureComment: str = None, + **kwargs): + super().__init__(**kwargs) + + self.RequestType = RequestType + if not self.RequestType: + raise IntegrityError( + statement="Foreign key ReqTypeID cannot be null.", + orig="PackageRequests.ReqTypeID", + params=("NULL")) + + self.PackageBase = PackageBase + if not self.PackageBase: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageRequests.PackageBaseID", + params=("NULL")) + + self.PackageBaseName = PackageBaseName + if not self.PackageBaseName: + raise IntegrityError( + statement="Column PackageBaseName cannot be null.", + orig="PackageRequests.PackageBaseName", + params=("NULL")) + + self.User = User + if not self.User: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageRequests.UsersID", + params=("NULL")) + + self.Comments = Comments + if self.Comments is None: + raise IntegrityError( + statement="Column Comments cannot be null.", + orig="PackageRequests.Comments", + params=("NULL")) + + self.ClosureComment = ClosureComment + if self.ClosureComment is None: + raise IntegrityError( + statement="Column ClosureComment cannot be null.", + orig="PackageRequests.ClosureComment", + params=("NULL")) diff --git a/test/test_package_request.py b/test/test_package_request.py new file mode 100644 index 00000000..fc839836 --- /dev/null +++ b/test/test_package_request.py @@ -0,0 +1,119 @@ +from datetime import datetime + +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query, rollback +from aurweb.models.package_base import PackageBase +from aurweb.models.package_request import PackageRequest +from aurweb.models.request_type import RequestType +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = pkgbase = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, pkgbase + + setup_test_db("PackageRequests", "PackageBases", "Users") + + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + + +def test_package_request_creation(): + request_type = query(RequestType, RequestType.Name == "merge").first() + assert request_type.Name == "merge" + + package_request = create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) + + assert bool(package_request.ID) + assert package_request.RequestType == request_type + assert package_request.User == user + assert package_request.PackageBase == pkgbase + assert package_request.PackageBaseName == pkgbase.Name + assert package_request.Comments == str() + assert package_request.ClosureComment == str() + + # Make sure that everything is cross-referenced with relationships. + assert package_request in request_type.package_requests + assert package_request in user.package_requests + assert package_request in pkgbase.requests + + +def test_package_request_closed(): + request_type = query(RequestType, RequestType.Name == "merge").first() + assert request_type.Name == "merge" + + ts = int(datetime.utcnow().timestamp()) + package_request = create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Closer=user, ClosedTS=ts, + Comments=str(), ClosureComment=str()) + + assert package_request.Closer == user + assert package_request.ClosedTS == ts + + # Test relationships. + assert package_request in user.closed_requests + + +def test_package_request_null_request_type_raises_exception(): + with pytest.raises(IntegrityError): + create(PackageRequest, User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) + rollback() + + +def test_package_request_null_user_raises_exception(): + request_type = query(RequestType, RequestType.Name == "merge").first() + with pytest.raises(IntegrityError): + create(PackageRequest, RequestType=request_type, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) + rollback() + + +def test_package_request_null_package_base_raises_exception(): + request_type = query(RequestType, RequestType.Name == "merge").first() + with pytest.raises(IntegrityError): + create(PackageRequest, RequestType=request_type, + User=user, PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) + rollback() + + +def test_package_request_null_package_base_name_raises_exception(): + request_type = query(RequestType, RequestType.Name == "merge").first() + with pytest.raises(IntegrityError): + create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + Comments=str(), ClosureComment=str()) + rollback() + + +def test_package_request_null_comments_raises_exception(): + request_type = query(RequestType, RequestType.Name == "merge").first() + with pytest.raises(IntegrityError): + create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + ClosureComment=str()) + rollback() + + +def test_package_request_null_closure_comment_raises_exception(): + request_type = query(RequestType, RequestType.Name == "merge").first() + with pytest.raises(IntegrityError): + create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + Comments=str()) + rollback() From 8c345a04488a07b9836cff9559bb17722b5fc77e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 21:48:39 -0700 Subject: [PATCH 0265/1451] TUVoteInfo: generalize Quorum SQLite does not support native DECIMAL columns, and for that reason, we had to switch to using Strings that can hold the data in the case we are using sqlite. This commit sets the TUVoteInfo model up in a generic way, that it always converts to string when setting Quorum (OK for DECIMAL) and always converts to float when getting Quorum. This way, we can treat TUVoteInfo.Quorum as the same thing everywhere. Signed-off-by: Kevin Morris --- aurweb/models/tu_voteinfo.py | 15 ++++++++++++++- test/test_tu_voteinfo.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/aurweb/models/tu_voteinfo.py b/aurweb/models/tu_voteinfo.py index 2225b4d7..a246f132 100644 --- a/aurweb/models/tu_voteinfo.py +++ b/aurweb/models/tu_voteinfo.py @@ -1,3 +1,5 @@ +import typing + from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship @@ -64,7 +66,7 @@ class TUVoteInfo(Base): statement="Column Quorum cannot be null.", orig="TU_VoteInfo.Quorum", params=("NULL")) - self.Quorum = str(Quorum) + self.Quorum = Quorum self.Submitter = Submitter if not self.Submitter: @@ -72,3 +74,14 @@ class TUVoteInfo(Base): statement="Foreign key SubmitterID cannot be null.", orig="TU_VoteInfo.SubmitterID", params=("NULL")) + + def __setattr__(self, key: str, value: typing.Any): + """ Customize setattr to stringify any Quorum keys given. """ + if key == "Quorum": + value = str(value) + return super().__setattr__(key, value) + + def __getattribute__(self, key: str): + """ Customize getattr to floatify any fetched Quorum values. """ + attr = super().__getattribute__(key) + return float(attr) if key == "Quorum" else attr diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py index e95f174b..37609efd 100644 --- a/test/test_tu_voteinfo.py +++ b/test/test_tu_voteinfo.py @@ -39,7 +39,7 @@ def test_tu_voteinfo_creation(): assert tu_voteinfo.User == user.Username assert tu_voteinfo.Submitted == ts assert tu_voteinfo.End == ts + 5 - assert float(tu_voteinfo.Quorum) == 0.5 + assert tu_voteinfo.Quorum == 0.5 assert tu_voteinfo.Submitter == user assert tu_voteinfo.Yes == 0 assert tu_voteinfo.No == 0 From 0c1241f8bbe53b587cb149d0daee32731bffed46 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 22:14:38 -0700 Subject: [PATCH 0266/1451] add TUVote SQLAlchemy ORM model Signed-off-by: Kevin Morris --- aurweb/models/tu_vote.py | 43 ++++++++++++++++++++++++++++++ test/test_tu_vote.py | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 aurweb/models/tu_vote.py create mode 100644 test/test_tu_vote.py diff --git a/aurweb/models/tu_vote.py b/aurweb/models/tu_vote.py new file mode 100644 index 00000000..2b7bf2d0 --- /dev/null +++ b/aurweb/models/tu_vote.py @@ -0,0 +1,43 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +import aurweb.models.tu_voteinfo +import aurweb.models.user + +from aurweb.models.declarative import Base + + +class TUVote(Base): + __tablename__ = "TU_Votes" + + VoteID = Column(Integer, ForeignKey("TU_VoteInfo.ID", ondelete="CASCADE"), + nullable=False) + VoteInfo = relationship( + "TUVoteInfo", backref=backref("tu_votes", lazy="dynamic"), + foreign_keys=[VoteID]) + + UserID = Column(Integer, ForeignKey("Users.ID", ondelete="CASCADE"), + nullable=False) + User = relationship( + "User", backref=backref("tu_votes", lazy="dynamic"), + foreign_keys=[UserID]) + + __mapper_args__ = {"primary_key": [VoteID, UserID]} + + def __init__(self, + VoteInfo: aurweb.models.tu_voteinfo.TUVoteInfo = None, + User: aurweb.models.user.User = None): + self.VoteInfo = VoteInfo + if self.VoteInfo is None: + raise IntegrityError( + statement="Foreign key VoteID cannot be null.", + orig="TU_Votes.VoteID", + params=("NULL")) + + self.User = User + if self.User is None: + raise IntegrityError( + statement="Foreign key UserID cannot be null.", + orig="TU_Votes.UserID", + params=("NULL")) diff --git a/test/test_tu_vote.py b/test/test_tu_vote.py new file mode 100644 index 00000000..9ff4a8d9 --- /dev/null +++ b/test/test_tu_vote.py @@ -0,0 +1,56 @@ +from datetime import datetime + +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb.db import create, query, rollback +from aurweb.models.account_type import AccountType +from aurweb.models.tu_vote import TUVote +from aurweb.models.tu_voteinfo import TUVoteInfo +from aurweb.models.user import User +from aurweb.testing import setup_test_db + +user = tu_voteinfo = None + + +@pytest.fixture(autouse=True) +def setup(): + global user, tu_voteinfo + + setup_test_db("Users", "TU_VoteInfo", "TU_Votes") + + tu_type = query(AccountType, + AccountType.AccountType == "Trusted User").first() + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=tu_type) + + ts = int(datetime.utcnow().timestamp()) + tu_voteinfo = create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 5, + Quorum=0.5, + Submitter=user) + + +def test_tu_vote_creation(): + tu_vote = create(TUVote, User=user, VoteInfo=tu_voteinfo) + assert tu_vote.VoteInfo == tu_voteinfo + assert tu_vote.User == user + + assert tu_vote in user.tu_votes + assert tu_vote in tu_voteinfo.tu_votes + + +def test_tu_vote_null_user_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVote, VoteInfo=tu_voteinfo) + rollback() + + +def test_tu_vote_null_voteinfo_raises_exception(): + with pytest.raises(IntegrityError): + create(TUVote, User=user) + rollback() From 18ec8e3cc8ca3e18f87c1b892b68a0857be39597 Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 20 Nov 2020 00:19:15 +0100 Subject: [PATCH 0267/1451] RSS: Add ability to specify isPermaLink="false" for GUID --- web/lib/feedcreator.class.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/lib/feedcreator.class.php b/web/lib/feedcreator.class.php index a1fe24c9..bfc29b20 100644 --- a/web/lib/feedcreator.class.php +++ b/web/lib/feedcreator.class.php @@ -183,7 +183,7 @@ class FeedItem extends HtmlDescribable { /** * Optional attributes of an item. */ - var $author, $authorEmail, $image, $category, $comments, $guid, $source, $creator; + var $author, $authorEmail, $image, $category, $comments, $guid, $guidIsPermaLink, $source, $creator; /** * Publishing date of an item. May be in one of the following formats: @@ -995,7 +995,11 @@ class RSSCreator091 extends FeedCreator { $feed.= " ".htmlspecialchars($itemDate->rfc822())."\n"; } if ($this->items[$i]->guid!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->guid)."\n"; + $feed.= " items[$i]->guidIsPermaLink == false) { + $feed.= " isPermaLink=\"false\""; + } + $feed.= ">".htmlspecialchars($this->items[$i]->guid)."\n"; } $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); $feed.= " \n"; From 2bb30f9bf53446189b6079ac349acc82909c3a49 Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 20 Nov 2020 00:19:54 +0100 Subject: [PATCH 0268/1451] Add RSS feed for modified packages --- web/html/modified-rss.php | 62 +++++++++++++++++++++++++++++++++++++++ web/lib/pkgfuncs.inc.php | 17 +++++++++-- web/lib/routing.inc.php | 1 + 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 web/html/modified-rss.php diff --git a/web/html/modified-rss.php b/web/html/modified-rss.php new file mode 100644 index 00000000..4c5c47e0 --- /dev/null +++ b/web/html/modified-rss.php @@ -0,0 +1,62 @@ +cssStyleSheet = false; +$rss->xslStyleSheet = false; + +# Use UTF-8 (fixes FS#10706). +$rss->encoding = "UTF-8"; + +#All the general RSS setup +$rss->title = "AUR Latest Modified Packages"; +$rss->description = "The latest modified packages in the AUR"; +$rss->link = "${protocol}://{$host}"; +$rss->syndicationURL = "{$protocol}://{$host}" . get_uri('/rss/'); +$image = new FeedImage(); +$image->title = "AUR Latest Modified Packages"; +$image->url = "{$protocol}://{$host}/css/archnavbar/aurlogo.png"; +$image->link = $rss->link; +$image->description = "AUR Latest Modified Packages Feed"; +$rss->image = $image; + +#Get the latest packages and add items for them +$packages = latest_modified_pkgs(100); + +foreach ($packages as $indx => $row) { + $item = new FeedItem(); + $item->title = $row["Name"]; + $item->link = "{$protocol}://{$host}" . get_pkg_uri($row["Name"]); + $item->description = $row["Description"]; + $item->date = intval($row["ModifiedTS"]); + $item->source = "{$protocol}://{$host}"; + $item->author = username_from_id($row["MaintainerUID"]); + $item->guidIsPermaLink = true; + $item->guid = $row["Name"] . "-" . $row["ModifiedTS"]; + $rss->addItem($item); +} + +#save it so that useCached() can find it +$feedContent = $rss->createFeed(); +set_cache_value($feed_key, $feedContent, 600); +echo $feedContent; +?> diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index eb3afab6..140c7ec1 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -925,13 +925,13 @@ function sanitize_ids($ids) { * * @return array $packages Package info for the specified number of recent packages */ -function latest_pkgs($numpkgs) { +function latest_pkgs($numpkgs, $orderBy='SubmittedTS') { $dbh = DB::connect(); - $q = "SELECT Packages.*, MaintainerUID, SubmittedTS "; + $q = "SELECT Packages.*, MaintainerUID, SubmittedTS, ModifiedTS "; $q.= "FROM Packages LEFT JOIN PackageBases ON "; $q.= "PackageBases.ID = Packages.PackageBaseID "; - $q.= "ORDER BY SubmittedTS DESC "; + $q.= "ORDER BY " . $orderBy . " DESC "; $q.= "LIMIT " . intval($numpkgs); $result = $dbh->query($q); @@ -944,3 +944,14 @@ function latest_pkgs($numpkgs) { return $packages; } + +/** + * Determine package information for latest modified packages + * + * @param int $numpkgs Number of packages to get information on + * + * @return array $packages Package info for the specified number of recently modified packages + */ +function latest_modified_pkgs($numpkgs) { + return latest_pkgs($numpkgs, 'ModifiedTS'); +} diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php index 7d9750a0..73c667d2 100644 --- a/web/lib/routing.inc.php +++ b/web/lib/routing.inc.php @@ -15,6 +15,7 @@ $ROUTES = array( '/logout' => 'logout.php', '/passreset' => 'passreset.php', '/rpc' => 'rpc.php', + '/rss/modified' => 'modified-rss.php', '/rss' => 'rss.php', '/tos' => 'tos.php', '/tu' => 'tu.php', From 537349e124c158a6537b4026ed3a1394a75a7206 Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 20 Nov 2020 00:20:26 +0100 Subject: [PATCH 0269/1451] Add modified packages RSS feed to frontend --- web/html/css/archweb.css | 4 ++++ web/template/header.php | 1 + web/template/stats/updates_table.php | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/html/css/archweb.css b/web/html/css/archweb.css index f95e3843..b935d7db 100644 --- a/web/html/css/archweb.css +++ b/web/html/css/archweb.css @@ -556,6 +556,10 @@ h3 span.arrow { margin: -2em 0 0 0; } + #pkg-updates .rss-icon.latest { + margin-right: 1em; + } + #pkg-updates table { margin: 0; } diff --git a/web/template/header.php b/web/template/header.php index f7409400..afe7a9b6 100644 --- a/web/template/header.php +++ b/web/template/header.php @@ -9,6 +9,7 @@ ' /> + ' /> diff --git a/web/template/stats/updates_table.php b/web/template/stats/updates_table.php index b4c6215f..23a86288 100644 --- a/web/template/stats/updates_table.php +++ b/web/template/stats/updates_table.php @@ -1,6 +1,7 @@

    ()

    -RSS Feed +RSS Feed +RSS Feed From e7db894eb716132411bd88c094bc8df8b5f378de Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 20 Nov 2020 00:19:15 +0100 Subject: [PATCH 0270/1451] RSS: Add ability to specify isPermaLink="false" for GUID --- web/lib/feedcreator.class.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/lib/feedcreator.class.php b/web/lib/feedcreator.class.php index a1fe24c9..bfc29b20 100644 --- a/web/lib/feedcreator.class.php +++ b/web/lib/feedcreator.class.php @@ -183,7 +183,7 @@ class FeedItem extends HtmlDescribable { /** * Optional attributes of an item. */ - var $author, $authorEmail, $image, $category, $comments, $guid, $source, $creator; + var $author, $authorEmail, $image, $category, $comments, $guid, $guidIsPermaLink, $source, $creator; /** * Publishing date of an item. May be in one of the following formats: @@ -995,7 +995,11 @@ class RSSCreator091 extends FeedCreator { $feed.= " ".htmlspecialchars($itemDate->rfc822())."\n"; } if ($this->items[$i]->guid!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->guid)."\n"; + $feed.= " items[$i]->guidIsPermaLink == false) { + $feed.= " isPermaLink=\"false\""; + } + $feed.= ">".htmlspecialchars($this->items[$i]->guid)."\n"; } $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); $feed.= " \n"; From 4330fe4f335a2c1b3b68743337576501dc1f6c92 Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 20 Nov 2020 00:19:54 +0100 Subject: [PATCH 0271/1451] Add RSS feed for modified packages --- web/html/modified-rss.php | 62 +++++++++++++++++++++++++++++++++++++++ web/lib/pkgfuncs.inc.php | 17 +++++++++-- web/lib/routing.inc.php | 1 + 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 web/html/modified-rss.php diff --git a/web/html/modified-rss.php b/web/html/modified-rss.php new file mode 100644 index 00000000..4c5c47e0 --- /dev/null +++ b/web/html/modified-rss.php @@ -0,0 +1,62 @@ +cssStyleSheet = false; +$rss->xslStyleSheet = false; + +# Use UTF-8 (fixes FS#10706). +$rss->encoding = "UTF-8"; + +#All the general RSS setup +$rss->title = "AUR Latest Modified Packages"; +$rss->description = "The latest modified packages in the AUR"; +$rss->link = "${protocol}://{$host}"; +$rss->syndicationURL = "{$protocol}://{$host}" . get_uri('/rss/'); +$image = new FeedImage(); +$image->title = "AUR Latest Modified Packages"; +$image->url = "{$protocol}://{$host}/css/archnavbar/aurlogo.png"; +$image->link = $rss->link; +$image->description = "AUR Latest Modified Packages Feed"; +$rss->image = $image; + +#Get the latest packages and add items for them +$packages = latest_modified_pkgs(100); + +foreach ($packages as $indx => $row) { + $item = new FeedItem(); + $item->title = $row["Name"]; + $item->link = "{$protocol}://{$host}" . get_pkg_uri($row["Name"]); + $item->description = $row["Description"]; + $item->date = intval($row["ModifiedTS"]); + $item->source = "{$protocol}://{$host}"; + $item->author = username_from_id($row["MaintainerUID"]); + $item->guidIsPermaLink = true; + $item->guid = $row["Name"] . "-" . $row["ModifiedTS"]; + $rss->addItem($item); +} + +#save it so that useCached() can find it +$feedContent = $rss->createFeed(); +set_cache_value($feed_key, $feedContent, 600); +echo $feedContent; +?> diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index eb3afab6..140c7ec1 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -925,13 +925,13 @@ function sanitize_ids($ids) { * * @return array $packages Package info for the specified number of recent packages */ -function latest_pkgs($numpkgs) { +function latest_pkgs($numpkgs, $orderBy='SubmittedTS') { $dbh = DB::connect(); - $q = "SELECT Packages.*, MaintainerUID, SubmittedTS "; + $q = "SELECT Packages.*, MaintainerUID, SubmittedTS, ModifiedTS "; $q.= "FROM Packages LEFT JOIN PackageBases ON "; $q.= "PackageBases.ID = Packages.PackageBaseID "; - $q.= "ORDER BY SubmittedTS DESC "; + $q.= "ORDER BY " . $orderBy . " DESC "; $q.= "LIMIT " . intval($numpkgs); $result = $dbh->query($q); @@ -944,3 +944,14 @@ function latest_pkgs($numpkgs) { return $packages; } + +/** + * Determine package information for latest modified packages + * + * @param int $numpkgs Number of packages to get information on + * + * @return array $packages Package info for the specified number of recently modified packages + */ +function latest_modified_pkgs($numpkgs) { + return latest_pkgs($numpkgs, 'ModifiedTS'); +} diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php index 7d9750a0..73c667d2 100644 --- a/web/lib/routing.inc.php +++ b/web/lib/routing.inc.php @@ -15,6 +15,7 @@ $ROUTES = array( '/logout' => 'logout.php', '/passreset' => 'passreset.php', '/rpc' => 'rpc.php', + '/rss/modified' => 'modified-rss.php', '/rss' => 'rss.php', '/tos' => 'tos.php', '/tu' => 'tu.php', From 8d9f20939c864800a45fc6f8994ad9af8e8fe837 Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 20 Nov 2020 00:20:26 +0100 Subject: [PATCH 0272/1451] Add modified packages RSS feed to frontend --- web/html/css/archweb.css | 4 ++++ web/template/header.php | 1 + web/template/stats/updates_table.php | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/html/css/archweb.css b/web/html/css/archweb.css index f95e3843..b935d7db 100644 --- a/web/html/css/archweb.css +++ b/web/html/css/archweb.css @@ -556,6 +556,10 @@ h3 span.arrow { margin: -2em 0 0 0; } + #pkg-updates .rss-icon.latest { + margin-right: 1em; + } + #pkg-updates table { margin: 0; } diff --git a/web/template/header.php b/web/template/header.php index f7409400..afe7a9b6 100644 --- a/web/template/header.php +++ b/web/template/header.php @@ -9,6 +9,7 @@ ' /> + ' /> diff --git a/web/template/stats/updates_table.php b/web/template/stats/updates_table.php index b4c6215f..23a86288 100644 --- a/web/template/stats/updates_table.php +++ b/web/template/stats/updates_table.php @@ -1,6 +1,7 @@

    ()

    -RSS Feed +RSS Feed +RSS Feed
    From bd8f5280112b2cf1cf6113453977629be2ee92c5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 13 Jun 2021 10:48:31 -0700 Subject: [PATCH 0273/1451] add Base.as_dict() and Base.json() Two utility functions for all of our ORM models that will allow us to easily convert them to Python structures and JSON data. Signed-off-by: Kevin Morris --- aurweb/models/declarative.py | 30 ++++++++++++++++++++++++++++++ aurweb/util.py | 8 ++++++++ test/test_user.py | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/aurweb/models/declarative.py b/aurweb/models/declarative.py index 45a629ce..96ee1829 100644 --- a/aurweb/models/declarative.py +++ b/aurweb/models/declarative.py @@ -1,10 +1,40 @@ +import json + from sqlalchemy.ext.declarative import declarative_base import aurweb.db +from aurweb import util + + +def to_dict(model): + return { + c.name: getattr(model, c.name) + for c in model.__table__.columns + } + + +def to_json(model, indent: int = None): + return json.dumps({ + k: util.jsonify(v) + for k, v in to_dict(model).items() + }, indent=indent) + + Base = declarative_base() + +# Setup __table_args__ applicable to every table. Base.__table_args__ = { "autoload": True, "autoload_with": aurweb.db.get_engine(), "extend_existing": True } + + +# Setup Base.as_dict and Base.json. +# +# With this, declarative models can use .as_dict() or .json() +# at any time to produce a dict and json out of table columns. +# +Base.as_dict = to_dict +Base.json = to_json diff --git a/aurweb/util.py b/aurweb/util.py index 8e4b291d..ad8ac6b7 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -3,6 +3,7 @@ import random import re import string +from datetime import datetime from urllib.parse import urlparse from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email @@ -94,3 +95,10 @@ def account_url(context, user): if request.url.scheme == "http" and request.url.port != 80: base += f":{request.url.port}" return f"{base}/account/{user.Username}" + + +def jsonify(obj): + """ Perform a conversion on obj if it's needed. """ + if isinstance(obj, datetime): + obj = int(obj.timestamp()) + return obj diff --git a/test/test_user.py b/test/test_user.py index 8b4da61e..06585207 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -1,4 +1,5 @@ import hashlib +import json from datetime import datetime, timedelta @@ -198,3 +199,21 @@ def test_user_credential_types(): assert aurweb.auth.trusted_user(user) assert aurweb.auth.developer(user) assert aurweb.auth.trusted_user_or_dev(user) + + +def test_user_json(): + data = json.loads(user.json()) + assert data.get("ID") == user.ID + assert data.get("Username") == user.Username + assert data.get("Email") == user.Email + # .json() converts datetime values to integer timestamps. + assert isinstance(data.get("RegistrationTS"), int) + + +def test_user_as_dict(): + data = user.as_dict() + assert data.get("ID") == user.ID + assert data.get("Username") == user.Username + assert data.get("Email") == user.Email + # .as_dict() does not convert values to json-capable types. + assert isinstance(data.get("RegistrationTS"), datetime) From 40448ccd34d32bcb4c1f5357e6196dc8f5c17dc6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 13 Jun 2021 12:16:13 -0700 Subject: [PATCH 0274/1451] aurweb.db: add commit(), add() and autocommit arg With the addition of these two, some code has been swapped to use these in some of the other db wrappers with an additional autocommit kwarg in create and delete, to control batch transactions. Signed-off-by: Kevin Morris --- aurweb/db.py | 21 ++++++++++++++++----- test/test_db.py | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/aurweb/db.py b/aurweb/db.py index 04c8653a..c0147720 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -59,24 +59,35 @@ def query(model, *args, **kwargs): return session.query(model).filter(*args, **kwargs) -def create(model, *args, **kwargs): +def create(model, autocommit: bool = True, *args, **kwargs): instance = model(*args, **kwargs) - session.add(instance) - session.commit() + add(instance) + if autocommit is True: + commit() return instance -def delete(model, *args, **kwargs): +def delete(model, *args, autocommit: bool = True, **kwargs): instance = session.query(model).filter(*args, **kwargs) for record in instance: session.delete(record) - session.commit() + if autocommit is True: + commit() def rollback(): session.rollback() +def add(model): + session.add(model) + return model + + +def commit(): + session.commit() + + def get_sqlalchemy_url(): """ Build an SQLAlchemy for use with create_engine based on the aurweb configuration. diff --git a/test/test_db.py b/test/test_db.py index 9298c53d..d7a91813 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -273,6 +273,31 @@ def test_create_delete(): record = db.query(AccountType, AccountType.AccountType == "test").first() assert record is None + # Create and delete a record with autocommit=False. + db.create(AccountType, AccountType="test", autocommit=False) + db.commit() + db.delete(AccountType, AccountType.AccountType == "test", autocommit=False) + db.commit() + record = db.query(AccountType, AccountType.AccountType == "test").first() + assert record is None + + +def test_add_commit(): + # Use db.add and db.commit to add a temporary record. + account_type = AccountType(AccountType="test") + db.add(account_type) + db.commit() + + # Assert it got created in the DB. + assert bool(account_type.ID) + + # Query the DB for it and compare the record with our object. + record = db.query(AccountType, AccountType.AccountType == "test").first() + assert record == account_type + + # Remove the record. + db.delete(AccountType, AccountType.ID == account_type.ID) + def test_connection_executor_mysql_paramstyle(): executor = db.ConnectionExecutor(None, backend="mysql") From 7ae95ac90813dc3f521d810ccb95adfc74a64496 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 04:39:58 -0700 Subject: [PATCH 0275/1451] bugfix: removed extra space in " My Account" nav link Signed-off-by: Kevin Morris --- templates/partials/archdev-navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index 4d54f6af..c935fd41 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -9,7 +9,7 @@ {% if request.user.is_authenticated() %}
  • - {% trans %} My Account{% endtrans %} + {% trans %}My Account{% endtrans %}
  • From b7d67bf5fcf02d172fce1a290c500f9fb432c49f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 05:06:24 -0700 Subject: [PATCH 0276/1451] render_template: convert HTTPStatus objects This will automate a lot of conversion that happens around the codebase in terms of status_code. As of this commit, we should improve usage and remove int(status_code) casts wherever we can. Signed-off-by: Kevin Morris --- aurweb/templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/templates.py b/aurweb/templates.py index c0472b2e..7474da1c 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -65,7 +65,7 @@ async def make_variable_context(request: Request, title: str, next: str = None): def render_template(request: Request, path: str, context: dict, - status_code=int(HTTPStatus.OK)): + status_code: HTTPStatus = HTTPStatus.OK): """ Render a Jinja2 multi-lingual template with some context. """ # Create a deep copy of our jinja2 environment. The environment in @@ -81,7 +81,7 @@ def render_template(request: Request, template = templates.get_template(path) rendered = template.render(context) - response = HTMLResponse(rendered, status_code=status_code) + response = HTMLResponse(rendered, status_code=int(status_code)) response.set_cookie("AURLANG", context.get("language")) response.set_cookie("AURTZ", context.get("timezone")) return response From f89d06d092aa0f20a7b267b6cb274ae175c294df Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 05:20:23 -0700 Subject: [PATCH 0277/1451] setup_test_db: remove mysql-dependent coverage path Signed-off-by: Kevin Morris --- aurweb/testing/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 90d46720..65d34253 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -50,7 +50,7 @@ def setup_test_db(*args): db_backend = aurweb.config.get("database", "backend") - if db_backend != "sqlite": + if db_backend != "sqlite": # pragma: no cover aurweb.db.session.execute("SET FOREIGN_KEY_CHECKS = 0") else: # We're using sqlite, setup tables to be deleted without violating @@ -61,7 +61,7 @@ def setup_test_db(*args): for table in tables: aurweb.db.session.execute(f"DELETE FROM {table}") - if db_backend != "sqlite": + if db_backend != "sqlite": # pragma: no cover aurweb.db.session.execute("SET FOREIGN_KEY_CHECKS = 1") # Expunge all objects from SQLAlchemy's IdentityMap. From ac67268a28f02dcdc2fb765c6bd6d76555e0056a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 16:49:41 -0700 Subject: [PATCH 0278/1451] add util.timezone_to_datetime -> `dt` Jinja2 filter Signed-off-by: Kevin Morris --- aurweb/templates.py | 3 +++ aurweb/util.py | 4 ++++ test/test_util.py | 9 +++++++++ 3 files changed, 16 insertions(+) create mode 100644 test/test_util.py diff --git a/aurweb/templates.py b/aurweb/templates.py index 7474da1c..8c6f3294 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -23,6 +23,9 @@ env = jinja2.Environment(loader=loader, autoescape=True, # Add tr translation filter. env.filters["tr"] = l10n.tr +# Utility filters. +env.filters["dt"] = util.timestamp_to_datetime + # Add captcha filters. env.filters["captcha_salt"] = captcha.captcha_salt_filter env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter diff --git a/aurweb/util.py b/aurweb/util.py index ad8ac6b7..ce18853b 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -97,6 +97,10 @@ def account_url(context, user): return f"{base}/account/{user.Username}" +def timestamp_to_datetime(timestamp: int): + return datetime.utcfromtimestamp(int(timestamp)) + + def jsonify(obj): """ Perform a conversion on obj if it's needed. """ if isinstance(obj, datetime): diff --git a/test/test_util.py b/test/test_util.py new file mode 100644 index 00000000..cd7b7a57 --- /dev/null +++ b/test/test_util.py @@ -0,0 +1,9 @@ +from datetime import datetime + +from aurweb import util + + +def test_timestamp_to_datetime(): + ts = datetime.utcnow().timestamp() + dt = datetime.utcfromtimestamp(int(ts)) + assert util.timestamp_to_datetime(ts) == dt From b1baf769985949bfa496c9d8276dbcd2e101072b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 16:55:01 -0700 Subject: [PATCH 0279/1451] add util.as_timezone -> `as_timezone` Jinja2 filter Signed-off-by: Kevin Morris --- aurweb/templates.py | 1 + aurweb/util.py | 5 +++++ test/test_util.py | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/aurweb/templates.py b/aurweb/templates.py index 8c6f3294..9439f3a3 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -25,6 +25,7 @@ env.filters["tr"] = l10n.tr # Utility filters. env.filters["dt"] = util.timestamp_to_datetime +env.filters["as_timezone"] = util.as_timezone # Add captcha filters. env.filters["captcha_salt"] = captcha.captcha_salt_filter diff --git a/aurweb/util.py b/aurweb/util.py index ce18853b..1615e00a 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -5,6 +5,7 @@ import string from datetime import datetime from urllib.parse import urlparse +from zoneinfo import ZoneInfo from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email from jinja2 import pass_context @@ -101,6 +102,10 @@ def timestamp_to_datetime(timestamp: int): return datetime.utcfromtimestamp(int(timestamp)) +def as_timezone(dt: datetime, timezone: str): + return dt.astimezone(tz=ZoneInfo(timezone)) + + def jsonify(obj): """ Perform a conversion on obj if it's needed. """ if isinstance(obj, datetime): diff --git a/test/test_util.py b/test/test_util.py index cd7b7a57..d58a8ae2 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,4 +1,5 @@ from datetime import datetime +from zoneinfo import ZoneInfo from aurweb import util @@ -7,3 +8,9 @@ def test_timestamp_to_datetime(): ts = datetime.utcnow().timestamp() dt = datetime.utcfromtimestamp(int(ts)) assert util.timestamp_to_datetime(ts) == dt + + +def test_as_timezone(): + ts = datetime.utcnow().timestamp() + dt = util.timestamp_to_datetime(ts) + assert util.as_timezone(dt, "UTC") == dt.astimezone(tz=ZoneInfo("UTC")) From d5e650a33930286bae0263e8b0aff12ed94a319e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 16:57:06 -0700 Subject: [PATCH 0280/1451] add util.dedupe_qs -> `dedupe_qs` Jinja2 filter Signed-off-by: Kevin Morris --- aurweb/templates.py | 1 + aurweb/util.py | 26 +++++++++++++++++++++++++- test/test_util.py | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/aurweb/templates.py b/aurweb/templates.py index 9439f3a3..1e09bf61 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -26,6 +26,7 @@ env.filters["tr"] = l10n.tr # Utility filters. env.filters["dt"] = util.timestamp_to_datetime env.filters["as_timezone"] = util.as_timezone +env.filters["dedupe_qs"] = util.dedupe_qs # Add captcha filters. env.filters["captcha_salt"] = captcha.captcha_salt_filter diff --git a/aurweb/util.py b/aurweb/util.py index 1615e00a..0aec6f45 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -3,8 +3,9 @@ import random import re import string +from collections import OrderedDict from datetime import datetime -from urllib.parse import urlparse +from urllib.parse import quote_plus, urlparse from zoneinfo import ZoneInfo from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email @@ -106,6 +107,29 @@ def as_timezone(dt: datetime, timezone: str): return dt.astimezone(tz=ZoneInfo(timezone)) +def dedupe_qs(query_string: str, *additions): + """ Dedupe keys found in a query string by rewriting it without + duplicates found while iterating from the end to the beginning, + using an ordered memo to track keys found and persist locations. + + That is, query string 'a=1&b=1&a=2' will be deduped and converted + to 'b=1&a=2'. + + :param query_string: An HTTP URL query string. + :param *additions: Optional additional fields to add to the query string. + :return: Deduped query string, including *additions at the tail. + """ + for addition in list(additions): + query_string += f"&{addition}" + + qs = OrderedDict() + for item in reversed(query_string.split('&')): + key, value = item.split('=') + if key not in qs: + qs[key] = value + return '&'.join([f"{k}={quote_plus(v)}" for k, v in reversed(qs.items())]) + + def jsonify(obj): """ Perform a conversion on obj if it's needed. """ if isinstance(obj, datetime): diff --git a/test/test_util.py b/test/test_util.py index d58a8ae2..074de494 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from datetime import datetime from zoneinfo import ZoneInfo @@ -14,3 +15,18 @@ def test_as_timezone(): ts = datetime.utcnow().timestamp() dt = util.timestamp_to_datetime(ts) assert util.as_timezone(dt, "UTC") == dt.astimezone(tz=ZoneInfo("UTC")) + + +def test_dedupe_qs(): + items = OrderedDict() + items["key1"] = "test" + items["key2"] = "blah" + items["key3"] = 1 + + # Construct and test our query string. + query_string = '&'.join([f"{k}={v}" for k, v in items.items()]) + assert query_string == "key1=test&key2=blah&key3=1" + + # Add key1=changed and key2=changed to the query and dedupe it. + deduped = util.dedupe_qs(query_string, "key1=changed", "key3=changed") + assert deduped == "key2=blah&key1=changed&key3=changed" From d7941e6bedfed32df0299a5869df40f33b202374 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 16:57:44 -0700 Subject: [PATCH 0281/1451] urllib.parse.quote_plus -> `urlencode` Jinja2 filter Signed-off-by: Kevin Morris --- aurweb/templates.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aurweb/templates.py b/aurweb/templates.py index 1e09bf61..015f8c9f 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -4,6 +4,7 @@ import zoneinfo from datetime import datetime from http import HTTPStatus +from urllib.parse import quote_plus import jinja2 @@ -27,6 +28,7 @@ env.filters["tr"] = l10n.tr env.filters["dt"] = util.timestamp_to_datetime env.filters["as_timezone"] = util.as_timezone env.filters["dedupe_qs"] = util.dedupe_qs +env.filters["urlencode"] = quote_plus # Add captcha filters. env.filters["captcha_salt"] = captcha.captcha_salt_filter From 8b6f92f9e907267288c9a2d757c7747b22c7bca8 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Tue, 11 May 2021 00:01:13 +0200 Subject: [PATCH 0282/1451] Use the clipboard API for copy paste The Document.execCommand API is deprecated and no longer recommended to be used. It's replacement is the much simpler navigator.clipboard API which is supported in all browsers except internet explorer. Signed-off-by: Eli Schwartz --- web/template/pkg_details.php | 10 +++------- web/template/pkgbase_details.php | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/web/template/pkg_details.php b/web/template/pkg_details.php index c6bb32d8..047de9a7 100644 --- a/web/template/pkg_details.php +++ b/web/template/pkg_details.php @@ -308,14 +308,10 @@ endif; diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php index a6857c4e..35ad217a 100644 --- a/web/template/pkgbase_details.php +++ b/web/template/pkgbase_details.php @@ -137,14 +137,10 @@ endif; From d7603fa4d3d31e8c50b2988730652809ae1f42b7 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 10 May 2021 23:55:36 +0200 Subject: [PATCH 0283/1451] Port package details page to pure JavaScript Use a CSS animation for jQuery.Animate and replace the rest with pure vanilla JavaScript. Signed-off-by: Eli Schwartz --- web/html/css/aurweb.css | 5 +++ web/html/packages.php | 96 +++++++++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 81bf9ab6..bb4e3ad7 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -199,3 +199,8 @@ label.confirmation, .error { color: red; } + +.article-content > div { + overflow: hidden; + transition: height 1s; +} diff --git a/web/html/packages.php b/web/html/packages.php index a989428e..559a8f45 100644 --- a/web/html/packages.php +++ b/web/html/packages.php @@ -46,70 +46,94 @@ if (isset($pkgname)) { html_header($title, $details); ?> - From 06fa8ab5f32061ae1d06abbe1cc502883f3884da Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 14 Jun 2021 22:13:07 +0200 Subject: [PATCH 0284/1451] Convert comment editing to vanilla JavaScript Signed-off-by: Eli Schwartz --- web/template/pkg_comments.php | 90 ++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/web/template/pkg_comments.php b/web/template/pkg_comments.php index 3bcf1a38..ffa9e137 100644 --- a/web/template/pkg_comments.php +++ b/web/template/pkg_comments.php @@ -169,37 +169,71 @@ if ($comment_section == "package") { From af76e660d0f03712901d2d1ddda07d383fafcb08 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 21 Jun 2021 21:35:05 -0700 Subject: [PATCH 0285/1451] auth_required: allow formattable template tuples See docstring for updates. template= has been modified. status_code= has been added as an optional template status_code. Signed-off-by: Kevin Morris --- aurweb/auth.py | 67 +++++++++++++++++++++++++++++++----- aurweb/routers/accounts.py | 16 ++++++--- test/test_accounts_routes.py | 1 - 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 401ed6ae..f57e18bf 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -10,9 +10,10 @@ from starlette.requests import HTTPConnection import aurweb.config +from aurweb import l10n from aurweb.models.session import Session from aurweb.models.user import User -from aurweb.templates import make_context, render_template +from aurweb.templates import make_variable_context, render_template class AnonymousUser: @@ -60,7 +61,8 @@ class BasicAuthBackend(AuthenticationBackend): def auth_required(is_required: bool = True, redirect: str = "/", - template: tuple = None): + template: tuple = None, + status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): """ Authentication route decorator. If redirect is given, the user will be redirected if the auth state @@ -69,26 +71,73 @@ def auth_required(is_required: bool = True, If template is given, it will be rendered with Unauthorized if is_required does not match and take priority over redirect. + A precondition of this function is that, if template is provided, + it **must** match the following format: + + template=("template.html", ["Some Template For", "{}"], ["username"]) + + Where `username` is a FastAPI request path parameter, fitting + a route like: `/some_route/{username}`. + + If you wish to supply a non-formatted template, just omit any Python + format strings (with the '{}' substring). The third tuple element + will not be used, and so anything can be supplied. + + template=("template.html", ["Some Page"], None) + + All title shards and format parameters will be translated before + applying any format operations. + :param is_required: A boolean indicating whether the function requires auth :param redirect: Path to redirect to if is_required isn't True - :param template: A template tuple: ("template.html", "Template Page") + :param template: A three-element template tuple: + (path, title_iterable, variable_iterable) + :param status_code: An optional status_code for template render. + Redirects are always SEE_OTHER. """ def decorator(func): @functools.wraps(func) async def wrapper(request, *args, **kwargs): if request.user.is_authenticated() != is_required: - status_code = int(HTTPStatus.UNAUTHORIZED) url = "/" if redirect: - status_code = int(HTTPStatus.SEE_OTHER) url = redirect if template: - path, title = template - context = make_context(request, title) + # template=("template.html", + # ["Some Title", "someFormatted {}"], + # ["variable"]) + # => render template.html with title: + # "Some Title someFormatted variables" + path, title_parts, variables = template + _ = l10n.get_translator_for_request(request) + + # Step through title_parts; for each part which contains + # a '{}' in it, apply .format(var) where var = the current + # iteration of variables. + # + # This implies that len(variables) is equal to + # len([part for part in title_parts if '{}' in part]) + # and this must always be true. + # + sanitized = [] + _variables = iter(variables) + for part in title_parts: + if "{}" in part: # If this part is formattable. + key = next(_variables) + var = request.path_params.get(key) + sanitized.append(_(part.format(var))) + else: # Otherwise, just add the translated part. + sanitized.append(_(part)) + + # Glue all title parts together, separated by spaces. + title = " ".join(sanitized) + + context = await make_variable_context(request, title) return render_template(request, path, context, - status_code=int(HTTPStatus.UNAUTHORIZED)) - return RedirectResponse(url=url, status_code=status_code) + status_code=status_code) + return RedirectResponse(url, + status_code=int(HTTPStatus.SEE_OTHER)) return await func(request, *args, **kwargs) return wrapper diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index c7c96003..966f8409 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -555,13 +555,21 @@ async def account_edit_post(request: Request, return util.migrate_cookies(request, response) +account_template = ( + "account/show.html", + ["Account", "{}"], + ["username"] # Query parameters to replace in the title string. +) + + @router.get("/account/{username}") -@auth_required(True, template=("account/show.html", "Accounts")) +@auth_required(True, template=account_template, + status_code=HTTPStatus.UNAUTHORIZED) async def account(request: Request, username: str): + _ = l10n.get_translator_for_request(request) + context = await make_variable_context(request, _("Account") + username) + user = db.query(User, User.Username == username).first() - - context = await make_variable_context(request, "Accounts") - if not user: raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index d5fd089e..bd0d9d4b 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -915,7 +915,6 @@ def test_get_account_not_found(): def test_get_account_unauthenticated(): with client as request: response = request.get("/account/test", allow_redirects=False) - assert response.status_code == int(HTTPStatus.UNAUTHORIZED) content = response.content.decode() From 959e535126bdb6863f62ccf1e4a32482793b1386 Mon Sep 17 00:00:00 2001 From: Kristian Klausen Date: Wed, 23 Jun 2021 03:09:37 +0200 Subject: [PATCH 0286/1451] Use the real ml email address instead of alias All the arch-x@archlinux.org -> arch-x@lists.archlinux.org aliases will be dropped soon[1]. [1] https://lists.archlinux.org/pipermail/arch-dev-public/2021-June/030462.html --- conf/config.defaults | 2 +- test/setup.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/config.defaults b/conf/config.defaults index e6961520..b7bc0368 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -22,7 +22,7 @@ git_clone_uri_anon = https://aur.archlinux.org/%s.git git_clone_uri_priv = ssh://aur@aur.archlinux.org/%s.git max_rpc_results = 5000 max_depends = 1000 -aur_request_ml = aur-requests@archlinux.org +aur_request_ml = aur-requests@lists.archlinux.org request_idle_time = 1209600 request_archive_time = 15552000 auto_orphan_age = 15552000 diff --git a/test/setup.sh b/test/setup.sh index 4a6eb3b1..589c8c3f 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -26,7 +26,7 @@ name = aur.db [options] aur_location = https://aur.archlinux.org -aur_request_ml = aur-requests@archlinux.org +aur_request_ml = aur-requests@lists.archlinux.org enable-maintenance = 0 maintenance-exceptions = 127.0.0.1 commit_uri = https://aur.archlinux.org/cgit/aur.git/log/?h=%s&id=%s From ec632a7091df6df3940f62cfee3d9a09641dd4b5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 11 Jun 2021 23:09:34 -0700 Subject: [PATCH 0287/1451] use secure=True when options.disable_http_login is enabled We'll piggyback off of the current existing configuration item, `disable_http_login`, to decide how we should submit cookies to an HTTP response. Previously, in `sso.py`, the http schema was used to make this decision. There is an issue with that, however: We cannot actually test properly if we depend on the https schema. This change allows us to toggle `disable_http_login` to modify the behavior of cookies sent with an http response to be secure. We test this behavior in test/test_auth_routes.py#L81: `test_secure_login(mock)`. Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 5 +++- aurweb/routers/html.py | 6 ++++- aurweb/routers/sso.py | 8 ++++--- aurweb/templates.py | 9 +++++--- aurweb/util.py | 3 ++- test/test_auth_routes.py | 50 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index e4864424..4aca9304 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -59,7 +59,10 @@ async def login_post(request: Request, response = RedirectResponse(url=next, status_code=int(HTTPStatus.SEE_OTHER)) - response.set_cookie("AURSID", sid, expires=expires_at) + + secure_cookies = aurweb.config.getboolean("options", "disable_http_login") + response.set_cookie("AURSID", sid, expires=expires_at, + secure=secure_cookies, httponly=True) return response diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 890aff88..ed0c039b 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -6,6 +6,8 @@ from http import HTTPStatus from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse +import aurweb.config + from aurweb.templates import make_context, render_template router = APIRouter() @@ -45,7 +47,9 @@ async def language(request: Request, # In any case, set the response's AURLANG cookie that never expires. response = RedirectResponse(url=f"{next}{query_string}", status_code=int(HTTPStatus.SEE_OTHER)) - response.set_cookie("AURLANG", set_lang) + secure_cookies = aurweb.config.getboolean("options", "disable_http_login") + response.set_cookie("AURLANG", set_lang, + secure=secure_cookies, httponly=True) return response diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 4b12b932..093807fe 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -131,13 +131,15 @@ async def authenticate(request: Request, redirect: str = None, conn=Depends(aurw elif len(aur_accounts) == 1: sid = open_session(request, conn, aur_accounts[0][Users.c.ID]) response = RedirectResponse(redirect if redirect and is_aur_url(redirect) else "/") + secure_cookies = aurweb.config.getboolean("options", "disable_http_login") response.set_cookie(key="AURSID", value=sid, httponly=True, - secure=request.url.scheme == "https") + secure=secure_cookies) if "id_token" in token: # We save the id_token for the SSO logout. It’s not too important # though, so if we can’t find it, we can live without it. - response.set_cookie(key="SSO_ID_TOKEN", value=token["id_token"], path="/sso/", - httponly=True, secure=request.url.scheme == "https") + response.set_cookie(key="SSO_ID_TOKEN", value=token["id_token"], + path="/sso/", httponly=True, + secure=secure_cookies) return response else: # We’ve got a severe integrity violation. diff --git a/aurweb/templates.py b/aurweb/templates.py index 015f8c9f..640b9447 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -88,7 +88,10 @@ def render_template(request: Request, template = templates.get_template(path) rendered = template.render(context) - response = HTMLResponse(rendered, status_code=int(status_code)) - response.set_cookie("AURLANG", context.get("language")) - response.set_cookie("AURTZ", context.get("timezone")) + response = HTMLResponse(rendered, status_code=status_code) + secure_cookies = aurweb.config.getboolean("options", "disable_http_login") + response.set_cookie("AURLANG", context.get("language"), + secure=secure_cookies, httponly=True) + response.set_cookie("AURTZ", context.get("timezone"), + secure=secure_cookies, httponly=True) return response diff --git a/aurweb/util.py b/aurweb/util.py index 0aec6f45..1da85606 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -85,8 +85,9 @@ def valid_ssh_pubkey(pk): def migrate_cookies(request, response): + secure_cookies = aurweb.config.getboolean("options", "disable_http_login") for k, v in request.cookies.items(): - response.set_cookie(k, v) + response.set_cookie(k, v, secure=secure_cookies, httponly=True) return response diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 360b48cc..a443be72 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -1,5 +1,6 @@ from datetime import datetime from http import HTTPStatus +from unittest import mock import pytest @@ -70,6 +71,55 @@ def test_login_logout(): assert "AURSID" not in response.cookies +def mock_getboolean(a, b): + if a == "options" and b == "disable_http_login": + return True + return bool(aurweb.config.get(a, b)) + + +@mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean) +def test_secure_login(mock): + """ In this test, we check to verify the course of action taken + by starlette when providing secure=True to a response cookie. + This is achieved by mocking aurweb.config.getboolean to return + True (or 1) when looking for `options.disable_http_login`. + When we receive a response with `disable_http_login` enabled, + we check the fields in cookies received for the secure and + httponly fields, in addition to the rest of the fields given + on such a request. """ + + # Create a local TestClient here since we mocked configuration. + client = TestClient(app) + + # Data used for our upcoming http post request. + post_data = { + "user": user.Username, + "passwd": "testPassword", + "next": "/" + } + + # Perform a login request with the data matching our user. + with client as request: + response = request.post("/login", data=post_data, + allow_redirects=False) + + # Make sure we got the expected status out of it. + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + # Let's check what we got in terms of cookies for AURSID. + # Make sure that a secure cookie got passed to us. + cookie = next(c for c in response.cookies if c.name == "AURSID") + assert cookie.secure is True + assert cookie.has_nonstandard_attr("HttpOnly") is True + assert cookie.value is not None and len(cookie.value) > 0 + + # Let's make sure we actually have a session relationship + # with the AURSID we ended up with. + record = query(Session, Session.SessionID == cookie.value).first() + assert record is not None and record.User == user + assert user.session == record + + def test_authenticated_login_forbidden(): post_data = { "user": "test", From 91dc3efc75700fd9c559a908028ff39eeb7d2bfe Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 12 Jun 2021 03:23:58 -0700 Subject: [PATCH 0288/1451] add util.add_samesite_fields(response, value) This function adds f"SameSite={value}" to each cookie's header stored in response. This is needed because starlette does not currently support the `samesite` argument in Response.set_cookie. It is merged, however, and waiting for next release. Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 3 ++- aurweb/routers/html.py | 3 ++- aurweb/routers/sso.py | 3 ++- aurweb/templates.py | 2 +- aurweb/util.py | 15 ++++++++++++++- test/test_auth_routes.py | 2 ++ 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 4aca9304..2b05784b 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse import aurweb.config +from aurweb import util from aurweb.auth import auth_required from aurweb.models.user import User from aurweb.templates import make_context, render_template @@ -63,7 +64,7 @@ async def login_post(request: Request, secure_cookies = aurweb.config.getboolean("options", "disable_http_login") response.set_cookie("AURSID", sid, expires=expires_at, secure=secure_cookies, httponly=True) - return response + return util.add_samesite_fields(response, "strict") @router.get("/logout") diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index ed0c039b..580ee0d4 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -8,6 +8,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse import aurweb.config +from aurweb import util from aurweb.templates import make_context, render_template router = APIRouter() @@ -50,7 +51,7 @@ async def language(request: Request, secure_cookies = aurweb.config.getboolean("options", "disable_http_login") response.set_cookie("AURLANG", set_lang, secure=secure_cookies, httponly=True) - return response + return util.add_samesite_fields(response, "strict") @router.get("/", response_class=HTMLResponse) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index 093807fe..edeb7c6b 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -14,6 +14,7 @@ from starlette.requests import Request import aurweb.config import aurweb.db +from aurweb import util from aurweb.l10n import get_translator_for_request from aurweb.schema import Bans, Sessions, Users @@ -140,7 +141,7 @@ async def authenticate(request: Request, redirect: str = None, conn=Depends(aurw response.set_cookie(key="SSO_ID_TOKEN", value=token["id_token"], path="/sso/", httponly=True, secure=secure_cookies) - return response + return util.add_samesite_fields(response, "strict") else: # We’ve got a severe integrity violation. raise Exception("Multiple accounts found for SSO account " + sub) diff --git a/aurweb/templates.py b/aurweb/templates.py index 640b9447..bb4047f4 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -94,4 +94,4 @@ def render_template(request: Request, secure=secure_cookies, httponly=True) response.set_cookie("AURTZ", context.get("timezone"), secure=secure_cookies, httponly=True) - return response + return util.add_samesite_fields(response, "strict") diff --git a/aurweb/util.py b/aurweb/util.py index 1da85606..b34226a2 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -9,6 +9,7 @@ from urllib.parse import quote_plus, urlparse from zoneinfo import ZoneInfo from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email +from fastapi.responses import Response from jinja2 import pass_context import aurweb.config @@ -88,7 +89,7 @@ def migrate_cookies(request, response): secure_cookies = aurweb.config.getboolean("options", "disable_http_login") for k, v in request.cookies.items(): response.set_cookie(k, v, secure=secure_cookies, httponly=True) - return response + return add_samesite_fields(response, "strict") @pass_context @@ -136,3 +137,15 @@ def jsonify(obj): if isinstance(obj, datetime): obj = int(obj.timestamp()) return obj + + +def add_samesite_fields(response: Response, value: str): + """ Set the SameSite field on all cookie headers found. + Taken from https://github.com/tiangolo/fastapi/issues/1099. """ + for idx, header in enumerate(response.raw_headers): + if header[0].decode() == "set-cookie": + cookie = header[1].decode() + if f"SameSite={value}" not in cookie: + cookie += f"; SameSite={value}" + response.raw_headers[idx] = (header[0], cookie.encode()) + return response diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index a443be72..b0dd5648 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -111,6 +111,8 @@ def test_secure_login(mock): cookie = next(c for c in response.cookies if c.name == "AURSID") assert cookie.secure is True assert cookie.has_nonstandard_attr("HttpOnly") is True + assert cookie.has_nonstandard_attr("SameSite") is True + assert cookie.get_nonstandard_attr("SameSite") == "strict" assert cookie.value is not None and len(cookie.value) > 0 # Let's make sure we actually have a session relationship From 13456fea1e6eab4971e4ec9f38456ccaeccda352 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 12 Jun 2021 03:26:05 -0700 Subject: [PATCH 0289/1451] set AURLANG + AURTZ on login Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 2b05784b..8f37fe27 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -64,6 +64,10 @@ async def login_post(request: Request, secure_cookies = aurweb.config.getboolean("options", "disable_http_login") response.set_cookie("AURSID", sid, expires=expires_at, secure=secure_cookies, httponly=True) + response.set_cookie("AURTZ", user.Timezone, + secure=secure_cookies, httponly=True) + response.set_cookie("AURLANG", user.LangPreference, + secure=secure_cookies, httponly=True) return util.add_samesite_fields(response, "strict") From 865c41450466c8392605f7c1aabc5607b77cb5a1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 12 Jun 2021 03:54:41 -0700 Subject: [PATCH 0290/1451] aurweb.asgi: add security headers middleware This commit introduces a middleware function which adds the following security headers to each response: - Content-Security-Policy - This includes a new `nonce`, which is tied to a user via authentication middleware. Both an anonymous user and an authenticated user recieve their own random nonces. - X-Content-Type-Options - Referrer-Policy - X-Frame-Options They are then tested for existence in test/test_routes.py. Note: The overcomplicated-looking asyncio behavior in the middleware function is used to avoid a warning about the old coroutine awaits being deprecated. See https://docs.python.org/3/library/asyncio-task.html#asyncio.wait for more detail. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 43 ++++++++++++++++++++++++++++++- aurweb/auth.py | 10 ++++++- aurweb/models/user.py | 1 + aurweb/util.py | 11 ++++++++ templates/partials/typeahead.html | 2 +- test/test_routes.py | 42 ++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 3 deletions(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 861f6056..6c4d457d 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -1,6 +1,8 @@ +import asyncio import http +import typing -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.authentication import AuthenticationMiddleware @@ -55,3 +57,42 @@ async def http_exception_handler(request, exc): phrase = http.HTTPStatus(exc.status_code).phrase return HTMLResponse(f"

    {exc.status_code} {phrase}

    {exc.detail}

    ", status_code=exc.status_code) + + +@app.middleware("http") +async def add_security_headers(request: Request, call_next: typing.Callable): + """ This middleware adds the CSP, XCTO, XFO and RP security + headers to the HTTP response associated with request. + + CSP: Content-Security-Policy + XCTO: X-Content-Type-Options + RP: Referrer-Policy + XFO: X-Frame-Options + """ + response = asyncio.create_task(call_next(request)) + await asyncio.wait({response}, return_when=asyncio.FIRST_COMPLETED) + response = response.result() + + # Add CSP header. + nonce = request.user.nonce + csp = "default-src 'self'; " + script_hosts = [ + "ajax.googleapis.com", + "cdn.jsdelivr.net" + ] + csp += f"script-src 'self' 'nonce-{nonce}' " + ' '.join(script_hosts) + response.headers["Content-Security-Policy"] = csp + + # Add XTCO header. + xcto = "nosniff" + response.headers["X-Content-Type-Options"] = xcto + + # Add Referrer Policy header. + rp = "same-origin" + response.headers["Referrer-Policy"] = rp + + # Add X-Frame-Options header. + xfo = "SAMEORIGIN" + response.headers["X-Frame-Options"] = xfo + + return response diff --git a/aurweb/auth.py b/aurweb/auth.py index f57e18bf..ba5f0fea 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -10,7 +10,7 @@ from starlette.requests import HTTPConnection import aurweb.config -from aurweb import l10n +from aurweb import l10n, util from aurweb.models.session import Session from aurweb.models.user import User from aurweb.templates import make_variable_context, render_template @@ -25,6 +25,12 @@ class AnonymousUser: # A stub ssh_pub_key relationship. ssh_pub_key = None + # A nonce attribute, needed for all browser sessions; set in __init__. + nonce = None + + def __init__(self): + self.nonce = util.make_nonce() + @staticmethod def is_authenticated(): return False @@ -55,7 +61,9 @@ class BasicAuthBackend(AuthenticationBackend): # exists, due to ForeignKey constraints in the schema upheld # by mysqlclient. user = session.query(User).filter(User.ID == record.UsersID).first() + user.nonce = util.make_nonce() user.authenticated = True + return AuthCredentials(["authenticated"]), user diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 83cde5f1..9db9add0 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -37,6 +37,7 @@ class User(Base): # High-level variables used to track authentication (not in DB). authenticated = False + nonce = None def __init__(self, Passwd: str = str(), **kwargs): super().__init__(**kwargs) diff --git a/aurweb/util.py b/aurweb/util.py index b34226a2..e5f510ce 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,6 +1,8 @@ import base64 +import math import random import re +import secrets import string from collections import OrderedDict @@ -20,6 +22,15 @@ def make_random_string(length): string.digits, k=length)) +def make_nonce(length: int = 8): + """ Generate a single random nonce. Here, token_hex generates a hex + string of 2 hex characters per byte, where the length give is + nbytes. This means that to get our proper string length, we need to + cut it in half and truncate off any remaining (in the case that + length was uneven). """ + return secrets.token_hex(math.ceil(length / 2))[:length] + + def valid_username(username): min_len = aurweb.config.getint("options", "username_min_len") max_len = aurweb.config.getint("options", "username_max_len") diff --git a/templates/partials/typeahead.html b/templates/partials/typeahead.html index d943dbc4..c218b8d1 100644 --- a/templates/partials/typeahead.html +++ b/templates/partials/typeahead.html @@ -1,6 +1,6 @@ - - - + "+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery) \ No newline at end of file diff --git a/web/html/js/typeahead.js b/web/html/js/typeahead.js new file mode 100644 index 00000000..1b7252d7 --- /dev/null +++ b/web/html/js/typeahead.js @@ -0,0 +1,151 @@ +"use strict"; + +const typeahead = (function() { + var input; + var form; + var suggest_type; + var list; + var submit = true; + + function resetResults() { + if (!list) return; + list.style.display = "none"; + list.innerHTML = ""; + } + + function getCompleteList() { + if (!list) { + list = document.createElement("UL"); + list.setAttribute("class", "pkgsearch-typeahead"); + form.appendChild(list); + setListLocation(); + } + return list; + } + + function onListClick(e) { + let target = e.target; + while (!target.getAttribute('data-value')) { + target = target.parentNode; + } + input.value = target.getAttribute('data-value'); + if (submit) { + form.submit(); + } + } + + function setListLocation() { + if (!list) return; + const rects = input.getClientRects()[0]; + list.style.top = (rects.top + rects.height) + "px"; + list.style.left = rects.left + "px"; + } + + function loadData(letter, data) { + const pkgs = data.slice(0, 10); // Show maximum of 10 results + + resetResults(); + + if (pkgs.length === 0) { + return; + } + + const ul = getCompleteList(); + ul.style.display = "block"; + const fragment = document.createDocumentFragment(); + + for (let i = 0; i < pkgs.length; i++) { + const item = document.createElement("li"); + const text = pkgs[i].replace(letter, '' + letter + ''); + item.innerHTML = '' + text + ''; + item.setAttribute('data-value', pkgs[i]); + fragment.appendChild(item); + } + + ul.appendChild(fragment); + ul.addEventListener('click', onListClick); + } + + function fetchData(letter) { + const url = '/rpc?type=' + suggest_type + '&arg=' + letter; + fetch(url).then(function(response) { + return response.json(); + }).then(function(data) { + loadData(letter, data); + }); + } + + function onInputClick() { + if (input.value === "") { + resetResults(); + return; + } + fetchData(input.value); + } + + function onKeyDown(e) { + if (!list) return; + + const elem = document.querySelector(".pkgsearch-typeahead li.active"); + switch(e.keyCode) { + case 13: // enter + if (!submit) { + return; + } + if (elem) { + input.value = elem.getAttribute('data-value'); + form.submit(); + } else { + form.submit(); + } + e.preventDefault(); + break; + case 38: // up + if (elem && elem.previousElementSibling) { + elem.className = ""; + elem.previousElementSibling.className = "active"; + } + e.preventDefault(); + break; + case 40: // down + if (elem && elem.nextElementSibling) { + elem.className = ""; + elem.nextElementSibling.className = "active"; + } else if (!elem && list.childElementCount !== 0) { + list.children[0].className = "active"; + } + e.preventDefault(); + break; + } + } + + // debounce https://davidwalsh.name/javascript-debounce-function + function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + } + + return { + init: function(type, inputfield, formfield, submitdata = true) { + suggest_type = type; + input = inputfield; + form = formfield; + submit = submitdata; + + input.addEventListener("input", onInputClick); + input.addEventListener("keydown", onKeyDown); + window.addEventListener('resize', debounce(setListLocation, 150)); + document.addEventListener("click", resetResults); + } + } +}()); diff --git a/web/html/pkgmerge.php b/web/html/pkgmerge.php index d583c239..d96562a7 100644 --- a/web/html/pkgmerge.php +++ b/web/html/pkgmerge.php @@ -25,7 +25,7 @@ if (has_credential(CRED_PKGBASE_DELETE)): ?>

    -
    +
    @@ -33,25 +33,17 @@ if (has_credential(CRED_PKGBASE_DELETE)): ?> - - +

    -

    +

    " />

    diff --git a/web/template/pkgreq_form.php b/web/template/pkgreq_form.php index d80a422c..9d74093e 100644 --- a/web/template/pkgreq_form.php +++ b/web/template/pkgreq_form.php @@ -9,7 +9,7 @@
  • - +
    @@ -24,44 +24,41 @@

    - - +

    - +

    From c8d88464b1f62154a58d8ee403f3eefb3168aa0f Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Fri, 25 Jun 2021 17:25:24 +0200 Subject: [PATCH 0305/1451] Update mailing list address https://lists.archlinux.org/pipermail/arch-dev-public/2021-June/030462.html --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b9ff466..9c8d747e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Patches should be sent to the [aur-dev@archlinux.org][1] mailing list. +Patches should be sent to the [aur-dev@lists.archlinux.org][1] mailing list. Before sending patches, you are recommended to run `flake8` and `isort`. From d95e4ec4431ff4e08e409eb1a670f1b0c90035cc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Jun 2021 17:09:21 -0700 Subject: [PATCH 0306/1451] Docker: create missing 'aurweb' DB if needed Signed-off-by: Kevin Morris --- docker/scripts/run-mariadb.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker/scripts/run-mariadb.sh b/docker/scripts/run-mariadb.sh index 7e908129..d27d8124 100755 --- a/docker/scripts/run-mariadb.sh +++ b/docker/scripts/run-mariadb.sh @@ -8,9 +8,16 @@ done # Create test database. mysql -u root -e "CREATE USER 'aur'@'%' IDENTIFIED BY 'aur'" \ 2>/dev/null || /bin/true + +# Create a brand new 'aurweb_test' DB. mysql -u root -e "DROP DATABASE aurweb_test" 2>/dev/null || /bin/true mysql -u root -e "CREATE DATABASE aurweb_test" mysql -u root -e "GRANT ALL PRIVILEGES ON aurweb_test.* TO 'aur'@'%'" + +# Create the 'aurweb' DB if it does not yet exist. +mysql -u root -e "CREATE DATABASE aurweb" 2>/dev/null || /bin/true +mysql -u root -e "GRANT ALL PRIVILEGES ON aurweb.* TO 'aur'@'%'" + mysql -u root -e "FLUSH PRIVILEGES" # Shutdown mariadb. From 201a04ffb9dddadbd7be2fc587057017426ace2e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Jun 2021 16:17:38 -0700 Subject: [PATCH 0307/1451] gendummydata: employ a salted hash for users As of Python updates, we are no longer considering rows with empty salts to be legacy hashes. Update gendummydata.py to generate salts for the legacy passwords it uses with salt rounds = 4. Signed-off-by: Kevin Morris --- schema/gendummydata.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 35805d6c..11f2838a 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -16,6 +16,8 @@ import random import sys import time +import bcrypt + LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" USER_ID = 5 # Users.ID of first bogus user @@ -182,11 +184,17 @@ for u in user_keys: # pass + # For dummy data, we just use 4 salt rounds. + salt = bcrypt.gensalt(rounds=4).decode() + + # "{salt}{username}" + to_hash = f"{salt}{u}" + h = hashlib.new('md5') - h.update(u.encode()) - s = ("INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd)" - " VALUES (%d, %d, '%s', '%s@example.com', '%s');\n") - s = s % (seen_users[u], account_type, u, u, h.hexdigest()) + h.update(to_hash.encode()) + s = ("INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd, Salt)" + " VALUES (%d, %d, '%s', '%s@example.com', '%s', '%s');\n") + s = s % (seen_users[u], account_type, u, u, h.hexdigest(), salt) out.write(s) log.debug("Number of developers: %d" % len(developers)) From eb56305091f13c44716e45746d23fe850c22803c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Jun 2021 17:27:43 -0700 Subject: [PATCH 0308/1451] gendummydata: lower record counts This commit halves MAX_USERS and MAX_PKGS, in addition to setting OPEN_PROPOSALS to 15 and CLOSE_PROPOSALS to 50. A few counts are now configurable via environment variable: - MAX_USERS, default: 38000 - MAX_PKGS, default: 32000 - OPEN_PROPOSALS, default: 15 - CLOSE_PROPOSALS, default: 15 Signed-off-by: Kevin Morris --- schema/gendummydata.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 11f2838a..9224b051 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -22,18 +22,22 @@ LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" USER_ID = 5 # Users.ID of first bogus user PKG_ID = 1 # Packages.ID of first package -MAX_USERS = 76000 # how many users to 'register' +# how many users to 'register' +MAX_USERS = int(os.environ.get("MAX_USERS", 38000)) MAX_DEVS = .1 # what percentage of MAX_USERS are Developers MAX_TUS = .2 # what percentage of MAX_USERS are Trusted Users -MAX_PKGS = 64000 # how many packages to load +# how many packages to load +MAX_PKGS = int(os.environ.get("MAX_PKGS", 32000)) PKG_DEPS = (1, 15) # min/max depends a package has PKG_RELS = (1, 5) # min/max relations a package has PKG_SRC = (1, 3) # min/max sources a package has PKG_CMNTS = (1, 5) # min/max number of comments a package has CATEGORIES_COUNT = 17 # the number of categories from aur-schema VOTING = (0, .001) # percentage range for package voting -OPEN_PROPOSALS = 5 # number of open trusted user proposals -CLOSE_PROPOSALS = 15 # number of closed trusted user proposals +# number of open trusted user proposals +OPEN_PROPOSALS = int(os.environ.get("OPEN_PROPOSALS", 15)) +# number of closed trusted user proposals +CLOSE_PROPOSALS = int(os.environ.get("CLOSE_PROPOSALS", 50)) RANDOM_TLDS = ("edu", "com", "org", "net", "tw", "ru", "pl", "de", "es") RANDOM_URL = ("http://www.", "ftp://ftp.", "http://", "ftp://") RANDOM_LOCS = ("pub", "release", "files", "downloads", "src") From d8556b0d868ef4d3696bbe5a3fe1d5d400537ee4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Jun 2021 21:22:54 -0700 Subject: [PATCH 0309/1451] config: add options.salt_rounds During development, the lower this value is (must be >= 4) equals faster User generation. This is particularly useful for running tests. In production, a higher value (like 12 which is used by various popular frameworks) should be used. Signed-off-by: Kevin Morris --- conf/config.defaults | 1 + conf/config.dev | 2 ++ 2 files changed, 3 insertions(+) diff --git a/conf/config.defaults b/conf/config.defaults index 6da4d754..ebc21e51 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -40,6 +40,7 @@ localedir = /srv/http/aurweb/aur.git/web/locale/ cache = none cache_pkginfo_ttl = 86400 memcache_servers = 127.0.0.1:11211 +salt_rounds = 12 [ratelimit] request_limit = 4000 diff --git a/conf/config.dev b/conf/config.dev index 6ef3bb79..fc3bde91 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -26,6 +26,8 @@ aur_location = http://127.0.0.1:8080 disable_http_login = 0 enable-maintenance = 0 localedir = YOUR_AUR_ROOT/web/locale +; In production, salt_rounds should be higher; suggested: 12. +salt_rounds = 4 [notifications] ; For development/testing, use /usr/bin/sendmail From cec07c76b63460865de326e60ab4be8c148b6bc0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Jun 2021 21:24:33 -0700 Subject: [PATCH 0310/1451] User: use aurweb.config options.salt_rounds Signed-off-by: Kevin Morris --- aurweb/config.py | 4 ++-- aurweb/models/user.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 2a6cfc3e..73db58dc 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -44,5 +44,5 @@ def getboolean(section, option): return _get_parser().getboolean(section, option) -def getint(section, option): - return _get_parser().getint(section, option) +def getint(section, option, fallback=None): + return _get_parser().getint(section, option, fallback=fallback) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 9db9add0..bcb47754 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -15,6 +15,8 @@ import aurweb.schema from aurweb.models.ban import is_banned from aurweb.models.declarative import Base +SALT_ROUNDS_DEFAULT = 12 + class User(Base): """ An ORM model of a single Users record. """ @@ -39,16 +41,24 @@ class User(Base): authenticated = False nonce = None + # Make this static to the class just in case SQLAlchemy ever + # does something to bypass our constructor. + salt_rounds = aurweb.config.getint("options", "salt_rounds", + SALT_ROUNDS_DEFAULT) + def __init__(self, Passwd: str = str(), **kwargs): super().__init__(**kwargs) + # Run this again in the constructor in case we rehashed config. + self.salt_rounds = aurweb.config.getint("options", "salt_rounds", + SALT_ROUNDS_DEFAULT) if Passwd: self.update_password(Passwd) - def update_password(self, password, salt_rounds=12): + def update_password(self, password): self.Passwd = bcrypt.hashpw( password.encode(), - bcrypt.gensalt(rounds=salt_rounds)).decode() + bcrypt.gensalt(rounds=self.salt_rounds)).decode() @staticmethod def minimum_passwd_length(): From ff3519ae113dd0f8b19051e9f08ddffefb7adf56 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Jun 2021 22:12:01 -0700 Subject: [PATCH 0311/1451] [alembic] Log db name being used in a migration Signed-off-by: Kevin Morris --- migrations/env.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/migrations/env.py b/migrations/env.py index dfe14804..7130d141 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,3 +1,4 @@ +import logging import logging.config import sqlalchemy @@ -19,12 +20,14 @@ target_metadata = aurweb.schema.metadata # my_important_option = config.get_main_option("my_important_option") # ... etc. - # If configure_logger is either True or not specified, # configure the logger via fileConfig. if config.attributes.get("configure_logger", True): logging.config.fileConfig(config.config_file_name) +# This grabs the root logger in env.py. +logger = logging.getLogger(__name__) + def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -38,6 +41,8 @@ def run_migrations_offline(): script output. """ + db_name = aurweb.config.get("database", "name") + logging.info(f"Performing offline migration on database '{db_name}'.") context.configure( url=aurweb.db.get_sqlalchemy_url(), target_metadata=target_metadata, @@ -56,6 +61,8 @@ def run_migrations_online(): and associate a connection with the context. """ + db_name = aurweb.config.get("database", "name") + logging.info(f"Performing online migration on database '{db_name}'.") connectable = sqlalchemy.create_engine( aurweb.db.get_sqlalchemy_url(), poolclass=sqlalchemy.pool.NullPool, From 9ee7be4a1c8299010df384e91c8e56ec25cc925a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Jun 2021 00:33:32 -0700 Subject: [PATCH 0312/1451] Docker: remove web/locale from volume mounts This caused a bug where generated locale would not be used. Also, removed appending to /etc/hosts which was bugging out on Mac OS X. archlinux:base-devel seems to come with a valid /etc/hosts. Additionally, remove AUR_CONFIG from Dockerfile. We don't set it up; just use the defaults during installation. Signed-off-by: Kevin Morris --- Dockerfile | 3 --- docker-compose.yml | 28 +++++++++++++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5fd3ec07..da9c8d3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ FROM archlinux:base-devel # Setup some default system stuff. -RUN bash -c 'echo "127.0.0.1 localhost" >> /etc/hosts' -RUN bash -c 'echo "::1 localhost" >> /etc/hosts' RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime RUN mkdir -p .pkg-cache @@ -28,6 +26,5 @@ WORKDIR /aurweb COPY . . ENV PYTHONPATH=/aurweb -ENV AUR_CONFIG=conf/config RUN make -C po all install diff --git a/docker-compose.yml b/docker-compose.yml index c2c948f5..795236c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -116,7 +116,9 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web:/aurweb/web + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates fastapi: @@ -148,7 +150,9 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web:/aurweb/web + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates nginx: @@ -180,7 +184,9 @@ services: - git_data:/aurweb/aur.git - ./cache:/cache - ./logs:/var/log/nginx - - ./web:/aurweb/web + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib sharness: image: aurweb:latest @@ -202,7 +208,9 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web:/aurweb/web + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates pytest-mysql: @@ -229,7 +237,9 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web:/aurweb/web + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates pytest-sqlite: @@ -248,7 +258,9 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web:/aurweb/web + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates depends_on: git: @@ -277,7 +289,9 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web:/aurweb/web + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates volumes: From 07c4be0afbfb467d7c09620a6f01c0761dc0ba6e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Jun 2021 00:42:20 -0700 Subject: [PATCH 0313/1451] Docker: add .dockerignore Currently, this ignores compiled translation files. Signed-off-by: Kevin Morris --- .dockerignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..30747517 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +*/*.mo From 4927a61378a15cdd8ab0fe277c4acdc8cfd973d9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 04:40:54 -0700 Subject: [PATCH 0314/1451] add TUVoteInfo.is_running() method Signed-off-by: Kevin Morris --- aurweb/models/tu_voteinfo.py | 5 +++++ test/test_tu_voteinfo.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/aurweb/models/tu_voteinfo.py b/aurweb/models/tu_voteinfo.py index a246f132..fd0031a7 100644 --- a/aurweb/models/tu_voteinfo.py +++ b/aurweb/models/tu_voteinfo.py @@ -1,5 +1,7 @@ import typing +from datetime import datetime + from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship @@ -85,3 +87,6 @@ class TUVoteInfo(Base): """ Customize getattr to floatify any fetched Quorum values. """ attr = super().__getattribute__(key) return float(attr) if key == "Quorum" else attr + + def is_running(self): + return self.End > int(datetime.utcnow().timestamp()) diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py index 37609efd..bd5709fb 100644 --- a/test/test_tu_voteinfo.py +++ b/test/test_tu_voteinfo.py @@ -4,7 +4,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query, rollback +from aurweb.db import commit, create, query, rollback from aurweb.models.account_type import AccountType from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User @@ -49,6 +49,21 @@ def test_tu_voteinfo_creation(): assert tu_voteinfo in user.tu_voteinfo_set +def test_tu_voteinfo_is_running(): + ts = int(datetime.utcnow().timestamp()) + tu_voteinfo = create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 1000, + Quorum=0.5, + Submitter=user) + assert tu_voteinfo.is_running() is True + + tu_voteinfo.End = ts - 5 + commit() + assert tu_voteinfo.is_running() is False + + def test_tu_voteinfo_null_submitter_raises_exception(): with pytest.raises(IntegrityError): create(TUVoteInfo, From ef4a7308ee16e0e42b6adff170dccc259807cade Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 24 Jun 2021 19:28:36 -0700 Subject: [PATCH 0315/1451] add AccountType constants New constants (in aurweb.models.account_type): - USER: "User" - USER_ID: USER's ID - TRUSTED_USER: "Trusted User" - TRUSTED_USER_ID: TRUSTED_USER's ID - DEVELOPER: "Developer" - DEVELOPER_ID: DEVELOPER's ID - TRUSTED_USER_AND_DEV: "TRUSTED_USER_AND_DEV" - TRUSTED_USER_AND_DEV_ID: TRUSTED_USER_AND_DEV's ID Signed-off-by: Kevin Morris --- aurweb/models/account_type.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py index 502a86b1..ca302e5b 100644 --- a/aurweb/models/account_type.py +++ b/aurweb/models/account_type.py @@ -1,5 +1,6 @@ from sqlalchemy import Column, Integer +from aurweb import db from aurweb.models.declarative import Base @@ -20,3 +21,30 @@ class AccountType(Base): def __repr__(self): return "" % ( self.ID, str(self)) + + +# Define some AccountType.AccountType constants. +USER = "User" +TRUSTED_USER = "Trusted User" +DEVELOPER = "Developer" +TRUSTED_USER_AND_DEV = "Trusted User & Developer" + +# Fetch account type IDs from the database for constants. +_account_types = db.query(AccountType) +USER_ID = _account_types.filter( + AccountType.AccountType == USER).first().ID +TRUSTED_USER_ID = _account_types.filter( + AccountType.AccountType == TRUSTED_USER).first().ID +DEVELOPER_ID = _account_types.filter( + AccountType.AccountType == DEVELOPER).first().ID +TRUSTED_USER_AND_DEV_ID = _account_types.filter( + AccountType.AccountType == TRUSTED_USER_AND_DEV).first().ID +_account_types = None # Get rid of the query handle. + +# Map string constants to integer constants. +ACCOUNT_TYPE_ID = { + USER: USER_ID, + TRUSTED_USER: TRUSTED_USER_ID, + DEVELOPER: DEVELOPER_ID, + TRUSTED_USER_AND_DEV: TRUSTED_USER_AND_DEV_ID +} From d606ebc0f1c5805d92a2a1dae86d097dde38ab30 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 04:41:28 -0700 Subject: [PATCH 0316/1451] add User.is_trusted_user() and User.is_developer() Signed-off-by: Kevin Morris --- aurweb/models/user.py | 12 ++++++++++++ test/test_user.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index bcb47754..1762f004 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -156,6 +156,18 @@ class User(Base): session.delete(self.session) session.commit() + def is_trusted_user(self): + return self.AccountType.ID in { + aurweb.models.account_type.TRUSTED_USER_ID, + aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID + } + + def is_developer(self): + return self.AccountType.ID in { + aurweb.models.account_type.DEVELOPER_ID, + aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID + } + def __repr__(self): return "" % ( self.ID, str(self.AccountType), self.Username) diff --git a/test/test_user.py b/test/test_user.py index 06585207..9ab40801 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -9,7 +9,7 @@ import pytest import aurweb.auth import aurweb.config -from aurweb.db import create, query +from aurweb.db import commit, create, query from aurweb.models.account_type import AccountType from aurweb.models.ban import Ban from aurweb.models.session import Session @@ -217,3 +217,35 @@ def test_user_as_dict(): assert data.get("Email") == user.Email # .as_dict() does not convert values to json-capable types. assert isinstance(data.get("RegistrationTS"), datetime) + + +def test_user_is_trusted_user(): + tu_type = query(AccountType, + AccountType.AccountType == "Trusted User").first() + user.AccountType = tu_type + commit() + assert user.is_trusted_user() is True + + # Do it again with the combined role. + tu_type = query( + AccountType, + AccountType.AccountType == "Trusted User & Developer").first() + user.AccountType = tu_type + commit() + assert user.is_trusted_user() is True + + +def test_user_is_developer(): + dev_type = query(AccountType, + AccountType.AccountType == "Developer").first() + user.AccountType = dev_type + commit() + assert user.is_developer() is True + + # Do it again with the combined role. + dev_type = query( + AccountType, + AccountType.AccountType == "Trusted User & Developer").first() + user.AccountType = dev_type + commit() + assert user.is_developer() is True From a6bba601a98aef9b19a4a2b5114557b21706c1d5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 10:46:44 -0700 Subject: [PATCH 0317/1451] add util.get_vote -> `get_vote` Jinja2 filter This filter gets a vote of a request's user toward a voteinfo. Example: {% set vote = (voteinfo | get_vote(request)) %} Signed-off-by: Kevin Morris --- aurweb/templates.py | 1 + aurweb/util.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/aurweb/templates.py b/aurweb/templates.py index bb4047f4..b8853593 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -29,6 +29,7 @@ env.filters["dt"] = util.timestamp_to_datetime env.filters["as_timezone"] = util.as_timezone env.filters["dedupe_qs"] = util.dedupe_qs env.filters["urlencode"] = quote_plus +env.filters["get_vote"] = util.get_vote # Add captcha filters. env.filters["captcha_salt"] = captcha.captcha_salt_filter diff --git a/aurweb/util.py b/aurweb/util.py index e5f510ce..adbff755 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -10,6 +10,8 @@ from datetime import datetime from urllib.parse import quote_plus, urlparse from zoneinfo import ZoneInfo +import fastapi + from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email from fastapi.responses import Response from jinja2 import pass_context @@ -143,6 +145,11 @@ def dedupe_qs(query_string: str, *additions): return '&'.join([f"{k}={quote_plus(v)}" for k, v in reversed(qs.items())]) +def get_vote(voteinfo, request: fastapi.Request): + from aurweb.models.tu_vote import TUVote + return voteinfo.tu_votes.filter(TUVote.User == request.user).first() + + def jsonify(obj): """ Perform a conversion on obj if it's needed. """ if isinstance(obj, datetime): From d674aaf736383a82d0bc900a5b8bcfc7e41537b9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Jun 2021 04:33:48 -0700 Subject: [PATCH 0318/1451] add /tu/ (get) index This commit implements the '/tu' Trusted User index page. In addition to this functionality, this commit introduces the following jinja2 filters: - dt: util.timestamp_to_datetime - as_timezone: util.as_timezone - dedupe_qs: util.dedupe_qs - urlencode: urllib.parse.quote_plus There's also a new decorator that can be used to enforce permissions: `account_type_required`. If a user does not meet account type requirements, they are redirected to '/'. ``` @auth_required(True) @account_type_required({"Trusted User"}) async def some_route(request: fastapi.Request): return Response("You are a Trusted User!") ``` Routes added: - `GET /tu`: aurweb.routers.trusted_user.trusted_user Signed-off-by: Kevin Morris --- aurweb/asgi.py | 3 +- aurweb/auth.py | 39 +++ aurweb/models/account_type.py | 5 + aurweb/routers/trusted_user.py | 97 ++++++ templates/partials/archdev-navbar.html | 18 + templates/partials/tu/last_votes.html | 33 ++ templates/partials/tu/proposals.html | 120 +++++++ templates/tu/index.html | 35 ++ test/test_auth.py | 18 +- test/test_trusted_user_routes.py | 443 +++++++++++++++++++++++++ 10 files changed, 808 insertions(+), 3 deletions(-) create mode 100644 aurweb/routers/trusted_user.py create mode 100644 templates/partials/tu/last_votes.html create mode 100644 templates/partials/tu/proposals.html create mode 100644 templates/tu/index.html create mode 100644 test/test_trusted_user_routes.py diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 65318907..a674fec6 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -16,7 +16,7 @@ from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine, query from aurweb.models.accepted_term import AcceptedTerm from aurweb.models.term import Term -from aurweb.routers import accounts, auth, errors, html, sso +from aurweb.routers import accounts, auth, errors, html, sso, trusted_user # Setup the FastAPI app. app = FastAPI(exception_handlers=errors.exceptions) @@ -47,6 +47,7 @@ async def app_startup(): app.include_router(html.router) app.include_router(auth.router) app.include_router(accounts.router) + app.include_router(trusted_user.router) # Initialize the database engine and ORM. get_engine() diff --git a/aurweb/auth.py b/aurweb/auth.py index ba5f0fea..316e7293 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -3,6 +3,8 @@ import functools from datetime import datetime from http import HTTPStatus +import fastapi + from fastapi.responses import RedirectResponse from sqlalchemy import and_ from starlette.authentication import AuthCredentials, AuthenticationBackend @@ -11,6 +13,7 @@ from starlette.requests import HTTPConnection import aurweb.config from aurweb import l10n, util +from aurweb.models.account_type import ACCOUNT_TYPE_ID from aurweb.models.session import Session from aurweb.models.user import User from aurweb.templates import make_variable_context, render_template @@ -152,6 +155,42 @@ def auth_required(is_required: bool = True, return decorator +def account_type_required(one_of: set): + """ A decorator that can be used on FastAPI routes to dictate + that a user belongs to one of the types defined in one_of. + + This decorator should be run after an @auth_required(True) is + dictated. + + - Example code: + + @router.get('/some_route') + @auth_required(True) + @account_type_required({"Trusted User", "Trusted User & Developer"}) + async def some_route(request: fastapi.Request): + return Response() + + :param one_of: A set consisting of strings to match against AccountType. + :return: Return the FastAPI function this decorator wraps. + """ + # Convert any account type string constants to their integer IDs. + one_of = { + ACCOUNT_TYPE_ID[atype] + for atype in one_of + if isinstance(atype, str) + } + + def decorator(func): + @functools.wraps(func) + async def wrapper(request: fastapi.Request, *args, **kwargs): + if request.user.AccountType.ID not in one_of: + return RedirectResponse("/", + status_code=int(HTTPStatus.SEE_OTHER)) + return await func(request, *args, **kwargs) + return wrapper + return decorator + + CRED_ACCOUNT_CHANGE_TYPE = 1 CRED_ACCOUNT_EDIT = 2 CRED_ACCOUNT_EDIT_DEV = 3 diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py index ca302e5b..0db37ced 100644 --- a/aurweb/models/account_type.py +++ b/aurweb/models/account_type.py @@ -3,6 +3,11 @@ from sqlalchemy import Column, Integer from aurweb import db from aurweb.models.declarative import Base +USER = "User" +TRUSTED_USER = "Trusted User" +DEVELOPER = "Developer" +TRUSTED_USER_AND_DEV = "Trusted User & Developer" + class AccountType(Base): """ An ORM model of a single AccountTypes record. """ diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py new file mode 100644 index 00000000..c027f67d --- /dev/null +++ b/aurweb/routers/trusted_user.py @@ -0,0 +1,97 @@ +from datetime import datetime +from urllib.parse import quote_plus + +from fastapi import APIRouter, Request +from sqlalchemy import and_, or_ + +from aurweb import db +from aurweb.auth import account_type_required, auth_required +from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND_DEV +from aurweb.models.tu_vote import TUVote +from aurweb.models.tu_voteinfo import TUVoteInfo +from aurweb.models.user import User +from aurweb.templates import make_context, render_template + +router = APIRouter() + +# Some TU route specific constants. +ITEMS_PER_PAGE = 10 # Paged table size. +MAX_AGENDA_LENGTH = 75 # Agenda table column length. + +# A set of account types that will approve a user for TU actions. +REQUIRED_TYPES = { + TRUSTED_USER, + DEVELOPER, + TRUSTED_USER_AND_DEV +} + + +@router.get("/tu") +@auth_required(True, redirect="/") +@account_type_required(REQUIRED_TYPES) +async def trusted_user(request: Request, + coff: int = 0, # current offset + cby: str = "desc", # current by + poff: int = 0, # past offset + pby: str = "desc"): # past by + context = make_context(request, "Trusted User") + + current_by, past_by = cby, pby + current_off, past_off = coff, poff + + context["pp"] = pp = ITEMS_PER_PAGE + context["prev_len"] = MAX_AGENDA_LENGTH + + ts = int(datetime.utcnow().timestamp()) + + if current_by not in {"asc", "desc"}: + # If a malicious by was given, default to desc. + current_by = "desc" + context["current_by"] = current_by + + if past_by not in {"asc", "desc"}: + # If a malicious by was given, default to desc. + past_by = "desc" + context["past_by"] = past_by + + current_votes = db.query(TUVoteInfo, TUVoteInfo.End > ts).order_by( + TUVoteInfo.Submitted.desc()) + context["current_votes_count"] = current_votes.count() + current_votes = current_votes.limit(pp).offset(current_off) + context["current_votes"] = reversed(current_votes.all()) \ + if current_by == "asc" else current_votes.all() + context["current_off"] = current_off + + past_votes = db.query(TUVoteInfo, TUVoteInfo.End <= ts).order_by( + TUVoteInfo.Submitted.desc()) + context["past_votes_count"] = past_votes.count() + past_votes = past_votes.limit(pp).offset(past_off) + context["past_votes"] = reversed(past_votes.all()) \ + if past_by == "asc" else past_votes.all() + context["past_off"] = past_off + + # TODO + # We order last votes by TUVote.VoteID and User.Username. + # This is really bad. We should add a Created column to + # TUVote of type Timestamp and order by that instead. + last_votes_by_tu = db.query(TUVote).filter( + and_(TUVote.VoteID == TUVoteInfo.ID, + TUVoteInfo.End <= ts, + TUVote.UserID == User.ID, + or_(User.AccountTypeID == 2, + User.AccountTypeID == 4)) + ).group_by(User.ID).order_by( + TUVote.VoteID.desc(), User.Username.asc()) + context["last_votes_by_tu"] = last_votes_by_tu.all() + + context["current_by_next"] = "asc" if current_by == "desc" else "desc" + context["past_by_next"] = "asc" if past_by == "desc" else "desc" + + context["q"] = '&'.join([ + f"coff={current_off}", + f"cby={quote_plus(current_by)}", + f"poff={past_off}", + f"pby={quote_plus(past_by)}" + ]) + + return render_template(request, "tu/index.html", context) diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index c935fd41..c6cd3f19 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -7,11 +7,29 @@ {% endif %}

  • {% trans %}Packages{% endtrans %}
  • {% if request.user.is_authenticated() %} + + {% if request.user.is_trusted_user() or request.user.is_developer() %} +
  • + {% trans %}Requests{% endtrans %} +
  • + +
  • + {% trans %}Accounts{% endtrans %} +
  • + {% endif %} +
  • {% trans %}My Account{% endtrans %}
  • + + {% if request.user.is_trusted_user() %} +
  • + {% trans %}Trusted User{% endtrans %} +
  • + {% endif %} +
  • {% trans %}Logout{% endtrans %} diff --git a/templates/partials/tu/last_votes.html b/templates/partials/tu/last_votes.html new file mode 100644 index 00000000..94b9c1e8 --- /dev/null +++ b/templates/partials/tu/last_votes.html @@ -0,0 +1,33 @@ +
    +

    {% trans %}{{ title }}{% endtrans %}

    + +
  • + + + + + + + {% if not votes %} + + + + + {% else %} + {% for vote in votes %} + + + + + {% endfor %} + {% endif %} + +
    {{ "User" | tr }}{{ "Last vote" | tr }}
    + {{ "No results found." | tr }} +
    {{ vote.User.Username }} + + {{ vote.VoteID }} + +
    + + diff --git a/templates/partials/tu/proposals.html b/templates/partials/tu/proposals.html new file mode 100644 index 00000000..13e705fc --- /dev/null +++ b/templates/partials/tu/proposals.html @@ -0,0 +1,120 @@ +
    +

    {% trans %}{{ title }}{% endtrans %}

    + + {% if title == "Current Votes" %} +
    + {% endif %} + + {% if not results %} +

    + {% trans %}No results found.{% endtrans %} +

    + {% else %} + + + + + + + + + {% if title != "Current Votes" %} + + + {% endif %} + + + + + + {% for result in results %} + + + + + {% set submitted = result.Submitted | dt | as_timezone(timezone) %} + + + + {% set end = result.End | dt | as_timezone(timezone) %} + + + + + {% if title != "Current Votes" %} + + + {% endif %} + + {% set vote = (result | get_vote(request)) %} + + + {% endfor %} + +
    {{ "Proposal" | tr }} + {% set off_qs = "%s=%d" | format(off_param, off) %} + {% set by_qs = "%s=%s" | format(by_param, by_next | urlencode) %} + + {{ "Start" | tr }} + + {{ "End" | tr }}{{ "User" | tr }}{{ "Yes" | tr }}{{ "No" | tr }}{{ "Voted" | tr }}
    + + {% set agenda = result.Agenda[:prev_len] %} + {{ agenda }} + {{ submitted.strftime("%Y-%m-%d") }}{{ end.strftime("%Y-%m-%d") }} + {% if not result.User %} + N/A + {% else %} + + {{ result.User }} + + {% endif %} + {{ result.Yes }}{{ result.No }} + {% if vote %} + + {{ "Yes" | tr }} + + {% else %} + + {{ "No" | tr }} + + {% endif %} +
    + +
    +

    + {% if total_votes > pp %} + + {% if off > 0 %} + {% set off_qs = "%s=%d" | format(off_param, off - 10) %} + {% set by_qs = "%s=%s" | format(by_param, by | urlencode) %} + + ‹ Back + + {% endif %} + + {% if off < total_votes - pp %} + {% set off_qs = "%s=%d" | format(off_param, off + 10) %} + {% set by_qs = "%s=%s" | format(by_param, by | urlencode) %} + + Next › + + {% endif %} + + {% endif %} +

    +
    + + {% endif %} + +
    diff --git a/templates/tu/index.html b/templates/tu/index.html new file mode 100644 index 00000000..5060e1f7 --- /dev/null +++ b/templates/tu/index.html @@ -0,0 +1,35 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% + with table_class = "current-votes", + total_votes = current_votes_count, + results = current_votes, + off_param = "coff", + by_param = "cby", + by_next = current_by_next, + title = "Current Votes", + off = current_off, + by = current_by + %} + {% include "partials/tu/proposals.html" %} + {% endwith %} + + {% + with table_class = "past-votes", + total_votes = past_votes_count, + results = past_votes, + off_param = "poff", + by_param = "pby", + by_next = past_by_next, + title = "Past Votes", + off = past_off, + by = past_by + %} + {% include "partials/tu/proposals.html" %} + {% endwith %} + + {% with title = "Last Votes by TU", votes = last_votes_by_tu %} + {% include "partials/tu/last_votes.html" %} + {% endwith %} +{% endblock %} diff --git a/test/test_auth.py b/test/test_auth.py index e5e1de11..b386bea1 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -4,9 +4,9 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.auth import BasicAuthBackend, has_credential +from aurweb.auth import BasicAuthBackend, account_type_required, has_credential from aurweb.db import create, query -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER, USER_ID, AccountType from aurweb.models.session import Session from aurweb.models.user import User from aurweb.testing import setup_test_db @@ -76,3 +76,17 @@ async def test_basic_auth_backend(): def test_has_fake_credential_fails(): # Fake credential 666 does not exist. assert not has_credential(user, 666) + + +def test_account_type_required(): + """ This test merely asserts that a few different paths + do not raise exceptions. """ + # This one shouldn't raise. + account_type_required({USER}) + + # This one also shouldn't raise. + account_type_required({USER_ID}) + + # But this one should! We have no "FAKE" key. + with pytest.raises(KeyError): + account_type_required({'FAKE'}) diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py new file mode 100644 index 00000000..a6527e6f --- /dev/null +++ b/test/test_trusted_user_routes.py @@ -0,0 +1,443 @@ +import re + +from datetime import datetime +from http import HTTPStatus +from io import StringIO + +import lxml.etree +import pytest + +from fastapi.testclient import TestClient + +from aurweb import db +from aurweb.models.account_type import AccountType +from aurweb.models.tu_vote import TUVote +from aurweb.models.tu_voteinfo import TUVoteInfo +from aurweb.models.user import User +from aurweb.testing import setup_test_db +from aurweb.testing.requests import Request + +DATETIME_REGEX = r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$' + + +def parse_root(html): + parser = lxml.etree.HTMLParser(recover=True) + tree = lxml.etree.parse(StringIO(html), parser) + return tree.getroot() + + +def get_table(root, class_name): + table = root.xpath(f'//table[contains(@class, "{class_name}")]')[0] + return table + + +def get_table_rows(table): + tbody = table.xpath("./tbody")[0] + return tbody.xpath("./tr") + + +def get_pkglist_directions(table): + stats = table.getparent().xpath("./div[@class='pkglist-stats']")[0] + nav = stats.xpath("./p[@class='pkglist-nav']")[0] + return nav.xpath("./a") + + +def get_a(node): + return node.xpath('./a')[0].text.strip() + + +def get_span(node): + return node.xpath('./span')[0].text.strip() + + +def assert_current_vote_html(row, expected): + columns = row.xpath("./td") + proposal, start, end, user, voted = columns + p, s, e, u, v = expected # Column expectations. + assert re.match(p, get_a(proposal)) is not None + assert re.match(s, start.text) is not None + assert re.match(e, end.text) is not None + assert re.match(u, get_a(user)) is not None + assert re.match(v, get_span(voted)) is not None + + +def assert_past_vote_html(row, expected): + columns = row.xpath("./td") + proposal, start, end, user, yes, no, voted = columns # Real columns. + p, s, e, u, y, n, v = expected # Column expectations. + assert re.match(p, get_a(proposal)) is not None + assert re.match(s, start.text) is not None + assert re.match(e, end.text) is not None + assert re.match(u, get_a(user)) is not None + assert re.match(y, yes.text) is not None + assert re.match(n, no.text) is not None + assert re.match(v, get_span(voted)) is not None + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db("TU_Votes", "TU_VoteInfo", "Users") + + +@pytest.fixture +def client(): + from aurweb.asgi import app + yield TestClient(app=app) + + +@pytest.fixture +def tu_user(): + tu_type = db.query(AccountType, + AccountType.AccountType == "Trusted User").first() + yield db.create(User, Username="test_tu", Email="test_tu@example.org", + RealName="Test TU", Passwd="testPassword", + AccountType=tu_type) + + +@pytest.fixture +def user(): + user_type = db.query(AccountType, + AccountType.AccountType == "User").first() + yield db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=user_type) + + +def test_tu_index_guest(client): + with client as request: + response = request.get("/tu", allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + +def test_tu_index_unauthorized(client, user): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + # Login as a normal user, not a TU. + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + +def test_tu_empty_index(client, tu_user): + """ Check an empty index when we don't create any records. """ + + # Make a default get request to /tu. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Parse lxml root. + root = parse_root(response.text) + + # Check that .current-votes does not exist. + tables = root.xpath('//table[contains(@class, "current-votes")]') + assert len(tables) == 0 + + # Check that .past-votes has does not exist. + tables = root.xpath('//table[contains(@class, "current-votes")]') + assert len(tables) == 0 + + +def test_tu_index(client, tu_user): + ts = int(datetime.utcnow().timestamp()) + + # Create some test votes: (Agenda, Start, End). + votes = [ + ("Test agenda 1", ts - 5, ts + 1000), # Still running. + ("Test agenda 2", ts - 1000, ts - 5) # Not running anymore. + ] + vote_records = [] + for vote in votes: + agenda, start, end = vote + vote_records.append( + db.create(TUVoteInfo, Agenda=agenda, + User=tu_user.Username, + Submitted=start, End=end, + Quorum=0.0, + Submitter=tu_user)) + + # Vote on an ended proposal. + vote_record = vote_records[1] + vote_record.Yes += 1 + vote_record.ActiveTUs += 1 + db.create(TUVote, VoteInfo=vote_record, User=tu_user) + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + # Pass an invalid cby and pby; let them default to "desc". + response = request.get("/tu", cookies=cookies, params={ + "cby": "BAD!", + "pby": "blah" + }, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + # Rows we expect to exist in HTML produced by /tu for current votes. + expected_rows = [ + ( + r'Test agenda 1', + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^(Yes|No)$' + ) + ] + + # Assert that we are matching the number of current votes. + current_votes = [c for c in votes if c[2] > ts] + assert len(current_votes) == len(expected_rows) + + # Parse lxml.etree root. + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + for i, row in enumerate(rows): + assert_current_vote_html(row, expected_rows[i]) + + # Assert that we are matching the number of past votes. + past_votes = [c for c in votes if c[2] <= ts] + assert len(past_votes) == len(expected_rows) + + # Rows we expect to exist in HTML produced by /tu for past votes. + expected_rows = [ + ( + r'Test agenda 2', + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^\d+$', + r'^\d+$', + r'^(Yes|No)$' + ) + ] + + table = get_table(root, "past-votes") + rows = get_table_rows(table) + for i, row in enumerate(rows): + assert_past_vote_html(row, expected_rows[i]) + + # Get the .last-votes table and check that our vote shows up. + table = get_table(root, "last-votes") + rows = get_table_rows(table) + assert len(rows) == 1 + + # Check to see the rows match up to our user and related vote. + username, vote_id = rows[0] + vote_id = vote_id.xpath("./a")[0] + assert username.text.strip() == tu_user.Username + assert int(vote_id.text.strip()) == vote_records[1].ID + + +def test_tu_index_table_paging(client, tu_user): + ts = int(datetime.utcnow().timestamp()) + + for i in range(25): + # Create 25 current votes. + db.create(TUVoteInfo, Agenda=f"Agenda #{i}", + User=tu_user.Username, + Submitted=(ts - 5), End=(ts + 1000), + Quorum=0.0, + Submitter=tu_user, autocommit=False) + + for i in range(25): + # Create 25 past votes. + db.create(TUVoteInfo, Agenda=f"Agenda #{25 + i}", + User=tu_user.Username, + Submitted=(ts - 1000), End=(ts - 5), + Quorum=0.0, + Submitter=tu_user, autocommit=False) + db.commit() + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Parse lxml.etree root. + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + assert len(rows) == 10 + + def make_expectation(offset, i): + return [ + f"Agenda #{offset + i}", + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^(Yes|No)$' + ] + + for i, row in enumerate(rows): + assert_current_vote_html(row, make_expectation(0, i)) + + # Parse out Back/Next buttons. + directions = get_pkglist_directions(table) + assert len(directions) == 1 + assert "Next" in directions[0].text + + # Now, get the next page of current votes. + offset = 10 # Specify coff=10 + with client as request: + response = request.get("/tu", cookies=cookies, params={ + "coff": offset + }, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + old_rows = rows + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + assert rows != old_rows + + for i, row in enumerate(rows): + assert_current_vote_html(row, make_expectation(offset, i)) + + # Parse out Back/Next buttons. + directions = get_pkglist_directions(table) + assert len(directions) == 2 + assert "Back" in directions[0].text + assert "Next" in directions[1].text + + # Make sure past-votes' Back/Next were not affected. + past_votes = get_table(root, "past-votes") + past_directions = get_pkglist_directions(past_votes) + assert len(past_directions) == 1 + assert "Next" in past_directions[0].text + + offset = 20 # Specify coff=10 + with client as request: + response = request.get("/tu", cookies=cookies, params={ + "coff": offset + }, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Do it again, we only have five left. + old_rows = rows + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + assert rows != old_rows + for i, row in enumerate(rows): + assert_current_vote_html(row, make_expectation(offset, i)) + + # Parse out Back/Next buttons. + directions = get_pkglist_directions(table) + assert len(directions) == 1 + assert "Back" in directions[0].text + + # Make sure past-votes' Back/Next were not affected. + past_votes = get_table(root, "past-votes") + past_directions = get_pkglist_directions(past_votes) + assert len(past_directions) == 1 + assert "Next" in past_directions[0].text + + +def test_tu_index_sorting(client, tu_user): + ts = int(datetime.utcnow().timestamp()) + + for i in range(2): + # Create 'Agenda #1' and 'Agenda #2'. + db.create(TUVoteInfo, Agenda=f"Agenda #{i + 1}", + User=tu_user.Username, + Submitted=(ts + 5), End=(ts + 1000), + Quorum=0.0, + Submitter=tu_user, autocommit=False) + + # Let's order each vote one day after the other. + # This will allow us to test the sorting nature + # of the tables. + ts += 86405 + + # Make a default request to /tu. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Get lxml handles of the document. + root = parse_root(response.text) + table = get_table(root, "current-votes") + rows = get_table_rows(table) + + # The latest Agenda is at the top by default. + expected = [ + "Agenda #2", + "Agenda #1" + ] + + assert len(rows) == len(expected) + for i, row in enumerate(rows): + assert_current_vote_html(row, [ + expected[i], + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^(Yes|No)$' + ]) + + # Make another request; one that sorts the current votes + # in ascending order instead of the default descending order. + with client as request: + response = request.get("/tu", cookies=cookies, params={ + "cby": "asc" + }, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Get lxml handles of the document. + root = parse_root(response.text) + table = get_table(root, "current-votes") + rows = get_table_rows(table) + + # Reverse our expectations and assert that the proposals got flipped. + rev_expected = list(reversed(expected)) + assert len(rows) == len(rev_expected) + for i, row in enumerate(rows): + assert_current_vote_html(row, [ + rev_expected[i], + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^(Yes|No)$' + ]) + + +def test_tu_index_last_votes(client, tu_user, user): + ts = int(datetime.utcnow().timestamp()) + + # Create a proposal which has ended. + voteinfo = db.create(TUVoteInfo, Agenda="Test agenda", + User=user.Username, + Submitted=(ts - 1000), + End=(ts - 5), + Yes=1, + ActiveTUs=1, + Quorum=0.0, + Submitter=tu_user) + + # Create a vote on it from tu_user. + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + + # Now, check that tu_user got populated in the .last-votes table. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + table = get_table(root, "last-votes") + rows = get_table_rows(table) + assert len(rows) == 1 + + last_vote = rows[0] + user, vote_id = last_vote.xpath("./td") + vote_id = vote_id.xpath("./a")[0] + + assert user.text.strip() == tu_user.Username + assert int(vote_id.text.strip()) == voteinfo.ID From e534704a98ed7d421c04279e0c4f57aa8ea837b1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Jun 2021 01:10:20 -0700 Subject: [PATCH 0319/1451] [FastAPI] remove unused Requests navbar item Signed-off-by: Kevin Morris --- templates/partials/archdev-navbar.html | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index c6cd3f19..13459e1a 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -7,17 +7,13 @@ {% endif %}
  • {% trans %}Packages{% endtrans %}
  • {% if request.user.is_authenticated() %} - {% if request.user.is_trusted_user() or request.user.is_developer() %}
  • - {% trans %}Requests{% endtrans %} -
  • - -
  • - {% trans %}Accounts{% endtrans %} + + {% trans %}Accounts{% endtrans %} +
  • {% endif %} -
  • {% trans %}My Account{% endtrans %} From dc4cc9b604a9085f631c2649909f6767e6f2ce3e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 01:10:53 -0700 Subject: [PATCH 0320/1451] add aurweb.asgi.id_redirect_middleware A new middleware which redirects requests going to '/route?id=some_id' to '/route/some_id'. In the FastAPI application, we'll prefer using restful layouts where possible where resource-based ids are parameters of the request uri: '/route/{resource_id}'. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 22 ++++++++++++++++++++++ test/test_routes.py | 10 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index a674fec6..35166c73 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -2,6 +2,8 @@ import asyncio import http import typing +from urllib.parse import quote_plus + from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles @@ -120,3 +122,23 @@ async def check_terms_of_service(request: Request, call_next: typing.Callable): task = asyncio.create_task(call_next(request)) await asyncio.wait({task}, return_when=asyncio.FIRST_COMPLETED) return task.result() + + +@app.middleware("http") +async def id_redirect_middleware(request: Request, call_next: typing.Callable): + id = request.query_params.get("id") + + if id is not None: + # Preserve query string. + qs = [] + for k, v in request.query_params.items(): + if k != "id": + qs.append(f"{k}={quote_plus(str(v))}") + qs = str() if not qs else '?' + '&'.join(qs) + + path = request.url.path.rstrip('/') + return RedirectResponse(f"{path}/{id}{qs}") + + task = asyncio.create_task(call_next(request)) + await asyncio.wait({task}, return_when=asyncio.FIRST_COMPLETED) + return task.result() diff --git a/test/test_routes.py b/test/test_routes.py index d67f4a48..a2d1786e 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -148,3 +148,13 @@ def test_nonce_csp(): if not (nonce_verified := (script.get("nonce") == nonce)): break assert nonce_verified is True + + +def test_id_redirect(): + with client as request: + response = request.get("/", params={ + "id": "test", # This param will be rewritten into Location. + "key": "value", # Test that this param persists. + "key2": "value2" # And this one. + }, allow_redirects=False) + assert response.headers.get("location") == "/test?key=value&key2=value2" From ac1779b705d9b0ad87deaa1856e9b5e05d9ae944 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 01:14:10 -0700 Subject: [PATCH 0321/1451] add util.number_format -> `number_format` Jinja2 filter Implement a `number_format` equivalent to PHP's version. Signed-off-by: Kevin Morris --- aurweb/templates.py | 1 + aurweb/util.py | 5 +++++ test/test_util.py | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/aurweb/templates.py b/aurweb/templates.py index b8853593..8b507425 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -30,6 +30,7 @@ env.filters["as_timezone"] = util.as_timezone env.filters["dedupe_qs"] = util.dedupe_qs env.filters["urlencode"] = quote_plus env.filters["get_vote"] = util.get_vote +env.filters["number_format"] = util.number_format # Add captcha filters. env.filters["captcha_salt"] = captcha.captcha_salt_filter diff --git a/aurweb/util.py b/aurweb/util.py index adbff755..539af40e 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -150,6 +150,11 @@ def get_vote(voteinfo, request: fastapi.Request): return voteinfo.tu_votes.filter(TUVote.User == request.user).first() +def number_format(value: float, places: int): + """ A converter function similar to PHP's number_format. """ + return f"{value:.{places}f}" + + def jsonify(obj): """ Perform a conversion on obj if it's needed. """ if isinstance(obj, datetime): diff --git a/test/test_util.py b/test/test_util.py index 074de494..f54a98a0 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -30,3 +30,8 @@ def test_dedupe_qs(): # Add key1=changed and key2=changed to the query and dedupe it. deduped = util.dedupe_qs(query_string, "key1=changed", "key3=changed") assert deduped == "key2=blah&key1=changed&key3=changed" + + +def test_number_format(): + assert util.number_format(0.222, 2) == "0.22" + assert util.number_format(0.226, 2) == "0.23" From 83c038a42ac50a087bff82490b21acc7e55d65b9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 01:18:40 -0700 Subject: [PATCH 0322/1451] add TUVoteInfo.total_votes() Returns the sum of TUVoteInfo.Yes, TUVoteInfo.No and TUVoteInfo.Abstain. Signed-off-by: Kevin Morris --- aurweb/models/tu_voteinfo.py | 3 +++ test/test_tu_voteinfo.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/aurweb/models/tu_voteinfo.py b/aurweb/models/tu_voteinfo.py index fd0031a7..b80073f4 100644 --- a/aurweb/models/tu_voteinfo.py +++ b/aurweb/models/tu_voteinfo.py @@ -90,3 +90,6 @@ class TUVoteInfo(Base): def is_running(self): return self.End > int(datetime.utcnow().timestamp()) + + def total_votes(self): + return self.Yes + self.No + self.Abstain diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py index bd5709fb..494300c5 100644 --- a/test/test_tu_voteinfo.py +++ b/test/test_tu_voteinfo.py @@ -64,6 +64,24 @@ def test_tu_voteinfo_is_running(): assert tu_voteinfo.is_running() is False +def test_tu_voteinfo_total_votes(): + ts = int(datetime.utcnow().timestamp()) + tu_voteinfo = create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 1000, + Quorum=0.5, + Submitter=user) + + tu_voteinfo.Yes = 1 + tu_voteinfo.No = 3 + tu_voteinfo.Abstain = 5 + commit() + + # total_votes() should be the sum of Yes, No and Abstain: 1 + 3 + 5 = 9. + assert tu_voteinfo.total_votes() == 9 + + def test_tu_voteinfo_null_submitter_raises_exception(): with pytest.raises(IntegrityError): create(TUVoteInfo, From 85ba4a33a865ab78cd9e3e1b4e9bd6e410ea8816 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 05:08:25 -0700 Subject: [PATCH 0323/1451] add /tu/{proposal_id} (get, post) routes This commit ports the `/tu/?id={proposal_id}` PHP routes to FastAPI into two individual GET and POST routes. With this port of the single proposal view and POST logic, several things have changed. - The only parameter used is now `decision`, which must contain `Yes`, `No`, or `Abstain` as a string. When an invalid value is given, a BAD_REQUEST response is returned in plaintext: Invalid 'decision' value. - The `doVote` parameter has been removed. - The details section has been rearranged into a set of divs with specific classes that can be used for testing. CSS has been added to persist the layout with the element changes. - Several errors that can be discovered in the POST path now trigger their own non-200 HTTPStatus codes. Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 127 ++++++++- templates/partials/tu/proposal/details.html | 106 +++++++ templates/partials/tu/proposal/form.html | 14 + templates/partials/tu/proposal/voters.html | 10 + templates/tu/show.html | 20 ++ test/test_trusted_user_routes.py | 288 ++++++++++++++++++++ web/html/css/aurweb.css | 8 + 7 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 templates/partials/tu/proposal/details.html create mode 100644 templates/partials/tu/proposal/form.html create mode 100644 templates/partials/tu/proposal/voters.html create mode 100644 templates/tu/show.html diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index c027f67d..efdcfc73 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -1,7 +1,11 @@ +import typing + from datetime import datetime +from http import HTTPStatus from urllib.parse import quote_plus -from fastapi import APIRouter, Request +from fastapi import APIRouter, Form, HTTPException, Request +from fastapi.responses import Response from sqlalchemy import and_, or_ from aurweb import db @@ -10,7 +14,7 @@ from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND from aurweb.models.tu_vote import TUVote from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User -from aurweb.templates import make_context, render_template +from aurweb.templates import make_context, make_variable_context, render_template router = APIRouter() @@ -95,3 +99,122 @@ async def trusted_user(request: Request, ]) return render_template(request, "tu/index.html", context) + + +def render_proposal(request: Request, + context: dict, + proposal: int, + voteinfo: TUVoteInfo, + voters: typing.Iterable[User], + vote: TUVote, + status_code: HTTPStatus = HTTPStatus.OK): + """ Render a single TU proposal. """ + context["proposal"] = proposal + context["voteinfo"] = voteinfo + context["voters"] = voters + + participation = voteinfo.ActiveTUs / voteinfo.total_votes() \ + if voteinfo.total_votes() else 0 + context["participation"] = participation + + accepted = (voteinfo.Yes > voteinfo.ActiveTUs / 2) or \ + (participation > voteinfo.Quorum and voteinfo.Yes > voteinfo.No) + context["accepted"] = accepted + + can_vote = voters.filter(TUVote.User == request.user).first() is None + context["can_vote"] = can_vote + + if not voteinfo.is_running(): + context["error"] = "Voting is closed for this proposal." + + context["vote"] = vote + context["has_voted"] = vote is not None + + return render_template(request, "tu/show.html", context, + status_code=status_code) + + +@router.get("/tu/{proposal}") +@auth_required(True, redirect="/") +@account_type_required(REQUIRED_TYPES) +async def trusted_user_proposal(request: Request, proposal: int): + context = await make_variable_context(request, "Trusted User") + proposal = int(proposal) + + voteinfo = db.query(TUVoteInfo, TUVoteInfo.ID == proposal).first() + if not voteinfo: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + + voters = db.query(User).join(TUVote).filter(TUVote.VoteID == voteinfo.ID) + vote = db.query(TUVote, and_(TUVote.UserID == request.user.ID, + TUVote.VoteID == voteinfo.ID)).first() + + if not request.user.is_trusted_user(): + context["error"] = "Only Trusted Users are allowed to vote." + elif voteinfo.User == request.user.Username: + context["error"] = "You cannot vote in an proposal about you." + elif vote is not None: + context["error"] = "You've already voted for this proposal." + + context["vote"] = vote + return render_proposal(request, context, proposal, voteinfo, voters, vote) + + +@router.post("/tu/{proposal}") +@auth_required(True, redirect="/") +@account_type_required(REQUIRED_TYPES) +async def trusted_user_proposal_post(request: Request, + proposal: int, + decision: str = Form(...)): + context = await make_variable_context(request, "Trusted User") + proposal = int(proposal) # Make sure it's an int. + + voteinfo = db.query(TUVoteInfo, TUVoteInfo.ID == proposal).first() + if not voteinfo: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + + voters = db.query(User).join(TUVote).filter(TUVote.VoteID == voteinfo.ID) + + # status_code we'll use for responses later. + status_code = HTTPStatus.OK + + if not request.user.is_trusted_user(): + # Test: Create a proposal and view it as a "Developer". It + # should give us this error. + context["error"] = "Only Trusted Users are allowed to vote." + status_code = HTTPStatus.UNAUTHORIZED + elif voteinfo.User == request.user.Username: + context["error"] = "You cannot vote in an proposal about you." + status_code = HTTPStatus.BAD_REQUEST + + vote = db.query(TUVote, and_(TUVote.UserID == request.user.ID, + TUVote.VoteID == voteinfo.ID)).first() + + if status_code != HTTPStatus.OK: + return render_proposal(request, context, proposal, + voteinfo, voters, vote, + status_code=status_code) + + if vote is not None: + context["error"] = "You've already voted for this proposal." + status_code = HTTPStatus.BAD_REQUEST + + if status_code != HTTPStatus.OK: + return render_proposal(request, context, proposal, + voteinfo, voters, vote, + status_code=status_code) + + if decision in {"Yes", "No", "Abstain"}: + # Increment whichever decision was given to us. + setattr(voteinfo, decision, getattr(voteinfo, decision) + 1) + else: + return Response("Invalid 'decision' value.", + status_code=int(HTTPStatus.BAD_REQUEST)) + + vote = db.create(TUVote, User=request.user, VoteInfo=voteinfo, + autocommit=False) + voteinfo.ActiveTUs += 1 + db.commit() + + context["error"] = "You've already voted for this proposal." + return render_proposal(request, context, proposal, voteinfo, voters, vote) diff --git a/templates/partials/tu/proposal/details.html b/templates/partials/tu/proposal/details.html new file mode 100644 index 00000000..3f15a6eb --- /dev/null +++ b/templates/partials/tu/proposal/details.html @@ -0,0 +1,106 @@ +

    {% trans %}Proposal Details{% endtrans %}

    + +{% if voteinfo.is_running() %} +

    + {% trans %}This vote is still running.{% endtrans %} +

    +{% endif %} + + +
    + + + {% set submitted = voteinfo.Submitted | dt | as_timezone(timezone) %} + {% set end = voteinfo.End | dt | as_timezone(timezone) %} + + +
    + {{ "End" | tr }}: + + {{ end.strftime("%Y-%m-%d %H:%M") }} + +
    + + {% if not voteinfo.is_running() %} +
    + {{ "Result" | tr }}: + {% if not voteinfo.ActiveTUs %} + {{ "unknown" | tr }} + {% elif accepted %} + + {{ "Accepted" | tr }} + + {% else %} + + {{ "Rejected" | tr }} + + {% endif %} +
    + {% endif %} +
    + +
    +

    + + {{ voteinfo.Agenda | replace("\n", "
    \n") | safe | e }} +

    +
    + + + + {% if not voteinfo.is_running() %} + + + + {% endif %} + + + + + + + + {% if not voteinfo.is_running() %} + + + + {% endif %} + + + + + +
    {{ "Yes" | tr }}{{ "No" | tr }}{{ "Abstain" | tr }}{{ "Total" | tr }}{{ "Voted" | tr }}{{ "Participation" | tr }}
    {{ voteinfo.Yes }}{{ voteinfo.No }}{{ voteinfo.Abstain }}{{ voteinfo.total_votes() }} + {% if not has_voted %} + + {{ "No" | tr }} + + {% else %} + + {{ "Yes" | tr }} + + {% endif %} + + {% if voteinfo.ActiveTUs %} + {{ (participation * 100) | number_format(2) }}% + {% else %} + {{ "unknown" | tr }} + {% endif %} +
    diff --git a/templates/partials/tu/proposal/form.html b/templates/partials/tu/proposal/form.html new file mode 100644 index 00000000..d783a622 --- /dev/null +++ b/templates/partials/tu/proposal/form.html @@ -0,0 +1,14 @@ + + +
    + + + +
    + diff --git a/templates/partials/tu/proposal/voters.html b/templates/partials/tu/proposal/voters.html new file mode 100644 index 00000000..2fd42bdf --- /dev/null +++ b/templates/partials/tu/proposal/voters.html @@ -0,0 +1,10 @@ +

    {{ "Voters" | tr }}

    + diff --git a/templates/tu/show.html b/templates/tu/show.html new file mode 100644 index 00000000..ca5cbe63 --- /dev/null +++ b/templates/tu/show.html @@ -0,0 +1,20 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    + {% include "partials/tu/proposal/details.html" %} +
    + +
    + {% include "partials/tu/proposal/voters.html" %} +
    + +
    + {% if error %} + {{ error | tr }} + {% else %} + {% include "partials/tu/proposal/form.html" %} + {% endif %} +
    + +{% endblock %} diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index a6527e6f..73cea9bf 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -18,6 +18,7 @@ from aurweb.testing import setup_test_db from aurweb.testing.requests import Request DATETIME_REGEX = r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$' +PARTICIPATION_REGEX = r'^1?[0-9]{2}[%]$' # 0% - 100% def parse_root(html): @@ -103,6 +104,26 @@ def user(): AccountType=user_type) +@pytest.fixture +def proposal(tu_user): + ts = int(datetime.utcnow().timestamp()) + agenda = "Test proposal." + start = ts - 5 + end = ts + 1000 + + user_type = db.query(AccountType, + AccountType.AccountType == "User").first() + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=user_type) + + voteinfo = db.create(TUVoteInfo, + Agenda=agenda, Quorum=0.0, + User=user.Username, Submitter=tu_user, + Submitted=start, End=end) + yield (tu_user, user, voteinfo) + + def test_tu_index_guest(client): with client as request: response = request.get("/tu", allow_redirects=False) @@ -441,3 +462,270 @@ def test_tu_index_last_votes(client, tu_user, user): assert user.text.strip() == tu_user.Username assert int(vote_id.text.strip()) == voteinfo.ID + + +def test_tu_proposal_not_found(client, tu_user): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", params={"id": 1}, cookies=cookies) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_tu_running_proposal(client, proposal): + tu_user, user, voteinfo = proposal + + # Initiate an authenticated GET request to /tu/{proposal_id}. + proposal_id = voteinfo.ID + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get(f"/tu/{proposal_id}", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Alright, now let's continue on to verifying some markup. + # First, let's verify that the proposal details match. + root = parse_root(response.text) + details = root.xpath('//div[@class="proposal details"]')[0] + + vote_running = root.xpath('//p[contains(@class, "vote-running")]')[0] + assert vote_running.text.strip() == "This vote is still running." + + # Verify User field. + username = details.xpath( + './div[contains(@class, "user")]/strong/a/text()')[0] + assert username.strip() == user.Username + + submitted = details.xpath( + './div[contains(@class, "submitted")]/text()')[0] + assert re.match(r'^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} by .+$', + submitted.strip()) is not None + + end = details.xpath('./div[contains(@class, "end")]')[0] + end_label = end.xpath("./text()")[0] + assert end_label.strip() == "End:" + + end_datetime = end.xpath("./strong/text()")[0] + assert re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$', + end_datetime.strip()) is not None + + # We have not voted yet. Assert that our voting form is shown. + form = root.xpath('//form[contains(@class, "action-form")]')[0] + fields = form.xpath("./fieldset")[0] + buttons = fields.xpath('./button[@name="decision"]') + assert len(buttons) == 3 + + # Check the button names and values. + yes, no, abstain = buttons + + # Yes + assert yes.attrib["name"] == "decision" + assert yes.attrib["value"] == "Yes" + + # No + assert no.attrib["name"] == "decision" + assert no.attrib["value"] == "No" + + # Abstain + assert abstain.attrib["name"] == "decision" + assert abstain.attrib["value"] == "Abstain" + + # Create a vote. + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + voteinfo.ActiveTUs += 1 + voteinfo.Yes += 1 + db.commit() + + # Make another request now that we've voted. + with client as request: + response = request.get( + "/tu", params={"id": voteinfo.ID}, cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Parse our new root. + root = parse_root(response.text) + + # Check that we no longer have a voting form. + form = root.xpath('//form[contains(@class, "action-form")]') + assert not form + + # Check that we're told we've voted. + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You've already voted for this proposal." + + +def test_tu_ended_proposal(client, proposal): + tu_user, user, voteinfo = proposal + + ts = int(datetime.utcnow().timestamp()) + voteinfo.End = ts - 5 # 5 seconds ago. + db.commit() + + # Initiate an authenticated GET request to /tu/{proposal_id}. + proposal_id = voteinfo.ID + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get(f"/tu/{proposal_id}", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Alright, now let's continue on to verifying some markup. + # First, let's verify that the proposal details match. + root = parse_root(response.text) + details = root.xpath('//div[@class="proposal details"]')[0] + + vote_running = root.xpath('//p[contains(@class, "vote-running")]') + assert not vote_running + + result_node = details.xpath('./div[contains(@class, "result")]')[0] + result_label = result_node.xpath("./text()")[0] + assert result_label.strip() == "Result:" + + result = result_node.xpath("./span/text()")[0] + assert result.strip() == "unknown" + + # Check that voting has ended. + form = root.xpath('//form[contains(@class, "action-form")]') + assert not form + + # We should see a status about it. + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "Voting is closed for this proposal." + + +def test_tu_proposal_vote_not_found(client, tu_user): + """ Test POST request to a missing vote. """ + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "Yes"} + response = request.post("/tu/1", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_tu_proposal_vote(client, proposal): + tu_user, user, voteinfo = proposal + + # Store the current related values. + yes = voteinfo.Yes + active_tus = voteinfo.ActiveTUs + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "Yes"} + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data) + assert response.status_code == int(HTTPStatus.OK) + + # Check that the proposal record got updated. + assert voteinfo.Yes == yes + 1 + assert voteinfo.ActiveTUs == active_tus + 1 + + # Check that the new TUVote exists. + vote = db.query(TUVote, TUVote.VoteInfo == voteinfo, + TUVote.User == tu_user).first() + assert vote is not None + + root = parse_root(response.text) + + # Check that we're told we've voted. + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You've already voted for this proposal." + + +def test_tu_proposal_vote_unauthorized(client, proposal): + tu_user, user, voteinfo = proposal + + dev_type = db.query(AccountType, + AccountType.AccountType == "Developer").first() + tu_user.AccountType = dev_type + db.commit() + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "Yes"} + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.UNAUTHORIZED) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "Only Trusted Users are allowed to vote." + + with client as request: + data = {"decision": "Yes"} + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "Only Trusted Users are allowed to vote." + + +def test_tu_proposal_vote_cant_self_vote(client, proposal): + tu_user, user, voteinfo = proposal + + # Update voteinfo.User. + voteinfo.User = tu_user.Username + db.commit() + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "Yes"} + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You cannot vote in an proposal about you." + + with client as request: + data = {"decision": "Yes"} + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You cannot vote in an proposal about you." + + +def test_tu_proposal_vote_already_voted(client, proposal): + tu_user, user, voteinfo = proposal + + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + voteinfo.Yes += 1 + voteinfo.ActiveTUs += 1 + db.commit() + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "Yes"} + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You've already voted for this proposal." + + with client as request: + data = {"decision": "Yes"} + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You've already voted for this proposal." + + +def test_tu_proposal_vote_invalid_decision(client, proposal): + tu_user, user, voteinfo = proposal + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "EVIL"} + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + assert response.text == "Invalid 'decision' value." diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index bb4e3ad7..2748462f 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -204,3 +204,11 @@ label.confirmation, overflow: hidden; transition: height 1s; } + +.proposal.details { + margin: .33em 0 1em; +} + +button[type="submit"] { + padding: 0 0.6em; +} From bfffdd4d912eb012f947a81ff4c51489015fa2df Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Jun 2021 04:13:28 -0700 Subject: [PATCH 0324/1451] aurweb.asgi: Allow unsafe-inline style-src in CSP Signed-off-by: Kevin Morris --- aurweb/asgi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 35166c73..26893232 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -88,6 +88,8 @@ async def add_security_headers(request: Request, call_next: typing.Callable): "cdn.jsdelivr.net" ] csp += f"script-src 'self' 'nonce-{nonce}' " + ' '.join(script_hosts) + # It's fine if css is inlined. + csp += f"; style-src 'self' 'unsafe-inline'" response.headers["Content-Security-Policy"] = csp # Add XTCO header. From 04ab98907aa6b7e432bbc3b0bd71462bf9ecd513 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Jun 2021 04:20:55 -0700 Subject: [PATCH 0325/1451] aurweb.asgi: patch invalid f-string Signed-off-by: Kevin Morris --- aurweb/asgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 26893232..228b9a65 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -89,7 +89,7 @@ async def add_security_headers(request: Request, call_next: typing.Callable): ] csp += f"script-src 'self' 'nonce-{nonce}' " + ' '.join(script_hosts) # It's fine if css is inlined. - csp += f"; style-src 'self' 'unsafe-inline'" + csp += "; style-src 'self' 'unsafe-inline'" response.headers["Content-Security-Policy"] = csp # Add XTCO header. From 97c1247b577adb13fac793577d411f0f2be9274a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 26 Jun 2021 04:43:00 -0700 Subject: [PATCH 0326/1451] /tu/{proposal_id}: Do not show voters if there are none This was different than PHP. Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 2 +- templates/tu/show.html | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index efdcfc73..55f7b7e1 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -111,7 +111,7 @@ def render_proposal(request: Request, """ Render a single TU proposal. """ context["proposal"] = proposal context["voteinfo"] = voteinfo - context["voters"] = voters + context["voters"] = voters.all() participation = voteinfo.ActiveTUs / voteinfo.total_votes() \ if voteinfo.total_votes() else 0 diff --git a/templates/tu/show.html b/templates/tu/show.html index ca5cbe63..ff2d4bb6 100644 --- a/templates/tu/show.html +++ b/templates/tu/show.html @@ -4,10 +4,12 @@
    {% include "partials/tu/proposal/details.html" %}
    - -
    - {% include "partials/tu/proposal/voters.html" %} -
    + + {% if voters %} +
    + {% include "partials/tu/proposal/voters.html" %} +
    + {% endif %}
    {% if error %} From 83f93c8dbb1bc823fa8bcc3966d572255dc742e2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 27 Jun 2021 03:54:13 -0700 Subject: [PATCH 0327/1451] aurweb.routers.accounts: strip host out of ssh pubkeys We must store the paired key, otherwise aurweb-git-auth will fail. Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 3e3469ca..36871595 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -1,4 +1,5 @@ import copy +import logging import typing from datetime import datetime @@ -24,6 +25,7 @@ from aurweb.scripts.notify import ResetKeyNotification from aurweb.templates import make_variable_context, render_template router = APIRouter() +logger = logging.getLogger(__name__) @router.get("/passreset", response_class=HTMLResponse) @@ -402,6 +404,10 @@ async def account_register_post(request: Request, if PK: # Get the second element in the PK, which is the actual key. pubkey = PK.strip().rstrip() + parts = pubkey.split(" ") + if len(parts) == 3: + # Remove the host part. + pubkey = parts[0] + " " + parts[1] fingerprint = get_fingerprint(pubkey) user.ssh_pub_key = SSHPubKey(UserID=user.ID, PubKey=pubkey, @@ -522,15 +528,19 @@ async def account_edit_post(request: Request, if PK: # Get the second token in the public key, which is the actual key. pubkey = PK.strip().rstrip() + parts = pubkey.split(" ") + if len(parts) == 3: + # Remove the host part. + pubkey = parts[0] + " " + parts[1] fingerprint = get_fingerprint(pubkey) if not user.ssh_pub_key: # No public key exists, create one. user.ssh_pub_key = SSHPubKey(UserID=user.ID, - PubKey=PK, + PubKey=pubkey, Fingerprint=fingerprint) - elif user.ssh_pub_key.Fingerprint != fingerprint: + elif user.ssh_pub_key.PubKey != pubkey: # A public key already exists, update it. - user.ssh_pub_key.PubKey = PK + user.ssh_pub_key.PubKey = pubkey user.ssh_pub_key.Fingerprint = fingerprint elif user.ssh_pub_key: # Else, if the user has a public key already, delete it. From 0a3aa40f209e755f4a345ff86015d7a42acdb1f4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 27 Jun 2021 05:16:12 -0700 Subject: [PATCH 0328/1451] Docker: Fix `git` sshd This was completely bugged out. This commit fixes git, provides two separate cgit servers for the different URL bases and also supplies a smartgit service for $AURWEB_URL/repo.git interaction. Docker image needs to be rebuilt with this change: $ docker build -t aurweb:latest . Signed-off-by: Kevin Morris --- Dockerfile | 11 +++-- docker-compose.yml | 72 +++++++++++++++++++++++++----- docker/cgit-entrypoint.sh | 7 ++- docker/fastapi-entrypoint.sh | 3 ++ docker/git-entrypoint.sh | 81 +++++++++++++++++++++++++++------- docker/health/cgit.sh | 2 +- docker/health/smartgit.sh | 2 + docker/health/sshd.sh | 5 ++- docker/nginx-entrypoint.sh | 38 ++++++++++++++-- docker/php-entrypoint.sh | 3 ++ docker/scripts/run-cgit.sh | 4 ++ docker/scripts/run-smartgit.sh | 9 ++++ docker/scripts/run-sshd.sh | 2 +- docker/smartgit-entrypoint.sh | 4 ++ 14 files changed, 202 insertions(+), 41 deletions(-) create mode 100755 docker/health/smartgit.sh create mode 100755 docker/scripts/run-cgit.sh create mode 100755 docker/scripts/run-smartgit.sh create mode 100755 docker/smartgit-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index da9c8d3b..4141a4c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ FROM archlinux:base-devel +ENV PYTHONPATH=/aurweb +ENV AUR_CONFIG=conf/config # Setup some default system stuff. RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime @@ -16,7 +18,7 @@ RUN pacman -Syu --noconfirm --noprogressbar \ python-pytest-asyncio python-coverage hypercorn python-bcrypt \ python-email-validator openssh python-lxml mariadb mariadb-libs \ python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ - python-asgiref uvicorn + python-asgiref uvicorn python-pip python-wheel RUN useradd -U -d /aurweb -c 'AUR User' aur @@ -25,6 +27,9 @@ COPY docker /docker WORKDIR /aurweb COPY . . -ENV PYTHONPATH=/aurweb - RUN make -C po all install +RUN pip3 install -t /aurweb/app --upgrade -I . + +# Set permissions on directories and binaries. +RUN bash -c 'find /aurweb/app -type d -exec chmod 755 {} \;' +RUN chmod 755 /aurweb/app/bin/* diff --git a/docker-compose.yml b/docker-compose.yml index 795236c7..6bf36166 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,33 +50,77 @@ services: image: aurweb:latest init: true environment: - - AUR_CONFIG=conf/config + - AUR_CONFIG=/aurweb/conf/config entrypoint: /docker/git-entrypoint.sh command: /docker/scripts/run-sshd.sh ports: - - "2222:22" + - "2222:2222" healthcheck: test: "bash /docker/health/sshd.sh" interval: 2s timeout: 30s + depends_on: + mariadb: + condition: service_healthy + links: + - mariadb volumes: - mariadb_run:/var/run/mysqld - mariadb_data:/var/lib/mysql - git_data:/aurweb/aur.git - ./cache:/cache - cgit: + smartgit: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=/aurweb/conf/config + entrypoint: /docker/smartgit-entrypoint.sh + command: /docker/scripts/run-smartgit.sh + healthcheck: + test: "bash /docker/health/smartgit.sh" + interval: 2s + timeout: 30s + depends_on: + mariadb: + condition: service_healthy + links: + - mariadb + volumes: + - mariadb_run:/var/run/mysqld + - mariadb_data:/var/lib/mysql + - git_data:/aurweb/aur.git + - ./cache:/cache + - smartgit_run:/var/run/smartgit + + cgit-php: image: aurweb:latest init: true environment: - AUR_CONFIG=/aurweb/conf/config entrypoint: /docker/cgit-entrypoint.sh - command: >- - uwsgi --socket 0.0.0.0:3000 - --plugins cgi - --cgi /usr/share/webapps/cgit/cgit.cgi + command: /docker/scripts/run-cgit.sh 3000 "https://localhost:8443/cgit" healthcheck: - test: "bash /docker/health/cgit.sh" + test: "bash /docker/health/cgit.sh 3000" + interval: 2s + timeout: 30s + depends_on: + git: + condition: service_healthy + links: + - git + volumes: + - git_data:/aurweb/aur.git + + cgit-fastapi: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=/aurweb/conf/config + entrypoint: /docker/cgit-entrypoint.sh + command: /docker/scripts/run-cgit.sh 3000 "https://localhost:8444/cgit" + healthcheck: + test: "bash /docker/health/cgit.sh 3000" interval: 2s timeout: 30s depends_on: @@ -170,14 +214,20 @@ services: interval: 2s timeout: 30s depends_on: - cgit: + cgit-php: + condition: service_healthy + cgit-fastapi: + condition: service_healthy + smartgit: condition: service_healthy fastapi: condition: service_healthy php-fpm: condition: service_healthy links: - - cgit + - cgit-php + - cgit-fastapi + - smartgit - fastapi - php-fpm volumes: @@ -187,6 +237,7 @@ services: - ./web/html:/aurweb/web/html - ./web/template:/aurweb/web/template - ./web/lib:/aurweb/web/lib + - smartgit_run:/var/run/smartgit sharness: image: aurweb:latest @@ -298,3 +349,4 @@ volumes: mariadb_run: {} # Share /var/run/mysqld/mysqld.sock mariadb_data: {} # Share /var/lib/mysql git_data: {} # Share aurweb/aur.git + smartgit_run: {} diff --git a/docker/cgit-entrypoint.sh b/docker/cgit-entrypoint.sh index e05e1b7a..9abc5091 100755 --- a/docker/cgit-entrypoint.sh +++ b/docker/cgit-entrypoint.sh @@ -1,13 +1,12 @@ #!/bin/bash set -eou pipefail -cp -vf conf/cgitrc.proto /etc/cgitrc +mkdir -p /var/cache/cgit -sed -ri 's|clone-prefix=.*|clone-prefix=https://localhost:8443|' /etc/cgitrc +cp -vf conf/cgitrc.proto /etc/cgitrc +sed -ri "s|clone-prefix=.*|clone-prefix=${2}|" /etc/cgitrc sed -ri 's|header=.*|header=/aurweb/web/template/cgit/header.html|' /etc/cgitrc sed -ri 's|footer=.*|footer=/aurweb/web/template/cgit/footer.html|' /etc/cgitrc sed -ri 's|repo\.path=.*|repo.path=/aurweb/aur.git|' /etc/cgitrc -mkdir -p /var/cache/cgit - exec "$@" diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index 2f04c29f..11b8ac5a 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -7,4 +7,7 @@ bash $dir/test-mysql-entrypoint.sh sed -ri "s;^(aur_location) = .+;\1 = https://localhost:8444;" conf/config sed -ri 's/^(name) = .+/\1 = aurweb/' conf/config +sed -ri "s|^(git_clone_uri_anon) = .+|\1 = https://localhost:8444/cgit/aur.git -b %s|" conf/config.defaults +sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ssh://aur@localhost:2222/%s.git|" conf/config.defaults + exec "$@" diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index d17ceeaf..e6d3ad97 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -2,44 +2,91 @@ set -eou pipefail SSHD_CONFIG=/etc/ssh/sshd_config +AUTH_SCRIPT=/aurweb/app/git-auth.sh -GIT_REPO=aur.git -GIT_KEY=/cache/git.key +GIT_REPO=/aurweb/aur.git +GIT_BRANCH=master # 'Master' branch. -# Setup SSH Keys. -ssh-keygen -A +if ! grep -q 'PYTHONPATH' /etc/environment; then + echo "PYTHONPATH='/aurweb:/aurweb/app'" >> /etc/environment +else + sed -ri "s|^(PYTHONPATH)=.*$|\1='/aurweb:/aurweb/app'|" /etc/environment +fi + +if ! grep -q 'AUR_CONFIG' /etc/environment; then + echo "AUR_CONFIG='/aurweb/conf/config'" >> /etc/environment +else + sed -ri "s|^(AUR_CONFIG)=.*$|\1='/aurweb/conf/config'|" /etc/environment +fi + +if ! grep -q '/aurweb/app/bin' /etc/environment; then + echo "PATH='/aurweb/app/bin:\${PATH}'" >> /etc/environment +fi # Add AUR SSH config. cat >> $SSHD_CONFIG << EOF Match User aur PasswordAuthentication no - AuthorizedKeysCommand /usr/local/bin/aurweb-git-auth "%t" "%k" + AuthorizedKeysCommand $AUTH_SCRIPT "%t" "%k" AuthorizedKeysCommandUser aur AcceptEnv AUR_OVERWRITE - SetEnv AUR_CONFIG=/aurweb/config/config EOF +cat >> $AUTH_SCRIPT << EOF +#!/usr/bin/env bash +export PYTHONPATH="$PYTHONPATH" +export AUR_CONFIG="$AUR_CONFIG" +export PATH="/aurweb/app/bin:\${PATH}" + +exec /aurweb/app/bin/aurweb-git-auth "\$@" +EOF +chmod 755 $AUTH_SCRIPT + +DB_NAME="aurweb" +DB_HOST="mariadb" +DB_USER="aur" +DB_PASS="aur" + +# Setup a config for our mysql db. +cp -vf conf/config.dev $AUR_CONFIG +sed -i "s;YOUR_AUR_ROOT;$(pwd);g" $AUR_CONFIG +sed -ri "s/^(name) = .+/\1 = ${DB_NAME}/" $AUR_CONFIG +sed -ri "s/^(host) = .+/\1 = ${DB_HOST}/" $AUR_CONFIG +sed -ri "s/^(user) = .+/\1 = ${DB_USER}/" $AUR_CONFIG +sed -ri "s/^;?(password) = .+/\1 = ${DB_PASS}/" $AUR_CONFIG +sed -i "s|/usr/local/bin|/aurweb/app/bin|g" $AUR_CONFIG + +AUR_CONFIG_DEFAULTS="${AUR_CONFIG}.defaults" + +if [[ "$AUR_CONFIG_DEFAULTS" != "/aurweb/conf/config.defaults" ]]; then + cp -vf conf/config.defaults $AUR_CONFIG_DEFAULTS +fi + +# Set some defaults needed for pathing and ssh uris. +sed -i "s|/usr/local/bin|/aurweb/app/bin|g" $AUR_CONFIG_DEFAULTS +sed -ri "s|^(repo-path) = .+|\1 = /aurweb/aur.git/|" $AUR_CONFIG_DEFAULTS + +ssh_cmdline='ssh ssh://aur@localhost:2222' +sed -ri "s|^(ssh-cmdline) = .+|\1 = $ssh_cmdline|" $AUR_CONFIG_DEFAULTS + +# Setup SSH Keys. +ssh-keygen -A + # Taken from INSTALL. mkdir -pv $GIT_REPO # Initialize git repository. if [ ! -f $GIT_REPO/config ]; then + curdir="$(pwd)" cd $GIT_REPO + git config --global init.defaultBranch $GIT_BRANCH git init --bare git config --local transfer.hideRefs '^refs/' git config --local --add transfer.hideRefs '!refs/' git config --local --add transfer.hideRefs '!HEAD' - ln -sf /usr/local/bin/aurweb-git-update hooks/update - chown -R aur . - cd .. + ln -sf /aurweb/app/bin/aurweb-git-update hooks/update + cd $curdir + chown -R aur:aur $GIT_REPO fi -if [ ! -f $GIT_KEY ]; then - # Create a DSA ssh private/pubkey at /cache/git.key{.pub,}. - ssh-keygen -f $GIT_KEY -t dsa -N '' -C 'AUR Git Key' -fi - -# Users should modify these permissions on their local machines. -chmod 666 ${GIT_KEY}{.pub,} - exec "$@" diff --git a/docker/health/cgit.sh b/docker/health/cgit.sh index add33031..2f0cfeb1 100755 --- a/docker/health/cgit.sh +++ b/docker/health/cgit.sh @@ -1,2 +1,2 @@ #!/bin/bash -exec printf "" >>/dev/tcp/127.0.0.1/3000 +exec printf "" >>/dev/tcp/127.0.0.1/${1} diff --git a/docker/health/smartgit.sh b/docker/health/smartgit.sh new file mode 100755 index 00000000..b4e7ebd4 --- /dev/null +++ b/docker/health/smartgit.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec pgrep uwsgi diff --git a/docker/health/sshd.sh b/docker/health/sshd.sh index 6befdfb5..d9da9ea1 100755 --- a/docker/health/sshd.sh +++ b/docker/health/sshd.sh @@ -1,2 +1,5 @@ #!/bin/bash -exec printf "" >>/dev/tcp/127.0.0.1/22 +# Opt to just pgrep sshd instead of connecting here. This health +# script is used on a regular interval and it ends up spamming +# the git service's logs with accesses. +exec pgrep sshd diff --git a/docker/nginx-entrypoint.sh b/docker/nginx-entrypoint.sh index 1e442ef7..238cd167 100755 --- a/docker/nginx-entrypoint.sh +++ b/docker/nginx-entrypoint.sh @@ -45,8 +45,16 @@ http { server fastapi:8000; } - upstream cgit { - server cgit:3000; + upstream cgit-php { + server cgit-php:3000; + } + + upstream cgit-fastapi { + server cgit-fastapi:3000; + } + + upstream smartgit { + server unix:/var/run/smartgit/smartgit.sock; } server { @@ -59,12 +67,23 @@ http { root /aurweb/web/html; index index.php; + location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { + include uwsgi_params; + uwsgi_pass smartgit; + uwsgi_modifier1 9; + uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; + uwsgi_param PATH_INFO /aur.git/\$3; + uwsgi_param GIT_HTTP_EXPORT_ALL ""; + uwsgi_param GIT_NAMESPACE \$1; + uwsgi_param GIT_PROJECT_ROOT /aurweb; + } + location ~ ^/cgit { include uwsgi_params; rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=\$1&\$2 last; uwsgi_modifier1 9; uwsgi_param CGIT_CONFIG /etc/cgitrc; - uwsgi_pass uwsgi://cgit; + uwsgi_pass uwsgi://cgit-php; } location ~ ^/[^/]+\.php($|/) { @@ -95,12 +114,23 @@ http { try_files \$uri @proxy_to_app; } + location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { + include uwsgi_params; + uwsgi_pass smartgit; + uwsgi_modifier1 9; + uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; + uwsgi_param PATH_INFO /aur.git/\$3; + uwsgi_param GIT_HTTP_EXPORT_ALL ""; + uwsgi_param GIT_NAMESPACE \$1; + uwsgi_param GIT_PROJECT_ROOT /aurweb; + } + location ~ ^/cgit { include uwsgi_params; rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=\$1&\$2 last; uwsgi_modifier1 9; uwsgi_param CGIT_CONFIG /etc/cgitrc; - uwsgi_pass uwsgi://cgit; + uwsgi_pass uwsgi://cgit-fastapi; } location @proxy_to_app { diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index 19c6d059..4d49ef17 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -7,6 +7,9 @@ bash $dir/test-mysql-entrypoint.sh sed -ri "s;^(aur_location) = .+;\1 = https://localhost:8443;" conf/config sed -ri 's/^(name) = .+/\1 = aurweb/' conf/config +sed -ri "s|^(git_clone_uri_anon) = .+|\1 = https://localhost:8443/cgit/aur.git -b %s|" conf/config.defaults +sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ssh://aur@localhost:2222/%s.git|" conf/config.defaults + sed -ri 's/^(listen).*/\1 = 0.0.0.0:9000/' /etc/php/php-fpm.d/www.conf sed -ri 's/^;?(clear_env).*/\1 = no/' /etc/php/php-fpm.d/www.conf diff --git a/docker/scripts/run-cgit.sh b/docker/scripts/run-cgit.sh new file mode 100755 index 00000000..67bdc079 --- /dev/null +++ b/docker/scripts/run-cgit.sh @@ -0,0 +1,4 @@ +#!/bin/bash +exec uwsgi --socket 0.0.0.0:${1} \ + --plugins cgi \ + --cgi /usr/share/webapps/cgit/cgit.cgi diff --git a/docker/scripts/run-smartgit.sh b/docker/scripts/run-smartgit.sh new file mode 100755 index 00000000..b6869a6c --- /dev/null +++ b/docker/scripts/run-smartgit.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +exec uwsgi \ + --socket /var/run/smartgit/smartgit.sock \ + --uid root \ + --gid http \ + --chmod-socket=666 \ + --plugins cgi \ + --cgi /usr/lib/git-core/git-http-backend diff --git a/docker/scripts/run-sshd.sh b/docker/scripts/run-sshd.sh index a69af7e2..d488e80d 100755 --- a/docker/scripts/run-sshd.sh +++ b/docker/scripts/run-sshd.sh @@ -1,2 +1,2 @@ #!/bin/bash -exec /usr/sbin/sshd -D +exec /usr/sbin/sshd -e -p 2222 -D diff --git a/docker/smartgit-entrypoint.sh b/docker/smartgit-entrypoint.sh new file mode 100755 index 00000000..daa9edeb --- /dev/null +++ b/docker/smartgit-entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -eou pipefail + +exec "$@" From 12911a101e8a5adb4097ed503c3923bedaf98fa9 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sun, 27 Jun 2021 13:55:51 +0200 Subject: [PATCH 0329/1451] Port homepage intro to fastapi Port the main home page content to fastapi. --- aurweb/config.py | 6 +++ aurweb/routers/html.py | 2 + aurweb/util.py | 10 +++++ templates/index.html | 92 ++++++++++++++++++++++++++++++++++++++++++ test/test_homepage.py | 36 +++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 test/test_homepage.py diff --git a/aurweb/config.py b/aurweb/config.py index 73db58dc..52fadda2 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -46,3 +46,9 @@ def getboolean(section, option): def getint(section, option, fallback=None): return _get_parser().getint(section, option, fallback=fallback) + + +def get_section(section_name): + for section in _get_parser().sections(): + if section == section_name: + return _get_parser()[section] diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 580ee0d4..f6f1a54e 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -58,6 +58,8 @@ async def language(request: Request, async def index(request: Request): """ Homepage route. """ context = make_context(request, "Home") + context['ssh_fingerprints'] = util.get_ssh_fingerprints() + return render_template(request, "index.html", context) diff --git a/aurweb/util.py b/aurweb/util.py index 539af40e..d4a0b221 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -172,3 +172,13 @@ def add_samesite_fields(response: Response, value: str): cookie += f"; SameSite={value}" response.raw_headers[idx] = (header[0], cookie.encode()) return response + + +def get_ssh_fingerprints(): + fingerprints = {} + fingerprint_section = aurweb.config.get_section("fingerprints") + + if fingerprint_section: + fingerprints = {key: fingerprint_section[key] for key in fingerprint_section.keys()} + + return fingerprints diff --git a/templates/index.html b/templates/index.html index 27d3375d..8cd1cc78 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,4 +1,96 @@ {% extends 'partials/layout.html' %} {% block pageContent %} +
    +

    AUR {% trans %}Home{% endtrans %}

    +

    + {{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information." + | tr + | format('', "", + '', "") + | safe + }} + {{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!" + | tr + | format("", "", + '', + "") + | safe + }} + {% trans %}Remember to vote for your favourite packages!{% endtrans %} + {% trans %}Some packages may be provided as binaries in [community].{% endtrans %} +

    + {% trans %}DISCLAIMER{% endtrans %}: + {% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %} +

    +

    {% trans %}Learn more...{% endtrans %}

    +

    +
    +
    +

    {% trans %}Support{% endtrans %}

    +

    {% trans %}Package Requests{% endtrans %}

    +
    +

    + {{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:" + | tr + | format("", "") + | safe + }} +

    +
      +
    • {% trans %}Orphan Request{% endtrans %}: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}
    • +
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}
    • +
    • {% trans %}Merge Request{% endtrans %}: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}
    • +
    +

    + {{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests." + | tr + | format('', "") + | safe + }} +

    +
    +

    {% trans %}Submitting Packages{% endtrans %}

    +
    +

    + {{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details." + | tr + | format('', "") + | safe + }} +

    + {% if ssh_fingerprints %} +

    + {% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %} +

    +

      + {% for keytype in ssh_fingerprints %} +
    • {{ keytype }}: {{ ssh_fingerprints[keytype] }} + {% endfor %} +
    + {% endif %} +
    +

    {% trans %}Discussion{% endtrans %}

    +
    +

    + {{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." + | tr + | format('', "", + '', "") + | safe + }} +

    +

    +

    {% trans %}Bug Reporting{% endtrans %}

    +
    +

    + {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page." + | tr + | format('', "", + "", "") + | safe + }} +

    +
    +
    {% endblock %} diff --git a/test/test_homepage.py b/test/test_homepage.py new file mode 100644 index 00000000..23d7185f --- /dev/null +++ b/test/test_homepage.py @@ -0,0 +1,36 @@ +from http import HTTPStatus +from unittest.mock import patch + +from fastapi.testclient import TestClient + +from aurweb.asgi import app + +client = TestClient(app) + + +def test_homepage(): + with client as request: + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + + +@patch('aurweb.util.get_ssh_fingerprints') +def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock): + fingerprints = {'Ed25519': "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"} + get_ssh_fingerprints_mock.return_value = fingerprints + + with client as request: + response = request.get("/") + + assert list(fingerprints.values())[0] in response.content.decode() + assert 'The following SSH fingerprints are used for the AUR' in response.content.decode() + + +@patch('aurweb.util.get_ssh_fingerprints') +def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock): + get_ssh_fingerprints_mock.return_value = {} + + with client as request: + response = request.get("/") + + assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode() From acc100eb5220169b257d33e40818ab9361ecb5e5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 27 Jun 2021 06:26:18 -0700 Subject: [PATCH 0330/1451] Docker: Fix installation, remove pip, simplify sshd Signed-off-by: Kevin Morris --- Dockerfile | 8 ++------ docker/git-entrypoint.sh | 28 +++++++++++----------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4141a4c9..cec36158 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN pacman -Syu --noconfirm --noprogressbar \ python-pytest-asyncio python-coverage hypercorn python-bcrypt \ python-email-validator openssh python-lxml mariadb mariadb-libs \ python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ - python-asgiref uvicorn python-pip python-wheel + python-asgiref uvicorn RUN useradd -U -d /aurweb -c 'AUR User' aur @@ -28,8 +28,4 @@ WORKDIR /aurweb COPY . . RUN make -C po all install -RUN pip3 install -t /aurweb/app --upgrade -I . - -# Set permissions on directories and binaries. -RUN bash -c 'find /aurweb/app -type d -exec chmod 755 {} \;' -RUN chmod 755 /aurweb/app/bin/* +RUN python3 setup.py install --install-scripts=/usr/local/bin diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index e6d3ad97..89537853 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -2,7 +2,7 @@ set -eou pipefail SSHD_CONFIG=/etc/ssh/sshd_config -AUTH_SCRIPT=/aurweb/app/git-auth.sh +AUTH_SCRIPT=/app/git-auth.sh GIT_REPO=/aurweb/aur.git GIT_BRANCH=master # 'Master' branch. @@ -10,7 +10,7 @@ GIT_BRANCH=master # 'Master' branch. if ! grep -q 'PYTHONPATH' /etc/environment; then echo "PYTHONPATH='/aurweb:/aurweb/app'" >> /etc/environment else - sed -ri "s|^(PYTHONPATH)=.*$|\1='/aurweb:/aurweb/app'|" /etc/environment + sed -ri "s|^(PYTHONPATH)=.*$|\1='/aurweb'|" /etc/environment fi if ! grep -q 'AUR_CONFIG' /etc/environment; then @@ -19,9 +19,15 @@ else sed -ri "s|^(AUR_CONFIG)=.*$|\1='/aurweb/conf/config'|" /etc/environment fi -if ! grep -q '/aurweb/app/bin' /etc/environment; then - echo "PATH='/aurweb/app/bin:\${PATH}'" >> /etc/environment -fi +mkdir -p /app +chmod 755 /app + +cat >> $AUTH_SCRIPT << EOF +#!/usr/bin/env bash +export AUR_CONFIG="$AUR_CONFIG" +exec /usr/local/bin/aurweb-git-auth "\$@" +EOF +chmod 755 $AUTH_SCRIPT # Add AUR SSH config. cat >> $SSHD_CONFIG << EOF @@ -32,16 +38,6 @@ Match User aur AcceptEnv AUR_OVERWRITE EOF -cat >> $AUTH_SCRIPT << EOF -#!/usr/bin/env bash -export PYTHONPATH="$PYTHONPATH" -export AUR_CONFIG="$AUR_CONFIG" -export PATH="/aurweb/app/bin:\${PATH}" - -exec /aurweb/app/bin/aurweb-git-auth "\$@" -EOF -chmod 755 $AUTH_SCRIPT - DB_NAME="aurweb" DB_HOST="mariadb" DB_USER="aur" @@ -54,7 +50,6 @@ sed -ri "s/^(name) = .+/\1 = ${DB_NAME}/" $AUR_CONFIG sed -ri "s/^(host) = .+/\1 = ${DB_HOST}/" $AUR_CONFIG sed -ri "s/^(user) = .+/\1 = ${DB_USER}/" $AUR_CONFIG sed -ri "s/^;?(password) = .+/\1 = ${DB_PASS}/" $AUR_CONFIG -sed -i "s|/usr/local/bin|/aurweb/app/bin|g" $AUR_CONFIG AUR_CONFIG_DEFAULTS="${AUR_CONFIG}.defaults" @@ -63,7 +58,6 @@ if [[ "$AUR_CONFIG_DEFAULTS" != "/aurweb/conf/config.defaults" ]]; then fi # Set some defaults needed for pathing and ssh uris. -sed -i "s|/usr/local/bin|/aurweb/app/bin|g" $AUR_CONFIG_DEFAULTS sed -ri "s|^(repo-path) = .+|\1 = /aurweb/aur.git/|" $AUR_CONFIG_DEFAULTS ssh_cmdline='ssh ssh://aur@localhost:2222' From b2491ddc07fefe9612a14fb8ac9ed4bac9da8f79 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sun, 27 Jun 2021 17:25:46 +0200 Subject: [PATCH 0331/1451] Use type=email for email fields Setting the input type gives the use a hint that the field should be an email and also shows an error when a non-email is filled into the email field. --- templates/partials/account_form.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 5ae18131..6455c351 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -89,7 +89,7 @@ {% trans %}Email Address{% endtrans %}: - ({% trans %}required{% endtrans %})

    @@ -119,7 +119,7 @@ {% trans %}Backup Email Address{% endtrans %}: -

    From 222d995e95ebedab00169be9e44e161d36efc292 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sun, 27 Jun 2021 17:27:44 +0200 Subject: [PATCH 0332/1451] Use backup_email field for backup email The context gives backup_email and not backup for the backup email field. Fixes: #91 --- templates/partials/account_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 6455c351..05009594 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -120,7 +120,7 @@ + maxlength="254" name="BE" value="{{ backup_email }}">

    From a26e70334333c99ade535d92c5957fd19f7d796e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 04:04:52 -0700 Subject: [PATCH 0333/1451] bugfix: use empty string if backup_email is None Signed-off-by: Kevin Morris --- templates/partials/account_form.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 05009594..6374fd5e 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -90,7 +90,7 @@ + size="30" maxlength="254" name="E" value="{{ email or '' }}"> ({% trans %}required{% endtrans %})

    @@ -120,7 +120,7 @@ + maxlength="254" name="BE" value="{{ backup_email or '' }}">

    From 28300ee889f74eaf29fec5dc0bb2f4492375b0d2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 04:12:29 -0700 Subject: [PATCH 0334/1451] bugfix: populate context on invalid password (account edit) Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 36871595..5d798ae8 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -483,15 +483,15 @@ async def account_edit_post(request: Request, context = await make_variable_context(request, "Accounts") context["user"] = user + args = dict(await request.form()) + context = make_account_form_context(context, request, user, args) + ok, errors = process_account_form(request, user, args) + if not passwd: context["errors"] = ["Invalid password."] return render_template(request, "account/edit.html", context, status_code=int(HTTPStatus.BAD_REQUEST)) - args = dict(await request.form()) - context = make_account_form_context(context, request, user, args) - ok, errors = process_account_form(request, user, args) - if not ok: context["errors"] = errors return render_template(request, "account/edit.html", context, From 3c6b2203e92e8359f421aed5f421a8d6424fe8de Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 05:36:12 -0700 Subject: [PATCH 0335/1451] Docker: bugfix: /usr/local/bin instead of /aurweb/app/bin Signed-off-by: Kevin Morris --- docker/git-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index 89537853..57752ac5 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -78,7 +78,7 @@ if [ ! -f $GIT_REPO/config ]; then git config --local transfer.hideRefs '^refs/' git config --local --add transfer.hideRefs '!refs/' git config --local --add transfer.hideRefs '!HEAD' - ln -sf /aurweb/app/bin/aurweb-git-update hooks/update + ln -sf /usr/local/bin/aurweb-git-update hooks/update cd $curdir chown -R aur:aur $GIT_REPO fi From f8d2d4c82a5fd442e10fa205811548cffea59da7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 08:22:34 -0700 Subject: [PATCH 0336/1451] PackageBase.package -> PackageBase.packages A PackageBase can have more than one package associated with it. Signed-off-by: Kevin Morris --- aurweb/models/package.py | 2 +- test/test_package.py | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/aurweb/models/package.py b/aurweb/models/package.py index ff518f20..e8159d85 100644 --- a/aurweb/models/package.py +++ b/aurweb/models/package.py @@ -17,7 +17,7 @@ class Package(Base): Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), nullable=False) PackageBase = relationship( - "PackageBase", backref=backref("package", uselist=False), + "PackageBase", backref=backref("packages", lazy="dynamic"), foreign_keys=[PackageBaseID]) __mapper_args__ = {"primary_key": [ID]} diff --git a/test/test_package.py b/test/test_package.py index 9532823d..1e940164 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -1,9 +1,7 @@ import pytest from sqlalchemy import and_ -from sqlalchemy.exc import IntegrityError, OperationalError - -import aurweb.config +from sqlalchemy.exc import IntegrityError from aurweb.db import create, query from aurweb.models.account_type import AccountType @@ -57,20 +55,6 @@ def test_package(): assert record is not None -def test_package_package_base_cant_change(): - """ Test case insensitivity of the database table. """ - if aurweb.config.get("database", "backend") == "sqlite": - return None # SQLite doesn't seem handle this. - - from aurweb.db import session - - with pytest.raises(OperationalError): - create(Package, - PackageBase=pkgbase, - Name="invalidates-old-package-packagebase-relationship") - session.rollback() - - def test_package_null_pkgbase_raises_exception(): from aurweb.db import session From 7d695f0c6af4b35688aa53c3602aee09e05eff68 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 08:32:46 -0700 Subject: [PATCH 0337/1451] Update .gitignore Signed-off-by: Kevin Morris --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 5d1a4de7..27ff977a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ fastapi_aw/ .idea /cache/* /logs/* +/build/ +/dist/ +/aurweb.egg-info/ From dbbafc15fae9567deba5fd02b7a4dfdc5969d1ad Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 12:44:55 -0700 Subject: [PATCH 0338/1451] bugfix: PackageKeyword should have two PKs Signed-off-by: Kevin Morris --- aurweb/models/package_keyword.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py index 2926740d..803e6bca 100644 --- a/aurweb/models/package_keyword.py +++ b/aurweb/models/package_keyword.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy import Column, ForeignKey, Integer, String, text from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship @@ -18,7 +18,11 @@ class PackageKeyword(Base): "PackageBase", backref=backref("keywords", lazy="dynamic"), foreign_keys=[PackageBaseID]) - __mapper_args__ = {"primary_key": [PackageBaseID]} + Keyword = Column( + String(255), primary_key=True, nullable=False, + server_default=text("''")) + + __mapper_args__ = {"primary_key": [PackageBaseID, Keyword]} def __init__(self, PackageBase: aurweb.models.package_base.PackageBase = None, From 2f5d9c63c479211aec63a5c29ef5a7dc8c464225 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 28 Jun 2021 22:32:56 +0100 Subject: [PATCH 0339/1451] [php] Support DB mysql backend with port instead of socket Signed-off-by: Leonidas Spyropoulos --- web/lib/DB.class.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/web/lib/DB.class.php b/web/lib/DB.class.php index dfdbbf96..c7b3c745 100644 --- a/web/lib/DB.class.php +++ b/web/lib/DB.class.php @@ -20,15 +20,23 @@ class DB { $backend = config_get('database', 'backend'); $host = config_get('database', 'host'); $socket = config_get('database', 'socket'); + $port = config_get('database', 'port'); $name = config_get('database', 'name'); $user = config_get('database', 'user'); $password = config_get('database', 'password'); if ($backend == "mysql") { - $dsn = $backend . - ':host=' . $host . - ';unix_socket=' . $socket . - ';dbname=' . $name; + if ($port != '') { + $dsn = $backend . + ':host=' . $host . + ';port=' . $port . + ';dbname=' . $name; + } else { + $dsn = $backend . + ':host=' . $host . + ';unix_socket=' . $socket . + ';dbname=' . $name; + } self::$dbh = new PDO($dsn, $user, $password); self::$dbh->exec("SET NAMES 'utf8' COLLATE 'utf8_general_ci';"); From c3a29171cde3b7fa16de82c1f60aa9e3329393bc Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 28 Jun 2021 22:33:51 +0100 Subject: [PATCH 0340/1451] [php] aurweb.spawn avoid permission denied when running as user Signed-off-by: Leonidas Spyropoulos --- aurweb/spawn.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index f7c07dd7..6d553dde 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -58,6 +58,11 @@ def generate_nginx_config(): pid {os.path.join(temporary_dir, "nginx.pid")}; http {{ access_log /dev/stdout; + client_body_temp_path {os.path.join(temporary_dir, "client_body")}; + proxy_temp_path {os.path.join(temporary_dir, "proxy")}; + fastcgi_temp_path {os.path.join(temporary_dir, "fastcgi")}1 2; + uwsgi_temp_path {os.path.join(temporary_dir, "uwsgi")}; + scgi_temp_path {os.path.join(temporary_dir, "scgi")}; server {{ listen {aur_location_parts.netloc}; location / {{ From af96be7d0928172a42f92d9a4a264ca6eb2c4710 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 10:27:45 -0700 Subject: [PATCH 0341/1451] Docker: move nginx config to its own file Signed-off-by: Kevin Morris --- docker/config/nginx.conf | 133 ++++++++++++++++++++++++++++++++++++ docker/nginx-entrypoint.sh | 135 +------------------------------------ 2 files changed, 134 insertions(+), 134 deletions(-) create mode 100644 docker/config/nginx.conf diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf new file mode 100644 index 00000000..c1957d71 --- /dev/null +++ b/docker/config/nginx.conf @@ -0,0 +1,133 @@ +daemon off; +user root; +worker_processes auto; +pid /var/run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 256; +} + +http { + sendfile on; + tcp_nopush on; + types_hash_max_size 4096; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + + upstream hypercorn { + server fastapi:8000; + } + + upstream cgit-php { + server cgit-php:3000; + } + + upstream cgit-fastapi { + server cgit-fastapi:3000; + } + + upstream smartgit { + server unix:/var/run/smartgit/smartgit.sock; + } + + server { + listen 8443 ssl http2; + server_name localhost default_server; + + ssl_certificate /etc/ssl/certs/localhost.cert.pem; + ssl_certificate_key /etc/ssl/private/localhost.key.pem; + + root /aurweb/web/html; + index index.php; + + location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { + include uwsgi_params; + uwsgi_pass smartgit; + uwsgi_modifier1 9; + uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; + uwsgi_param PATH_INFO /aur.git/$3; + uwsgi_param GIT_HTTP_EXPORT_ALL ""; + uwsgi_param GIT_NAMESPACE $1; + uwsgi_param GIT_PROJECT_ROOT /aurweb; + } + + location ~ ^/cgit { + include uwsgi_params; + rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=$1&$2 last; + uwsgi_modifier1 9; + uwsgi_param CGIT_CONFIG /etc/cgitrc; + uwsgi_pass uwsgi://cgit-php; + } + + location ~ ^/[^/]+\.php($|/) { + fastcgi_pass php-fpm:9000; + fastcgi_index index.php; + fastcgi_split_path_info ^(/[^/]+\.php)(/.*)$; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + include fastcgi_params; + } + + location ~ .* { + rewrite ^/(.*)$ /index.php/$1 last; + } + + } + + server { + listen 8444 ssl http2; + server_name localhost default_server; + + ssl_certificate /etc/ssl/certs/localhost.cert.pem; + ssl_certificate_key /etc/ssl/private/localhost.key.pem; + + root /aurweb/web/html; + + location / { + try_files $uri @proxy_to_app; + } + + location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { + include uwsgi_params; + uwsgi_pass smartgit; + uwsgi_modifier1 9; + uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; + uwsgi_param PATH_INFO /aur.git/$3; + uwsgi_param GIT_HTTP_EXPORT_ALL ""; + uwsgi_param GIT_NAMESPACE $1; + uwsgi_param GIT_PROJECT_ROOT /aurweb; + } + + location ~ ^/cgit { + include uwsgi_params; + rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=$1&$2 last; + uwsgi_modifier1 9; + uwsgi_param CGIT_CONFIG /etc/cgitrc; + uwsgi_pass uwsgi://cgit-fastapi; + } + + location @proxy_to_app { + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_buffering off; + proxy_pass https://hypercorn; + } + } + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } +} + diff --git a/docker/nginx-entrypoint.sh b/docker/nginx-entrypoint.sh index 238cd167..347af50f 100755 --- a/docker/nginx-entrypoint.sh +++ b/docker/nginx-entrypoint.sh @@ -15,139 +15,6 @@ sed -ri 's/^(disable_http_login) = .+/\1 = 1/' conf/config cp -vf /cache/localhost.cert.pem /etc/ssl/certs/localhost.cert.pem cp -vf /cache/localhost.key.pem /etc/ssl/private/localhost.key.pem -cat > /etc/nginx/nginx.conf << EOF -daemon off; -user root; -worker_processes auto; -pid /var/run/nginx.pid; -include /etc/nginx/modules-enabled/*.conf; - -events { - worker_connections 256; -} - -http { - sendfile on; - tcp_nopush on; - types_hash_max_size 4096; - include /etc/nginx/mime.types; - default_type application/octet-stream; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - gzip on; - - upstream hypercorn { - server fastapi:8000; - } - - upstream cgit-php { - server cgit-php:3000; - } - - upstream cgit-fastapi { - server cgit-fastapi:3000; - } - - upstream smartgit { - server unix:/var/run/smartgit/smartgit.sock; - } - - server { - listen 8443 ssl http2; - server_name localhost default_server; - - ssl_certificate /etc/ssl/certs/localhost.cert.pem; - ssl_certificate_key /etc/ssl/private/localhost.key.pem; - - root /aurweb/web/html; - index index.php; - - location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { - include uwsgi_params; - uwsgi_pass smartgit; - uwsgi_modifier1 9; - uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; - uwsgi_param PATH_INFO /aur.git/\$3; - uwsgi_param GIT_HTTP_EXPORT_ALL ""; - uwsgi_param GIT_NAMESPACE \$1; - uwsgi_param GIT_PROJECT_ROOT /aurweb; - } - - location ~ ^/cgit { - include uwsgi_params; - rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=\$1&\$2 last; - uwsgi_modifier1 9; - uwsgi_param CGIT_CONFIG /etc/cgitrc; - uwsgi_pass uwsgi://cgit-php; - } - - location ~ ^/[^/]+\.php($|/) { - fastcgi_pass php-fpm:9000; - fastcgi_index index.php; - fastcgi_split_path_info ^(/[^/]+\.php)(/.*)\$; - fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; - fastcgi_param PATH_INFO \$fastcgi_path_info; - include fastcgi_params; - } - - location ~ .* { - rewrite ^/(.*)$ /index.php/\$1 last; - } - - } - - server { - listen 8444 ssl http2; - server_name localhost default_server; - - ssl_certificate /etc/ssl/certs/localhost.cert.pem; - ssl_certificate_key /etc/ssl/private/localhost.key.pem; - - root /aurweb/web/html; - - location / { - try_files \$uri @proxy_to_app; - } - - location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { - include uwsgi_params; - uwsgi_pass smartgit; - uwsgi_modifier1 9; - uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; - uwsgi_param PATH_INFO /aur.git/\$3; - uwsgi_param GIT_HTTP_EXPORT_ALL ""; - uwsgi_param GIT_NAMESPACE \$1; - uwsgi_param GIT_PROJECT_ROOT /aurweb; - } - - location ~ ^/cgit { - include uwsgi_params; - rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=\$1&\$2 last; - uwsgi_modifier1 9; - uwsgi_param CGIT_CONFIG /etc/cgitrc; - uwsgi_pass uwsgi://cgit-fastapi; - } - - location @proxy_to_app { - proxy_set_header Host \$http_host; - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto \$scheme; - proxy_redirect off; - proxy_buffering off; - proxy_pass https://hypercorn; - } - } - - map \$http_upgrade \$connection_upgrade { - default upgrade; - '' close; - } -} -EOF +cp -vf /docker/config/nginx.conf /etc/nginx/nginx.conf exec "$@" From 3bacfe6cd97049926893a37595bbb40158be7a48 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 10:29:24 -0700 Subject: [PATCH 0342/1451] Docker: increase nginx and php-fpm logging Log toward stdout/stderr which is accessible via `docker-compose logs `. Examples: - `docker-compose logs nginx` - `docker-compose logs php-fpm` Signed-off-by: Kevin Morris --- docker/config/nginx.conf | 4 ++-- docker/php-entrypoint.sh | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index c1957d71..d7c0196a 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -18,8 +18,8 @@ http { ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; + access_log /dev/stdout; + error_log /dev/stderr; gzip on; diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index 4d49ef17..350871d6 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -13,6 +13,11 @@ sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ssh://aur@localhost:2222/%s.git|" con sed -ri 's/^(listen).*/\1 = 0.0.0.0:9000/' /etc/php/php-fpm.d/www.conf sed -ri 's/^;?(clear_env).*/\1 = no/' /etc/php/php-fpm.d/www.conf +# Log to stderr. View logs via `docker-compose logs php-fpm`. +sed -ri 's|^(error_log) = .*$|\1 = /proc/self/fd/2|g' /etc/php/php-fpm.conf +sed -ri 's|^;?(access\.log) = .*$|\1 = /proc/self/fd/2|g' \ + /etc/php/php-fpm.d/www.conf + sed -ri 's/^;?(extension=pdo_mysql)/\1/' /etc/php/php.ini sed -ri 's/^;?(open_basedir).*$/\1 = \//' /etc/php/php.ini From a120af5a005450b348dd260ac4c9b92e979d95e7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 10:30:26 -0700 Subject: [PATCH 0343/1451] Docker: remove asset forward to index.php This makes logging look a little better for development purposes. Now, `docker-compose logs php-fpm` will only show details about PHP accesses, while `docker-compose logs nginx` will show accesses regarding PHP assets. Signed-off-by: Kevin Morris --- docker/config/nginx.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index d7c0196a..3a8de801 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -77,6 +77,10 @@ http { include fastcgi_params; } + location ~ .+\.(css|js?|jpe?g|png|svg|ico)/?$ { + try_files $uri =404; + } + location ~ .* { rewrite ^/(.*)$ /index.php/$1 last; } From 4442ba6703de42b1cba568f582ea4f668629ac14 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 10:41:54 -0700 Subject: [PATCH 0344/1451] bugfix: return null if config key doesn't exist This was previously causing a PHP warning due to returning a missing key. Signed-off-by: Kevin Morris --- web/lib/confparser.inc.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/lib/confparser.inc.php b/web/lib/confparser.inc.php index 1152e132..fdd2b78e 100644 --- a/web/lib/confparser.inc.php +++ b/web/lib/confparser.inc.php @@ -30,7 +30,9 @@ function config_get($section, $key) { global $AUR_CONFIG; config_load(); - return $AUR_CONFIG[$section][$key]; + return isset($AUR_CONFIG[$section][$key]) + ? $AUR_CONFIG[$section][$key] + : null; } function config_get_int($section, $key) { From 6c7bb04b93161580946c2ee96d0002f3bd7858d1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 21:33:47 -0700 Subject: [PATCH 0345/1451] Docker: Improve mariadb init Signed-off-by: Kevin Morris --- docker-compose.yml | 8 +++++++- docker/mariadb-entrypoint.sh | 29 ++++++++++++++++++++++++++++- docker/scripts/run-mariadb.sh | 26 -------------------------- docker/scripts/run-pytests.sh | 3 ++- docker/test-mysql-entrypoint.sh | 3 ++- 5 files changed, 39 insertions(+), 30 deletions(-) delete mode 100755 docker/scripts/run-mariadb.sh diff --git a/docker-compose.yml b/docker-compose.yml index 6bf36166..40d9bc5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,8 +32,10 @@ services: mariadb: image: aurweb:latest init: true + environment: + - DB_HOST="%" entrypoint: /docker/mariadb-entrypoint.sh - command: /docker/scripts/run-mariadb.sh mysqld_safe --datadir=/var/lib/mysql + command: /usr/bin/mysqld_safe --datadir=/var/lib/mysql ports: # This will expose mariadbd on 127.0.0.1:13306 in the host. # Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb` @@ -136,6 +138,7 @@ services: init: true environment: - AUR_CONFIG=/aurweb/conf/config + - DB_HOST=mariadb entrypoint: /docker/php-entrypoint.sh command: /docker/scripts/run-php.sh healthcheck: @@ -170,6 +173,7 @@ services: init: true environment: - AUR_CONFIG=conf/config + - DB_HOST=mariadb entrypoint: /docker/fastapi-entrypoint.sh command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" healthcheck: @@ -269,6 +273,7 @@ services: init: true environment: - AUR_CONFIG=conf/config + - DB_HOST=mariadb entrypoint: /docker/test-mysql-entrypoint.sh command: /docker/scripts/run-pytests.sh clean stdin_open: true @@ -324,6 +329,7 @@ services: init: true environment: - AUR_CONFIG=conf/config + - DB_HOST=mariadb entrypoint: /docker/tests-entrypoint.sh command: /docker/scripts/run-tests.sh stdin_open: true diff --git a/docker/mariadb-entrypoint.sh b/docker/mariadb-entrypoint.sh index e33c61c7..48e87045 100755 --- a/docker/mariadb-entrypoint.sh +++ b/docker/mariadb-entrypoint.sh @@ -1,6 +1,33 @@ #!/bin/bash set -eou pipefail -mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql +MYSQL_DATA=/var/lib/mysql +DB_HOST="localhost" + +mariadb-install-db --user=mysql --basedir=/usr --datadir=$MYSQL_DATA + +# Start it up. +mysqld_safe --datadir=$MYSQL_DATA --skip-networking & +while ! mysqladmin ping 2>/dev/null; do + sleep 1s +done + +# Configure databases. +DATABASE="aurweb" # Persistent database for fastapi/php-fpm. +TEST_DB="aurweb_test" # Test database (ephemereal). + +echo "Taking care of primary database '${DATABASE}'..." +mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'$DB_HOST' IDENTIFIED BY 'aur';" +mysql -u root -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" +mysql -u root -e "GRANT ALL ON ${DATABASE}.* TO 'aur'@'$DB_HOST';" + +# Drop and create our test database. +echo "Dropping test database '$TEST_DB'..." +mysql -u root -e "DROP DATABASE IF EXISTS $TEST_DB;" +mysql -u root -e "CREATE DATABASE $TEST_DB;" +mysql -u root -e "GRANT ALL ON ${TEST_DB}.* TO 'aur'@'$DB_HOST';" +echo "Created new '$TEST_DB'!" + +mysqladmin -uroot shutdown exec "$@" diff --git a/docker/scripts/run-mariadb.sh b/docker/scripts/run-mariadb.sh deleted file mode 100755 index d27d8124..00000000 --- a/docker/scripts/run-mariadb.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -mysqld_safe --datadir=/var/lib/mysql --skip-networking & -until mysqladmin ping --silent; do - sleep 1s -done - -# Create test database. -mysql -u root -e "CREATE USER 'aur'@'%' IDENTIFIED BY 'aur'" \ - 2>/dev/null || /bin/true - -# Create a brand new 'aurweb_test' DB. -mysql -u root -e "DROP DATABASE aurweb_test" 2>/dev/null || /bin/true -mysql -u root -e "CREATE DATABASE aurweb_test" -mysql -u root -e "GRANT ALL PRIVILEGES ON aurweb_test.* TO 'aur'@'%'" - -# Create the 'aurweb' DB if it does not yet exist. -mysql -u root -e "CREATE DATABASE aurweb" 2>/dev/null || /bin/true -mysql -u root -e "GRANT ALL PRIVILEGES ON aurweb.* TO 'aur'@'%'" - -mysql -u root -e "FLUSH PRIVILEGES" - -# Shutdown mariadb. -mysqladmin -uroot shutdown - -exec "$@" diff --git a/docker/scripts/run-pytests.sh b/docker/scripts/run-pytests.sh index 021603b1..c6baa939 100755 --- a/docker/scripts/run-pytests.sh +++ b/docker/scripts/run-pytests.sh @@ -23,7 +23,8 @@ while [ $# -ne 0 ]; do done # Initialize the new database; ignore errors. -python -m aurweb.initdb 2>/dev/null || /bin/true +python -m aurweb.initdb 2>/dev/null || \ + (echo "Error: aurweb.initdb failed; already initialized?" && /bin/true) # Run pytest with optional targets in front of it. make -C test "${PARAMS[@]}" pytest diff --git a/docker/test-mysql-entrypoint.sh b/docker/test-mysql-entrypoint.sh index ea4df868..9594318f 100755 --- a/docker/test-mysql-entrypoint.sh +++ b/docker/test-mysql-entrypoint.sh @@ -1,8 +1,9 @@ #!/bin/bash set -eou pipefail +[[ -z "$DB_HOST" ]] && echo 'Error: $DB_HOST required but missing.' && exit 1 + DB_NAME="aurweb_test" -DB_HOST="mariadb" DB_USER="aur" DB_PASS="aur" From f4406ccf5cc27806843d7bb8a216c5aa6ad1a214 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 21:28:12 -0700 Subject: [PATCH 0346/1451] Docker: Centralize repo dependencies Now, we have `docker/scripts/install-deps.sh`, a script used by both Docker and .gitlab-ci.yml. We can now focus on changing deps in this script along as well as documentation going forward. Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 47 +++++++++++----------------------- Dockerfile | 37 +++++++++++++------------- docker/scripts/install-deps.sh | 19 ++++++++++++++ 3 files changed, 52 insertions(+), 51 deletions(-) create mode 100755 docker/scripts/install-deps.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6a9d80cb..7b8da2ae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: archlinux +image: archlinux:base-devel cache: key: system-v1 @@ -7,45 +7,28 @@ cache: - .pkg-cache variables: - AUR_CONFIG: conf/config + AUR_CONFIG: conf/config # Default MySQL config setup in before_script. + DB_HOST: localhost before_script: - - pacman -Syu --noconfirm --noprogressbar --needed --cachedir .pkg-cache - base-devel git gpgme protobuf pyalpm python-mysqlclient - python-pygit2 python-srcinfo python-bleach python-markdown - python-sqlalchemy python-alembic python-pytest python-werkzeug - python-pytest-tap python-fastapi hypercorn nginx python-authlib - python-itsdangerous python-httpx python-jinja python-pytest-cov - python-requests python-aiofiles python-python-multipart - python-pytest-asyncio python-coverage python-bcrypt - python-email-validator openssh python-lxml mariadb - python-isort flake8 - - bash -c "echo '127.0.0.1 localhost' > /etc/hosts" - - bash -c "echo '::1 localhost' >> /etc/hosts" - - mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql + - ./docker/scripts/install-deps.sh + - useradd -U -d /aurweb -c 'AUR User' aur + - ./docker/mariadb-entrypoint.sh - (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & - 'until : > /dev/tcp/127.0.0.1/3306; do sleep 1s; done' - - mysql -u root -e "CREATE USER 'aur'@'localhost' IDENTIFIED BY 'aur';" - - mysql -u root -e "CREATE DATABASE aurweb_test;" - - mysql -u root -e "GRANT ALL ON aurweb_test.* TO 'aur'@'localhost';" - - mysql -u root -e "FLUSH PRIVILEGES;" - - sed -r "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.dev > conf/config - - cp conf/config conf/config.sqlite - - cp conf/config.defaults conf/config.sqlite.defaults - - sed -i -r 's;backend = .*;backend = sqlite;' conf/config.sqlite - - sed -i -r "s;name = .*;name = $(pwd)/aurweb.sqlite3;" conf/config.sqlite + - ./docker/test-mysql-entrypoint.sh # Create mysql AUR_CONFIG. + - ./docker/test-sqlite-entrypoint.sh # Create sqlite AUR_CONFIG. + - make -C po all install + - python setup.py install --install-scripts=/usr/local/bin + - python -m aurweb.initdb # Initialize MySQL tables. - AUR_CONFIG=conf/config.sqlite python -m aurweb.initdb + - make -C test clean test: script: - - python setup.py install - - make -C po all install - - python -m aurweb.initdb - - make -C test sh # sharness tests use sqlite. - - make -C test pytest # pytest with mysql. - - AUR_CONFIG=conf/config.sqlite make -C test pytest # pytest with sqlite. - - coverage report --include='aurweb/*' - - coverage xml --include='aurweb/*' + - make -C test sh pytest # sharness tests use sqlite & pytest w/ mysql. + - AUR_CONFIG=conf/config.sqlite make -C test pytest + - make -C test coverage # Produce coverage reports. - flake8 --count aurweb # Assert no flake8 violations in aurweb. - flake8 --count test # Assert no flake8 violations in test. - flake8 --count migrations # Assert no flake8 violations in migrations. diff --git a/Dockerfile b/Dockerfile index cec36158..2843fa1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,30 @@ FROM archlinux:base-devel + ENV PYTHONPATH=/aurweb ENV AUR_CONFIG=conf/config +# Copy our single bootstrap script. +COPY docker/scripts/install-deps.sh /install-deps.sh +RUN /install-deps.sh + +# Add our aur user. +RUN useradd -U -d /aurweb -c 'AUR User' aur + # Setup some default system stuff. RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime -RUN mkdir -p .pkg-cache +# Copy the rest of docker. +COPY ./docker /docker +COPY ./docker/scripts/*.sh /usr/local/bin/ -# Install dependencies. -RUN pacman -Syu --noconfirm --noprogressbar \ - --cachedir .pkg-cache git gpgme protobuf pyalpm \ - python-mysqlclient python-pygit2 python-srcinfo python-bleach \ - python-markdown python-sqlalchemy python-alembic python-pytest \ - python-werkzeug python-pytest-tap python-fastapi nginx python-authlib \ - python-itsdangerous python-httpx python-jinja python-pytest-cov \ - python-requests python-aiofiles python-python-multipart \ - python-pytest-asyncio python-coverage hypercorn python-bcrypt \ - python-email-validator openssh python-lxml mariadb mariadb-libs \ - python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ - python-asgiref uvicorn - -RUN useradd -U -d /aurweb -c 'AUR User' aur - -COPY docker /docker +# Copy from host to container. +COPY . /aurweb +# Working directory is aurweb root @ /aurweb. WORKDIR /aurweb -COPY . . +# Install translations. RUN make -C po all install -RUN python3 setup.py install --install-scripts=/usr/local/bin + +# Install package and scripts. +RUN python setup.py install --install-scripts=/usr/local/bin diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh new file mode 100755 index 00000000..fc15313f --- /dev/null +++ b/docker/scripts/install-deps.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Install Arch Linux dependencies. This is centralized here +# for CI and Docker usage and should always reflect the most +# robust development ecosystem. +set -eou pipefail + +pacman -Syu --noconfirm --noprogressbar \ + --cachedir .pkg-cache git gpgme protobuf pyalpm \ + python-mysqlclient python-pygit2 python-srcinfo python-bleach \ + python-markdown python-sqlalchemy python-alembic python-pytest \ + python-werkzeug python-pytest-tap python-fastapi nginx python-authlib \ + python-itsdangerous python-httpx python-jinja python-pytest-cov \ + python-requests python-aiofiles python-python-multipart \ + python-pytest-asyncio python-coverage hypercorn python-bcrypt \ + python-email-validator openssh python-lxml mariadb mariadb-libs \ + python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ + python-asgiref uvicorn + +exec "$@" From 3f60f5048e210f5248bb2b56fcfbe346e5ebac2a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 21:35:00 -0700 Subject: [PATCH 0347/1451] Docker: add scripts/setup-sqlite.sh This script purely removes any existing sqlite and is used before tests are run. This causes the test flow to run `aurweb.initdb` again (if ever). Signed-off-by: Kevin Morris --- docker-compose.yml | 4 ++-- docker/scripts/setup-sqlite.sh | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100755 docker/scripts/setup-sqlite.sh diff --git a/docker-compose.yml b/docker-compose.yml index 40d9bc5b..22495a95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -304,7 +304,7 @@ services: environment: - AUR_CONFIG=conf/config.sqlite entrypoint: /docker/test-sqlite-entrypoint.sh - command: /docker/scripts/run-pytests.sh clean + command: setup-sqlite.sh run-pytests.sh clean stdin_open: true tty: true volumes: @@ -331,7 +331,7 @@ services: - AUR_CONFIG=conf/config - DB_HOST=mariadb entrypoint: /docker/tests-entrypoint.sh - command: /docker/scripts/run-tests.sh + command: setup-sqlite.sh run-tests.sh stdin_open: true tty: true depends_on: diff --git a/docker/scripts/setup-sqlite.sh b/docker/scripts/setup-sqlite.sh new file mode 100755 index 00000000..e0b8de50 --- /dev/null +++ b/docker/scripts/setup-sqlite.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Run an sqlite test. This script really just prepares sqlite +# tests by deleting any existing databases so the test can +# initialize cleanly. +DB_NAME="$(grep 'name =' conf/config.sqlite | sed -r 's/^name = (.+)$/\1/')" +rm -vf $DB_NAME +exec "$@" From 427a30ef8adb9ff92c9ee6745c3d2985929f3a7a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 21:37:45 -0700 Subject: [PATCH 0348/1451] Docker: Remove deprecated `links` In addition, remove some unneeded dependencies on tests. Though, in the future we _should_ craft tests that use these. Signed-off-by: Kevin Morris --- docker-compose.yml | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 22495a95..ab8d7c41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,8 +64,6 @@ services: depends_on: mariadb: condition: service_healthy - links: - - mariadb volumes: - mariadb_run:/var/run/mysqld - mariadb_data:/var/lib/mysql @@ -86,8 +84,6 @@ services: depends_on: mariadb: condition: service_healthy - links: - - mariadb volumes: - mariadb_run:/var/run/mysqld - mariadb_data:/var/lib/mysql @@ -109,8 +105,6 @@ services: depends_on: git: condition: service_healthy - links: - - git volumes: - git_data:/aurweb/aur.git @@ -128,8 +122,6 @@ services: depends_on: git: condition: service_healthy - links: - - git volumes: - git_data:/aurweb/aur.git @@ -152,10 +144,6 @@ services: condition: service_healthy mariadb: condition: service_healthy - links: - - ca - - git - - mariadb volumes: - mariadb_run:/var/run/mysqld # Bind socket in this volume. - mariadb_data:/var/lib/mysql @@ -187,10 +175,6 @@ services: condition: service_healthy mariadb: condition: service_healthy - links: - - ca - - git - - mariadb volumes: - mariadb_run:/var/run/mysqld # Bind socket in this volume. - mariadb_data:/var/lib/mysql @@ -228,12 +212,6 @@ services: condition: service_healthy php-fpm: condition: service_healthy - links: - - cgit-php - - cgit-fastapi - - smartgit - - fastapi - - php-fpm volumes: - git_data:/aurweb/aur.git - ./cache:/cache @@ -255,8 +233,6 @@ services: depends_on: git: condition: service_healthy - links: - - git volumes: - git_data:/aurweb/aur.git - ./cache:/cache @@ -281,11 +257,6 @@ services: depends_on: mariadb: condition: service_healthy - git: - condition: service_healthy - links: - - mariadb - - git volumes: - mariadb_run:/var/run/mysqld - git_data:/aurweb/aur.git @@ -318,11 +289,6 @@ services: - ./web/template:/aurweb/web/template - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates - depends_on: - git: - condition: service_healthy - links: - - git test: image: aurweb:latest @@ -337,8 +303,6 @@ services: depends_on: mariadb: condition: service_healthy - links: - - mariadb volumes: - mariadb_run:/var/run/mysqld - git_data:/aurweb/aur.git From 3a74f76ff9835dd03b4709a585195611f6a20e38 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 29 Jun 2021 22:44:41 -0700 Subject: [PATCH 0349/1451] FastAPI: use internal typeahead and remove jquery Awesome! Signed-off-by: Kevin Morris --- aurweb/asgi.py | 5 +---- templates/index.html | 10 ++++++++++ templates/partials/head.html | 3 +++ templates/partials/layout.html | 1 - templates/partials/typeahead.html | 30 ------------------------------ web/html/js/typeahead-home.js | 6 ++++++ 6 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 templates/partials/typeahead.html create mode 100644 web/html/js/typeahead-home.js diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 228b9a65..5f0ad01d 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -83,10 +83,7 @@ async def add_security_headers(request: Request, call_next: typing.Callable): # Add CSP header. nonce = request.user.nonce csp = "default-src 'self'; " - script_hosts = [ - "ajax.googleapis.com", - "cdn.jsdelivr.net" - ] + script_hosts = [] csp += f"script-src 'self' 'nonce-{nonce}' " + ' '.join(script_hosts) # It's fine if css is inlined. csp += "; style-src 'self' 'unsafe-inline'" diff --git a/templates/index.html b/templates/index.html index 8cd1cc78..f8745f33 100644 --- a/templates/index.html +++ b/templates/index.html @@ -93,4 +93,14 @@

    + + + + + + + {% endblock %} diff --git a/templates/partials/head.html b/templates/partials/head.html index 0351fd6e..9b438255 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -12,5 +12,8 @@ + + + AUR ({{ language }}) - {{ title | tr }} diff --git a/templates/partials/layout.html b/templates/partials/layout.html index 019ebff7..68637ed7 100644 --- a/templates/partials/layout.html +++ b/templates/partials/layout.html @@ -6,6 +6,5 @@ {% include 'partials/navbar.html' %} {% extends 'partials/body.html' %} - {% include 'partials/typeahead.html' %} diff --git a/templates/partials/typeahead.html b/templates/partials/typeahead.html deleted file mode 100644 index c218b8d1..00000000 --- a/templates/partials/typeahead.html +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/web/html/js/typeahead-home.js b/web/html/js/typeahead-home.js new file mode 100644 index 00000000..5af51c53 --- /dev/null +++ b/web/html/js/typeahead-home.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + const input = document.getElementById('pkgsearch-field'); + const form = document.getElementById('pkgsearch-form'); + const type = 'suggest'; + typeahead.init(type, input, form); +}); From 450469e3d66059d2002a68c4ad8d435793a7bfe4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Jun 2021 08:54:22 -0700 Subject: [PATCH 0350/1451] add /addvote/ (get, post) routes Another part of the "Trusted User" collection of routes. This allows a Trusted User to create a proposal. New Routes: - get `/addvote/` - post `/addvote/` Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 119 ++++++++++++++++++++++++++----- templates/addvote.html | 68 ++++++++++++++++++ test/test_trusted_user_routes.py | 93 ++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 16 deletions(-) create mode 100644 templates/addvote.html diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 55f7b7e1..fd5ebb04 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -1,3 +1,6 @@ +import html +import logging +import re import typing from datetime import datetime @@ -5,10 +8,10 @@ from http import HTTPStatus from urllib.parse import quote_plus from fastapi import APIRouter, Form, HTTPException, Request -from fastapi.responses import Response +from fastapi.responses import RedirectResponse, Response from sqlalchemy import and_, or_ -from aurweb import db +from aurweb import db, l10n from aurweb.auth import account_type_required, auth_required from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND_DEV from aurweb.models.tu_vote import TUVote @@ -17,6 +20,7 @@ from aurweb.models.user import User from aurweb.templates import make_context, make_variable_context, render_template router = APIRouter() +logger = logging.getLogger(__name__) # Some TU route specific constants. ITEMS_PER_PAGE = 10 # Paged table size. @@ -29,6 +33,17 @@ REQUIRED_TYPES = { TRUSTED_USER_AND_DEV } +ADDVOTE_SPECIFICS = { + # This dict stores a vote duration and quorum for a proposal. + # When a proposal is added, duration is added to the current + # timestamp. + # "addvote_type": (duration, quorum) + "add_tu": (7 * 24 * 60 * 60, 0.66), + "remove_tu": (7 * 24 * 60 * 60, 0.75), + "remove_inactive_tu": (5 * 24 * 60 * 60, 0.66), + "bylaws": (7 * 24 * 60 * 60, 0.75) +} + @router.get("/tu") @auth_required(True, redirect="/") @@ -174,28 +189,17 @@ async def trusted_user_proposal_post(request: Request, raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) voters = db.query(User).join(TUVote).filter(TUVote.VoteID == voteinfo.ID) + vote = db.query(TUVote, and_(TUVote.UserID == request.user.ID, + TUVote.VoteID == voteinfo.ID)).first() - # status_code we'll use for responses later. status_code = HTTPStatus.OK - if not request.user.is_trusted_user(): - # Test: Create a proposal and view it as a "Developer". It - # should give us this error. context["error"] = "Only Trusted Users are allowed to vote." status_code = HTTPStatus.UNAUTHORIZED elif voteinfo.User == request.user.Username: context["error"] = "You cannot vote in an proposal about you." status_code = HTTPStatus.BAD_REQUEST - - vote = db.query(TUVote, and_(TUVote.UserID == request.user.ID, - TUVote.VoteID == voteinfo.ID)).first() - - if status_code != HTTPStatus.OK: - return render_proposal(request, context, proposal, - voteinfo, voters, vote, - status_code=status_code) - - if vote is not None: + elif vote is not None: context["error"] = "You've already voted for this proposal." status_code = HTTPStatus.BAD_REQUEST @@ -218,3 +222,86 @@ async def trusted_user_proposal_post(request: Request, context["error"] = "You've already voted for this proposal." return render_proposal(request, context, proposal, voteinfo, voters, vote) + + +@router.get("/addvote") +@auth_required(True) +@account_type_required({"Trusted User", "Trusted User & Developer"}) +async def trusted_user_addvote(request: Request, + user: str = str(), + type: str = "add_tu", + agenda: str = str()): + context = await make_variable_context(request, "Add Proposal") + + if type not in ADDVOTE_SPECIFICS: + context["error"] = "Invalid type." + type = "add_tu" # Default it. + + context["user"] = user + context["type"] = type + context["agenda"] = agenda + + return render_template(request, "addvote.html", context) + + +@router.post("/addvote") +@auth_required(True) +@account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) +async def trusted_user_addvote_post(request: Request, + user: str = Form(default=str()), + type: str = Form(default=str()), + agenda: str = Form(default=str())): + # Build a context. + context = await make_variable_context(request, "Add Proposal") + + context["type"] = type + context["user"] = user + context["agenda"] = agenda + + def render_addvote(context, status_code): + """ Simplify render_template a bit for this test. """ + return render_template(request, "addvote.html", context, status_code) + + # Alright, get some database records, if we can. + if type != "bylaws": + user_record = db.query(User, User.Username == user).first() + if user_record is None: + context["error"] = "Username does not exist." + return render_addvote(context, HTTPStatus.NOT_FOUND) + + voteinfo = db.query(TUVoteInfo, TUVoteInfo.User == user).count() + if voteinfo: + _ = l10n.get_translator_for_request(request) + context["error"] = _( + "%s already has proposal running for them.") % ( + html.escape(user),) + return render_addvote(context, HTTPStatus.BAD_REQUEST) + + if type not in ADDVOTE_SPECIFICS: + context["error"] = "Invalid type." + context["type"] = type = "add_tu" # Default for rendering. + return render_addvote(context, HTTPStatus.BAD_REQUEST) + + if not agenda: + context["error"] = "Proposal cannot be empty." + return render_addvote(context, HTTPStatus.BAD_REQUEST) + + # Gather some mapped constants and the current timestamp. + duration, quorum = ADDVOTE_SPECIFICS.get(type) + timestamp = int(datetime.utcnow().timestamp()) + + # Remove diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html new file mode 100644 index 00000000..a25e9c9e --- /dev/null +++ b/templates/partials/packages/details.html @@ -0,0 +1,145 @@ + + + + + + {% if show_package_details | default(False) %} + + + + + + + + + + + + + {% endif %} + {% if pkgbase.keywords.count() %} + + + {% if is_maintainer %} + + {% else %} + + {% endif %} + + {% endif %} + {% if licenses and licenses.count() and show_package_details | default(False) %} + + + + + {% endif %} + {% if show_package_details | default(False) %} + + + + + {% endif %} + + + + + + + + + + + + + + + {% if not is_maintainer %} + + {% else %} + + {% endif %} + + + + + + + {% set submitted = pkgbase.SubmittedTS | dt | as_timezone(timezone) %} + + + + + + {% set updated = pkgbase.ModifiedTS | dt | as_timezone(timezone) %} + + +
    {{ "Git Clone URL" | tr }}: + {{ git_clone_uri_anon | format(pkgbase.Name) }} ({{ "read-only" | tr }}, {{ "click to copy" | tr }}) + {% if is_maintainer %} +
    {{ git_clone_uri_priv | format(pkgbase.Name) }} ({{ "click to copy" | tr }}) + {% endif %} +
    {{ "Package Base" | tr }}: + + {{ pkgbase.Name }} + +
    {{ "Description" | tr }}:{{ pkgbase.packages.first().Description }}
    {{ "Upstream URL" | tr }}: + {% set pkg = pkgbase.packages.first() %} + {% if pkg.URL %} + {{ pkg.URL }} + {% else %} + {{ "None" | tr }} + {% endif %} +
    {{ "Keywords" | tr }}: +
    +
    + + +
    +
    +
    + {% for keyword in pkgbase.keywords %} + + {{ keyword.Keyword }} + + {% endfor %} +
    {{ "Licenses" | tr }}:{{ licenses | join(', ', attribute='Name') | default('None' | tr) }}
    {{ "Conflicts" | tr }}: + {{ conflicts | join(', ', attribute='RelName') }} +
    {{ "Submitter" | tr }}: + {% if request.user.is_authenticated() %} + + {{ pkgbase.Submitter.Username | default("None" | tr) }} + + {% else %} + {{ pkgbase.Submitter.Username | default("None" | tr) }} + {% endif %} +
    {{ "Maintainer" | tr }}: + {% if request.user.is_authenticated() %} + + {{ pkgbase.Maintainer.Username | default("None" | tr) }} + + {% else %} + {{ pkgbase.Maintainer.Username | default("None" | tr) }} + {% endif %} +
    {{ "Last Packager" | tr }}: + {% if request.user.is_authenticated() %} + + {{ pkgbase.Packager.Username | default("None" | tr) }} + + {% else %} + {{ pkgbase.Packager.Username | default("None" | tr) }} + {% endif %} +
    {{ "Votes" | tr }}:{{ pkgbase.package_votes.count() }} + + {{ pkgbase.package_votes.count() }} + +
    {{ "Popularity" | tr }}:{{ pkgbase.Popularity | number_format(6 if pkgbase.Popularity <= 0.2 else 2) }}
    {{ "First Submitted" | tr }}:{{ "%s" | format(submitted.strftime("%Y-%m-%d %H:%M")) }}
    {{ "Last Updated" | tr }}:{{ "%s" | format(updated.strftime("%Y-%m-%d %H:%M")) }}
    + + + diff --git a/templates/partials/packages/package_actions.html b/templates/partials/packages/package_actions.html deleted file mode 100644 index 4e7da882..00000000 --- a/templates/partials/packages/package_actions.html +++ /dev/null @@ -1,87 +0,0 @@ - - diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html new file mode 100644 index 00000000..767e25a9 --- /dev/null +++ b/templates/partials/packages/package_metadata.html @@ -0,0 +1,54 @@ +
    +

    Dependencies ({{ dependencies.count() }})

    +
      + {% for dep in dependencies.all() %} +
    • + {% set broken = not dep.is_package() %} + {% if broken %} + + {% else %} + + {% endif %} + {{ dep.DepName }} + {% if broken %} + + {% else %} + + {% endif %} + {{ dep.Package | provides_list(dep.DepName) | safe }} + {% set extra = dep | dep_extra %} + {% if extra %} + {{ dep | dep_extra_desc }} + {% endif %} +
    • + {% endfor %} +
    +
    + +
    +

    Required by ({{ required_by.count() }})

    + +
    + +
    +

    Sources ({{ sources.count() }})

    +
    + +
    + +
    diff --git a/templates/pkgbase.html b/templates/pkgbase.html deleted file mode 100644 index d608fa2e..00000000 --- a/templates/pkgbase.html +++ /dev/null @@ -1,95 +0,0 @@ -{% extends "partials/layout.html" %} - -{% block pageContent %} - {% include "partials/packages/search.html" %} -
    -

    Package Details: {{ pkgbase.Name }}

    - - {% set result = pkgbase %} - {% set pkgname = "result.Name" %} - {% include "partials/packages/package_actions.html" %} - - - - - - - - - {% if is_maintainer %} - - {% else %} - - {% endif %} - - - - - - - - - - - - - - - - - - - - - - - {% set submitted = pkgbase.SubmittedTS | dt | as_timezone(timezone) %} - - - - - - {% set updated = pkgbase.ModifiedTS | dt | as_timezone(timezone) %} - - -
    {{ "Git Clone URL" | tr }}: - {{ git_clone_uri_anon | format(pkgbase.Name) }} ({{ "read-only" | tr }}, {{ "click to copy" | tr }}) - {% if is_maintainer %} -
    {{ git_clone_uri_priv | format(pkgbase.Name) }} ({{ "click to copy" | tr }}) - {% endif %} -
    {{ "Keywords" | tr }}: -
    -
    - - - -
    -
    -
    - {% for item in pkgbase.keywords %} - {{ item.Keyword }} - {% endfor %} -
    {{ "Submitter" | tr }}:{{ pkgbase.Submitter.Username | default("None") }}
    {{ "Maintainer" | tr }}:{{ pkgbase.Maintainer.Username | default("None") }}
    {{ "Last Packager" | tr }}:{{ pkgbase.Packager.Username | default("None") }}
    {{ "Votes" | tr }}:{{ pkgbase.NumVotes }}
    {{ "Popularity" | tr }}:{{ '%0.2f' % pkgbase.Popularity | float }}
    {{ "First Submitted" | tr }}:{{ "%s" | tr | format(submitted.strftime("%Y-%m-%d %H:%M")) }}
    {{ "Last Updated" | tr }}:{{ "%s" | tr | format(updated.strftime("%Y-%m-%d %H:%M")) }}
    - -
    -
    - -

    Packages ({{ packages_count }})

    - -
    -
    -
    - {% set pkgname = result.Name %} - {% set pkgbase_id = result.ID %} - {% set comments = comments %} - {% include "partials/packages/comments.html" %} -{% endblock %} diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py index d39091aa..e28f1781 100644 --- a/test/test_package_dependency.py +++ b/test/test_package_dependency.py @@ -77,6 +77,13 @@ def test_package_dependencies(): assert pkgdep in optdepends.package_dependencies assert pkgdep in package.package_dependencies + assert not pkgdep.is_package() + + base = create(PackageBase, Name=pkgdep.DepName, Maintainer=user) + create(Package, PackageBase=base, Name=pkgdep.DepName) + + assert pkgdep.is_package() + def test_package_dependencies_null_package_raises_exception(): from aurweb.db import session diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py new file mode 100644 index 00000000..f9592238 --- /dev/null +++ b/test/test_packages_routes.py @@ -0,0 +1,283 @@ +from datetime import datetime +from http import HTTPStatus + +import pytest + +from fastapi.testclient import TestClient + +from aurweb import asgi, db +from aurweb.models.account_type import USER_ID, AccountType +from aurweb.models.dependency_type import DependencyType +from aurweb.models.official_provider import OfficialProvider +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comment import PackageComment +from aurweb.models.package_dependency import PackageDependency +from aurweb.models.package_keyword import PackageKeyword +from aurweb.models.package_relation import PackageRelation +from aurweb.models.relation_type import PROVIDES_ID, RelationType +from aurweb.models.user import User +from aurweb.testing import setup_test_db +from aurweb.testing.html import parse_root +from aurweb.testing.requests import Request + + +def package_endpoint(package: Package) -> str: + return f"/packages/{package.Name}" + + +def create_package(pkgname: str, maintainer: User, + autocommit: bool = True) -> Package: + pkgbase = db.create(PackageBase, + Name=pkgname, + Maintainer=maintainer, + autocommit=False) + return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase, + autocommit=autocommit) + + +def create_package_dep(package: Package, depname: str, + dep_type_name: str = "depends", + autocommit: bool = True) -> PackageDependency: + dep_type = db.query(DependencyType, + DependencyType.Name == dep_type_name).first() + return db.create(PackageDependency, + DependencyType=dep_type, + Package=package, + DepName=depname, + autocommit=autocommit) + + +def create_package_rel(package: Package, + relname: str, + autocommit: bool = True) -> PackageRelation: + rel_type = db.query(RelationType, + RelationType.ID == PROVIDES_ID).first() + return db.create(PackageRelation, + RelationType=rel_type, + Package=package, + RelName=relname) + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db( + User.__tablename__, + Package.__tablename__, + PackageBase.__tablename__, + PackageDependency.__tablename__, + PackageRelation.__tablename__, + PackageKeyword.__tablename__, + OfficialProvider.__tablename__ + ) + + +@pytest.fixture +def client() -> TestClient: + """ Yield a FastAPI TestClient. """ + yield TestClient(app=asgi.app) + + +@pytest.fixture +def user() -> User: + """ Yield a user. """ + account_type = db.query(AccountType, AccountType.ID == USER_ID).first() + yield db.create(User, Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountType=account_type) + + +@pytest.fixture +def maintainer() -> User: + """ Yield a specific User used to maintain packages. """ + account_type = db.query(AccountType, AccountType.ID == USER_ID).first() + yield db.create(User, Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountType=account_type) + + +@pytest.fixture +def package(maintainer: User) -> Package: + """ Yield a Package created by user. """ + pkgbase = db.create(PackageBase, + Name="test-package", + Maintainer=maintainer) + yield db.create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name) + + +def test_package_not_found(client: TestClient): + with client as request: + resp = request.get("/packages/not_found") + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_package_official_not_found(client: TestClient, package: Package): + """ When a Package has a matching OfficialProvider record, it is not + hosted on AUR, but in the official repositories. Getting a package + with this kind of record should return a status code 404. """ + db.create(OfficialProvider, + Name=package.Name, + Repo="core", + Provides=package.Name) + + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_package(client: TestClient, package: Package): + """ Test a single /packages/{name} route. """ + with client as request: + + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + h2 = root.find('.//div[@id="pkgdetails"]/h2') + + sections = h2.text.split(":") + assert sections[0] == "Package Details" + + name, version = sections[1].lstrip().split(" ") + assert name == package.Name + version == package.Version + + rows = root.findall('.//table[@id="pkginfo"]//tr') + row = rows[1] # Second row is our target. + + pkgbase = row.find("./td/a") + assert pkgbase.text.strip() == package.PackageBase.Name + + +def test_package_comments(client: TestClient, user: User, package: Package): + now = (datetime.utcnow().timestamp()) + comment = db.create(PackageComment, PackageBase=package.PackageBase, + User=user, Comments="Test comment", CommentTS=now) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(package_endpoint(package), cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + expected = [ + comment.Comments + ] + comments = root.xpath('.//div[contains(@class, "package-comments")]' + '/div[@class="article-content"]/div/text()') + for i, row in enumerate(expected): + assert comments[i].strip() == row + + +def test_package_authenticated(client: TestClient, user: User, + package: Package): + """ We get the same here for either authenticated or not + authenticated. Form inputs are presented to maintainers. + This process also occurs when pkgbase.html is rendered. """ + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(package_endpoint(package), cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + expected = [ + "View PKGBUILD", + "View Changes", + "Download snapshot", + "Search wiki", + "Flag package out-of-date", + "Vote for this package", + "Enable notifications", + "Submit Request" + ] + for expected_text in expected: + assert expected_text in resp.text + + +def test_package_authenticated_maintainer(client: TestClient, + maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + resp = request.get(package_endpoint(package), cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + expected = [ + "View PKGBUILD", + "View Changes", + "Download snapshot", + "Search wiki", + "Flag package out-of-date", + "Vote for this package", + "Enable notifications", + "Manage Co-Maintainers", + "Submit Request", + "Delete Package", + "Merge Package", + "Disown Package" + ] + for expected_text in expected: + assert expected_text in resp.text + + +def test_package_dependencies(client: TestClient, maintainer: User, + package: Package): + # Create a normal dependency of type depends. + dep_pkg = create_package("test-dep-1", maintainer, autocommit=False) + dep = create_package_dep(package, dep_pkg.Name, autocommit=False) + + # Also, create a makedepends. + make_dep_pkg = create_package("test-dep-2", maintainer, autocommit=False) + make_dep = create_package_dep(package, make_dep_pkg.Name, + dep_type_name="makedepends", + autocommit=False) + + # And... a checkdepends! + check_dep_pkg = create_package("test-dep-3", maintainer, autocommit=False) + check_dep = create_package_dep(package, check_dep_pkg.Name, + dep_type_name="checkdepends", + autocommit=False) + + # Geez. Just stop. This is optdepends. + opt_dep_pkg = create_package("test-dep-4", maintainer, autocommit=False) + opt_dep = create_package_dep(package, opt_dep_pkg.Name, + dep_type_name="optdepends", + autocommit=False) + + broken_dep = create_package_dep(package, "test-dep-5", + dep_type_name="depends", + autocommit=False) + + # Create an official provider record. + db.create(OfficialProvider, Name="test-dep-99", + Repo="core", Provides="test-dep-99", + autocommit=False) + official_dep = create_package_dep(package, "test-dep-99", + autocommit=False) + + # Also, create a provider who provides our test-dep-99. + provider = create_package("test-provider", maintainer, autocommit=False) + create_package_rel(provider, dep.DepName) + + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + + expected = [ + dep.DepName, + make_dep.DepName, + check_dep.DepName, + opt_dep.DepName, + official_dep.DepName + ] + pkgdeps = root.findall('.//ul[@id="pkgdepslist"]/li/a') + for i, expectation in enumerate(expected): + assert pkgdeps[i].text.strip() == expectation + + broken_node = root.find('.//ul[@id="pkgdepslist"]/li/span') + assert broken_node.text.strip() == broken_dep.DepName diff --git a/test/test_packages_util.py b/test/test_packages_util.py new file mode 100644 index 00000000..17978490 --- /dev/null +++ b/test/test_packages_util.py @@ -0,0 +1,51 @@ +import pytest + +from fastapi.testclient import TestClient + +from aurweb import asgi, db +from aurweb.models.account_type import USER_ID, AccountType +from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.user import User +from aurweb.packages import util +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db( + User.__tablename__, + Package.__tablename__, + PackageBase.__tablename__, + OfficialProvider.__tablename__ + ) + + +@pytest.fixture +def maintainer() -> User: + account_type = db.query(AccountType, AccountType.ID == USER_ID).first() + yield db.create(User, Username="test_maintainer", + Email="test_maintainer@examepl.org", + Passwd="testPassword", + AccountType=account_type) + + +@pytest.fixture +def package(maintainer: User) -> Package: + pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=maintainer) + yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) + + +def test_package_link(client: TestClient, maintainer: User, package: Package): + db.create(OfficialProvider, + Name=package.Name, + Repo="core", + Provides=package.Name) + expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}" + assert util.package_link(package) == expected diff --git a/test/test_templates.py b/test/test_templates.py new file mode 100644 index 00000000..8e3017b4 --- /dev/null +++ b/test/test_templates.py @@ -0,0 +1,15 @@ +import pytest + +from aurweb.templates import register_filter + + +@register_filter("func") +def func(): pass + + +def test_register_filter_exists_key_error(): + """ Most instances of register_filter are tested through module + imports or template renders, so we only test failures here. """ + with pytest.raises(KeyError): + @register_filter("func") + def some_func(): pass diff --git a/web/html/js/copy.js b/web/html/js/copy.js new file mode 100644 index 00000000..f46299b3 --- /dev/null +++ b/web/html/js/copy.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.copy').addEventListener('click', function(e) { + e.preventDefault(); + navigator.clipboard.writeText(event.target.text); + }); +}); From 88569b6d0987dd58e11198964e7bf597ed75d311 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 15 Jul 2021 22:54:41 -0700 Subject: [PATCH 0373/1451] add /pkgbase/{name} route Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 17 +++++++++ .../partials/packages/pkgbase_metadata.html | 13 +++++++ templates/pkgbase.html | 21 +++++++++++ test/test_packages_routes.py | 37 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 templates/partials/packages/pkgbase_metadata.html create mode 100644 templates/pkgbase.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 9650df85..cb7f4a18 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -96,3 +96,20 @@ async def package(request: Request, name: str) -> Response: context["conflicts"] = conflicts return render_template(request, "packages/show.html", context) + + +@router.get("/pkgbase/{name}") +async def package_base(request: Request, name: str) -> Response: + # Get the PackageBase. + pkgbase = get_pkgbase(name) + + # If this is not a split package, redirect to /packages/{name}. + if pkgbase.packages.count() == 1: + return RedirectResponse(f"/packages/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + # Add our base information. + context = await make_single_context(request, pkgbase) + context["packages"] = pkgbase.packages.all() # Doesn't need to be here. + + return render_template(request, "pkgbase.html", context) diff --git a/templates/partials/packages/pkgbase_metadata.html b/templates/partials/packages/pkgbase_metadata.html new file mode 100644 index 00000000..ba27fda5 --- /dev/null +++ b/templates/partials/packages/pkgbase_metadata.html @@ -0,0 +1,13 @@ +
    +

    Packages ({{ packages_count }})

    + +
    diff --git a/templates/pkgbase.html b/templates/pkgbase.html new file mode 100644 index 00000000..315cdf67 --- /dev/null +++ b/templates/pkgbase.html @@ -0,0 +1,21 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% include "partials/packages/search.html" %} +
    +

    {{ 'Package Base Details' | tr }}: {{ pkgbase.Name }}

    + + {% set result = pkgbase %} + {% include "partials/packages/actions.html" %} + {% include "partials/packages/details.html" %} + +
    + {% include "partials/packages/pkgbase_metadata.html" %} +
    +
    + + {% set pkgname = result.Name %} + {% set pkgbase_id = result.ID %} + {% set comments = comments %} + {% include "partials/packages/comments.html" %} +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index f9592238..44ef7fcd 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -281,3 +281,40 @@ def test_package_dependencies(client: TestClient, maintainer: User, broken_node = root.find('.//ul[@id="pkgdepslist"]/li/span') assert broken_node.text.strip() == broken_dep.DepName + + +def test_pkgbase_not_found(client: TestClient): + with client as request: + resp = request.get("/pkgbase/not_found") + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_redirect(client: TestClient, package: Package): + with client as request: + resp = request.get(f"/pkgbase/{package.Name}", + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/packages/{package.Name}" + + +def test_pkgbase(client: TestClient, package: Package): + second = db.create(Package, Name="second-pkg", + PackageBase=package.PackageBase) + + expected = [package.Name, second.Name] + with client as request: + resp = request.get(f"/pkgbase/{package.Name}", + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + + # Check the details box title. + title = root.find('.//div[@id="pkgdetails"]/h2') + title, pkgname = title.text.split(": ") + assert title == "Package Base Details" + assert pkgname == package.Name + + pkgs = root.findall('.//div[@id="pkgs"]/ul/li/a') + for i, name in enumerate(expected): + assert pkgs[i].text.strip() == name From 04d1c81d3dc3af59749ca3555a9fd45f4a9fcb78 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 27 Jul 2021 22:03:38 -0700 Subject: [PATCH 0374/1451] bugfix: fix extra dependency annotations These were being displayed regardless of the dep type and state of DepDesc. This is fixed with this commit. Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 2 ++ templates/partials/packages/package_metadata.html | 6 ++++-- test/test_packages_routes.py | 11 ++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 6681d479..698ae1af 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -46,6 +46,8 @@ def dep_extra(dep: PackageDependency) -> str: @register_filter("dep_extra_desc") def dep_extra_desc(dep: PackageDependency) -> str: extra = dep_extra(dep) + if not dep.DepDesc: + return extra return extra + f" – {dep.DepDesc}" diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html index 767e25a9..7ec95699 100644 --- a/templates/partials/packages/package_metadata.html +++ b/templates/partials/packages/package_metadata.html @@ -16,9 +16,11 @@ {% endif %} {{ dep.Package | provides_list(dep.DepName) | safe }} - {% set extra = dep | dep_extra %} - {% if extra %} + + {% if dep.DepTypeID == 4 %} {{ dep | dep_extra_desc }} + {% else %} + {{ dep | dep_extra }} {% endif %}
  • {% endfor %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 44ef7fcd..82fbba40 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -247,7 +247,15 @@ def test_package_dependencies(client: TestClient, maintainer: User, dep_type_name="optdepends", autocommit=False) - broken_dep = create_package_dep(package, "test-dep-5", + # Heh. Another optdepends to test one with a description. + opt_desc_dep_pkg = create_package("test-dep-5", maintainer, + autocommit=False) + opt_desc_dep = create_package_dep(package, opt_desc_dep_pkg.Name, + dep_type_name="optdepends", + autocommit=False) + opt_desc_dep.DepDesc = "Test description." + + broken_dep = create_package_dep(package, "test-dep-6", dep_type_name="depends", autocommit=False) @@ -273,6 +281,7 @@ def test_package_dependencies(client: TestClient, maintainer: User, make_dep.DepName, check_dep.DepName, opt_dep.DepName, + opt_desc_dep.DepName, official_dep.DepName ] pkgdeps = root.findall('.//ul[@id="pkgdepslist"]/li/a') From bace345da4c6ff4f89e6cda0f916d8789ea5dba8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 8 Aug 2021 18:32:35 -0700 Subject: [PATCH 0375/1451] Docker: support both '%' and 'localhost' in mariadb This is needed to be able to reach the mysql service from other hosts or through localhost. Handling both cases here means that we can support both localhost access and host access. Signed-off-by: Kevin Morris --- docker/mariadb-entrypoint.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docker/mariadb-entrypoint.sh b/docker/mariadb-entrypoint.sh index e38900c8..945a4b82 100755 --- a/docker/mariadb-entrypoint.sh +++ b/docker/mariadb-entrypoint.sh @@ -18,15 +18,19 @@ DATABASE="aurweb" # Persistent database for fastapi/php-fpm. TEST_DB="aurweb_test" # Test database (ephemereal). echo "Taking care of primary database '${DATABASE}'..." -mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'$DB_HOST' IDENTIFIED BY 'aur';" +mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'localhost' IDENTIFIED BY 'aur';" +mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" mysql -u root -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" -mysql -u root -e "GRANT ALL ON ${DATABASE}.* TO 'aur'@'$DB_HOST';" +mysql -u root -e "GRANT ALL ON ${DATABASE}.* TO 'aur'@'localhost';" +mysql -u root -e "GRANT ALL ON ${DATABASE}.* TO 'aur'@'%';" # Drop and create our test database. echo "Dropping test database '$TEST_DB'..." mysql -u root -e "DROP DATABASE IF EXISTS $TEST_DB;" mysql -u root -e "CREATE DATABASE $TEST_DB;" -mysql -u root -e "GRANT ALL ON ${TEST_DB}.* TO 'aur'@'$DB_HOST';" +mysql -u root -e "GRANT ALL ON ${TEST_DB}.* TO 'aur'@'localhost';" +mysql -u root -e "GRANT ALL ON ${TEST_DB}.* TO 'aur'@'%';" + echo "Created new '$TEST_DB'!" mysqladmin -uroot shutdown From 4ade8b053992663242df361477ca26282cadfc20 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 6 Aug 2021 00:59:38 -0700 Subject: [PATCH 0376/1451] routers.packages: Simplify some existence checks Signed-off-by: Kevin Morris --- aurweb/auth.py | 15 ++++++++ aurweb/models/package_dependency.py | 2 +- aurweb/packages/util.py | 18 ++++------ aurweb/routers/packages.py | 44 +++++++++++------------ templates/partials/packages/comments.html | 2 +- templates/partials/packages/details.html | 10 +++--- 6 files changed, 48 insertions(+), 43 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 316e7293..26e4073d 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -19,6 +19,17 @@ from aurweb.models.user import User from aurweb.templates import make_variable_context, render_template +class StubQuery: + """ Acts as a stubbed version of an orm.Query. Typically used + to masquerade fake records for an AnonymousUser. """ + + def filter(self, *args): + return StubQuery() + + def scalar(self): + return 0 + + class AnonymousUser: # Stub attributes used to mimic a real user. ID = 0 @@ -28,6 +39,10 @@ class AnonymousUser: # A stub ssh_pub_key relationship. ssh_pub_key = None + # Add stubbed relationship backrefs. + package_notifications = StubQuery() + package_votes = StubQuery() + # A nonce attribute, needed for all browser sessions; set in __init__. nonce = None diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index 0e5b028b..9ce0b019 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -69,4 +69,4 @@ class PackageDependency(Base): pkg = db.query(Package, Package.Name == self.DepName) official = db.query(OfficialProvider, OfficialProvider.Name == self.DepName) - return pkg.count() > 0 or official.count() > 0 + return pkg.scalar() or official.scalar() diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 698ae1af..60db2962 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -54,10 +54,9 @@ def dep_extra_desc(dep: PackageDependency) -> str: @register_filter("pkgname_link") def pkgname_link(pkgname: str) -> str: base = "/".join([OFFICIAL_BASE, "packages"]) - pkg = db.query(Package).filter(Package.Name == pkgname) official = db.query(OfficialProvider).filter( OfficialProvider.Name == pkgname) - if not pkg.count() or official.count(): + if official.scalar(): return f"{base}/?q={pkgname}" return f"/packages/{pkgname}" @@ -67,7 +66,7 @@ def package_link(package: Package) -> str: base = "/".join([OFFICIAL_BASE, "packages"]) official = db.query(OfficialProvider).filter( OfficialProvider.Name == package.Name) - if official.count(): + if official.scalar(): return f"{base}/?q={package.Name}" return f"/packages/{package.Name}" @@ -82,19 +81,14 @@ def provides_list(package: Package, depname: str) -> list: ) ) - string = str() - has_providers = providers.count() > 0 - - if has_providers: - string += "(" - - string += ", ".join([ + string = ", ".join([ f'{pkg.Name}' for pkg in providers ]) - if has_providers: - string += ")" + if string: + # If we actually constructed a string, wrap it. + string = f"({string})" return string diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index cb7f4a18..0ffcbfb9 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -19,9 +19,9 @@ from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote -from aurweb.models.relation_type import CONFLICTS_ID, RelationType +from aurweb.models.relation_type import CONFLICTS_ID from aurweb.packages.util import get_pkgbase -from aurweb.templates import make_variable_context, render_template +from aurweb.templates import make_context, render_template router = APIRouter() @@ -34,7 +34,7 @@ async def make_single_context(request: Request, :param pkgbase: PackageBase instance :return: A pkgbase context without specific differences """ - context = await make_variable_context(request, pkgbase.Name) + context = make_context(request, pkgbase.Name) context["git_clone_uri_anon"] = aurweb.config.get("options", "git_clone_uri_anon") context["git_clone_uri_priv"] = aurweb.config.get("options", @@ -44,20 +44,15 @@ async def make_single_context(request: Request, context["keywords"] = pkgbase.keywords context["comments"] = pkgbase.comments context["is_maintainer"] = (request.user.is_authenticated() - and request.user == pkgbase.Maintainer) - context["notified"] = db.query( - PackageNotification).join(PackageBase).filter( - and_(PackageBase.ID == pkgbase.ID, - PackageNotification.UserID == request.user.ID)).count() > 0 + and request.user.ID == pkgbase.MaintainerUID) + context["notified"] = request.user.package_notifications.filter( + PackageNotification.PackageBaseID == pkgbase.ID + ).scalar() context["out_of_date"] = bool(pkgbase.OutOfDateTS) - context["voted"] = pkgbase.package_votes.filter( - PackageVote.UsersID == request.user.ID).count() > 0 - - context["notifications_enabled"] = db.query( - PackageNotification).join(PackageBase).filter( - PackageBase.ID == pkgbase.ID).count() > 0 + context["voted"] = request.user.package_votes.filter( + PackageVote.PackageBaseID == pkgbase.ID).scalar() return context @@ -71,13 +66,12 @@ async def package(request: Request, name: str) -> Response: context = await make_single_context(request, pkgbase) # Package sources. - sources = db.query(PackageSource).join(Package).filter( - Package.PackageBaseID == pkgbase.ID) - context["sources"] = sources + context["sources"] = db.query(PackageSource).join(Package).join( + PackageBase).filter(PackageBase.ID == pkgbase.ID) # Package dependencies. - dependencies = db.query(PackageDependency).join(Package).filter( - Package.PackageBaseID == pkgbase.ID) + dependencies = db.query(PackageDependency).join(Package).join( + PackageBase).filter(PackageBase.ID == pkgbase.ID) context["dependencies"] = dependencies # Package requirements (other packages depend on this one). @@ -86,13 +80,15 @@ async def package(request: Request, name: str) -> Response: Package.Name.asc()) context["required_by"] = required_by - licenses = db.query(License).join(PackageLicense).join(Package).filter( - PackageLicense.PackageID == pkgbase.packages.first().ID) + licenses = db.query(License).join(PackageLicense).join(Package).join( + PackageBase).filter(PackageBase.ID == pkgbase.ID) context["licenses"] = licenses - conflicts = db.query(PackageRelation).join(RelationType).join(Package).join(PackageBase).filter( - and_(RelationType.ID == CONFLICTS_ID, - PackageBase.ID == pkgbase.ID)) + conflicts = db.query(PackageRelation).join(Package).join( + PackageBase).filter( + and_(PackageRelation.RelTypeID == CONFLICTS_ID, + PackageBase.ID == pkgbase.ID) + ) context["conflicts"] = conflicts return render_template(request, "packages/show.html", context) diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index f1bc020d..051849b0 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -49,7 +49,7 @@ {% endif %} -{% if comments.count() %} +{% if comments.scalar() %}

    diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index a25e9c9e..83c6d53b 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -1,3 +1,4 @@ +{% set pkg = pkgbase.packages.first() %} @@ -19,12 +20,11 @@ - + {% endif %} - {% if pkgbase.keywords.count() %} + {% if pkgbase.keywords.scalar() %} {% if is_maintainer %} @@ -63,13 +63,13 @@ {% endif %} {% endif %} - {% if licenses and licenses.count() and show_package_details | default(False) %} + {% if licenses and licenses.scalar() and show_package_details %} {% endif %} - {% if show_package_details | default(False) %} + {% if show_package_details %} - - + {% if request.user.is_authenticated() %} + + + {% endif %} @@ -21,33 +23,33 @@ {{ pkg.Name }} - {% if flagged %} - - {% else %} - - {% endif %} + {{ pkg.Version }} - - - + + {% if request.user.is_authenticated() %} + + + {% endif %} {% endfor %} From f147ef34767a91c0171b6d69ca2a2749ea0f4994 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 18 Aug 2021 22:14:35 -0700 Subject: [PATCH 0392/1451] models.account_type: remove duplicated constants Clearly made in mistake, removing to keep things organized. Signed-off-by: Kevin Morris --- aurweb/models/account_type.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py index 0db37ced..2e3dde06 100644 --- a/aurweb/models/account_type.py +++ b/aurweb/models/account_type.py @@ -28,12 +28,6 @@ class AccountType(Base): self.ID, str(self)) -# Define some AccountType.AccountType constants. -USER = "User" -TRUSTED_USER = "Trusted User" -DEVELOPER = "Developer" -TRUSTED_USER_AND_DEV = "Trusted User & Developer" - # Fetch account type IDs from the database for constants. _account_types = db.query(AccountType) USER_ID = _account_types.filter( From fb908189b61cce6241d8b4afdb4010ea279dfeea Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sat, 28 Aug 2021 17:18:32 -0500 Subject: [PATCH 0393/1451] Began port of dependencies to pip Adds Python dependencies to requirements list to allow installation via pip --- requirements.txt | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..1c760057 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,40 @@ +# Arch Linux +pyalpm==0.10.6 +srcinfo==0.0.8 + +# Generic +authlib==0.15.2 +aiofiles==0.7.0 +asgiref==3.4.1 +bcrypt==3.2.0 +bleach==3.3.1 +coverage==5.5 +email-validator==1.1.3 +fakeredis==1.6.0 +fastapi==0.66.0 +feedgen==0.9.0 +flake8==3.9.2 +httpx==0.18.2 +hypercorn==0.11.2 +isort==5.9.3 +itsdangerous==2.0.1 +jinja2==3.0.1 +lxml==4.6.3 +markdown==3.3.4 +orjson==3.6.3 +protobuf==3.17.3 +pygit2==1.6.1 +pytest==6.2.4 +pytest-asyncio==0.15.1 +pytest-cov==2.12.1 +pytest-tap==3.2 +python-multipart==0.0.5 +redis==3.5.3 +requests==2.26.0 +uvicorn==0.15.0 +werkzeug==2.0.1 + +# SQL +alembic==1.6.5 +sqlalchemy==1.3.23 +mysqlclient==2.0.3 From b88fa8386ae9359fbf0d7cabf6efd008bd4d760b Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sat, 28 Aug 2021 19:25:51 -0500 Subject: [PATCH 0394/1451] Removed pyalpm and srcinfo from pip requirements; Changed section title Changed 'Generic' to 'General' --- requirements.txt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1c760057..37a12f61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,4 @@ -# Arch Linux -pyalpm==0.10.6 -srcinfo==0.0.8 - -# Generic +# General authlib==0.15.2 aiofiles==0.7.0 asgiref==3.4.1 From 0075ba3c33c18831e277ddfceac53d8653a9dcc6 Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sat, 28 Aug 2021 19:27:36 -0500 Subject: [PATCH 0395/1451] Added .python-version from Pyenv --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4fdfa790..885d9c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ doc/rpc.html # Ignore any user-configured .envrc files at the root. /.envrc + +# Ignore .python-version file from Pyenv +.python-version From e69004bc4a135551fc89581147947fb8c8f0e68c Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sat, 28 Aug 2021 19:29:44 -0500 Subject: [PATCH 0396/1451] Alphabetized .gitignore file so it looks prettier --- .gitignore | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 885d9c2f..f7ec5a95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,5 @@ -dummy-data.sql* -po/*.mo -po/*.po~ -po/POTFILES -web/locale/*/ -aur.git/ __pycache__/ *.py[cod] -test/test-results/ -test/trash directory* -schema/aur-schema-sqlite.sql -data.sql -aurweb.sqlite3 -conf/config -conf/config.sqlite -conf/config.sqlite.defaults -conf/docker -conf/docker.defaults -htmlcov/ -fastapi_aw/ .vim/ .pylintrc .coverage @@ -28,6 +10,24 @@ fastapi_aw/ /dist/ /aurweb.egg-info/ /pyrightconfig.json +aur.git/ +aurweb.sqlite3 +conf/config +conf/config.sqlite +conf/config.sqlite.defaults +conf/docker +conf/docker.defaults +data.sql +dummy-data.sql* +fastapi_aw/ +htmlcov/ +po/*.mo +po/*.po~ +po/POTFILES +schema/aur-schema-sqlite.sql +test/test-results/ +test/trash directory* +web/locale/*/ # Do not stage compiled asciidoc: make -C doc doc/rpc.html From e61050adcf41141d43a2018265b1c38b0c4031e4 Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sat, 28 Aug 2021 19:31:11 -0500 Subject: [PATCH 0397/1451] Added env/ to .gitignore Folder will be used under virtualenv for pip dependencies --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f7ec5a95..581d5c17 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ conf/docker conf/docker.defaults data.sql dummy-data.sql* +env/ fastapi_aw/ htmlcov/ po/*.mo From 85b1a05d0138888e2f06c8fc5b474314abd090e9 Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sat, 28 Aug 2021 19:51:05 -0500 Subject: [PATCH 0398/1451] Removed pip dependencies from docker/scripts/install-deps.sh --- docker/scripts/install-deps.sh | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index a532a6b2..d22fd460 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -5,16 +5,12 @@ set -eou pipefail pacman -Syu --noconfirm --noprogressbar \ - --cachedir .pkg-cache git gpgme protobuf pyalpm \ - python-mysqlclient python-pygit2 python-srcinfo python-bleach \ - python-markdown python-sqlalchemy python-alembic python-pytest \ - python-werkzeug python-pytest-tap python-fastapi nginx python-authlib \ - python-itsdangerous python-httpx python-jinja python-pytest-cov \ - python-requests python-aiofiles python-python-multipart \ - python-pytest-asyncio python-coverage hypercorn python-bcrypt \ - python-email-validator openssh python-lxml mariadb mariadb-libs \ - python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ - python-asgiref uvicorn python-feedgen memcached php-memcached \ - python-redis redis python-fakeredis python-orjson + --cachedir .pkg-cache git gpgme \ + nginx redis openssh \ + mariadb mariadb-libs \ + cgit uwsgi uwsgi-plugin-cgi \ + php php-fpm \ + memcached php-memcached \ + pyalpm python-srcinfo exec "$@" From eff7d478ab29d541c7a486cccd27c23d0e803e5d Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sat, 28 Aug 2021 20:12:35 -0500 Subject: [PATCH 0399/1451] Updated CI tests for pip dependencies; Changed styling in install-deps.sh --- .gitlab-ci.yml | 1 + Dockerfile | 26 +++++++++++++------------- docker/scripts/install-deps.sh | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b8da2ae..d360d483 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,7 @@ variables: before_script: - ./docker/scripts/install-deps.sh + - pip install -r requirements.txt - useradd -U -d /aurweb -c 'AUR User' aur - ./docker/mariadb-entrypoint.sh - (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & diff --git a/Dockerfile b/Dockerfile index 2843fa1b..b610b8c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,19 @@ FROM archlinux:base-devel ENV PYTHONPATH=/aurweb ENV AUR_CONFIG=conf/config -# Copy our single bootstrap script. -COPY docker/scripts/install-deps.sh /install-deps.sh -RUN /install-deps.sh +# Copy Docker scripts +COPY ./docker /docker +COPY ./docker/scripts/*.sh /usr/local/bin/ + +# Copy over all aurweb files. +COPY . /aurweb + +# Working directory is aurweb root @ /aurweb. +WORKDIR /aurweb + +# Install dependencies +RUN docker/scripts/install-deps.sh +RUN pip install -r requirements.txt # Add our aur user. RUN useradd -U -d /aurweb -c 'AUR User' aur @@ -13,16 +23,6 @@ RUN useradd -U -d /aurweb -c 'AUR User' aur # Setup some default system stuff. RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime -# Copy the rest of docker. -COPY ./docker /docker -COPY ./docker/scripts/*.sh /usr/local/bin/ - -# Copy from host to container. -COPY . /aurweb - -# Working directory is aurweb root @ /aurweb. -WORKDIR /aurweb - # Install translations. RUN make -C po all install diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index d22fd460..4985fe85 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -11,6 +11,6 @@ pacman -Syu --noconfirm --noprogressbar \ cgit uwsgi uwsgi-plugin-cgi \ php php-fpm \ memcached php-memcached \ - pyalpm python-srcinfo + python-pip pyalpm python-srcinfo exec "$@" From a0be0185475e38adcb628b776f440efc5685c23d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 28 Aug 2021 21:30:36 -0700 Subject: [PATCH 0400/1451] Docker: Reorder dependency installation for cache purposes Signed-off-by: Kevin Morris --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b610b8c1..6539bd94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,14 +7,16 @@ ENV AUR_CONFIG=conf/config COPY ./docker /docker COPY ./docker/scripts/*.sh /usr/local/bin/ +# Install system-wide dependencies. +RUN /docker/scripts/install-deps.sh + # Copy over all aurweb files. COPY . /aurweb # Working directory is aurweb root @ /aurweb. WORKDIR /aurweb -# Install dependencies -RUN docker/scripts/install-deps.sh +# Install pip directories now that we have access to /aurweb. RUN pip install -r requirements.txt # Add our aur user. From 1c26ce52a5e83abe99d5bea055aac8231b97428f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 30 Aug 2021 18:17:14 -0700 Subject: [PATCH 0401/1451] [FastAPI] include DepArch in dependency list Signed-off-by: Kevin Morris --- templates/partials/packages/package_metadata.html | 3 +++ test/test_packages_routes.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html index 7ec95699..e7b1aefb 100644 --- a/templates/partials/packages/package_metadata.html +++ b/templates/partials/packages/package_metadata.html @@ -16,6 +16,9 @@ {% endif %} {{ dep.Package | provides_list(dep.DepName) | safe }} + {% if dep.DepArch %} + ({{ dep.DepArch }}) + {% endif %} {% if dep.DepTypeID == 4 %} {{ dep | dep_extra_desc }} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 82fbba40..0c9d80e8 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -228,6 +228,7 @@ def test_package_dependencies(client: TestClient, maintainer: User, # Create a normal dependency of type depends. dep_pkg = create_package("test-dep-1", maintainer, autocommit=False) dep = create_package_dep(package, dep_pkg.Name, autocommit=False) + dep.DepArch = "x86_64" # Also, create a makedepends. make_dep_pkg = create_package("test-dep-2", maintainer, autocommit=False) @@ -288,6 +289,11 @@ def test_package_dependencies(client: TestClient, maintainer: User, for i, expectation in enumerate(expected): assert pkgdeps[i].text.strip() == expectation + # Let's make sure the DepArch was displayed for our first dep. + arch = root.findall('.//ul[@id="pkgdepslist"]/li')[0] + arch = arch.xpath('./em')[1] + assert arch.text.strip() == "(x86_64)" + broken_node = root.find('.//ul[@id="pkgdepslist"]/li/span') assert broken_node.text.strip() == broken_dep.DepName From 45fbf214b46fd5e3f90157ee2aaf94cdf62e62f4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 20 Aug 2021 16:29:04 -0700 Subject: [PATCH 0402/1451] jinja2: add 'tn' filter, a numerical translation The possibly plural version of `tr`, `tn` provides a way to translate strings into singular or plural form based on a given integer being 1 or not 1. Example use: ``` {{ 1 | tn("%d package found.", "%d packages found.") | format(1) }} ``` Signed-off-by: Kevin Morris --- aurweb/l10n.py | 18 ++++++++++++++++++ aurweb/templates.py | 3 ++- test/test_l10n.py | 12 ++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index 9270f3ce..c4938d64 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -93,3 +93,21 @@ def tr(context: typing.Any, value: str): """ A translation filter; example: {{ "Hello" | tr("de") }}. """ _ = get_translator_for_request(context.get("request")) return _(value) + + +@pass_context +def tn(context: typing.Dict[str, typing.Any], count: int, + singular: str, plural: str) -> str: + """ A singular and plural translation filter. + + Example: + {{ some_integer | tn("singular %d", "plural %d") }} + + :param context: Response context + :param count: The number used to decide singular or plural state + :param singular: The singular translation + :param plural: The plural translation + :return: Translated string + """ + gettext = get_raw_translator_for_request(context.get("request")) + return gettext.ngettext(singular, plural, count) diff --git a/aurweb/templates.py b/aurweb/templates.py index fa7aa039..7530472f 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -23,8 +23,9 @@ loader = jinja2.FileSystemLoader(os.path.join( env = jinja2.Environment(loader=loader, autoescape=True, extensions=["jinja2.ext.i18n"]) -# Add tr translation filter. +# Add t{r,n} translation filters. env.filters["tr"] = l10n.tr +env.filters["tn"] = l10n.tn # Utility filters. env.filters["dt"] = util.timestamp_to_datetime diff --git a/test/test_l10n.py b/test/test_l10n.py index e833cd44..1c2ae95a 100644 --- a/test/test_l10n.py +++ b/test/test_l10n.py @@ -36,3 +36,15 @@ def test_get_translator_for_request(): translate = l10n.get_translator_for_request(request) assert translate("Home") == "Startseite" + + +def test_tn_filter(): + request = Request() + request.cookies["AURLANG"] = "en" + context = {"language": "en", "request": request} + + translated = l10n.tn(context, 1, "%d package found.", "%d packages found.") + assert translated == "%d package found." + + translated = l10n.tn(context, 2, "%d package found.", "%d packages found.") + assert translated == "%d packages found." From 55c29c4519c200f32ad2463a63f5a46f7a850f2c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 30 Aug 2021 18:27:17 -0700 Subject: [PATCH 0403/1451] partials/packages/details.html: Add package request count This was missed during the original implementation merge. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 5 +++ templates/partials/packages/actions.html | 8 ++++- test/test_packages_routes.py | 45 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 0ffcbfb9..0873bd9f 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -17,6 +17,7 @@ from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_license import PackageLicense from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation +from aurweb.models.package_request import PackageRequest from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID @@ -54,6 +55,10 @@ async def make_single_context(request: Request, context["voted"] = request.user.package_votes.filter( PackageVote.PackageBaseID == pkgbase.ID).scalar() + context["requests"] = pkgbase.requests.filter( + PackageRequest.ClosedTS.is_(None) + ).count() + return context diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 87db3a3f..346537be 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -124,7 +124,13 @@ {% endif %} -
  • + {% if requests %} +
  • + + {{ requests | tn("%d pending request", "%d pending requests") | format(requests) }} + +
  • + {% endif %}
  • {% if not request.user.is_authenticated() %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 0c9d80e8..ad07ec17 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -15,7 +15,9 @@ from aurweb.models.package_comment import PackageComment from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_relation import PackageRelation +from aurweb.models.package_request import PackageRequest from aurweb.models.relation_type import PROVIDES_ID, RelationType +from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User from aurweb.testing import setup_test_db from aurweb.testing.html import parse_root @@ -173,6 +175,43 @@ def test_package_comments(client: TestClient, user: User, package: Package): assert comments[i].strip() == row +def test_package_requests_display(client: TestClient, user: User, + package: Package): + type_ = db.query(RequestType, RequestType.ID == DELETION_ID).first() + db.create(PackageRequest, PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + User=user, RequestType=type_, + Comments="Test comment.", + ClosureComment=str()) + + # Test that a single request displays "1 pending request". + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + selector = '//div[@id="actionlist"]/ul/li/span[@class="flagged"]' + target = root.xpath(selector)[0] + assert target.text.strip() == "1 pending request" + + type_ = db.query(RequestType, RequestType.ID == DELETION_ID).first() + db.create(PackageRequest, PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + User=user, RequestType=type_, + Comments="Test comment2.", + ClosureComment=str()) + + # Test that a two requests display "2 pending requests". + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + selector = '//div[@id="actionlist"]/ul/li/span[@class="flagged"]' + target = root.xpath(selector)[0] + assert target.text.strip() == "2 pending requests" + + def test_package_authenticated(client: TestClient, user: User, package: Package): """ We get the same here for either authenticated or not @@ -196,6 +235,12 @@ def test_package_authenticated(client: TestClient, user: User, for expected_text in expected: assert expected_text in resp.text + # When no requests are up, make sure we don't see the display for them. + root = parse_root(resp.text) + selector = '//div[@id="actionlist"]/ul/li/span[@class="flagged"]' + target = root.xpath(selector) + assert len(target) == 0 + def test_package_authenticated_maintainer(client: TestClient, maintainer: User, From 718ae1acba880d6ecf7488e88c3ac90bd357d493 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 30 Aug 2021 22:14:31 -0700 Subject: [PATCH 0404/1451] aurweb.templates: loader -> _loader, env -> _env These are module local globals and we don't want to expose global functionality to users, so privatize them with a leading `_` prefix. These things should **really** not be accessible by users. --- aurweb/templates.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/aurweb/templates.py b/aurweb/templates.py index 7530472f..48391b4a 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -18,29 +18,29 @@ import aurweb.config from aurweb import captcha, l10n, time, util # Prepare jinja2 objects. -loader = jinja2.FileSystemLoader(os.path.join( +_loader = jinja2.FileSystemLoader(os.path.join( aurweb.config.get("options", "aurwebdir"), "templates")) -env = jinja2.Environment(loader=loader, autoescape=True, - extensions=["jinja2.ext.i18n"]) +_env = jinja2.Environment(loader=_loader, autoescape=True, + extensions=["jinja2.ext.i18n"]) # Add t{r,n} translation filters. -env.filters["tr"] = l10n.tr -env.filters["tn"] = l10n.tn +_env.filters["tr"] = l10n.tr +_env.filters["tn"] = l10n.tn # Utility filters. -env.filters["dt"] = util.timestamp_to_datetime -env.filters["as_timezone"] = util.as_timezone -env.filters["dedupe_qs"] = util.dedupe_qs -env.filters["urlencode"] = quote_plus -env.filters["get_vote"] = util.get_vote -env.filters["number_format"] = util.number_format +_env.filters["dt"] = util.timestamp_to_datetime +_env.filters["as_timezone"] = util.as_timezone +_env.filters["dedupe_qs"] = util.dedupe_qs +_env.filters["urlencode"] = quote_plus +_env.filters["get_vote"] = util.get_vote +_env.filters["number_format"] = util.number_format # Add captcha filters. -env.filters["captcha_salt"] = captcha.captcha_salt_filter -env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter +_env.filters["captcha_salt"] = captcha.captcha_salt_filter +_env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter # Add account utility filters. -env.filters["account_url"] = util.account_url +_env.filters["account_url"] = util.account_url def register_filter(name: str) -> Callable: @@ -61,9 +61,9 @@ def register_filter(name: str) -> Callable: @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) - if name in env.filters: + if name in _env.filters: raise KeyError(f"Jinja already has a filter named '{name}'") - env.filters[name] = wrapper + _env.filters[name] = wrapper return wrapper return decorator @@ -105,12 +105,12 @@ def render_template(request: Request, status_code: HTTPStatus = HTTPStatus.OK): """ Render a Jinja2 multi-lingual template with some context. """ - # Create a deep copy of our jinja2 environment. The environment in + # Create a deep copy of our jinja2 _environment. The _environment in # total by itself is 48 bytes large (according to sys.getsizeof). # This is done so we can install gettext translations on the template - # environment being rendered without installing them into a global + # _environment being rendered without installing them into a global # which is reused in this function. - templates = copy.copy(env) + templates = copy.copy(_env) translator = l10n.get_raw_translator_for_request(context.get("request")) templates.install_gettext_translations(translator) From e15a18e9fb05afac2ddc1fdc12965b80eafa5435 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 30 Aug 2021 23:04:55 -0700 Subject: [PATCH 0405/1451] remove unneeded comment Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 0873bd9f..a20c97b1 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -111,6 +111,6 @@ async def package_base(request: Request, name: str) -> Response: # Add our base information. context = await make_single_context(request, pkgbase) - context["packages"] = pkgbase.packages.all() # Doesn't need to be here. + context["packages"] = pkgbase.packages.all() return render_template(request, "pkgbase.html", context) From 49cc12f99dbe2bc69edd09db39692ad2135473af Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 20 Aug 2021 14:44:36 -0700 Subject: [PATCH 0406/1451] jinja2: rename filter 'urlencode' to 'quote_plus' urlencode does more than just a quote_plus. Using urlencode was not sensible, so this commit addresses that. Signed-off-by: Kevin Morris --- aurweb/templates.py | 2 +- aurweb/util.py | 3 +++ templates/partials/packages/actions.html | 6 +++--- templates/partials/tu/proposal/voters.html | 2 +- templates/partials/tu/proposals.html | 6 +++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/aurweb/templates.py b/aurweb/templates.py index 48391b4a..a648d5a1 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -31,7 +31,7 @@ _env.filters["tn"] = l10n.tn _env.filters["dt"] = util.timestamp_to_datetime _env.filters["as_timezone"] = util.as_timezone _env.filters["dedupe_qs"] = util.dedupe_qs -_env.filters["urlencode"] = quote_plus +_env.filters["quote_plus"] = quote_plus _env.filters["get_vote"] = util.get_vote _env.filters["number_format"] = util.number_format diff --git a/aurweb/util.py b/aurweb/util.py index 860bdd12..494a988d 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,4 +1,5 @@ import base64 +import logging import math import random import re @@ -18,6 +19,8 @@ from jinja2 import pass_context import aurweb.config +logger = logging.getLogger(__name__) + def make_random_string(length): return ''.join(random.choices(string.ascii_lowercase diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 346537be..d552f2dd 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -42,12 +42,12 @@
  • {% endif %}
  • - + {{ "Vote for this package" | tr }}
  • - + {{ "Enable notifications" | tr }}
  • @@ -133,7 +133,7 @@ {% endif %}
  • {% if not request.user.is_authenticated() %} - + {{ "Submit Request" | tr }} {% else %} diff --git a/templates/partials/tu/proposal/voters.html b/templates/partials/tu/proposal/voters.html index 2fd42bdf..6069f97d 100644 --- a/templates/partials/tu/proposal/voters.html +++ b/templates/partials/tu/proposal/voters.html @@ -2,7 +2,7 @@
      {% for voter in voters %}
    • - + {{ voter.Username | e }}
    • diff --git a/templates/partials/tu/proposals.html b/templates/partials/tu/proposals.html index 13e705fc..ab90444e 100644 --- a/templates/partials/tu/proposals.html +++ b/templates/partials/tu/proposals.html @@ -23,7 +23,7 @@
  • @@ -97,7 +97,7 @@ {% set off_qs = "%s=%d" | format(off_param, off - 10) %} {% set by_qs = "%s=%s" | format(by_param, by | quote_plus) %} + href="?{{ q | extend_query([off_param, ([off - 10, 0] | max)], [by_param, by]) | urlencode }}"> ‹ Back {% endif %} @@ -106,7 +106,7 @@ {% set off_qs = "%s=%d" | format(off_param, off + 10) %} {% set by_qs = "%s=%s" | format(by_param, by | quote_plus) %} + href="?{{ q | extend_query([off_param, off + pp], [by_param, by]) | urlencode }}"> Next › {% endif %} diff --git a/test/test_util.py b/test/test_util.py index 06fc08d3..0cc45409 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,4 +1,3 @@ -from collections import OrderedDict from datetime import datetime from zoneinfo import ZoneInfo @@ -17,21 +16,6 @@ def test_as_timezone(): assert util.as_timezone(dt, "UTC") == dt.astimezone(tz=ZoneInfo("UTC")) -def test_dedupe_qs(): - items = OrderedDict() - items["key1"] = "test" - items["key2"] = "blah" - items["key3"] = 1 - - # Construct and test our query string. - query_string = '&'.join([f"{k}={v}" for k, v in items.items()]) - assert query_string == "key1=test&key2=blah&key3=1" - - # Add key1=changed and key2=changed to the query and dedupe it. - deduped = util.dedupe_qs(query_string, "key1=changed", "key3=changed") - assert deduped == "key2=blah&key1=changed&key3=changed" - - def test_number_format(): assert util.number_format(0.222, 2) == "0.22" assert util.number_format(0.226, 2) == "0.23" From b52059d43758e56729e475e01039b4ed19a64fab Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 31 Aug 2021 17:13:10 -0700 Subject: [PATCH 0410/1451] RPC: add deprecation warning for v1-v4 usage With FastAPI starting to come closer to a close, we've got to advertise this deprecation so that users have some time to adjust before making the changes. We have not specified a specific time here, but we'd like this message to reach users of the RPC API for at least a month before any modifications are made to the interface. Signed-off-by: Kevin Morris --- web/lib/aurjson.class.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php index 86eae22b..e7bc7f97 100644 --- a/web/lib/aurjson.class.php +++ b/web/lib/aurjson.class.php @@ -272,6 +272,15 @@ class AurJSON { 'results' => $data ); + if ($this->version != 5) { + $json_array['warning'] = 'The use of versions lower than 5 is ' + . 'now deprecated and will soon be unsupported. To ensure ' + . 'your API client supports the change without issue, it ' + . 'should use version 5 and adjust for any changes in the ' + . 'API interface. See https://aur.archlinux.org/rpc for ' + . 'documentation related to v5.'; + } + if ($error) { $json_array['error'] = $error; } From cfa95ef80ad758f1804896f582c73a8f907d9686 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 31 Aug 2021 17:13:10 -0700 Subject: [PATCH 0411/1451] RPC: add deprecation warning for v1-v4 usage With FastAPI starting to come closer to a close, we've got to advertise this deprecation so that users have some time to adjust before making the changes. We have not specified a specific time here, but we'd like this message to reach users of the RPC API for at least a month before any modifications are made to the interface. Signed-off-by: Kevin Morris --- web/lib/aurjson.class.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php index 86eae22b..e7bc7f97 100644 --- a/web/lib/aurjson.class.php +++ b/web/lib/aurjson.class.php @@ -272,6 +272,15 @@ class AurJSON { 'results' => $data ); + if ($this->version != 5) { + $json_array['warning'] = 'The use of versions lower than 5 is ' + . 'now deprecated and will soon be unsupported. To ensure ' + . 'your API client supports the change without issue, it ' + . 'should use version 5 and adjust for any changes in the ' + . 'API interface. See https://aur.archlinux.org/rpc for ' + . 'documentation related to v5.'; + } + if ($error) { $json_array['error'] = $error; } From a5943bf2add0231925d7836e2e0b587a4f5c7f05 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 2 Sep 2021 16:26:48 -0700 Subject: [PATCH 0412/1451] [FastAPI] Refactor db modifications For SQLAlchemy to automatically understand updates from the external world, it must use an `autocommit=True` in its session. This change breaks how we were using commit previously, as `autocommit=True` causes SQLAlchemy to commit when a SessionTransaction context hits __exit__. So, a refactoring was required of our tests: All usage of any `db.{create,delete}` must be called **within** a SessionTransaction context, created via new `db.begin()`. From this point forward, we're going to require: ``` with db.begin(): db.create(...) db.delete(...) db.session.delete(object) ``` With this, we now get external DB modifications automatically without reloading or restarting the FastAPI server, which we absolutely need for production. Signed-off-by: Kevin Morris --- aurweb/db.py | 44 +++++--- aurweb/models/user.py | 42 ++++---- aurweb/routers/accounts.py | 145 +++++++++++++-------------- aurweb/routers/html.py | 6 +- aurweb/routers/trusted_user.py | 20 ++-- test/test_account_type.py | 18 ++-- test/test_accounts_routes.py | 166 ++++++++++++++++--------------- test/test_api_rate_limit.py | 19 ++-- test/test_auth.py | 22 ++-- test/test_auth_routes.py | 9 +- test/test_ban.py | 10 +- test/test_db.py | 22 ++-- test/test_dependency_type.py | 14 ++- test/test_group.py | 11 +- test/test_homepage.py | 49 ++++----- test/test_license.py | 11 +- test/test_official_provider.py | 59 +++++------ test/test_package.py | 68 ++++++------- test/test_package_base.py | 61 ++++++------ test/test_package_blacklist.py | 16 +-- test/test_package_comment.py | 47 +++++---- test/test_package_dependency.py | 84 ++++++++-------- test/test_package_relation.py | 75 +++++++------- test/test_package_request.py | 99 ++++++++++-------- test/test_package_source.py | 23 +++-- test/test_packages_routes.py | 160 +++++++++++++++-------------- test/test_packages_util.py | 27 +++-- test/test_relation_type.py | 15 +-- test/test_request_type.py | 24 +++-- test/test_routes.py | 14 +-- test/test_rss.py | 15 ++- test/test_session.py | 34 ++++--- test/test_ssh_pub_key.py | 32 +++--- test/test_term.py | 19 ++-- test/test_trusted_user_routes.py | 166 ++++++++++++++++--------------- test/test_tu_voteinfo.py | 130 +++++++++++++----------- test/test_user.py | 124 ++++++++++++----------- 37 files changed, 998 insertions(+), 902 deletions(-) diff --git a/aurweb/db.py b/aurweb/db.py index c0147720..ea6b6918 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -59,20 +59,15 @@ def query(model, *args, **kwargs): return session.query(model).filter(*args, **kwargs) -def create(model, autocommit: bool = True, *args, **kwargs): +def create(model, *args, **kwargs): instance = model(*args, **kwargs) - add(instance) - if autocommit is True: - commit() - return instance + return add(instance) -def delete(model, *args, autocommit: bool = True, **kwargs): +def delete(model, *args, **kwargs): instance = session.query(model).filter(*args, **kwargs) for record in instance: session.delete(record) - if autocommit is True: - commit() def rollback(): @@ -84,8 +79,25 @@ def add(model): return model -def commit(): - session.commit() +def begin(): + """ Begin an SQLAlchemy SessionTransaction. + + This context is **required** to perform an modifications to the + database. + + Example: + + with db.begin(): + object = db.create(...) + # On __exit__, db.commit() is run. + + with db.begin(): + object = db.delete(...) + # On __exit__, db.commit() is run. + + :return: A new SessionTransaction based on session + """ + return session.begin() def get_sqlalchemy_url(): @@ -155,23 +167,23 @@ def get_engine(echo: bool = False): connect_args=connect_args, echo=echo) + Session = sessionmaker(autocommit=True, autoflush=False, bind=engine) + session = Session() + if db_backend == "sqlite": # For SQLite, we need to add some custom functions as # they are used in the reference graph method. def regexp(regex, item): return bool(re.search(regex, str(item))) - @event.listens_for(engine, "begin") - def do_begin(conn): + @event.listens_for(engine, "connect") + def do_begin(conn, record): create_deterministic_function = functools.partial( - conn.connection.create_function, + conn.create_function, deterministic=True ) create_deterministic_function("REGEXP", 2, regexp) - Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) - session = Session() - return engine diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 0ccf7329..70d15f88 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -102,7 +102,7 @@ class User(Base): def login(self, request: Request, password: str, session_time=0): """ Login and authenticate a request. """ - from aurweb.db import session + from aurweb import db from aurweb.models.session import Session, generate_unique_sid if not self._login_approved(request): @@ -112,10 +112,7 @@ class User(Base): if not self.authenticated: return None - self.LastLogin = now_ts = datetime.utcnow().timestamp() - self.LastLoginIPAddress = request.client.host - session.commit() - + now_ts = datetime.utcnow().timestamp() session_ts = now_ts + ( session_time if session_time else aurweb.config.getint("options", "login_timeout") @@ -123,22 +120,23 @@ class User(Base): sid = None - if not self.session: - sid = generate_unique_sid() - self.session = Session(UsersID=self.ID, SessionID=sid, - LastUpdateTS=session_ts) - session.add(self.session) - else: - last_updated = self.session.LastUpdateTS - if last_updated and last_updated < now_ts: - self.session.SessionID = sid = generate_unique_sid() + with db.begin(): + self.LastLogin = now_ts + self.LastLoginIPAddress = request.client.host + if not self.session: + sid = generate_unique_sid() + self.session = Session(UsersID=self.ID, SessionID=sid, + LastUpdateTS=session_ts) + db.add(self.session) else: - # Session is still valid; retrieve the current SID. - sid = self.session.SessionID + last_updated = self.session.LastUpdateTS + if last_updated and last_updated < now_ts: + self.session.SessionID = sid = generate_unique_sid() + else: + # Session is still valid; retrieve the current SID. + sid = self.session.SessionID - self.session.LastUpdateTS = session_ts - - session.commit() + self.session.LastUpdateTS = session_ts request.cookies["AURSID"] = self.session.SessionID return self.session.SessionID @@ -149,13 +147,11 @@ class User(Base): return aurweb.auth.has_credential(self, cred, approved) def logout(self, request): - from aurweb.db import session - del request.cookies["AURSID"] self.authenticated = False if self.session: - session.delete(self.session) - session.commit() + with db.begin(): + db.session.delete(self.session) def is_trusted_user(self): return self.AccountType.ID in { diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 466d129d..ef4b99af 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -43,8 +43,6 @@ async def passreset_post(request: Request, resetkey: str = Form(default=None), password: str = Form(default=None), confirm: str = Form(default=None)): - from aurweb.db import session - context = await make_variable_context(request, "Password Reset") # The user parameter being required, we can match against @@ -86,12 +84,11 @@ async def passreset_post(request: Request, # We got to this point; everything matched up. Update the password # and remove the ResetKey. - user.ResetKey = str() - user.update_password(password) - - if user.session: - session.delete(user.session) - session.commit() + with db.begin(): + user.ResetKey = str() + if user.session: + db.session.delete(user.session) + user.update_password(password) # Render ?step=complete. return RedirectResponse(url="/passreset?step=complete", @@ -99,8 +96,8 @@ async def passreset_post(request: Request, # If we got here, we continue with issuing a resetkey for the user. resetkey = db.make_random_value(User, User.ResetKey) - user.ResetKey = resetkey - session.commit() + with db.begin(): + user.ResetKey = resetkey executor = db.ConnectionExecutor(db.get_engine().raw_connection()) ResetKeyNotification(executor, user.ID).send() @@ -364,8 +361,6 @@ async def account_register_post(request: Request, ON: bool = Form(default=False), captcha: str = Form(default=None), captcha_salt: str = Form(...)): - from aurweb.db import session - context = await make_variable_context(request, "Register") args = dict(await request.form()) @@ -394,11 +389,13 @@ async def account_register_post(request: Request, AccountType.AccountType == "User").first() # Create a user given all parameters available. - user = db.create(User, Username=U, Email=E, HideEmail=H, BackupEmail=BE, - RealName=R, Homepage=HP, IRCNick=I, PGPKey=K, - LangPreference=L, Timezone=TZ, CommentNotify=CN, - UpdateNotify=UN, OwnershipNotify=ON, ResetKey=resetkey, - AccountType=account_type) + with db.begin(): + user = db.create(User, Username=U, + Email=E, HideEmail=H, BackupEmail=BE, + RealName=R, Homepage=HP, IRCNick=I, PGPKey=K, + LangPreference=L, Timezone=TZ, CommentNotify=CN, + UpdateNotify=UN, OwnershipNotify=ON, + ResetKey=resetkey, AccountType=account_type) # If a PK was given and either one does not exist or the given # PK mismatches the existing user's SSHPubKey.PubKey. @@ -410,10 +407,10 @@ async def account_register_post(request: Request, # Remove the host part. pubkey = parts[0] + " " + parts[1] fingerprint = get_fingerprint(pubkey) - user.ssh_pub_key = SSHPubKey(UserID=user.ID, - PubKey=pubkey, - Fingerprint=fingerprint) - session.commit() + with db.begin(): + user.ssh_pub_key = SSHPubKey(UserID=user.ID, + PubKey=pubkey, + Fingerprint=fingerprint) # Send a reset key notification to the new user. executor = db.ConnectionExecutor(db.get_engine().raw_connection()) @@ -499,63 +496,67 @@ async def account_edit_post(request: Request, status_code=int(HTTPStatus.BAD_REQUEST)) # Set all updated fields as needed. - user.Username = U or user.Username - user.Email = E or user.Email - user.HideEmail = bool(H) - user.BackupEmail = BE or user.BackupEmail - user.RealName = R or user.RealName - user.Homepage = HP or user.Homepage - user.IRCNick = I or user.IRCNick - user.PGPKey = K or user.PGPKey - user.InactivityTS = datetime.utcnow().timestamp() if J else 0 + with db.begin(): + user.Username = U or user.Username + user.Email = E or user.Email + user.HideEmail = bool(H) + user.BackupEmail = BE or user.BackupEmail + user.RealName = R or user.RealName + user.Homepage = HP or user.Homepage + user.IRCNick = I or user.IRCNick + user.PGPKey = K or user.PGPKey + user.InactivityTS = datetime.utcnow().timestamp() if J else 0 # If we update the language, update the cookie as well. if L and L != user.LangPreference: request.cookies["AURLANG"] = L - user.LangPreference = L + with db.begin(): + user.LangPreference = L context["language"] = L # If we update the timezone, also update the cookie. if TZ and TZ != user.Timezone: - user.Timezone = TZ + with db.begin(): + user.Timezone = TZ request.cookies["AURTZ"] = TZ context["timezone"] = TZ - user.CommentNotify = bool(CN) - user.UpdateNotify = bool(UN) - user.OwnershipNotify = bool(ON) + with db.begin(): + user.CommentNotify = bool(CN) + user.UpdateNotify = bool(UN) + user.OwnershipNotify = bool(ON) # If a PK is given, compare it against the target user's PK. - if PK: - # Get the second token in the public key, which is the actual key. - pubkey = PK.strip().rstrip() - parts = pubkey.split(" ") - if len(parts) == 3: - # Remove the host part. - pubkey = parts[0] + " " + parts[1] - fingerprint = get_fingerprint(pubkey) - if not user.ssh_pub_key: - # No public key exists, create one. - user.ssh_pub_key = SSHPubKey(UserID=user.ID, - PubKey=pubkey, - Fingerprint=fingerprint) - elif user.ssh_pub_key.PubKey != pubkey: - # A public key already exists, update it. - user.ssh_pub_key.PubKey = pubkey - user.ssh_pub_key.Fingerprint = fingerprint - elif user.ssh_pub_key: - # Else, if the user has a public key already, delete it. - session.delete(user.ssh_pub_key) - - # Commit changes, if any. - session.commit() + with db.begin(): + if PK: + # Get the second token in the public key, which is the actual key. + pubkey = PK.strip().rstrip() + parts = pubkey.split(" ") + if len(parts) == 3: + # Remove the host part. + pubkey = parts[0] + " " + parts[1] + fingerprint = get_fingerprint(pubkey) + if not user.ssh_pub_key: + # No public key exists, create one. + user.ssh_pub_key = SSHPubKey(UserID=user.ID, + PubKey=pubkey, + Fingerprint=fingerprint) + elif user.ssh_pub_key.PubKey != pubkey: + # A public key already exists, update it. + user.ssh_pub_key.PubKey = pubkey + user.ssh_pub_key.Fingerprint = fingerprint + elif user.ssh_pub_key: + # Else, if the user has a public key already, delete it. + session.delete(user.ssh_pub_key) if P and not user.valid_password(P): # Remove the fields we consumed for passwords. context["P"] = context["C"] = str() # If a password was given and it doesn't match the user's, update it. - user.update_password(P) + with db.begin(): + user.update_password(P) + if user == request.user: # If the target user is the request user, login with # the updated password and update AURSID. @@ -731,21 +732,17 @@ async def terms_of_service_post(request: Request, accept_needed = sorted(unaccepted + diffs) return render_terms_of_service(request, context, accept_needed) - # For each term we found, query for the matching accepted term - # and update its Revision to the term's current Revision. - for term in diffs: - accepted_term = request.user.accepted_terms.filter( - AcceptedTerm.TermsID == term.ID).first() - accepted_term.Revision = term.Revision + with db.begin(): + # For each term we found, query for the matching accepted term + # and update its Revision to the term's current Revision. + for term in diffs: + accepted_term = request.user.accepted_terms.filter( + AcceptedTerm.TermsID == term.ID).first() + accepted_term.Revision = term.Revision - # For each term that was never accepted, accept it! - for term in unaccepted: - db.create(AcceptedTerm, User=request.user, - Term=term, Revision=term.Revision, - autocommit=False) - - if diffs or unaccepted: - # If we had any terms to update, commit the changes. - db.commit() + # For each term that was never accepted, accept it! + for term in unaccepted: + db.create(AcceptedTerm, User=request.user, + Term=term, Revision=term.Revision) return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index c2375f69..c3fd3db1 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -44,8 +44,6 @@ async def language(request: Request, setting the language on any page, we want to preserve query parameters across the redirect. """ - from aurweb.db import session - if next[0] != '/': return HTMLResponse(b"Invalid 'next' parameter.", status_code=400) @@ -53,8 +51,8 @@ async def language(request: Request, # If the user is authenticated, update the user's LangPreference. if request.user.is_authenticated(): - request.user.LangPreference = set_lang - session.commit() + with db.begin(): + request.user.LangPreference = set_lang # In any case, set the response's AURLANG cookie that never expires. response = RedirectResponse(url=f"{next}{query_string}", diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 61cfec6c..a977b31a 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -214,10 +214,9 @@ async def trusted_user_proposal_post(request: Request, return Response("Invalid 'decision' value.", status_code=int(HTTPStatus.BAD_REQUEST)) - vote = db.create(TUVote, User=request.user, VoteInfo=voteinfo, - autocommit=False) - voteinfo.ActiveTUs += 1 - db.commit() + with db.begin(): + vote = db.create(TUVote, User=request.user, VoteInfo=voteinfo) + voteinfo.ActiveTUs += 1 context["error"] = "You've already voted for this proposal." return render_proposal(request, context, proposal, voteinfo, voters, vote) @@ -294,12 +293,13 @@ async def trusted_user_addvote_post(request: Request, agenda = re.sub(r'<[/]?style.*>', '', agenda) # Create a new TUVoteInfo (proposal)! - voteinfo = db.create(TUVoteInfo, - User=user, - Agenda=agenda, - Submitted=timestamp, End=timestamp + duration, - Quorum=quorum, - Submitter=request.user) + with db.begin(): + voteinfo = db.create(TUVoteInfo, + User=user, + Agenda=agenda, + Submitted=timestamp, End=timestamp + duration, + Quorum=quorum, + Submitter=request.user) # Redirect to the new proposal. return RedirectResponse(f"/tu/{voteinfo.ID}", diff --git a/test/test_account_type.py b/test/test_account_type.py index fa4bc5ad..86e68253 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -1,6 +1,6 @@ import pytest -from aurweb.db import create, delete, query +from aurweb.db import begin, create, delete, query from aurweb.models.account_type import AccountType from aurweb.models.user import User from aurweb.testing import setup_test_db @@ -14,11 +14,13 @@ def setup(): global account_type - account_type = create(AccountType, AccountType="TestUser") + with begin(): + account_type = create(AccountType, AccountType="TestUser") yield account_type - delete(AccountType, AccountType.ID == account_type.ID) + with begin(): + delete(AccountType, AccountType.ID == account_type.ID) def test_account_type(): @@ -38,12 +40,14 @@ def test_account_type(): def test_user_account_type_relationship(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + with begin(): + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) assert user.AccountType == account_type # This must be deleted here to avoid foreign key issues when # deleting the temporary AccountType in the fixture. - delete(User, User.ID == user.ID) + with begin(): + delete(User, User.ID == user.ID) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 567b3426..9120f23f 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -11,9 +11,9 @@ import pytest from fastapi.testclient import TestClient -from aurweb import captcha +from aurweb import captcha, db from aurweb.asgi import app -from aurweb.db import commit, create, query +from aurweb.db import create, query from aurweb.models.accepted_term import AcceptedTerm from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, AccountType from aurweb.models.ban import Ban @@ -57,9 +57,11 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, - RealName="Test UserZ", Passwd="testPassword", - IRCNick="testZ", AccountType=account_type) + + with db.begin(): + user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, + RealName="Test UserZ", Passwd="testPassword", + IRCNick="testZ", AccountType=account_type) yield user @@ -70,9 +72,10 @@ def setup(): @pytest.fixture def tu_user(): - user.AccountType = query(AccountType, - AccountType.ID == TRUSTED_USER_AND_DEV_ID).first() - commit() + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == TRUSTED_USER_AND_DEV_ID + ).first() yield user @@ -149,11 +152,9 @@ def test_post_passreset_user(): def test_post_passreset_resetkey(): - from aurweb.db import session - - user.session = Session(UsersID=user.ID, SessionID="blah", - LastUpdateTS=datetime.utcnow().timestamp()) - session.commit() + with db.begin(): + user.session = Session(UsersID=user.ID, SessionID="blah", + LastUpdateTS=datetime.utcnow().timestamp()) # Prepare a password reset. with client as request: @@ -357,7 +358,8 @@ def test_post_register_error_invalid_captcha(): def test_post_register_error_ip_banned(): # 'testclient' is used as request.client.host via FastAPI TestClient. - create(Ban, IPAddress="testclient", BanTS=datetime.utcnow()) + with db.begin(): + create(Ban, IPAddress="testclient", BanTS=datetime.utcnow()) with client as request: response = post_register(request) @@ -576,7 +578,8 @@ def test_post_register_error_ssh_pubkey_taken(): # Take the sha256 fingerprint of the ssh public key, create it. fp = get_fingerprint(pk) - create(SSHPubKey, UserID=user.ID, PubKey=pk, Fingerprint=fp) + with db.begin(): + create(SSHPubKey, UserID=user.ID, PubKey=pk, Fingerprint=fp) with client as request: response = post_register(request, PK=pk) @@ -660,13 +663,11 @@ def test_post_account_edit(): def test_post_account_edit_dev(): - from aurweb.db import session - # Modify our user to be a "Trusted User & Developer" name = "Trusted User & Developer" tu_or_dev = query(AccountType, AccountType.AccountType == name).first() - user.AccountType = tu_or_dev - session.commit() + with db.begin(): + user.AccountType = tu_or_dev request = Request() sid = user.login(request, "testPassword") @@ -1001,21 +1002,19 @@ def get_rows(html): def test_post_accounts(tu_user): # Set a PGPKey. - user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" + with db.begin(): + user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" # Create a few more users. users = [user] - for i in range(10): - _user = create(User, Username=f"test_{i}", - Email=f"test_{i}@example.org", - RealName=f"Test #{i}", - Passwd="testPassword", - IRCNick=f"test_#{i}", - autocommit=False) - users.append(_user) - - # Commit everything to the database. - commit() + with db.begin(): + for i in range(10): + _user = create(User, Username=f"test_{i}", + Email=f"test_{i}@example.org", + RealName=f"Test #{i}", + Passwd="testPassword", + IRCNick=f"test_#{i}") + users.append(_user) sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1085,11 +1084,12 @@ def test_post_accounts_account_type(tu_user): # test the `u` parameter. account_type = query(AccountType, AccountType.AccountType == "User").first() - create(User, Username="test_2", - Email="test_2@example.org", - RealName="Test User 2", - Passwd="testPassword", - AccountType=account_type) + with db.begin(): + create(User, Username="test_2", + Email="test_2@example.org", + RealName="Test User 2", + Passwd="testPassword", + AccountType=account_type) # Expect no entries; we marked our only user as a User type. with client as request: @@ -1113,9 +1113,10 @@ def test_post_accounts_account_type(tu_user): assert type.text.strip() == "User" # Set our only user to a Trusted User. - user.AccountType = query(AccountType, - AccountType.ID == TRUSTED_USER_ID).first() - commit() + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == TRUSTED_USER_ID + ).first() with client as request: response = request.post("/accounts/", cookies=cookies, @@ -1130,9 +1131,10 @@ def test_post_accounts_account_type(tu_user): assert type.text.strip() == "Trusted User" - user.AccountType = query(AccountType, - AccountType.ID == DEVELOPER_ID).first() - commit() + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == DEVELOPER_ID + ).first() with client as request: response = request.post("/accounts/", cookies=cookies, @@ -1147,10 +1149,10 @@ def test_post_accounts_account_type(tu_user): assert type.text.strip() == "Developer" - user.AccountType = query(AccountType, - AccountType.ID == TRUSTED_USER_AND_DEV_ID - ).first() - commit() + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == TRUSTED_USER_AND_DEV_ID + ).first() with client as request: response = request.post("/accounts/", cookies=cookies, @@ -1182,8 +1184,8 @@ def test_post_accounts_status(tu_user): username, type, status, realname, irc, pgp_key, edit = row assert status.text.strip() == "Active" - user.Suspended = True - commit() + with db.begin(): + user.Suspended = True with client as request: response = request.post("/accounts/", cookies=cookies, @@ -1244,12 +1246,13 @@ def test_post_accounts_sortby(tu_user): # Create a second user so we can compare sorts. account_type = query(AccountType, AccountType.ID == DEVELOPER_ID).first() - create(User, Username="test2", - Email="test2@example.org", - RealName="Test User 2", - Passwd="testPassword", - IRCNick="test2", - AccountType=account_type) + with db.begin(): + create(User, Username="test2", + Email="test2@example.org", + RealName="Test User 2", + Passwd="testPassword", + IRCNick="test2", + AccountType=account_type) sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1297,9 +1300,10 @@ def test_post_accounts_sortby(tu_user): # Test the rows are reversed when ordering by RealName. assert compare_text_values(4, first_rows, reversed(rows)) is True - user.AccountType = query(AccountType, - AccountType.ID == TRUSTED_USER_AND_DEV_ID).first() - commit() + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == TRUSTED_USER_AND_DEV_ID + ).first() # Fetch first_rows again with our new AccountType ordering. with client as request: @@ -1322,8 +1326,8 @@ def test_post_accounts_sortby(tu_user): def test_post_accounts_pgp_key(tu_user): - user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" - commit() + with db.begin(): + user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1343,15 +1347,14 @@ def test_post_accounts_paged(tu_user): users = [user] account_type = query(AccountType, AccountType.AccountType == "User").first() - for i in range(150): - _user = create(User, Username=f"test_#{i}", - Email=f"test_#{i}@example.org", - RealName=f"Test User #{i}", - Passwd="testPassword", - AccountType=account_type, - autocommit=False) - users.append(_user) - commit() + with db.begin(): + for i in range(150): + _user = create(User, Username=f"test_#{i}", + Email=f"test_#{i}@example.org", + RealName=f"Test User #{i}", + Passwd="testPassword", + AccountType=account_type) + users.append(_user) sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1414,8 +1417,9 @@ def test_post_accounts_paged(tu_user): def test_get_terms_of_service(): - term = create(Term, Description="Test term.", - URL="http://localhost", Revision=1) + with db.begin(): + term = create(Term, Description="Test term.", + URL="http://localhost", Revision=1) with client as request: response = request.get("/tos", allow_redirects=False) @@ -1436,8 +1440,9 @@ def test_get_terms_of_service(): response = request.get("/tos", cookies=cookies, allow_redirects=False) assert response.status_code == int(HTTPStatus.OK) - accepted_term = create(AcceptedTerm, User=user, - Term=term, Revision=term.Revision) + with db.begin(): + accepted_term = create(AcceptedTerm, User=user, + Term=term, Revision=term.Revision) with client as request: response = request.get("/tos", cookies=cookies, allow_redirects=False) @@ -1445,8 +1450,8 @@ def test_get_terms_of_service(): assert response.status_code == int(HTTPStatus.SEE_OTHER) # Bump the term's revision. - term.Revision = 2 - commit() + with db.begin(): + term.Revision = 2 with client as request: response = request.get("/tos", cookies=cookies, allow_redirects=False) @@ -1454,8 +1459,8 @@ def test_get_terms_of_service(): # yet been agreed to via AcceptedTerm update. assert response.status_code == int(HTTPStatus.OK) - accepted_term.Revision = term.Revision - commit() + with db.begin(): + accepted_term.Revision = term.Revision with client as request: response = request.get("/tos", cookies=cookies, allow_redirects=False) @@ -1471,8 +1476,9 @@ def test_post_terms_of_service(): cookies = {"AURSID": sid} # Auth cookie. # Create a fresh Term. - term = create(Term, Description="Test term.", - URL="http://localhost", Revision=1) + with db.begin(): + term = create(Term, Description="Test term.", + URL="http://localhost", Revision=1) # Test that the term we just created is listed. with client as request: @@ -1497,8 +1503,8 @@ def test_post_terms_of_service(): assert accepted_term.Term == term # Update the term to revision 2. - term.Revision = 2 - commit() + with db.begin(): + term.Revision = 2 # A GET request gives us the new revision to accept. with client as request: diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py index 536e3841..25cb3e0f 100644 --- a/test/test_api_rate_limit.py +++ b/test/test_api_rate_limit.py @@ -2,6 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError +from aurweb import db from aurweb.db import create from aurweb.models.api_rate_limit import ApiRateLimit from aurweb.testing import setup_test_db @@ -13,26 +14,28 @@ def setup(): def test_api_rate_key_creation(): - rate = create(ApiRateLimit, IP="127.0.0.1", Requests=10, WindowStart=1) + with db.begin(): + rate = create(ApiRateLimit, IP="127.0.0.1", Requests=10, WindowStart=1) assert rate.IP == "127.0.0.1" assert rate.Requests == 10 assert rate.WindowStart == 1 def test_api_rate_key_ip_default(): - api_rate_limit = create(ApiRateLimit, Requests=10, WindowStart=1) + with db.begin(): + api_rate_limit = create(ApiRateLimit, Requests=10, WindowStart=1) assert api_rate_limit.IP == str() def test_api_rate_key_null_requests_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(ApiRateLimit, IP="127.0.0.1", WindowStart=1) - session.rollback() + with db.begin(): + create(ApiRateLimit, IP="127.0.0.1", WindowStart=1) + db.rollback() def test_api_rate_key_null_window_start_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(ApiRateLimit, IP="127.0.0.1", Requests=1) - session.rollback() + with db.begin(): + create(ApiRateLimit, IP="127.0.0.1", Requests=1) + db.rollback() diff --git a/test/test_auth.py b/test/test_auth.py index b386bea1..caa39468 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -4,6 +4,7 @@ import pytest from sqlalchemy.exc import IntegrityError +from aurweb import db from aurweb.auth import BasicAuthBackend, account_type_required, has_credential from aurweb.db import create, query from aurweb.models.account_type import USER, USER_ID, AccountType @@ -23,9 +24,10 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.com", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + with db.begin(): + user = create(User, Username="test", Email="test@example.com", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) backend = BasicAuthBackend() request = Request() @@ -51,14 +53,13 @@ async def test_auth_backend_invalid_sid(): @pytest.mark.asyncio async def test_auth_backend_invalid_user_id(): - from aurweb.db import session - # Create a new session with a fake user id. now_ts = datetime.utcnow().timestamp() with pytest.raises(IntegrityError): - create(Session, UsersID=666, SessionID="realSession", - LastUpdateTS=now_ts + 5) - session.rollback() + with db.begin(): + create(Session, UsersID=666, SessionID="realSession", + LastUpdateTS=now_ts + 5) + db.rollback() @pytest.mark.asyncio @@ -66,8 +67,9 @@ async def test_basic_auth_backend(): # This time, everything matches up. We expect the user to # equal the real_user. now_ts = datetime.utcnow().timestamp() - create(Session, UsersID=user.ID, SessionID="realSession", - LastUpdateTS=now_ts + 5) + with db.begin(): + create(Session, UsersID=user.ID, SessionID="realSession", + LastUpdateTS=now_ts + 5) request.cookies["AURSID"] = "realSession" _, result = await backend.authenticate(request) assert result == user diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index b0dd5648..1d8f9cbe 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -9,7 +9,7 @@ from fastapi.testclient import TestClient import aurweb.config from aurweb.asgi import app -from aurweb.db import create, query +from aurweb.db import begin, create, query from aurweb.models.account_type import AccountType from aurweb.models.session import Session from aurweb.models.user import User @@ -32,9 +32,10 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + with begin(): + user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, + RealName="Test User", Passwd="testPassword", + AccountType=account_type) client = TestClient(app) diff --git a/test/test_ban.py b/test/test_ban.py index b728644b..f96e9d14 100644 --- a/test/test_ban.py +++ b/test/test_ban.py @@ -6,6 +6,7 @@ import pytest from sqlalchemy import exc as sa_exc +from aurweb import db from aurweb.db import create from aurweb.models.ban import Ban, is_banned from aurweb.testing import setup_test_db @@ -21,7 +22,8 @@ def setup(): setup_test_db("Bans") ts = datetime.utcnow() + timedelta(seconds=30) - ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) + with db.begin(): + ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) request = Request() @@ -35,17 +37,17 @@ def test_invalid_ban(): with pytest.raises(sa_exc.IntegrityError): bad_ban = Ban(BanTS=datetime.utcnow()) - session.add(bad_ban) # We're adding a ban with no primary key; this causes an # SQLAlchemy warnings when committing to the DB. # Ignore them. with warnings.catch_warnings(): warnings.simplefilter("ignore", sa_exc.SAWarning) - session.commit() + with db.begin(): + session.add(bad_ban) # Since we got a transaction failure, we need to rollback. - session.rollback() + db.rollback() def test_banned(): diff --git a/test/test_db.py b/test/test_db.py index 9ece25ea..7798d2f6 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -278,18 +278,15 @@ def test_connection_execute_paramstyle_unsupported(): def test_create_delete(): - db.create(AccountType, AccountType="test") + with db.begin(): + db.create(AccountType, AccountType="test") + record = db.query(AccountType, AccountType.AccountType == "test").first() assert record is not None - db.delete(AccountType, AccountType.AccountType == "test") - record = db.query(AccountType, AccountType.AccountType == "test").first() - assert record is None - # Create and delete a record with autocommit=False. - db.create(AccountType, AccountType="test", autocommit=False) - db.commit() - db.delete(AccountType, AccountType.AccountType == "test", autocommit=False) - db.commit() + with db.begin(): + db.delete(AccountType, AccountType.AccountType == "test") + record = db.query(AccountType, AccountType.AccountType == "test").first() assert record is None @@ -297,8 +294,8 @@ def test_create_delete(): def test_add_commit(): # Use db.add and db.commit to add a temporary record. account_type = AccountType(AccountType="test") - db.add(account_type) - db.commit() + with db.begin(): + db.add(account_type) # Assert it got created in the DB. assert bool(account_type.ID) @@ -308,7 +305,8 @@ def test_add_commit(): assert record == account_type # Remove the record. - db.delete(AccountType, AccountType.ID == account_type.ID) + with db.begin(): + db.delete(AccountType, AccountType.ID == account_type.ID) def test_connection_executor_mysql_paramstyle(): diff --git a/test/test_dependency_type.py b/test/test_dependency_type.py index 6c37cc58..4d555123 100644 --- a/test/test_dependency_type.py +++ b/test/test_dependency_type.py @@ -1,6 +1,6 @@ import pytest -from aurweb.db import create, delete, query +from aurweb.db import begin, create, delete, query from aurweb.models.dependency_type import DependencyType from aurweb.testing import setup_test_db @@ -19,13 +19,17 @@ def test_dependency_types(): def test_dependency_type_creation(): - dependency_type = create(DependencyType, Name="Test Type") + with begin(): + dependency_type = create(DependencyType, Name="Test Type") assert bool(dependency_type.ID) assert dependency_type.Name == "Test Type" - delete(DependencyType, DependencyType.ID == dependency_type.ID) + with begin(): + delete(DependencyType, DependencyType.ID == dependency_type.ID) def test_dependency_type_null_name_uses_default(): - dependency_type = create(DependencyType) + with begin(): + dependency_type = create(DependencyType) assert dependency_type.Name == str() - delete(DependencyType, DependencyType.ID == dependency_type.ID) + with begin(): + delete(DependencyType, DependencyType.ID == dependency_type.ID) diff --git a/test/test_group.py b/test/test_group.py index da017a96..cea69b68 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create +from aurweb import db from aurweb.models.group import Group from aurweb.testing import setup_test_db @@ -13,13 +13,14 @@ def setup(): def test_group_creation(): - group = create(Group, Name="Test Group") + with db.begin(): + group = db.create(Group, Name="Test Group") assert bool(group.ID) assert group.Name == "Test Group" def test_group_null_name_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(Group) - session.rollback() + with db.begin(): + db.create(Group) + db.rollback() diff --git a/test/test_homepage.py b/test/test_homepage.py index 2cd6682f..fef3532d 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -38,8 +38,10 @@ def setup(): @pytest.fixture def user(): - yield db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd="testPassword", AccountTypeID=USER_ID) + yield user @pytest.fixture @@ -68,17 +70,14 @@ def packages(user): # For i..num_packages, create a package named pkg_{i}. pkgs = [] now = int(datetime.utcnow().timestamp()) - for i in range(num_packages): - pkgbase = db.create(PackageBase, Name=f"pkg_{i}", - Maintainer=user, Packager=user, - autocommit=False, SubmittedTS=now, - ModifiedTS=now) - pkg = db.create(Package, PackageBase=pkgbase, - Name=pkgbase.Name, autocommit=False) - pkgs.append(pkg) - now += 1 - - db.commit() + with db.begin(): + for i in range(num_packages): + pkgbase = db.create(PackageBase, Name=f"pkg_{i}", + Maintainer=user, Packager=user, + SubmittedTS=now, ModifiedTS=now) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + pkgs.append(pkg) + now += 1 yield pkgs @@ -159,10 +158,11 @@ def test_homepage_updates(redis, packages): def test_homepage_dashboard(redis, packages, user): # Create Comaintainer records for all of the packages. - for pkg in packages: - db.create(PackageComaintainer, PackageBase=pkg.PackageBase, - User=user, Priority=1, autocommit=False) - db.commit() + with db.begin(): + for pkg in packages: + db.create(PackageComaintainer, + PackageBase=pkg.PackageBase, + User=user, Priority=1) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -193,11 +193,12 @@ def test_homepage_dashboard_requests(redis, packages, user): pkg = packages[0] reqtype = db.query(RequestType, RequestType.ID == DELETION_ID).first() - pkgreq = db.create(PackageRequest, PackageBase=pkg.PackageBase, - PackageBaseName=pkg.PackageBase.Name, - User=user, Comments=str(), - ClosureComment=str(), RequestTS=now, - RequestType=reqtype) + with db.begin(): + pkgreq = db.create(PackageRequest, PackageBase=pkg.PackageBase, + PackageBaseName=pkg.PackageBase.Name, + User=user, Comments=str(), + ClosureComment=str(), RequestTS=now, + RequestType=reqtype) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -213,8 +214,8 @@ def test_homepage_dashboard_requests(redis, packages, user): def test_homepage_dashboard_flagged_packages(redis, packages, user): # Set the first Package flagged by setting its OutOfDateTS column. pkg = packages[0] - pkg.PackageBase.OutOfDateTS = int(datetime.utcnow().timestamp()) - db.commit() + with db.begin(): + pkg.PackageBase.OutOfDateTS = int(datetime.utcnow().timestamp()) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: diff --git a/test/test_license.py b/test/test_license.py index feb7a396..2c52f058 100644 --- a/test/test_license.py +++ b/test/test_license.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create +from aurweb import db from aurweb.models.license import License from aurweb.testing import setup_test_db @@ -13,13 +13,14 @@ def setup(): def test_license_creation(): - license = create(License, Name="Test License") + with db.begin(): + license = db.create(License, Name="Test License") assert bool(license.ID) assert license.Name == "Test License" def test_license_null_name_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(License) - session.rollback() + with db.begin(): + db.create(License) + db.rollback() diff --git a/test/test_official_provider.py b/test/test_official_provider.py index a1d3d54a..0aa4f1d1 100644 --- a/test/test_official_provider.py +++ b/test/test_official_provider.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create +from aurweb import db from aurweb.models.official_provider import OfficialProvider from aurweb.testing import setup_test_db @@ -13,10 +13,11 @@ def setup(): def test_official_provider_creation(): - oprovider = create(OfficialProvider, - Name="some-name", - Repo="some-repo", - Provides="some-provides") + with db.begin(): + oprovider = db.create(OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides") assert bool(oprovider.ID) assert oprovider.Name == "some-name" assert oprovider.Repo == "some-repo" @@ -25,16 +26,18 @@ def test_official_provider_creation(): def test_official_provider_cs(): """ Test case sensitivity of the database table. """ - oprovider = create(OfficialProvider, - Name="some-name", - Repo="some-repo", - Provides="some-provides") + with db.begin(): + oprovider = db.create(OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides") assert bool(oprovider.ID) - oprovider_cs = create(OfficialProvider, - Name="SOME-NAME", - Repo="SOME-REPO", - Provides="SOME-PROVIDES") + with db.begin(): + oprovider_cs = db.create(OfficialProvider, + Name="SOME-NAME", + Repo="SOME-REPO", + Provides="SOME-PROVIDES") assert bool(oprovider_cs.ID) assert oprovider.ID != oprovider_cs.ID @@ -49,27 +52,27 @@ def test_official_provider_cs(): def test_official_provider_null_name_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(OfficialProvider, - Repo="some-repo", - Provides="some-provides") - session.rollback() + with db.begin(): + db.create(OfficialProvider, + Repo="some-repo", + Provides="some-provides") + db.rollback() def test_official_provider_null_repo_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(OfficialProvider, - Name="some-name", - Provides="some-provides") - session.rollback() + with db.begin(): + db.create(OfficialProvider, + Name="some-name", + Provides="some-provides") + db.rollback() def test_official_provider_null_provides_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(OfficialProvider, - Name="some-name", - Repo="some-repo") - session.rollback() + with db.begin(): + db.create(OfficialProvider, + Name="some-name", + Repo="some-repo") + db.rollback() diff --git a/test/test_package.py b/test/test_package.py index 1e940164..112ca9b4 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -3,7 +3,7 @@ import pytest from sqlalchemy import and_ from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query +from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -19,25 +19,25 @@ def setup(): setup_test_db("Packages", "PackageBases", "Users") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + account_type = db.query(AccountType, + AccountType.AccountType == "User").first() - pkgbase = create(PackageBase, - Name="beautiful-package", - Maintainer=user) - package = create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + + pkgbase = db.create(PackageBase, + Name="beautiful-package", + Maintainer=user) + package = db.create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") def test_package(): - from aurweb.db import session - assert pkgbase == package.PackageBase assert package.Name == "beautiful-package" assert package.Description == "Test description." @@ -45,33 +45,31 @@ def test_package(): assert package.URL == "https://test.package" # Update package Version. - package.Version = "1.2.3" - session.commit() + with db.begin(): + package.Version = "1.2.3" # Make sure it got updated in the database. - record = query(Package, - and_(Package.ID == package.ID, - Package.Version == "1.2.3")).first() + record = db.query(Package, + and_(Package.ID == package.ID, + Package.Version == "1.2.3")).first() assert record is not None def test_package_null_pkgbase_raises_exception(): - from aurweb.db import session - with pytest.raises(IntegrityError): - create(Package, - Name="some-package", - Description="Some description.", - URL="https://some.package") - session.rollback() + with db.begin(): + db.create(Package, + Name="some-package", + Description="Some description.", + URL="https://some.package") + db.rollback() def test_package_null_name_raises_exception(): - from aurweb.db import session - with pytest.raises(IntegrityError): - create(Package, - PackageBase=pkgbase, - Description="Some description.", - URL="https://some.package") - session.rollback() + with db.begin(): + db.create(Package, + PackageBase=pkgbase, + Description="Some description.", + URL="https://some.package") + db.rollback() diff --git a/test/test_package_base.py b/test/test_package_base.py index 0c0d0526..2bc6278f 100644 --- a/test/test_package_base.py +++ b/test/test_package_base.py @@ -4,7 +4,7 @@ from sqlalchemy.exc import IntegrityError import aurweb.config -from aurweb.db import create, query +from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.package_base import PackageBase from aurweb.models.user import User @@ -19,17 +19,19 @@ def setup(): setup_test_db("Users", "PackageBases") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + account_type = db.query(AccountType, + AccountType.AccountType == "User").first() + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) def test_package_base(): - pkgbase = create(PackageBase, - Name="beautiful-package", - Maintainer=user) + with db.begin(): + pkgbase = db.create(PackageBase, + Name="beautiful-package", + Maintainer=user) assert pkgbase in user.maintained_bases assert not pkgbase.OutOfDateTS @@ -38,7 +40,8 @@ def test_package_base(): # Set Popularity to a string, then get it by attribute to # exercise the string -> float conversion path. - pkgbase.Popularity = "0.0" + with db.begin(): + pkgbase.Popularity = "0.0" assert pkgbase.Popularity == 0.0 @@ -47,27 +50,28 @@ def test_package_base_ci(): if aurweb.config.get("database", "backend") == "sqlite": return None # SQLite doesn't seem handle this. - from aurweb.db import session - - pkgbase = create(PackageBase, - Name="beautiful-package", - Maintainer=user) + with db.begin(): + pkgbase = db.create(PackageBase, + Name="beautiful-package", + Maintainer=user) assert bool(pkgbase.ID) with pytest.raises(IntegrityError): - create(PackageBase, - Name="Beautiful-Package", - Maintainer=user) - session.rollback() + with db.begin(): + db.create(PackageBase, + Name="Beautiful-Package", + Maintainer=user) + db.rollback() def test_package_base_relationships(): - pkgbase = create(PackageBase, - Name="beautiful-package", - Flagger=user, - Maintainer=user, - Submitter=user, - Packager=user) + with db.begin(): + pkgbase = db.create(PackageBase, + Name="beautiful-package", + Flagger=user, + Maintainer=user, + Submitter=user, + Packager=user) assert pkgbase in user.flagged_bases assert pkgbase in user.maintained_bases assert pkgbase in user.submitted_bases @@ -75,8 +79,7 @@ def test_package_base_relationships(): def test_package_base_null_name_raises_exception(): - from aurweb.db import session - with pytest.raises(IntegrityError): - create(PackageBase) - session.rollback() + with db.begin(): + db.create(PackageBase) + db.rollback() diff --git a/test/test_package_blacklist.py b/test/test_package_blacklist.py index 3c64cc21..93f15de7 100644 --- a/test/test_package_blacklist.py +++ b/test/test_package_blacklist.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, rollback +from aurweb import db from aurweb.models.package_base import PackageBase from aurweb.models.package_blacklist import PackageBlacklist from aurweb.models.user import User @@ -17,18 +17,20 @@ def setup(): setup_test_db("PackageBlacklist", "PackageBases", "Users") - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) def test_package_blacklist_creation(): - package_blacklist = create(PackageBlacklist, Name="evil-package") + with db.begin(): + package_blacklist = db.create(PackageBlacklist, Name="evil-package") assert bool(package_blacklist.ID) assert package_blacklist.Name == "evil-package" def test_package_blacklist_null_name_raises_exception(): with pytest.raises(IntegrityError): - create(PackageBlacklist) - rollback() + with db.begin(): + db.create(PackageBlacklist) + db.rollback() diff --git a/test/test_package_comment.py b/test/test_package_comment.py index ca77b511..60f0333d 100644 --- a/test/test_package_comment.py +++ b/test/test_package_comment.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query, rollback +from aurweb.db import begin, create, query, rollback from aurweb.models.account_type import AccountType from aurweb.models.package_base import PackageBase from aurweb.models.package_comment import PackageComment @@ -20,45 +20,52 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + with begin(): + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) def test_package_comment_creation(): - package_comment = create(PackageComment, - PackageBase=pkgbase, - User=user, - Comments="Test comment.", - RenderedComment="Test rendered comment.") + with begin(): + package_comment = create(PackageComment, + PackageBase=pkgbase, + User=user, + Comments="Test comment.", + RenderedComment="Test rendered comment.") assert bool(package_comment.ID) def test_package_comment_null_package_base_raises_exception(): with pytest.raises(IntegrityError): - create(PackageComment, User=user, Comments="Test comment.", - RenderedComment="Test rendered comment.") + with begin(): + create(PackageComment, User=user, Comments="Test comment.", + RenderedComment="Test rendered comment.") rollback() def test_package_comment_null_user_raises_exception(): with pytest.raises(IntegrityError): - create(PackageComment, PackageBase=pkgbase, Comments="Test comment.", - RenderedComment="Test rendered comment.") + with begin(): + create(PackageComment, PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment="Test rendered comment.") rollback() def test_package_comment_null_comments_raises_exception(): with pytest.raises(IntegrityError): - create(PackageComment, PackageBase=pkgbase, User=user, - RenderedComment="Test rendered comment.") + with begin(): + create(PackageComment, PackageBase=pkgbase, User=user, + RenderedComment="Test rendered comment.") rollback() def test_package_comment_null_renderedcomment_defaults(): - record = create(PackageComment, - PackageBase=pkgbase, - User=user, - Comments="Test comment.") + with begin(): + record = create(PackageComment, + PackageBase=pkgbase, + User=user, + Comments="Test comment.") assert record.RenderedComment == str() diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py index e28f1781..2ddef68e 100644 --- a/test/test_package_dependency.py +++ b/test/test_package_dependency.py @@ -2,7 +2,8 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import commit, create, query +from aurweb import db +from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.dependency_type import DependencyType from aurweb.models.package import Package @@ -22,25 +23,28 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, - Name="test-package", - Maintainer=user) - package = create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + with db.begin(): + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + pkgbase = create(PackageBase, + Name="test-package", + Maintainer=user) + package = create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") def test_package_dependencies(): depends = query(DependencyType, DependencyType.Name == "depends").first() - pkgdep = create(PackageDependency, Package=package, - DependencyType=depends, - DepName="test-dep") + + with db.begin(): + pkgdep = create(PackageDependency, Package=package, + DependencyType=depends, + DepName="test-dep") assert pkgdep.DepName == "test-dep" assert pkgdep.Package == package assert pkgdep.DependencyType == depends @@ -49,8 +53,8 @@ def test_package_dependencies(): makedepends = query(DependencyType, DependencyType.Name == "makedepends").first() - pkgdep.DependencyType = makedepends - commit() + with db.begin(): + pkgdep.DependencyType = makedepends assert pkgdep.DepName == "test-dep" assert pkgdep.Package == package assert pkgdep.DependencyType == makedepends @@ -59,8 +63,8 @@ def test_package_dependencies(): checkdepends = query(DependencyType, DependencyType.Name == "checkdepends").first() - pkgdep.DependencyType = checkdepends - commit() + with db.begin(): + pkgdep.DependencyType = checkdepends assert pkgdep.DepName == "test-dep" assert pkgdep.Package == package assert pkgdep.DependencyType == checkdepends @@ -69,8 +73,8 @@ def test_package_dependencies(): optdepends = query(DependencyType, DependencyType.Name == "optdepends").first() - pkgdep.DependencyType = optdepends - commit() + with db.begin(): + pkgdep.DependencyType = optdepends assert pkgdep.DepName == "test-dep" assert pkgdep.Package == package assert pkgdep.DependencyType == optdepends @@ -79,39 +83,37 @@ def test_package_dependencies(): assert not pkgdep.is_package() - base = create(PackageBase, Name=pkgdep.DepName, Maintainer=user) - create(Package, PackageBase=base, Name=pkgdep.DepName) + with db.begin(): + base = create(PackageBase, Name=pkgdep.DepName, Maintainer=user) + create(Package, PackageBase=base, Name=pkgdep.DepName) assert pkgdep.is_package() def test_package_dependencies_null_package_raises_exception(): - from aurweb.db import session - depends = query(DependencyType, DependencyType.Name == "depends").first() with pytest.raises(IntegrityError): - create(PackageDependency, - DependencyType=depends, - DepName="test-dep") - session.rollback() + with db.begin(): + create(PackageDependency, + DependencyType=depends, + DepName="test-dep") + db.rollback() def test_package_dependencies_null_dependency_type_raises_exception(): - from aurweb.db import session - with pytest.raises(IntegrityError): - create(PackageDependency, - Package=package, - DepName="test-dep") - session.rollback() + with db.begin(): + create(PackageDependency, + Package=package, + DepName="test-dep") + db.rollback() def test_package_dependencies_null_depname_raises_exception(): - from aurweb.db import session - depends = query(DependencyType, DependencyType.Name == "depends").first() with pytest.raises(IntegrityError): - create(PackageDependency, - Package=package, - DependencyType=depends) - session.rollback() + with db.begin(): + create(PackageDependency, + Package=package, + DependencyType=depends) + db.rollback() diff --git a/test/test_package_relation.py b/test/test_package_relation.py index 766d0017..edb67078 100644 --- a/test/test_package_relation.py +++ b/test/test_package_relation.py @@ -2,7 +2,8 @@ import pytest from sqlalchemy.exc import IntegrityError, OperationalError -from aurweb.db import commit, create, query +from aurweb import db +from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -22,25 +23,28 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, - Name="test-package", - Maintainer=user) - package = create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + with db.begin(): + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + pkgbase = create(PackageBase, + Name="test-package", + Maintainer=user) + package = create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") def test_package_relation(): conflicts = query(RelationType, RelationType.Name == "conflicts").first() - pkgrel = create(PackageRelation, Package=package, - RelationType=conflicts, - RelName="test-relation") + + with db.begin(): + pkgrel = create(PackageRelation, Package=package, + RelationType=conflicts, + RelName="test-relation") assert pkgrel.RelName == "test-relation" assert pkgrel.Package == package assert pkgrel.RelationType == conflicts @@ -48,8 +52,8 @@ def test_package_relation(): assert pkgrel in package.package_relations provides = query(RelationType, RelationType.Name == "provides").first() - pkgrel.RelationType = provides - commit() + with db.begin(): + pkgrel.RelationType = provides assert pkgrel.RelName == "test-relation" assert pkgrel.Package == package assert pkgrel.RelationType == provides @@ -57,8 +61,8 @@ def test_package_relation(): assert pkgrel in package.package_relations replaces = query(RelationType, RelationType.Name == "replaces").first() - pkgrel.RelationType = replaces - commit() + with db.begin(): + pkgrel.RelationType = replaces assert pkgrel.RelName == "test-relation" assert pkgrel.Package == package assert pkgrel.RelationType == replaces @@ -67,36 +71,33 @@ def test_package_relation(): def test_package_relation_null_package_raises_exception(): - from aurweb.db import session - conflicts = query(RelationType, RelationType.Name == "conflicts").first() assert conflicts is not None with pytest.raises(IntegrityError): - create(PackageRelation, - RelationType=conflicts, - RelName="test-relation") - session.rollback() + with db.begin(): + create(PackageRelation, + RelationType=conflicts, + RelName="test-relation") + db.rollback() def test_package_relation_null_relation_type_raises_exception(): - from aurweb.db import session - with pytest.raises(IntegrityError): - create(PackageRelation, - Package=package, - RelName="test-relation") - session.rollback() + with db.begin(): + create(PackageRelation, + Package=package, + RelName="test-relation") + db.rollback() def test_package_relation_null_relname_raises_exception(): - from aurweb.db import session - depends = query(RelationType, RelationType.Name == "conflicts").first() assert depends is not None with pytest.raises((OperationalError, IntegrityError)): - create(PackageRelation, - Package=package, - RelationType=depends) - session.rollback() + with db.begin(): + create(PackageRelation, + Package=package, + RelationType=depends) + db.rollback() diff --git a/test/test_package_request.py b/test/test_package_request.py index c28af6bd..1589ffc2 100644 --- a/test/test_package_request.py +++ b/test/test_package_request.py @@ -4,7 +4,8 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import commit, create, query, rollback +from aurweb import db +from aurweb.db import create, query, rollback from aurweb.models.package_base import PackageBase from aurweb.models.package_request import (ACCEPTED, ACCEPTED_ID, CLOSED, CLOSED_ID, PENDING, PENDING_ID, REJECTED, REJECTED_ID, PackageRequest) @@ -21,19 +22,21 @@ def setup(): setup_test_db("PackageRequests", "PackageBases", "Users") - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + with db.begin(): + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = create(PackageBase, Name="test-package", Maintainer=user) def test_package_request_creation(): request_type = query(RequestType, RequestType.Name == "merge").first() assert request_type.Name == "merge" - package_request = create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + with db.begin(): + package_request = create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) assert bool(package_request.ID) assert package_request.RequestType == request_type @@ -54,11 +57,12 @@ def test_package_request_closed(): assert request_type.Name == "merge" ts = int(datetime.utcnow().timestamp()) - package_request = create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Closer=user, ClosedTS=ts, - Comments=str(), ClosureComment=str()) + with db.begin(): + package_request = create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Closer=user, ClosedTS=ts, + Comments=str(), ClosureComment=str()) assert package_request.Closer == user assert package_request.ClosedTS == ts @@ -69,54 +73,60 @@ def test_package_request_closed(): def test_package_request_null_request_type_raises_exception(): with pytest.raises(IntegrityError): - create(PackageRequest, User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + with db.begin(): + create(PackageRequest, User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) rollback() def test_package_request_null_user_raises_exception(): request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - create(PackageRequest, RequestType=request_type, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + with db.begin(): + create(PackageRequest, RequestType=request_type, + PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) rollback() def test_package_request_null_package_base_raises_exception(): request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - create(PackageRequest, RequestType=request_type, - User=user, PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + with db.begin(): + create(PackageRequest, RequestType=request_type, + User=user, PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) rollback() def test_package_request_null_package_base_name_raises_exception(): request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - Comments=str(), ClosureComment=str()) + with db.begin(): + create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + Comments=str(), ClosureComment=str()) rollback() def test_package_request_null_comments_raises_exception(): request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - ClosureComment=str()) + with db.begin(): + create(PackageRequest, RequestType=request_type, User=user, + PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + ClosureComment=str()) rollback() def test_package_request_null_closure_comment_raises_exception(): request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - Comments=str()) + with db.begin(): + create(PackageRequest, RequestType=request_type, User=user, + PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + Comments=str()) rollback() @@ -124,26 +134,27 @@ def test_package_request_status_display(): """ Test status_display() based on the Status column value. """ request_type = query(RequestType, RequestType.Name == "merge").first() - pkgreq = create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str(), - Status=PENDING_ID) + with db.begin(): + pkgreq = create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str(), + Status=PENDING_ID) assert pkgreq.status_display() == PENDING - pkgreq.Status = CLOSED_ID - commit() + with db.begin(): + pkgreq.Status = CLOSED_ID assert pkgreq.status_display() == CLOSED - pkgreq.Status = ACCEPTED_ID - commit() + with db.begin(): + pkgreq.Status = ACCEPTED_ID assert pkgreq.status_display() == ACCEPTED - pkgreq.Status = REJECTED_ID - commit() + with db.begin(): + pkgreq.Status = REJECTED_ID assert pkgreq.status_display() == REJECTED - pkgreq.Status = 124 - commit() + with db.begin(): + pkgreq.Status = 124 with pytest.raises(KeyError): pkgreq.status_display() diff --git a/test/test_package_source.py b/test/test_package_source.py index 7453f756..d1adcf9c 100644 --- a/test/test_package_source.py +++ b/test/test_package_source.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query, rollback +from aurweb.db import begin, create, query, rollback from aurweb.models.account_type import AccountType from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -21,17 +21,19 @@ def setup(): account_type = query(AccountType, AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, - Name="test-package", - Maintainer=user) - package = create(Package, PackageBase=pkgbase, Name="test-package") + with begin(): + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) + pkgbase = create(PackageBase, + Name="test-package", + Maintainer=user) + package = create(Package, PackageBase=pkgbase, Name="test-package") def test_package_source(): - pkgsource = create(PackageSource, Package=package) + with begin(): + pkgsource = create(PackageSource, Package=package) assert pkgsource.Package == package # By default, PackageSources.Source assigns the string '/dev/null'. assert pkgsource.Source == "/dev/null" @@ -40,5 +42,6 @@ def test_package_source(): def test_package_source_null_package_raises_exception(): with pytest.raises(IntegrityError): - create(PackageSource) + with begin(): + create(PackageSource) rollback() diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index ad07ec17..8a468c15 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -28,31 +28,25 @@ def package_endpoint(package: Package) -> str: return f"/packages/{package.Name}" -def create_package(pkgname: str, maintainer: User, - autocommit: bool = True) -> Package: +def create_package(pkgname: str, maintainer: User) -> Package: pkgbase = db.create(PackageBase, Name=pkgname, - Maintainer=maintainer, - autocommit=False) - return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase, - autocommit=autocommit) + Maintainer=maintainer) + return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) def create_package_dep(package: Package, depname: str, - dep_type_name: str = "depends", - autocommit: bool = True) -> PackageDependency: + dep_type_name: str = "depends") -> PackageDependency: dep_type = db.query(DependencyType, DependencyType.Name == dep_type_name).first() return db.create(PackageDependency, DependencyType=dep_type, Package=package, - DepName=depname, - autocommit=autocommit) + DepName=depname) def create_package_rel(package: Package, - relname: str, - autocommit: bool = True) -> PackageRelation: + relname: str) -> PackageRelation: rel_type = db.query(RelationType, RelationType.ID == PROVIDES_ID).first() return db.create(PackageRelation, @@ -84,31 +78,37 @@ def client() -> TestClient: def user() -> User: """ Yield a user. """ account_type = db.query(AccountType, AccountType.ID == USER_ID).first() - yield db.create(User, Username="test", - Email="test@example.org", - Passwd="testPassword", - AccountType=account_type) + with db.begin(): + user = db.create(User, Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountType=account_type) + yield user @pytest.fixture def maintainer() -> User: """ Yield a specific User used to maintain packages. """ account_type = db.query(AccountType, AccountType.ID == USER_ID).first() - yield db.create(User, Username="test_maintainer", - Email="test_maintainer@example.org", - Passwd="testPassword", - AccountType=account_type) + with db.begin(): + maintainer = db.create(User, Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountType=account_type) + yield maintainer @pytest.fixture def package(maintainer: User) -> Package: """ Yield a Package created by user. """ - pkgbase = db.create(PackageBase, - Name="test-package", - Maintainer=maintainer) - yield db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name) + with db.begin(): + pkgbase = db.create(PackageBase, + Name="test-package", + Maintainer=maintainer) + package = db.create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name) + yield package def test_package_not_found(client: TestClient): @@ -121,10 +121,11 @@ def test_package_official_not_found(client: TestClient, package: Package): """ When a Package has a matching OfficialProvider record, it is not hosted on AUR, but in the official repositories. Getting a package with this kind of record should return a status code 404. """ - db.create(OfficialProvider, - Name=package.Name, - Repo="core", - Provides=package.Name) + with db.begin(): + db.create(OfficialProvider, + Name=package.Name, + Repo="core", + Provides=package.Name) with client as request: resp = request.get(package_endpoint(package)) @@ -157,8 +158,9 @@ def test_package(client: TestClient, package: Package): def test_package_comments(client: TestClient, user: User, package: Package): now = (datetime.utcnow().timestamp()) - comment = db.create(PackageComment, PackageBase=package.PackageBase, - User=user, Comments="Test comment", CommentTS=now) + with db.begin(): + comment = db.create(PackageComment, PackageBase=package.PackageBase, + User=user, Comments="Test comment", CommentTS=now) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -178,11 +180,12 @@ def test_package_comments(client: TestClient, user: User, package: Package): def test_package_requests_display(client: TestClient, user: User, package: Package): type_ = db.query(RequestType, RequestType.ID == DELETION_ID).first() - db.create(PackageRequest, PackageBase=package.PackageBase, - PackageBaseName=package.PackageBase.Name, - User=user, RequestType=type_, - Comments="Test comment.", - ClosureComment=str()) + with db.begin(): + db.create(PackageRequest, PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + User=user, RequestType=type_, + Comments="Test comment.", + ClosureComment=str()) # Test that a single request displays "1 pending request". with client as request: @@ -195,11 +198,12 @@ def test_package_requests_display(client: TestClient, user: User, assert target.text.strip() == "1 pending request" type_ = db.query(RequestType, RequestType.ID == DELETION_ID).first() - db.create(PackageRequest, PackageBase=package.PackageBase, - PackageBaseName=package.PackageBase.Name, - User=user, RequestType=type_, - Comments="Test comment2.", - ClosureComment=str()) + with db.begin(): + db.create(PackageRequest, PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + User=user, RequestType=type_, + Comments="Test comment2.", + ClosureComment=str()) # Test that a two requests display "2 pending requests". with client as request: @@ -271,50 +275,43 @@ def test_package_authenticated_maintainer(client: TestClient, def test_package_dependencies(client: TestClient, maintainer: User, package: Package): # Create a normal dependency of type depends. - dep_pkg = create_package("test-dep-1", maintainer, autocommit=False) - dep = create_package_dep(package, dep_pkg.Name, autocommit=False) - dep.DepArch = "x86_64" + with db.begin(): + dep_pkg = create_package("test-dep-1", maintainer) + dep = create_package_dep(package, dep_pkg.Name) + dep.DepArch = "x86_64" - # Also, create a makedepends. - make_dep_pkg = create_package("test-dep-2", maintainer, autocommit=False) - make_dep = create_package_dep(package, make_dep_pkg.Name, - dep_type_name="makedepends", - autocommit=False) + # Also, create a makedepends. + make_dep_pkg = create_package("test-dep-2", maintainer) + make_dep = create_package_dep(package, make_dep_pkg.Name, + dep_type_name="makedepends") - # And... a checkdepends! - check_dep_pkg = create_package("test-dep-3", maintainer, autocommit=False) - check_dep = create_package_dep(package, check_dep_pkg.Name, - dep_type_name="checkdepends", - autocommit=False) + # And... a checkdepends! + check_dep_pkg = create_package("test-dep-3", maintainer) + check_dep = create_package_dep(package, check_dep_pkg.Name, + dep_type_name="checkdepends") - # Geez. Just stop. This is optdepends. - opt_dep_pkg = create_package("test-dep-4", maintainer, autocommit=False) - opt_dep = create_package_dep(package, opt_dep_pkg.Name, - dep_type_name="optdepends", - autocommit=False) + # Geez. Just stop. This is optdepends. + opt_dep_pkg = create_package("test-dep-4", maintainer) + opt_dep = create_package_dep(package, opt_dep_pkg.Name, + dep_type_name="optdepends") - # Heh. Another optdepends to test one with a description. - opt_desc_dep_pkg = create_package("test-dep-5", maintainer, - autocommit=False) - opt_desc_dep = create_package_dep(package, opt_desc_dep_pkg.Name, - dep_type_name="optdepends", - autocommit=False) - opt_desc_dep.DepDesc = "Test description." + # Heh. Another optdepends to test one with a description. + opt_desc_dep_pkg = create_package("test-dep-5", maintainer) + opt_desc_dep = create_package_dep(package, opt_desc_dep_pkg.Name, + dep_type_name="optdepends") + opt_desc_dep.DepDesc = "Test description." - broken_dep = create_package_dep(package, "test-dep-6", - dep_type_name="depends", - autocommit=False) + broken_dep = create_package_dep(package, "test-dep-6", + dep_type_name="depends") - # Create an official provider record. - db.create(OfficialProvider, Name="test-dep-99", - Repo="core", Provides="test-dep-99", - autocommit=False) - official_dep = create_package_dep(package, "test-dep-99", - autocommit=False) + # Create an official provider record. + db.create(OfficialProvider, Name="test-dep-99", + Repo="core", Provides="test-dep-99") + official_dep = create_package_dep(package, "test-dep-99") - # Also, create a provider who provides our test-dep-99. - provider = create_package("test-provider", maintainer, autocommit=False) - create_package_rel(provider, dep.DepName) + # Also, create a provider who provides our test-dep-99. + provider = create_package("test-provider", maintainer) + create_package_rel(provider, dep.DepName) with client as request: resp = request.get(package_endpoint(package)) @@ -358,8 +355,9 @@ def test_pkgbase_redirect(client: TestClient, package: Package): def test_pkgbase(client: TestClient, package: Package): - second = db.create(Package, Name="second-pkg", - PackageBase=package.PackageBase) + with db.begin(): + second = db.create(Package, Name="second-pkg", + PackageBase=package.PackageBase) expected = [package.Name, second.Name] with client as request: diff --git a/test/test_packages_util.py b/test/test_packages_util.py index bc6a941c..754e3b8d 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -26,17 +26,21 @@ def setup(): @pytest.fixture def maintainer() -> User: account_type = db.query(AccountType, AccountType.ID == USER_ID).first() - yield db.create(User, Username="test_maintainer", - Email="test_maintainer@examepl.org", - Passwd="testPassword", - AccountType=account_type) + with db.begin(): + maintainer = db.create(User, Username="test_maintainer", + Email="test_maintainer@examepl.org", + Passwd="testPassword", + AccountType=account_type) + yield maintainer @pytest.fixture def package(maintainer: User) -> Package: - pkgbase = db.create(PackageBase, Name="test-pkg", - Packager=maintainer, Maintainer=maintainer) - yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-pkg", + Packager=maintainer, Maintainer=maintainer) + package = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) + yield package @pytest.fixture @@ -45,10 +49,11 @@ def client() -> TestClient: def test_package_link(client: TestClient, maintainer: User, package: Package): - db.create(OfficialProvider, - Name=package.Name, - Repo="core", - Provides=package.Name) + with db.begin(): + db.create(OfficialProvider, + Name=package.Name, + Repo="core", + Provides=package.Name) expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}" assert util.package_link(package) == expected diff --git a/test/test_relation_type.py b/test/test_relation_type.py index bf23505c..fbc22c71 100644 --- a/test/test_relation_type.py +++ b/test/test_relation_type.py @@ -1,6 +1,6 @@ import pytest -from aurweb.db import create, delete, query +from aurweb import db from aurweb.models.relation_type import RelationType from aurweb.testing import setup_test_db @@ -11,22 +11,25 @@ def setup(): def test_relation_type_creation(): - relation_type = create(RelationType, Name="test-relation") + with db.begin(): + relation_type = db.create(RelationType, Name="test-relation") + assert bool(relation_type.ID) assert relation_type.Name == "test-relation" - delete(RelationType, RelationType.ID == relation_type.ID) + with db.begin(): + db.delete(RelationType, RelationType.ID == relation_type.ID) def test_relation_types(): - conflicts = query(RelationType, RelationType.Name == "conflicts").first() + conflicts = db.query(RelationType, RelationType.Name == "conflicts").first() assert conflicts is not None assert conflicts.Name == "conflicts" - provides = query(RelationType, RelationType.Name == "provides").first() + provides = db.query(RelationType, RelationType.Name == "provides").first() assert provides is not None assert provides.Name == "provides" - replaces = query(RelationType, RelationType.Name == "replaces").first() + replaces = db.query(RelationType, RelationType.Name == "replaces").first() assert replaces is not None assert replaces.Name == "replaces" diff --git a/test/test_request_type.py b/test/test_request_type.py index a3b3ccb8..8d21c2d9 100644 --- a/test/test_request_type.py +++ b/test/test_request_type.py @@ -1,6 +1,6 @@ import pytest -from aurweb.db import create, delete, query +from aurweb import db from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID, RequestType from aurweb.testing import setup_test_db @@ -11,25 +11,33 @@ def setup(): def test_request_type_creation(): - request_type = create(RequestType, Name="Test Request") + with db.begin(): + request_type = db.create(RequestType, Name="Test Request") + assert bool(request_type.ID) assert request_type.Name == "Test Request" - delete(RequestType, RequestType.ID == request_type.ID) + + with db.begin(): + db.delete(RequestType, RequestType.ID == request_type.ID) def test_request_type_null_name_returns_empty_string(): - request_type = create(RequestType) + with db.begin(): + request_type = db.create(RequestType) + assert bool(request_type.ID) assert request_type.Name == str() - delete(RequestType, RequestType.ID == request_type.ID) + + with db.begin(): + db.delete(RequestType, RequestType.ID == request_type.ID) def test_request_type_name_display(): - deletion = query(RequestType, RequestType.ID == DELETION_ID).first() + deletion = db.query(RequestType, RequestType.ID == DELETION_ID).first() assert deletion.name_display() == "Deletion" - orphan = query(RequestType, RequestType.ID == ORPHAN_ID).first() + orphan = db.query(RequestType, RequestType.ID == ORPHAN_ID).first() assert orphan.name_display() == "Orphan" - merge = query(RequestType, RequestType.ID == MERGE_ID).first() + merge = db.query(RequestType, RequestType.ID == MERGE_ID).first() assert merge.name_display() == "Merge" diff --git a/test/test_routes.py b/test/test_routes.py index a2d1786e..e3f69d7a 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -8,8 +8,8 @@ import pytest from fastapi.testclient import TestClient +from aurweb import db from aurweb.asgi import app -from aurweb.db import create, query from aurweb.models.account_type import AccountType from aurweb.models.user import User from aurweb.testing import setup_test_db @@ -24,11 +24,13 @@ def setup(): setup_test_db("Users", "Sessions") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + account_type = db.query(AccountType, + AccountType.AccountType == "User").first() + + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) client = TestClient(app) diff --git a/test/test_rss.py b/test/test_rss.py index 7dd5bb47..ce3bc71f 100644 --- a/test/test_rss.py +++ b/test/test_rss.py @@ -49,14 +49,13 @@ def packages(user): now = int(datetime.utcnow().timestamp()) # Create 101 packages; we limit 100 on RSS feeds. - for i in range(101): - pkgbase = db.create( - PackageBase, Maintainer=user, Name=f"test-package-{i}", - SubmittedTS=(now + i), ModifiedTS=(now + i), autocommit=False) - pkg = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase, - autocommit=False) - pkgs.append(pkg) - db.commit() + with db.begin(): + for i in range(101): + pkgbase = db.create( + PackageBase, Maintainer=user, Name=f"test-package-{i}", + SubmittedTS=(now + i), ModifiedTS=(now + i)) + pkg = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) + pkgs.append(pkg) yield pkgs diff --git a/test/test_session.py b/test/test_session.py index 1ba11556..4e6f4db4 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -4,7 +4,7 @@ from unittest import mock import pytest -from aurweb.db import create, query +from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.session import Session, generate_unique_sid from aurweb.models.user import User @@ -19,13 +19,16 @@ def setup(): setup_test_db("Users", "Sessions") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - ResetKey="testReset", Passwd="testPassword", - AccountType=account_type) - session = create(Session, UsersID=user.ID, SessionID="testSession", - LastUpdateTS=datetime.utcnow().timestamp()) + account_type = db.query(AccountType, + AccountType.AccountType == "User").first() + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + ResetKey="testReset", Passwd="testPassword", + AccountType=account_type) + + with db.begin(): + session = db.create(Session, UsersID=user.ID, SessionID="testSession", + LastUpdateTS=datetime.utcnow().timestamp()) def test_session(): @@ -35,12 +38,15 @@ def test_session(): def test_session_cs(): """ Test case sensitivity of the database table. """ - user2 = create(User, Username="test2", Email="test2@example.org", - ResetKey="testReset2", Passwd="testPassword", - AccountType=account_type) - session_cs = create(Session, UsersID=user2.ID, - SessionID="TESTSESSION", - LastUpdateTS=datetime.utcnow().timestamp()) + with db.begin(): + user2 = db.create(User, Username="test2", Email="test2@example.org", + ResetKey="testReset2", Passwd="testPassword", + AccountType=account_type) + + with db.begin(): + session_cs = db.create(Session, UsersID=user2.ID, + SessionID="TESTSESSION", + LastUpdateTS=datetime.utcnow().timestamp()) assert session_cs.SessionID == "TESTSESSION" assert session.SessionID == "testSession" diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index 0793199a..12a3e1ce 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -1,6 +1,6 @@ import pytest -from aurweb.db import create, query +from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.user import User @@ -19,19 +19,18 @@ def setup(): setup_test_db("Users", "SSHPubKeys") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + account_type = db.query(AccountType, + AccountType.AccountType == "User").first() + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) - assert account_type == user.AccountType - assert account_type.ID == user.AccountTypeID - - ssh_pub_key = create(SSHPubKey, - UserID=user.ID, - Fingerprint="testFingerprint", - PubKey="testPubKey") + with db.begin(): + ssh_pub_key = db.create(SSHPubKey, + UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey") def test_ssh_pub_key(): @@ -43,9 +42,10 @@ def test_ssh_pub_key(): def test_ssh_pub_key_cs(): """ Test case sensitivity of the database table. """ - ssh_pub_key_cs = create(SSHPubKey, UserID=user.ID, - Fingerprint="TESTFINGERPRINT", - PubKey="TESTPUBKEY") + with db.begin(): + ssh_pub_key_cs = db.create(SSHPubKey, UserID=user.ID, + Fingerprint="TESTFINGERPRINT", + PubKey="TESTPUBKEY") assert ssh_pub_key_cs.Fingerprint == "TESTFINGERPRINT" assert ssh_pub_key_cs.PubKey == "TESTPUBKEY" diff --git a/test/test_term.py b/test/test_term.py index 25108419..3f28311f 100644 --- a/test/test_term.py +++ b/test/test_term.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create +from aurweb import db from aurweb.models.term import Term from aurweb.testing import setup_test_db @@ -18,8 +18,9 @@ def setup(): def test_term_creation(): - term = create(Term, Description="Term description", - URL="https://fake_url.io") + with db.begin(): + term = db.create(Term, Description="Term description", + URL="https://fake_url.io") assert bool(term.ID) assert term.Description == "Term description" assert term.URL == "https://fake_url.io" @@ -27,14 +28,14 @@ def test_term_creation(): def test_term_null_description_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(Term, URL="https://fake_url.io") - session.rollback() + with db.begin(): + db.create(Term, URL="https://fake_url.io") + db.rollback() def test_term_null_url_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(Term, Description="Term description") - session.rollback() + with db.begin(): + db.create(Term, Description="Term description") + db.rollback() diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index 0c33f958..67181db3 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -90,37 +90,37 @@ def client(): def tu_user(): tu_type = db.query(AccountType, AccountType.AccountType == "Trusted User").first() - yield db.create(User, Username="test_tu", Email="test_tu@example.org", - RealName="Test TU", Passwd="testPassword", - AccountType=tu_type) + with db.begin(): + tu_user = db.create(User, Username="test_tu", + Email="test_tu@example.org", + RealName="Test TU", Passwd="testPassword", + AccountType=tu_type) + yield tu_user @pytest.fixture def user(): user_type = db.query(AccountType, AccountType.AccountType == "User").first() - yield db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=user_type) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=user_type) + yield user @pytest.fixture -def proposal(tu_user): +def proposal(user, tu_user): ts = int(datetime.utcnow().timestamp()) agenda = "Test proposal." start = ts - 5 end = ts + 1000 - user_type = db.query(AccountType, - AccountType.AccountType == "User").first() - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=user_type) - - voteinfo = db.create(TUVoteInfo, - Agenda=agenda, Quorum=0.0, - User=user.Username, Submitter=tu_user, - Submitted=start, End=end) + with db.begin(): + voteinfo = db.create(TUVoteInfo, + Agenda=agenda, Quorum=0.0, + User=user.Username, Submitter=tu_user, + Submitted=start, End=end) yield (tu_user, user, voteinfo) @@ -170,20 +170,22 @@ def test_tu_index(client, tu_user): ("Test agenda 2", ts - 1000, ts - 5) # Not running anymore. ] vote_records = [] - for vote in votes: - agenda, start, end = vote - vote_records.append( - db.create(TUVoteInfo, Agenda=agenda, - User=tu_user.Username, - Submitted=start, End=end, - Quorum=0.0, - Submitter=tu_user)) + with db.begin(): + for vote in votes: + agenda, start, end = vote + vote_records.append( + db.create(TUVoteInfo, Agenda=agenda, + User=tu_user.Username, + Submitted=start, End=end, + Quorum=0.0, + Submitter=tu_user)) - # Vote on an ended proposal. - vote_record = vote_records[1] - vote_record.Yes += 1 - vote_record.ActiveTUs += 1 - db.create(TUVote, VoteInfo=vote_record, User=tu_user) + with db.begin(): + # Vote on an ended proposal. + vote_record = vote_records[1] + vote_record.Yes += 1 + vote_record.ActiveTUs += 1 + db.create(TUVote, VoteInfo=vote_record, User=tu_user) cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: @@ -255,22 +257,22 @@ def test_tu_index(client, tu_user): def test_tu_index_table_paging(client, tu_user): ts = int(datetime.utcnow().timestamp()) - for i in range(25): - # Create 25 current votes. - db.create(TUVoteInfo, Agenda=f"Agenda #{i}", - User=tu_user.Username, - Submitted=(ts - 5), End=(ts + 1000), - Quorum=0.0, - Submitter=tu_user, autocommit=False) + with db.begin(): + for i in range(25): + # Create 25 current votes. + db.create(TUVoteInfo, Agenda=f"Agenda #{i}", + User=tu_user.Username, + Submitted=(ts - 5), End=(ts + 1000), + Quorum=0.0, + Submitter=tu_user) - for i in range(25): - # Create 25 past votes. - db.create(TUVoteInfo, Agenda=f"Agenda #{25 + i}", - User=tu_user.Username, - Submitted=(ts - 1000), End=(ts - 5), - Quorum=0.0, - Submitter=tu_user, autocommit=False) - db.commit() + for i in range(25): + # Create 25 past votes. + db.create(TUVoteInfo, Agenda=f"Agenda #{25 + i}", + User=tu_user.Username, + Submitted=(ts - 1000), End=(ts - 5), + Quorum=0.0, + Submitter=tu_user) cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: @@ -363,18 +365,19 @@ def test_tu_index_table_paging(client, tu_user): def test_tu_index_sorting(client, tu_user): ts = int(datetime.utcnow().timestamp()) - for i in range(2): - # Create 'Agenda #1' and 'Agenda #2'. - db.create(TUVoteInfo, Agenda=f"Agenda #{i + 1}", - User=tu_user.Username, - Submitted=(ts + 5), End=(ts + 1000), - Quorum=0.0, - Submitter=tu_user, autocommit=False) + with db.begin(): + for i in range(2): + # Create 'Agenda #1' and 'Agenda #2'. + db.create(TUVoteInfo, Agenda=f"Agenda #{i + 1}", + User=tu_user.Username, + Submitted=(ts + 5), End=(ts + 1000), + Quorum=0.0, + Submitter=tu_user) - # Let's order each vote one day after the other. - # This will allow us to test the sorting nature - # of the tables. - ts += 86405 + # Let's order each vote one day after the other. + # This will allow us to test the sorting nature + # of the tables. + ts += 86405 # Make a default request to /tu. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} @@ -432,18 +435,19 @@ def test_tu_index_sorting(client, tu_user): def test_tu_index_last_votes(client, tu_user, user): ts = int(datetime.utcnow().timestamp()) - # Create a proposal which has ended. - voteinfo = db.create(TUVoteInfo, Agenda="Test agenda", - User=user.Username, - Submitted=(ts - 1000), - End=(ts - 5), - Yes=1, - ActiveTUs=1, - Quorum=0.0, - Submitter=tu_user) + with db.begin(): + # Create a proposal which has ended. + voteinfo = db.create(TUVoteInfo, Agenda="Test agenda", + User=user.Username, + Submitted=(ts - 1000), + End=(ts - 5), + Yes=1, + ActiveTUs=1, + Quorum=0.0, + Submitter=tu_user) - # Create a vote on it from tu_user. - db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + # Create a vote on it from tu_user. + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) # Now, check that tu_user got populated in the .last-votes table. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} @@ -529,10 +533,10 @@ def test_tu_running_proposal(client, proposal): assert abstain.attrib["value"] == "Abstain" # Create a vote. - db.create(TUVote, VoteInfo=voteinfo, User=tu_user) - voteinfo.ActiveTUs += 1 - voteinfo.Yes += 1 - db.commit() + with db.begin(): + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + voteinfo.ActiveTUs += 1 + voteinfo.Yes += 1 # Make another request now that we've voted. with client as request: @@ -556,8 +560,8 @@ def test_tu_ended_proposal(client, proposal): tu_user, user, voteinfo = proposal ts = int(datetime.utcnow().timestamp()) - voteinfo.End = ts - 5 # 5 seconds ago. - db.commit() + with db.begin(): + voteinfo.End = ts - 5 # 5 seconds ago. # Initiate an authenticated GET request to /tu/{proposal_id}. proposal_id = voteinfo.ID @@ -635,8 +639,8 @@ def test_tu_proposal_vote_unauthorized(client, proposal): dev_type = db.query(AccountType, AccountType.AccountType == "Developer").first() - tu_user.AccountType = dev_type - db.commit() + with db.begin(): + tu_user.AccountType = dev_type cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: @@ -664,8 +668,8 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): tu_user, user, voteinfo = proposal # Update voteinfo.User. - voteinfo.User = tu_user.Username - db.commit() + with db.begin(): + voteinfo.User = tu_user.Username cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: @@ -692,10 +696,10 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): def test_tu_proposal_vote_already_voted(client, proposal): tu_user, user, voteinfo = proposal - db.create(TUVote, VoteInfo=voteinfo, User=tu_user) - voteinfo.Yes += 1 - voteinfo.ActiveTUs += 1 - db.commit() + with db.begin(): + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + voteinfo.Yes += 1 + voteinfo.ActiveTUs += 1 cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py index 494300c5..b60e2e6a 100644 --- a/test/test_tu_voteinfo.py +++ b/test/test_tu_voteinfo.py @@ -4,7 +4,8 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import commit, create, query, rollback +from aurweb import db +from aurweb.db import create, query, rollback from aurweb.models.account_type import AccountType from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User @@ -21,19 +22,21 @@ def setup(): tu_type = query(AccountType, AccountType.AccountType == "Trusted User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=tu_type) + with db.begin(): + user = create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=tu_type) def test_tu_voteinfo_creation(): ts = int(datetime.utcnow().timestamp()) - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 5, - Quorum=0.5, - Submitter=user) + with db.begin(): + tu_voteinfo = create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 5, + Quorum=0.5, + Submitter=user) assert bool(tu_voteinfo.ID) assert tu_voteinfo.Agenda == "Blah blah." assert tu_voteinfo.User == user.Username @@ -51,32 +54,33 @@ def test_tu_voteinfo_creation(): def test_tu_voteinfo_is_running(): ts = int(datetime.utcnow().timestamp()) - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 1000, - Quorum=0.5, - Submitter=user) + with db.begin(): + tu_voteinfo = create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 1000, + Quorum=0.5, + Submitter=user) assert tu_voteinfo.is_running() is True - tu_voteinfo.End = ts - 5 - commit() + with db.begin(): + tu_voteinfo.End = ts - 5 assert tu_voteinfo.is_running() is False def test_tu_voteinfo_total_votes(): ts = int(datetime.utcnow().timestamp()) - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 1000, - Quorum=0.5, - Submitter=user) + with db.begin(): + tu_voteinfo = create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 1000, + Quorum=0.5, + Submitter=user) - tu_voteinfo.Yes = 1 - tu_voteinfo.No = 3 - tu_voteinfo.Abstain = 5 - commit() + tu_voteinfo.Yes = 1 + tu_voteinfo.No = 3 + tu_voteinfo.Abstain = 5 # total_votes() should be the sum of Yes, No and Abstain: 1 + 3 + 5 = 9. assert tu_voteinfo.total_votes() == 9 @@ -84,61 +88,67 @@ def test_tu_voteinfo_total_votes(): def test_tu_voteinfo_null_submitter_raises_exception(): with pytest.raises(IntegrityError): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, End=0, - Quorum=0.50) + with db.begin(): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, End=0, + Quorum=0.50) rollback() def test_tu_voteinfo_null_agenda_raises_exception(): with pytest.raises(IntegrityError): - create(TUVoteInfo, - User=user.Username, - Submitted=0, End=0, - Quorum=0.50, - Submitter=user) + with db.begin(): + create(TUVoteInfo, + User=user.Username, + Submitted=0, End=0, + Quorum=0.50, + Submitter=user) rollback() def test_tu_voteinfo_null_user_raises_exception(): with pytest.raises(IntegrityError): - create(TUVoteInfo, - Agenda="Blah blah.", - Submitted=0, End=0, - Quorum=0.50, - Submitter=user) + with db.begin(): + create(TUVoteInfo, + Agenda="Blah blah.", + Submitted=0, End=0, + Quorum=0.50, + Submitter=user) rollback() def test_tu_voteinfo_null_submitted_raises_exception(): with pytest.raises(IntegrityError): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - End=0, - Quorum=0.50, - Submitter=user) + with db.begin(): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + End=0, + Quorum=0.50, + Submitter=user) rollback() def test_tu_voteinfo_null_end_raises_exception(): with pytest.raises(IntegrityError): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, - Quorum=0.50, - Submitter=user) + with db.begin(): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + Quorum=0.50, + Submitter=user) rollback() def test_tu_voteinfo_null_quorum_raises_exception(): with pytest.raises(IntegrityError): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, End=0, - Submitter=user) + with db.begin(): + create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, End=0, + Submitter=user) rollback() diff --git a/test/test_user.py b/test/test_user.py index 7756cff3..70eac079 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -9,7 +9,7 @@ import pytest import aurweb.auth import aurweb.config -from aurweb.db import commit, create, query +from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.ban import Ban from aurweb.models.package import Package @@ -40,12 +40,13 @@ def setup(): PackageNotification.__tablename__ ) - account_type = query(AccountType, - AccountType.AccountType == "User").first() + account_type = db.query(AccountType, + AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) def test_user_login_logout(): @@ -70,14 +71,14 @@ def test_user_login_logout(): assert "AURSID" in request.cookies # Expect that User session relationships work right. - user_session = query(Session, - Session.UsersID == user.ID).first() + user_session = db.query(Session, + Session.UsersID == user.ID).first() assert user_session == user.session assert user.session.SessionID == sid assert user.session.User == user # Search for the user via query API. - result = query(User, User.ID == user.ID).first() + result = db.query(User, User.ID == user.ID).first() # Compare the result and our original user. assert result == user @@ -114,7 +115,8 @@ def test_user_login_twice(): def test_user_login_banned(): # Add ban for the next 30 seconds. banned_timestamp = datetime.utcnow() + timedelta(seconds=30) - create(Ban, IPAddress="127.0.0.1", BanTS=banned_timestamp) + with db.begin(): + db.create(Ban, IPAddress="127.0.0.1", BanTS=banned_timestamp) request = Request() request.client.host = "127.0.0.1" @@ -122,18 +124,17 @@ def test_user_login_banned(): def test_user_login_suspended(): - from aurweb.db import session - user.Suspended = True - session.commit() + with db.begin(): + user.Suspended = True assert not user.login(Request(), "testPassword") def test_legacy_user_authentication(): - from aurweb.db import session - - user.Salt = bcrypt.gensalt().decode() - user.Passwd = hashlib.md5(f"{user.Salt}testPassword".encode()).hexdigest() - session.commit() + with db.begin(): + user.Salt = bcrypt.gensalt().decode() + user.Passwd = hashlib.md5( + f"{user.Salt}testPassword".encode() + ).hexdigest() assert not user.valid_password("badPassword") assert user.valid_password("testPassword") @@ -145,8 +146,9 @@ def test_legacy_user_authentication(): def test_user_login_with_outdated_sid(): # Make a session with a LastUpdateTS 5 seconds ago, causing # user.login to update it with a new sid. - create(Session, UsersID=user.ID, SessionID="stub", - LastUpdateTS=datetime.utcnow().timestamp() - 5) + with db.begin(): + db.create(Session, UsersID=user.ID, SessionID="stub", + LastUpdateTS=datetime.utcnow().timestamp() - 5) sid = user.login(Request(), "testPassword") assert sid and user.is_authenticated() assert sid != "stub" @@ -171,43 +173,42 @@ def test_user_has_credential(): def test_user_ssh_pub_key(): assert user.ssh_pub_key is None - ssh_pub_key = create(SSHPubKey, UserID=user.ID, - Fingerprint="testFingerprint", - PubKey="testPubKey") + with db.begin(): + ssh_pub_key = db.create(SSHPubKey, UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey") assert user.ssh_pub_key == ssh_pub_key def test_user_credential_types(): - from aurweb.db import session - assert aurweb.auth.user_developer_or_trusted_user(user) assert not aurweb.auth.trusted_user(user) assert not aurweb.auth.developer(user) assert not aurweb.auth.trusted_user_or_dev(user) - trusted_user_type = query(AccountType, - AccountType.AccountType == "Trusted User")\ - .first() - user.AccountType = trusted_user_type - session.commit() + trusted_user_type = db.query(AccountType).filter( + AccountType.AccountType == "Trusted User" + ).first() + with db.begin(): + user.AccountType = trusted_user_type assert aurweb.auth.trusted_user(user) assert aurweb.auth.trusted_user_or_dev(user) - developer_type = query(AccountType, - AccountType.AccountType == "Developer").first() - user.AccountType = developer_type - session.commit() + developer_type = db.query(AccountType, + AccountType.AccountType == "Developer").first() + with db.begin(): + user.AccountType = developer_type assert aurweb.auth.developer(user) assert aurweb.auth.trusted_user_or_dev(user) type_str = "Trusted User & Developer" - elevated_type = query(AccountType, - AccountType.AccountType == type_str).first() - user.AccountType = elevated_type - session.commit() + elevated_type = db.query(AccountType, + AccountType.AccountType == type_str).first() + with db.begin(): + user.AccountType = elevated_type assert aurweb.auth.trusted_user(user) assert aurweb.auth.developer(user) @@ -233,53 +234,56 @@ def test_user_as_dict(): def test_user_is_trusted_user(): - tu_type = query(AccountType, - AccountType.AccountType == "Trusted User").first() - user.AccountType = tu_type - commit() + tu_type = db.query(AccountType, + AccountType.AccountType == "Trusted User").first() + with db.begin(): + user.AccountType = tu_type assert user.is_trusted_user() is True # Do it again with the combined role. - tu_type = query( + tu_type = db.query( AccountType, AccountType.AccountType == "Trusted User & Developer").first() - user.AccountType = tu_type - commit() + with db.begin(): + user.AccountType = tu_type assert user.is_trusted_user() is True def test_user_is_developer(): - dev_type = query(AccountType, - AccountType.AccountType == "Developer").first() - user.AccountType = dev_type - commit() + dev_type = db.query(AccountType, + AccountType.AccountType == "Developer").first() + with db.begin(): + user.AccountType = dev_type assert user.is_developer() is True # Do it again with the combined role. - dev_type = query( + dev_type = db.query( AccountType, AccountType.AccountType == "Trusted User & Developer").first() - user.AccountType = dev_type - commit() + with db.begin(): + user.AccountType = dev_type assert user.is_developer() is True def test_user_voted_for(): now = int(datetime.utcnow().timestamp()) - pkgbase = create(PackageBase, Name="pkg1", Maintainer=user) - pkg = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) - create(PackageVote, PackageBase=pkgbase, User=user, VoteTS=now) + with db.begin(): + pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + db.create(PackageVote, PackageBase=pkgbase, User=user, VoteTS=now) assert user.voted_for(pkg) def test_user_notified(): - pkgbase = create(PackageBase, Name="pkg1", Maintainer=user) - pkg = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) - create(PackageNotification, PackageBase=pkgbase, User=user) + with db.begin(): + pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + db.create(PackageNotification, PackageBase=pkgbase, User=user) assert user.notified(pkg) def test_user_packages(): - pkgbase = create(PackageBase, Name="pkg1", Maintainer=user) - pkg = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + with db.begin(): + pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) assert pkg in user.packages() From 1b452d126471df269605ed4e39edcdd38f9e59d8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 3 Sep 2021 21:02:35 -0700 Subject: [PATCH 0413/1451] Add GPL 2.0 LICENSE file This was missing from the project and really needs to be here. Closes #107 Signed-off-by: Kevin Morris --- LICENSE | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d511905c --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. From 5c7e76ef891af78dbe4e3cacb42675b86d9f3a35 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 3 Sep 2021 21:02:35 -0700 Subject: [PATCH 0414/1451] Add GPL 2.0 LICENSE file This was missing from the project and really needs to be here. Closes #107 Signed-off-by: Kevin Morris --- LICENSE | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d511905c --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. From 5e6f0cb8d71266d8ba29ea24db267914b1e33973 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Sep 2021 10:02:40 -0700 Subject: [PATCH 0415/1451] Revert "Add GPL 2.0 LICENSE file" This was already in the repository in ./COPYING This reverts commit 1b452d126471df269605ed4e39edcdd38f9e59d8. --- LICENSE | 339 -------------------------------------------------------- 1 file changed, 339 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d511905c..00000000 --- a/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 2 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, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. From 4e5b67f0a6164a237bbab17f56db9c037cc365f0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Sep 2021 10:02:40 -0700 Subject: [PATCH 0416/1451] Revert "Add GPL 2.0 LICENSE file" This was already in the repository in ./COPYING This reverts commit 1b452d126471df269605ed4e39edcdd38f9e59d8. --- LICENSE | 339 -------------------------------------------------------- 1 file changed, 339 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d511905c..00000000 --- a/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 2 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, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. From 2f9994807becf152cc6617e5e0e6d6ba24b2c363 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Sep 2021 10:02:13 -0700 Subject: [PATCH 0417/1451] use Poetry to deal with deps and package install As the new-age Python package manager, Poetry brings a lot of good additions to the table. It allows us to more easily deal with virtualenvs for the project and resolve dependencies. As of this commit, `requirements.txt` is replaced by Poetry, configured at `pyproject.toml`. In Docker and GitLab, we currently use Poetry in a root fashion. We should work toward purely using virtualenvs in Docker, but, for now we'd like to move forward with other things. The project can still be installed to a virtualenv and used on a user's system through Poetry; it is just not yet doing so in Docker. Modifications: * docker/scripts/install-deps.sh * Remove python dependencies. * conf/config.defaults * Script paths have been updated to use '/usr/bin'. * docker/git-entrypoint.sh * Use '/usr/bin/aurweb-git-auth' instead of '/usr/local/bin/aurweb-git-auth'. Additions: * docker/scripts/install-python-deps.sh * A script used purely to install Python dependencies with Poetry. This has to be used within the aurweb project directory and requires system-wide dependencies are installed beforehand. * Also upgrades system-wide pip. Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 4 +- Dockerfile | 15 +- INSTALL | 54 +- conf/config.defaults | 10 +- docker/git-entrypoint.sh | 2 +- docker/scripts/install-deps.sh | 14 +- docker/scripts/install-python-deps.sh | 14 + poetry.lock | 1577 +++++++++++++++++++++++++ pyproject.toml | 100 ++ requirements.txt | 36 - setup.py | 36 - 11 files changed, 1756 insertions(+), 106 deletions(-) create mode 100755 docker/scripts/install-python-deps.sh create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d360d483..ffea5308 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,8 +11,9 @@ variables: DB_HOST: localhost before_script: + - export PATH="$HOME/.poetry/bin:${PATH}" - ./docker/scripts/install-deps.sh - - pip install -r requirements.txt + - ./docker/scripts/install-python-deps.sh - useradd -U -d /aurweb -c 'AUR User' aur - ./docker/mariadb-entrypoint.sh - (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & @@ -20,7 +21,6 @@ before_script: - ./docker/test-mysql-entrypoint.sh # Create mysql AUR_CONFIG. - ./docker/test-sqlite-entrypoint.sh # Create sqlite AUR_CONFIG. - make -C po all install - - python setup.py install --install-scripts=/usr/local/bin - python -m aurweb.initdb # Initialize MySQL tables. - AUR_CONFIG=conf/config.sqlite python -m aurweb.initdb - make -C test clean diff --git a/Dockerfile b/Dockerfile index 6539bd94..76da62f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,25 @@ FROM archlinux:base-devel +ENV PATH="$HOME/.poetry/bin:${PATH}" ENV PYTHONPATH=/aurweb ENV AUR_CONFIG=conf/config +# Install system-wide dependencies. +COPY ./docker/scripts/install-deps.sh /install-deps.sh +RUN /install-deps.sh + # Copy Docker scripts COPY ./docker /docker COPY ./docker/scripts/*.sh /usr/local/bin/ -# Install system-wide dependencies. -RUN /docker/scripts/install-deps.sh - # Copy over all aurweb files. COPY . /aurweb # Working directory is aurweb root @ /aurweb. WORKDIR /aurweb -# Install pip directories now that we have access to /aurweb. -RUN pip install -r requirements.txt +# Install Python dependencies. +RUN /docker/scripts/install-python-deps.sh # Add our aur user. RUN useradd -U -d /aurweb -c 'AUR User' aur @@ -27,6 +29,3 @@ RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime # Install translations. RUN make -C po all install - -# Install package and scripts. -RUN python setup.py install --install-scripts=/usr/local/bin diff --git a/INSTALL b/INSTALL index 4df59bd2..e14b9f31 100644 --- a/INSTALL +++ b/INSTALL @@ -45,22 +45,54 @@ read the instructions below. if the defaults file does not exist) and adjust the configuration (pay attention to disable_http_login, enable_maintenance and aur_location). -4) Install Python modules and dependencies: +4) Install dependencies. - # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ - python-bleach python-markdown python-alembic hypercorn \ - python-itsdangerous python-authlib python-httpx \ - python-jinja python-aiofiles python-python-multipart \ - python-requests hypercorn python-bcrypt python-email-validator \ - python-lxml python-feedgen - # python3 setup.py install +4a) Install system-wide dependencies: -(FastAPI-Specific) + # pacman -S git gpgme cgit pyalpm python-srcinfo curl openssh \ + uwsgi uwsgi-plugin-cgi php php-fpm - # pacman -S redis python-redis python-fakeredis python-orjson +4b) Install Python dependencies via poetry (required): + +**NOTE** Users do not need to install pip or poetry dependencies system-wide. +You may take advantage of Poetry's virtualenv integration to manage +dependencies. This is merely a demonstration to show users how to without +a virtualenv. In Docker and CI, we don't yet use a virtualenv. + + ## Install Poetry dependencies system-wide, if not using a virtualenv. + # pacman -S python-pip + + ## Ensure pip is upgraded. Poetry depends on it being up to date. + # pip install --upgrade pip + + ## Install Poetry. + # curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - + # export PATH="$HOME/.poetry/bin:${PATH}" + + ## Use Poetry to install dependencies and the aurweb package. + # poetry lock # Resolve dependencies + # poetry update # Install/update dependencies + # poetry build # Build the aurweb package + # poetry install # Install the aurweb package and scripts + +When installing in a virtualenv, config.defaults must contain the correct +absolute paths to aurweb scripts, which requires modification. + +4c) Setup FastAPI Redis cache (optional). + +First, install Redis and start its service. + + # pacman -S redis # systemctl enable --now redis -5) Create a new MySQL database and a user and import the aurweb SQL schema: +Now that Redis is running, ensure that you configure aurweb to use +the Redis cache by setting `cache = redis` in your AUR config. + +In `conf/config.defaults`, the `redis_address` configuration is set +to `redis://localhost`. This can be set to point to any Redis server +and will be used as long as `cache = redis`. + +5) Create a new database and a user and import the aurweb SQL schema: $ python -m aurweb.initdb diff --git a/conf/config.defaults b/conf/config.defaults index 1b4c3a74..1c96a55d 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -34,8 +34,8 @@ commit_uri = /cgit/aur.git/commit/?h=%s&id=%s snapshot_uri = /cgit/aur.git/snapshot/%s.tar.gz enable-maintenance = 1 maintenance-exceptions = 127.0.0.1 -render-comment-cmd = /usr/local/bin/aurweb-rendercomment -localedir = /srv/http/aurweb/aur.git/web/locale/ +render-comment-cmd = /usr/bin/aurweb-rendercomment +localedir = /srv/http/aurweb/web/locale/ ; memcache, apc, or redis ; memcache/apc are supported in PHP, redis is supported in Python. cache = none @@ -49,7 +49,7 @@ request_limit = 4000 window_length = 86400 [notifications] -notify-cmd = /usr/local/bin/aurweb-notify +notify-cmd = /usr/bin/aurweb-notify sendmail = smtp-server = localhost smtp-port = 25 @@ -68,7 +68,7 @@ RSA = SHA256:Ju+yWiMb/2O+gKQ9RJCDqvRg7l+Q95KFAeqM5sr6l2s [auth] valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ssh-ecdsa@openssh.com sk-ssh-ed25519@openssh.com username-regex = [a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$ -git-serve-cmd = /usr/local/bin/aurweb-git-serve +git-serve-cmd = /usr/bin/aurweb-git-serve ssh-options = restrict [sso] @@ -83,7 +83,7 @@ session_secret = repo-path = /srv/http/aurweb/aur.git/ repo-regex = [a-z0-9][a-z0-9.+_-]*$ git-shell-cmd = /usr/bin/git-shell -git-update-cmd = /usr/local/bin/aurweb-git-update +git-update-cmd = /usr/bin/aurweb-git-update ssh-cmdline = ssh aur@aur.archlinux.org [update] diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index 57752ac5..cfd159c9 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -25,7 +25,7 @@ chmod 755 /app cat >> $AUTH_SCRIPT << EOF #!/usr/bin/env bash export AUR_CONFIG="$AUR_CONFIG" -exec /usr/local/bin/aurweb-git-auth "\$@" +exec /usr/bin/aurweb-git-auth "\$@" EOF chmod 755 $AUTH_SCRIPT diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 4985fe85..f8881d05 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -5,12 +5,12 @@ set -eou pipefail pacman -Syu --noconfirm --noprogressbar \ - --cachedir .pkg-cache git gpgme \ - nginx redis openssh \ - mariadb mariadb-libs \ - cgit uwsgi uwsgi-plugin-cgi \ - php php-fpm \ - memcached php-memcached \ - python-pip pyalpm python-srcinfo + --cachedir .pkg-cache git gpgme nginx redis openssh \ + mariadb mariadb-libs cgit uwsgi uwsgi-plugin-cgi \ + php php-fpm memcached php-memcached python-pip pyalpm \ + python-srcinfo curl + +# https://python-poetry.org/docs/ Installation section. +curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - exec "$@" diff --git a/docker/scripts/install-python-deps.sh b/docker/scripts/install-python-deps.sh new file mode 100755 index 00000000..df9b5997 --- /dev/null +++ b/docker/scripts/install-python-deps.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -eou pipefail + +# Upgrade PIP; Arch Linux's version of pip is outdated for Poetry. +pip install --upgrade pip + +# Install the aurweb package and deps system-wide via poetry. +poetry config virtualenvs.create false +poetry lock +poetry update +poetry build +poetry install --no-interaction --no-ansi + +exec "$@" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..3cc84361 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1577 @@ +[[package]] +name = "aiofiles" +version = "0.7.0" +description = "File support for asyncio." +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "alembic" +version = "1.6.5" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +Mako = "*" +python-dateutil = "*" +python-editor = ">=0.3" +SQLAlchemy = ">=1.3.0" + +[[package]] +name = "anyio" +version = "3.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "authlib" +version = "0.15.2" +description = "The ultimate Python library in building OAuth and OpenID Connect servers." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cryptography = "*" + +[package.extras] +client = ["requests"] + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "bleach" +version = "3.3.1" +description = "An easy safelist-based HTML-sanitizing tool." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.14.6" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.1" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "cryptography" +version = "3.4.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "dnspython" +version = "2.1.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dnssec = ["cryptography (>=2.6)"] +doh = ["requests", "requests-toolbelt"] +idna = ["idna (>=2.1)"] +curio = ["curio (>=1.2)", "sniffio (>=1.1)"] +trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] + +[[package]] +name = "dunamai" +version = "1.6.0" +description = "Dynamic version generation" +category = "main" +optional = false +python-versions = ">=3.5,<4.0" + +[[package]] +name = "email-validator" +version = "1.1.3" +description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + +[[package]] +name = "fakeredis" +version = "1.6.0" +description = "Fake implementation of redis API for testing purposes." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +redis = "<3.6.0" +six = ">=1.12" +sortedcontainers = "*" + +[package.extras] +aioredis = ["aioredis"] +lua = ["lupa"] + +[[package]] +name = "fastapi" +version = "0.66.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.14.2" + +[package.extras] +all = ["requests (>=2.24.0,<3.0.0)", "aiofiles (>=0.5.0,<0.6.0)", "jinja2 (>=2.11.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<2.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "graphene (>=2.1.8,<3.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)"] +dev = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "graphene (>=2.1.8,<3.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "markdown-include (>=0.6.0,<0.7.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] +test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (==0.812)", "flake8 (>=3.8.3,<4.0.0)", "black (==20.8b1)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.4.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"] + +[[package]] +name = "feedgen" +version = "0.9.0" +description = "Feed Generator (ATOM, RSS, Podcasts)" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +lxml = "*" +python-dateutil = "*" + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "h2" +version = "4.0.0" +description = "HTTP/2 State-Machine based protocol implementation" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "httpcore" +version = "0.13.6" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] + +[[package]] +name = "httpx" +version = "0.18.2" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +certifi = "*" +httpcore = ">=0.13.3,<0.14.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi (>=1.0.0,<2.0.0)"] +http2 = ["h2 (>=3.0.0,<4.0.0)"] + +[[package]] +name = "hypercorn" +version = "0.11.2" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +h11 = "*" +h2 = ">=3.1.0" +priority = "*" +toml = "*" +wsproto = ">=0.14.0" + +[package.extras] +h3 = ["aioquic (>=0.9.0,<1.0)"] +tests = ["hypothesis", "mock", "pytest", "pytest-asyncio", "pytest-cov", "pytest-trio", "trio"] +trio = ["trio (>=0.11.0)"] +uvloop = ["uvloop"] + +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "idna" +version = "3.2" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.9.3" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "itsdangerous" +version = "2.0.1" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "jinja2" +version = "3.0.1" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lxml" +version = "4.6.3" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "mako" +version = "1.1.5" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +name = "markdown" +version = "3.3.4" +description = "Python implementation of Markdown." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mysqlclient" +version = "2.0.3" +description = "Python interface to MySQL" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "orjson" +version = "3.6.3" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "poetry-dynamic-versioning" +version = "0.13.1" +description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" +category = "main" +optional = false +python-versions = ">=3.5,<4.0" + +[package.dependencies] +dunamai = ">=1.5,<2.0" +jinja2 = {version = ">=2.11.1,<4", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} +tomlkit = ">=0.4" + +[[package]] +name = "priority" +version = "2.0.0" +description = "A pure-Python implementation of the HTTP/2 priority tree" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "protobuf" +version = "3.17.3" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9" + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygit2" +version = "1.6.1" +description = "Python bindings for libgit2." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cffi = ">=1.4.0" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.15.1" +description = "Pytest support for asyncio." +category = "dev" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +testing = ["coverage", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-tap" +version = "3.2" +description = "Test Anything Protocol (TAP) reporting plugin for pytest" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pytest = ">=3.0" +"tap.py" = ">=3.0,<4.0" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-editor" +version = "1.0.4" +description = "Programmatically open an editor, capture the result." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "redis" +version = "3.5.3" +description = "Python client for Redis key-value store" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +hiredis = ["hiredis (>=0.1.3)"] + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "sqlalchemy" +version = "1.3.23" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mysql = ["mysqlclient"] +oracle = ["cx-oracle"] +postgresql = ["psycopg2"] +postgresql_pg8000 = ["pg8000 (<1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] + +[[package]] +name = "starlette" +version = "0.14.2" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + +[[package]] +name = "tap.py" +version = "3.0" +description = "Test Anything Protocol (TAP) tools" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +yaml = ["more-itertools", "PyYAML (>=5.1)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomlkit" +version = "0.7.2" +description = "Style preserving TOML library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} + +[[package]] +name = "typing" +version = "3.7.4.3" +description = "Type Hints for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "uvicorn" +version = "0.15.0" +description = "The lightning-fast ASGI server." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "werkzeug" +version = "2.0.1" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "wsproto" +version = "1.0.0" +description = "WebSockets state-machine based protocol implementation" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[metadata] +lock-version = "1.1" +python-versions = "*" +content-hash = "96112731ca21a6ff5d0657c6c40979642bb992ae660ba8d6135421718737c6b0" + +[metadata.files] +aiofiles = [ + {file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"}, + {file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"}, +] +alembic = [ + {file = "alembic-1.6.5-py2.py3-none-any.whl", hash = "sha256:e78be5b919f5bb184e3e0e2dd1ca986f2362e29a2bc933c446fe89f39dbe4e9c"}, + {file = "alembic-1.6.5.tar.gz", hash = "sha256:a21fedebb3fb8f6bbbba51a11114f08c78709377051384c9c5ead5705ee93a51"}, +] +anyio = [ + {file = "anyio-3.3.0-py3-none-any.whl", hash = "sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0"}, + {file = "anyio-3.3.0.tar.gz", hash = "sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"}, +] +asgiref = [ + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +authlib = [ + {file = "Authlib-0.15.2-py2.py3-none-any.whl", hash = "sha256:078b900fa9fbebf9f8dae1d5dc1ca857b6a742493093ef9b0b36ad926f36e41f"}, + {file = "Authlib-0.15.2.tar.gz", hash = "sha256:21b34625c83ca48150684bbeca8f7c884cd281913c72d146dbf0e9d2fbfdec4e"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +bleach = [ + {file = "bleach-3.3.1-py2.py3-none-any.whl", hash = "sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78"}, + {file = "bleach-3.3.1.tar.gz", hash = "sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa"}, +] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] +cffi = [ + {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, + {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, + {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, + {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, + {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, + {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, + {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, + {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, + {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, + {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, + {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, + {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, + {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, + {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, + {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, + {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, + {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, + {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, + {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, +] +click = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] +cryptography = [ + {file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"}, + {file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"}, + {file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"}, + {file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"}, + {file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"}, +] +dnspython = [ + {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, + {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, +] +dunamai = [ + {file = "dunamai-1.6.0-py3-none-any.whl", hash = "sha256:44a94a4edebb145bb6198a2f26de957b12b77d43b7c9c0646be814c60cf5d8df"}, + {file = "dunamai-1.6.0.tar.gz", hash = "sha256:6f1111f47e869ed58d44a7d37f112e3e7c761dce3c71f2c5464526928d7e9896"}, +] +email-validator = [ + {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, + {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, +] +fakeredis = [ + {file = "fakeredis-1.6.0-py3-none-any.whl", hash = "sha256:3449b306f3a85102b28f8180c24722ef966fcb1e3c744758b6f635ec80321a5c"}, + {file = "fakeredis-1.6.0.tar.gz", hash = "sha256:11ccfc9769d718d37e45b382e64a6ba02586b622afa0371a6bd85766d72255f3"}, +] +fastapi = [ + {file = "fastapi-0.66.0-py3-none-any.whl", hash = "sha256:85d8aee8c3c46171f4cb7bb3651425a42c07cb9183345d100ef55d88ca2ce15f"}, + {file = "fastapi-0.66.0.tar.gz", hash = "sha256:6ea4225448786f3d6fae737713789f87631a7455f65580de0a4a2e50471060d9"}, +] +feedgen = [ + {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +h2 = [ + {file = "h2-4.0.0-py3-none-any.whl", hash = "sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25"}, + {file = "h2-4.0.0.tar.gz", hash = "sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d"}, +] +hpack = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] +httpcore = [ + {file = "httpcore-0.13.6-py3-none-any.whl", hash = "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"}, + {file = "httpcore-0.13.6.tar.gz", hash = "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e"}, +] +httpx = [ + {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"}, + {file = "httpx-0.18.2.tar.gz", hash = "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"}, +] +hypercorn = [ + {file = "Hypercorn-0.11.2-py3-none-any.whl", hash = "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"}, + {file = "Hypercorn-0.11.2.tar.gz", hash = "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a"}, +] +hyperframe = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] +idna = [ + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, +] +itsdangerous = [ + {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, + {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, +] +jinja2 = [ + {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, + {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, +] +lxml = [ + {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, + {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, + {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, + {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, + {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, + {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, + {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, + {file = "lxml-4.6.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4"}, + {file = "lxml-4.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16"}, + {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, + {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, + {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617"}, + {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, + {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, + {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92"}, + {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, + {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, + {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae"}, + {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, + {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, + {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a"}, + {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, + {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, + {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, +] +mako = [ + {file = "Mako-1.1.5-py2.py3-none-any.whl", hash = "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23"}, + {file = "Mako-1.1.5.tar.gz", hash = "sha256:169fa52af22a91900d852e937400e79f535496191c63712e3b9fda5a9bed6fc3"}, +] +markdown = [ + {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, + {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mysqlclient = [ + {file = "mysqlclient-2.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:3381ca1a4f37ff1155fcfde20836b46416d66531add8843f6aa6d968982731c3"}, + {file = "mysqlclient-2.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0ac0dd759c4ca02c35a9fedc24bc982cf75171651e8187c2495ec957a87dfff7"}, + {file = "mysqlclient-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:71c4b330cf2313bbda0307fc858cc9055e64493ba9bf28454d25cf8b3ee8d7f5"}, + {file = "mysqlclient-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:fc575093cf81b6605bed84653e48b277318b880dc9becf42dd47fa11ffd3e2b6"}, + {file = "mysqlclient-2.0.3.tar.gz", hash = "sha256:f6ebea7c008f155baeefe16c56cd3ee6239f7a5a9ae42396c2f1860f08a7c432"}, +] +orjson = [ + {file = "orjson-3.6.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:5f78ed46b179585272a5670537f2203dbb7b3e2f8e4db1be72839cc423e2daef"}, + {file = "orjson-3.6.3-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:a99f310960e3acdda72ba1e98df8bf8c9145d90a0f72719786f43f4ea6937846"}, + {file = "orjson-3.6.3-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:8a5e46418f51f03060f91d743b59aed70c8d02a5012428365cfa20b7f670e903"}, + {file = "orjson-3.6.3-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:084de43ca9b19ad58c618c9f1ff93784e0190df2d88a02ae24c3cdebe9f2e9f7"}, + {file = "orjson-3.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b68a601f49c0328bf16498309e56ab87c1d6c2bb0287abf70329eb958d565c62"}, + {file = "orjson-3.6.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:9e4a26212851ea8ff81dee7e4e0da7e1e63b5b4f4330a8b4f27e99f1ba3f758b"}, + {file = "orjson-3.6.3-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:5eb9d7f2f45e12cbc7500da4176f2d3221a73891b4be505fe79c52cbb800e872"}, + {file = "orjson-3.6.3-cp37-none-win_amd64.whl", hash = "sha256:39aa7d42c9760fba36c37adb1d9c6752696ce9443c5dcb65222dd0994b5735e1"}, + {file = "orjson-3.6.3-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:8d4430e0cc390c1d745aea3827fd0c6fd7aa5f0690de30a2fe25c406aa5efa20"}, + {file = "orjson-3.6.3-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:be79e0ddea7f3a47332ec9573365c0b8a8cce4357e9682050f53c1bc75c1571f"}, + {file = "orjson-3.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fce5ada0f8dd7c9e16c675626a29dfc5cc766e1eb67d8021b1e77d0861e4e850"}, + {file = "orjson-3.6.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:1014a6f514b39dc414fce60568c9e7f635de97a1f1f5972ebc38f88a6160944a"}, + {file = "orjson-3.6.3-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:c3beff02a339f194274ec1fcf03e2c1563e84f297b568eb3d45751722454a52e"}, + {file = "orjson-3.6.3-cp38-none-win_amd64.whl", hash = "sha256:8f105e9290f901a618a0ced87f785fce2fcf6ab753699de081d82ee05c90f038"}, + {file = "orjson-3.6.3-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7936bef5589c9955ebee3423df51709d5f3b37ef54b830239bddb9fa5ead99f4"}, + {file = "orjson-3.6.3-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:82e3afbf404cb91774f894ed7bf52fd83bb1cc6bd72221711f4ce4e7774f0560"}, + {file = "orjson-3.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c702c78c33416fc8a138c5ec36eef5166ecfe8990c8f99c97551cd37c396e4d"}, + {file = "orjson-3.6.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:4606907b9aaec9fea6159ac14f838dbd2851f18b05fb414c4b3143bff9f2bb0d"}, + {file = "orjson-3.6.3-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:4ebb464b8b557a1401a03da6f41761544886db95b52280e60d25549da7427453"}, + {file = "orjson-3.6.3-cp39-none-win_amd64.whl", hash = "sha256:720a7d7ba1dcf32bbd8fb380370b1fdd06ed916caea48403edd64f2ccf7883c1"}, + {file = "orjson-3.6.3.tar.gz", hash = "sha256:353cc079cedfe990ea2d2186306f766e0d47bba63acd072e22d6df96c67be993"}, +] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +poetry-dynamic-versioning = [ + {file = "poetry-dynamic-versioning-0.13.1.tar.gz", hash = "sha256:5c0e7b22560db76812057ef95dadad662ecc63eb270145787eabe73da7c222f9"}, + {file = "poetry_dynamic_versioning-0.13.1-py3-none-any.whl", hash = "sha256:6d79f76436c624653fc06eb9bb54fb4f39b1d54362bc366ad2496855711d3a78"}, +] +priority = [ + {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, + {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, +] +protobuf = [ + {file = "protobuf-3.17.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8"}, + {file = "protobuf-3.17.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637"}, + {file = "protobuf-3.17.3-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0"}, + {file = "protobuf-3.17.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62"}, + {file = "protobuf-3.17.3-cp35-cp35m-win32.whl", hash = "sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6"}, + {file = "protobuf-3.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037"}, + {file = "protobuf-3.17.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71"}, + {file = "protobuf-3.17.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4"}, + {file = "protobuf-3.17.3-cp36-cp36m-win32.whl", hash = "sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9"}, + {file = "protobuf-3.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d"}, + {file = "protobuf-3.17.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b"}, + {file = "protobuf-3.17.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764"}, + {file = "protobuf-3.17.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c"}, + {file = "protobuf-3.17.3-cp37-cp37m-win32.whl", hash = "sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5"}, + {file = "protobuf-3.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683"}, + {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, + {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, + {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, + {file = "protobuf-3.17.3-cp38-cp38-win32.whl", hash = "sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1"}, + {file = "protobuf-3.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2"}, + {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, + {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, + {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, + {file = "protobuf-3.17.3-cp39-cp39-win32.whl", hash = "sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554"}, + {file = "protobuf-3.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d"}, + {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, + {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pydantic = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pygit2 = [ + {file = "pygit2-1.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:547429774c11f5bc9d20a49aa86e4bd13c90a55140504ef05f55cf424470ee34"}, + {file = "pygit2-1.6.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e75865d7b6fc161d93b16f10365eaad353cd546e302a98f2de2097ddea1066b"}, + {file = "pygit2-1.6.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4a64b6090308ffd1c82e2dd4316cb79483715387b13818156d516134a5b17c"}, + {file = "pygit2-1.6.1-cp36-cp36m-win32.whl", hash = "sha256:2666a3970b2ea1222a9f0463b466f98c8d564f29ec84cf0a58d9b0d3865dbaaf"}, + {file = "pygit2-1.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2de12ca2d3b7eb86106223b40b2edc0c61103c71e7962e53092c6ddef71a194"}, + {file = "pygit2-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9c1d96c66fb6e69ec710078a73c19edff420bc1db430caa9e03a825eede3f25c"}, + {file = "pygit2-1.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:454d42550fa6a6cd0e6a6ad9ab3f3262135fd157f57bad245ce156c36ee93370"}, + {file = "pygit2-1.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce0827b77dd2f8a3465bdc181c4e65f27dd12dbd92635c038e58030cc90c2de0"}, + {file = "pygit2-1.6.1-cp37-cp37m-win32.whl", hash = "sha256:b0161a141888d450eb821472fdcdadd14a072ddeda841fee9984956d34d3e19d"}, + {file = "pygit2-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:af2fa259b6f7899227611ab978c600695724e85965836cb607d8b1e70cfea9b3"}, + {file = "pygit2-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0e1e02c28983ddc004c0f54063f3e46fca388225d468e32e16689cfb750e0bd6"}, + {file = "pygit2-1.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5dadc4844feb76cde5cc9a37656326a361dd8b5c8e8f8674dcd4a5ecf395db3"}, + {file = "pygit2-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07458e4172a31318663295083b43f957d611145738ff56aa76db593542a6e8"}, + {file = "pygit2-1.6.1-cp38-cp38-win32.whl", hash = "sha256:7a0c0a1f11fd41f57e8c6c64d903cc7fa4ec95d15592270be3217ed7f78eb023"}, + {file = "pygit2-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:2fd5c1b2d84dc6084f1bda836607afe37e95186a53a5a827a69083415e57fe4f"}, + {file = "pygit2-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9b88b7e9a5286a71be0b6c307f0523c9606aeedff6b61eb9c440e18817fa641"}, + {file = "pygit2-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac12d32b714c3383ebccffee5eb6aff0b69a2542a40a664fd5ad370afcb28ee7"}, + {file = "pygit2-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe682ed6afd2ab31127f6a502cf3e002dc1cc8d26c36a5d49dfd180250351eb6"}, + {file = "pygit2-1.6.1-cp39-cp39-win32.whl", hash = "sha256:dbbf66a23860aa899949068ac9b503b4bc21e6063e8f53870440adbdc909405e"}, + {file = "pygit2-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:f90775afb11f69376e2af21ab56fcfbb52f6bc84117059ddf0355f81e5e36352"}, + {file = "pygit2-1.6.1.tar.gz", hash = "sha256:c3303776f774d3e0115c1c4f6e1fc35470d15f113a7ae9401a0b90acfa1661ac"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, + {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +pytest-tap = [ + {file = "pytest-tap-3.2.tar.gz", hash = "sha256:1b585c4a636458dbd958d136381bbabb1752c5877d05fac7d6a6001a8a9ddc29"}, + {file = "pytest_tap-3.2-py3-none-any.whl", hash = "sha256:18f59047f8bc68247d37f807fae7f2f8897d2c7397aea2fd2870f0421dc566cb"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +python-editor = [ + {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, + {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, + {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"}, + {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, + {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"}, +] +python-multipart = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] +redis = [ + {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, + {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, +] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +sortedcontainers = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] +sqlalchemy = [ + {file = "SQLAlchemy-1.3.23-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec"}, + {file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1"}, + {file = "SQLAlchemy-1.3.23-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5"}, + {file = "SQLAlchemy-1.3.23-cp27-cp27m-win32.whl", hash = "sha256:b4b0e44d586cd64b65b507fa116a3814a1a53d55dce4836d7c1a6eb2823ff8d1"}, + {file = "SQLAlchemy-1.3.23-cp27-cp27m-win_amd64.whl", hash = "sha256:6b8b8c80c7f384f06825612dd078e4a31f0185e8f1f6b8c19e188ff246334205"}, + {file = "SQLAlchemy-1.3.23-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9e9c25522933e569e8b53ccc644dc993cab87e922fb7e142894653880fdd419d"}, + {file = "SQLAlchemy-1.3.23-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a0e306e9bb76fd93b29ae3a5155298e4c1b504c7cbc620c09c20858d32d16234"}, + {file = "SQLAlchemy-1.3.23-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:6c9e6cc9237de5660bcddea63f332428bb83c8e2015c26777281f7ffbd2efb84"}, + {file = "SQLAlchemy-1.3.23-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:94f667d86be82dd4cb17d08de0c3622e77ca865320e0b95eae6153faa7b4ecaf"}, + {file = "SQLAlchemy-1.3.23-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:751934967f5336a3e26fc5993ccad1e4fee982029f9317eb6153bc0bc3d2d2da"}, + {file = "SQLAlchemy-1.3.23-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:63677d0c08524af4c5893c18dbe42141de7178001360b3de0b86217502ed3601"}, + {file = "SQLAlchemy-1.3.23-cp35-cp35m-win32.whl", hash = "sha256:ddfb511e76d016c3a160910642d57f4587dc542ce5ee823b0d415134790eeeb9"}, + {file = "SQLAlchemy-1.3.23-cp35-cp35m-win_amd64.whl", hash = "sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862"}, + {file = "SQLAlchemy-1.3.23-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:d1a85dfc5dee741bf49cb9b6b6b8d2725a268e4992507cf151cba26b17d97c37"}, + {file = "SQLAlchemy-1.3.23-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:639940bbe1108ac667dcffc79925db2966826c270112e9159439ab6bb14f8d80"}, + {file = "SQLAlchemy-1.3.23-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e8a1750b44ad6422ace82bf3466638f1aa0862dbb9689690d5f2f48cce3476c8"}, + {file = "SQLAlchemy-1.3.23-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e5bb3463df697279e5459a7316ad5a60b04b0107f9392e88674d0ece70e9cf70"}, + {file = "SQLAlchemy-1.3.23-cp36-cp36m-win32.whl", hash = "sha256:e273367f4076bd7b9a8dc2e771978ef2bfd6b82526e80775a7db52bff8ca01dd"}, + {file = "SQLAlchemy-1.3.23-cp36-cp36m-win_amd64.whl", hash = "sha256:ac2244e64485c3778f012951fdc869969a736cd61375fde6096d08850d8be729"}, + {file = "SQLAlchemy-1.3.23-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:23927c3981d1ec6b4ea71eb99d28424b874d9c696a21e5fbd9fa322718be3708"}, + {file = "SQLAlchemy-1.3.23-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d90010304abb4102123d10cbad2cdf2c25a9f2e66a50974199b24b468509bad5"}, + {file = "SQLAlchemy-1.3.23-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a8bfc1e1afe523e94974132d7230b82ca7fa2511aedde1f537ec54db0399541a"}, + {file = "SQLAlchemy-1.3.23-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:269990b3ab53cb035d662dcde51df0943c1417bdab707dc4a7e4114a710504b4"}, + {file = "SQLAlchemy-1.3.23-cp37-cp37m-win32.whl", hash = "sha256:fdd2ed7395df8ac2dbb10cefc44737b66c6a5cd7755c92524733d7a443e5b7e2"}, + {file = "SQLAlchemy-1.3.23-cp37-cp37m-win_amd64.whl", hash = "sha256:6a939a868fdaa4b504e8b9d4a61f21aac11e3fecc8a8214455e144939e3d2aea"}, + {file = "SQLAlchemy-1.3.23-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:24f9569e82a009a09ce2d263559acb3466eba2617203170e4a0af91e75b4f075"}, + {file = "SQLAlchemy-1.3.23-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2578dbdbe4dbb0e5126fb37ffcd9793a25dcad769a95f171a2161030bea850ff"}, + {file = "SQLAlchemy-1.3.23-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1fe5d8d39118c2b018c215c37b73fd6893c3e1d4895be745ca8ff6eb83333ed3"}, + {file = "SQLAlchemy-1.3.23-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:c7dc052432cd5d060d7437e217dd33c97025287f99a69a50e2dc1478dd610d64"}, + {file = "SQLAlchemy-1.3.23-cp38-cp38-win32.whl", hash = "sha256:ecce8c021894a77d89808222b1ff9687ad84db54d18e4bd0500ca766737faaf6"}, + {file = "SQLAlchemy-1.3.23-cp38-cp38-win_amd64.whl", hash = "sha256:37b83bf81b4b85dda273aaaed5f35ea20ad80606f672d94d2218afc565fb0173"}, + {file = "SQLAlchemy-1.3.23-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:8be835aac18ec85351385e17b8665bd4d63083a7160a017bef3d640e8e65cadb"}, + {file = "SQLAlchemy-1.3.23-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6ec1044908414013ebfe363450c22f14698803ce97fbb47e53284d55c5165848"}, + {file = "SQLAlchemy-1.3.23-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:eab063a70cca4a587c28824e18be41d8ecc4457f8f15b2933584c6c6cccd30f0"}, + {file = "SQLAlchemy-1.3.23-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:baeb451ee23e264de3f577fee5283c73d9bbaa8cb921d0305c0bbf700094b65b"}, + {file = "SQLAlchemy-1.3.23-cp39-cp39-win32.whl", hash = "sha256:94208867f34e60f54a33a37f1c117251be91a47e3bfdb9ab8a7847f20886ad06"}, + {file = "SQLAlchemy-1.3.23-cp39-cp39-win_amd64.whl", hash = "sha256:f4d972139d5000105fcda9539a76452039434013570d6059993120dc2a65e447"}, + {file = "SQLAlchemy-1.3.23.tar.gz", hash = "sha256:6fca33672578666f657c131552c4ef8979c1606e494f78cd5199742dfb26918b"}, +] +starlette = [ + {file = "starlette-0.14.2-py3-none-any.whl", hash = "sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed"}, + {file = "starlette-0.14.2.tar.gz", hash = "sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa"}, +] +"tap.py" = [ + {file = "tap.py-3.0-py2.py3-none-any.whl", hash = "sha256:a598bfaa2e224d71f2e86147c2ef822c18ff2e1b8ef006397e5056b08f92f699"}, + {file = "tap.py-3.0.tar.gz", hash = "sha256:f5eeeeebfd64e53d32661752bb4c288589a3babbb96db3f391a4ec29f1359c70"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomlkit = [ + {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, + {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"}, +] +typing = [ + {file = "typing-3.7.4.3-py2-none-any.whl", hash = "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"}, + {file = "typing-3.7.4.3.tar.gz", hash = "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, +] +urllib3 = [ + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, +] +uvicorn = [ + {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, + {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] +werkzeug = [ + {file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"}, + {file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"}, +] +wsproto = [ + {file = "wsproto-1.0.0-py3-none-any.whl", hash = "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"}, + {file = "wsproto-1.0.0.tar.gz", hash = "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..8cb276ce --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +# Poetry build configuration for the aurweb project. +# +# Dependencies: +# * python >= 3.9 +# * pip +# * poetry +# * poetry-dynamic-versioning +# +[tool.poetry] +name = "aurweb" +version = "5.0.0" # Updated via poetry-dynamic-versioning +license = "GPL-2.0-only" +description = "Source code for the Arch User Repository's website" +homepage = "https://aur.archlinux.org" +repository = "https://gitlab.archlinux.org/archlinux/aurweb" +documentation = "https://gitlab.archlinux.org/archlinux/aurweb/-/blob/master/README.md" +keywords = ["aurweb", "aur", "Arch", "Linux"] +authors = [ + "Lucas Fleischer ", + "Eli Schwartz ", + "Kevin Morris " +] +maintainers = [ + "Eli Schwartz " +] +packages = [ + { include = "aurweb" } +] + +[tool.poetry-dynamic-versioning] +enable = true +vcs = "git" + +[build-system] +requires = ["poetry>=1.1.8", "poetry-dynamic-versioning"] +build-backend = "poetry.masonry.api" + +[tool.poetry.urls] +"Repository" = "https://gitlab.archlinux.org/archlinux/aurweb" +"Bug Tracker" = "https://gitlab.archlinux.org/archlinux/aurweb/-/issues" +"Development Mailing List" = "https://lists.archlinux.org/listinfo/aur-dev" +"General Mailing List" = "https://lists.archlinux.org/listinfo/aur-general" +"Request Mailing List" = "https://lists.archlinux.org/listinfo/aur-requests" + +[tool.poetry.dependencies] +# poetry-dynamic-versioning is used to produce tool.poetry.version +# based on git tags. +poetry-dynamic-versioning = { version = "0.13.1", python = "^3.9" } + +# General +authlib = { version = "0.15.2", python = "^3.9" } +aiofiles = { version = "0.7.0", python = "^3.9" } +asgiref = { version = "3.4.1", python = "^3.9" } +bcrypt = { version = "3.2.0", python = "^3.9" } +bleach = { version = "3.3.1", python = "^3.9" } +email-validator = { version = "1.1.3", python = "^3.9" } +fakeredis = { version = "1.6.0", python = "^3.9" } +fastapi = { version = "0.66.0", python = "^3.9" } +feedgen = { version = "0.9.0", python = "^3.9" } +httpx = { version = "0.18.2", python = "^3.9" } +hypercorn = { version = "0.11.2", python = "^3.9" } +itsdangerous = { version = "2.0.1", python = "^3.9" } +jinja2 = { version = "3.0.1", python = "^3.9" } +lxml = { version = "4.6.3", python = "^3.9" } +markdown = { version = "3.3.4", python = "^3.9" } +orjson = { version = "3.6.3", python = "^3.9" } +protobuf = { version = "3.17.3", python = "^3.9" } +pygit2 = { version = "1.6.1", python = "^3.9" } +python-multipart = { version = "0.0.5", python = "^3.9" } +redis = { version = "3.5.3", python = "^3.9" } +requests = { version = "2.26.0", python = "^3.9" } +werkzeug = { version = "2.0.1", python = "^3.9" } + +# SQL +alembic = { version = "1.6.5", python = "^3.9" } +sqlalchemy = { version = "1.3.23", python = "^3.9" } +mysqlclient = { version = "2.0.3", python = "^3.9" } + +[tool.poetry.dev-dependencies] +flake8 = { version = "3.9.2", python = "^3.9" } +isort = { version = "5.9.3", python = "^3.9" } +coverage = { version = "5.5", python = "^3.9" } +pytest = { version = "6.2.4", python = "^3.9" } +pytest-asyncio = { version = "0.15.1", python = "^3.9" } +pytest-cov = { version = "2.12.1", python = "^3.9" } +pytest-tap = { version = "3.2", python = "^3.9" } +uvicorn = { version = "0.15.0", python = "^3.9" } + +[tool.poetry.scripts] +aurweb-git-auth = "aurweb.git.auth:main" +aurweb-git-serve = "aurweb.git.serve:main" +aurweb-git-update = "aurweb.git.update:main" +aurweb-aurblup = "aurweb.scripts.aurblup:main" +aurweb-mkpkglists = "aurweb.scripts.mkpkglists:main" +aurweb-notify = "aurweb.scripts.notify:main" +aurweb-pkgmaint = "aurweb.scripts.pkgmaint:main" +aurweb-popupdate = "aurweb.scripts.popupdate:main" +aurweb-rendercomment = "aurweb.scripts.rendercomment:main" +aurweb-tuvotereminder = "aurweb.scripts.tuvotereminder:main" +aurweb-usermaint = "aurweb.scripts.usermaint:main" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 37a12f61..00000000 --- a/requirements.txt +++ /dev/null @@ -1,36 +0,0 @@ -# General -authlib==0.15.2 -aiofiles==0.7.0 -asgiref==3.4.1 -bcrypt==3.2.0 -bleach==3.3.1 -coverage==5.5 -email-validator==1.1.3 -fakeredis==1.6.0 -fastapi==0.66.0 -feedgen==0.9.0 -flake8==3.9.2 -httpx==0.18.2 -hypercorn==0.11.2 -isort==5.9.3 -itsdangerous==2.0.1 -jinja2==3.0.1 -lxml==4.6.3 -markdown==3.3.4 -orjson==3.6.3 -protobuf==3.17.3 -pygit2==1.6.1 -pytest==6.2.4 -pytest-asyncio==0.15.1 -pytest-cov==2.12.1 -pytest-tap==3.2 -python-multipart==0.0.5 -redis==3.5.3 -requests==2.26.0 -uvicorn==0.15.0 -werkzeug==2.0.1 - -# SQL -alembic==1.6.5 -sqlalchemy==1.3.23 -mysqlclient==2.0.3 diff --git a/setup.py b/setup.py deleted file mode 100644 index cf88488c..00000000 --- a/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -import re -import sys - -from setuptools import find_packages, setup - -version = None -with open('web/lib/version.inc.php', 'r') as f: - for line in f.readlines(): - match = re.match(r'^define\("AURWEB_VERSION", "v([0-9.]+)"\);$', line) - if match: - version = match.group(1) - -if not version: - sys.stderr.write('error: Failed to parse version file!') - sys.exit(1) - -setup( - name="aurweb", - version=version, - packages=find_packages(), - entry_points={ - 'console_scripts': [ - 'aurweb-git-auth = aurweb.git.auth:main', - 'aurweb-git-serve = aurweb.git.serve:main', - 'aurweb-git-update = aurweb.git.update:main', - 'aurweb-aurblup = aurweb.scripts.aurblup:main', - 'aurweb-mkpkglists = aurweb.scripts.mkpkglists:main', - 'aurweb-notify = aurweb.scripts.notify:main', - 'aurweb-pkgmaint = aurweb.scripts.pkgmaint:main', - 'aurweb-popupdate = aurweb.scripts.popupdate:main', - 'aurweb-rendercomment = aurweb.scripts.rendercomment:main', - 'aurweb-tuvotereminder = aurweb.scripts.tuvotereminder:main', - 'aurweb-usermaint = aurweb.scripts.usermaint:main', - ], - }, -) From 3f034ac1287ec7533589807e23240d0d136aaeca Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Sep 2021 18:59:24 -0700 Subject: [PATCH 0418/1451] Docker: Fix incorrect ENV PATH specification As root, seems that $HOME doesn't work like I expected it to. Tested this before, but I apparently had some cache still holding on. Fixing the issue in this commit here. Signed-off-by: Kevin Morris --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 76da62f7..b490d2fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM archlinux:base-devel -ENV PATH="$HOME/.poetry/bin:${PATH}" +ENV PATH="/root/.poetry/bin:${PATH}" ENV PYTHONPATH=/aurweb ENV AUR_CONFIG=conf/config From fa07f940514fd30886acf3d47c3a37d79940adfa Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Sep 2021 19:08:10 -0700 Subject: [PATCH 0419/1451] Docker: Fix FastAPI db initialization PHP was doing this correctly, but FastAPI was doing this in it's exec script @ docker/scripts/run-fastapi.sh. Modify the fastapi service so that it does the same thing as PHP, and the existing "fastapi restart quirk" is no more. Signed-off-by: Kevin Morris --- docker/fastapi-entrypoint.sh | 3 +++ docker/scripts/run-fastapi.sh | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index 41a88206..83a2cda8 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -14,4 +14,7 @@ sed -ri 's|^(redis_address) = .+|\1 = redis://redis|' conf/config sed -ri "s|^(git_clone_uri_anon) = .+|\1 = https://localhost:8444/%s.git|" conf/config.defaults sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ssh://aur@localhost:2222/%s.git|" conf/config.defaults +# Initialize the new database; ignore errors. +python -m aurweb.initdb 2>/dev/null || /bin/true + exec "$@" diff --git a/docker/scripts/run-fastapi.sh b/docker/scripts/run-fastapi.sh index 1db4c505..bb1a01a7 100755 --- a/docker/scripts/run-fastapi.sh +++ b/docker/scripts/run-fastapi.sh @@ -1,8 +1,5 @@ #!/bin/bash -# Initialize the new database; ignore errors. -python -m aurweb.initdb 2>/dev/null || /bin/true - if [ "$1" == "uvicorn" ] || [ "$1" == "" ]; then exec uvicorn --reload \ --ssl-certfile /cache/localhost.cert.pem \ From e93b0a9b452da9db9b28b1c734fb323e367c991d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 5 Sep 2021 00:08:47 -0700 Subject: [PATCH 0420/1451] Docker: expose fastapi (18000) and php-fpm (19000) Signed-off-by: Kevin Morris --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 0e91d6eb..e4eccb12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -166,6 +166,8 @@ services: - ./web/template:/aurweb/web/template - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates + ports: + - "19000:9000" fastapi: image: aurweb:latest @@ -197,6 +199,8 @@ services: - ./web/template:/aurweb/web/template - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates + ports: + - "18000:8000" nginx: image: aurweb:latest From 95357687f9e0a6c9519e7dc2475059a3506abcd3 Mon Sep 17 00:00:00 2001 From: Hunter Wittenborn Date: Sun, 5 Sep 2021 16:13:45 -0500 Subject: [PATCH 0421/1451] Added ability to specify fortune file via an environment variable --- schema/gendummydata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 9224b051..275b3601 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -41,7 +41,7 @@ CLOSE_PROPOSALS = int(os.environ.get("CLOSE_PROPOSALS", 50)) RANDOM_TLDS = ("edu", "com", "org", "net", "tw", "ru", "pl", "de", "es") RANDOM_URL = ("http://www.", "ftp://ftp.", "http://", "ftp://") RANDOM_LOCS = ("pub", "release", "files", "downloads", "src") -FORTUNE_FILE = "/usr/share/fortune/cookie" +FORTUNE_FILE = os.environ.get("FORTUNE_FILE", "/usr/share/fortune/cookie") # setup logging logformat = "%(levelname)s: %(message)s" From 2e3f69ab126e6ac6ffa92b4e149faab61bfa3374 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 8 Sep 2021 17:10:14 -0700 Subject: [PATCH 0422/1451] fix(docker): Fix git service's update hook The update hook was incorrectly linked to /usr/local/bin/aurweb-git-update, which was neglected during the original patch regarding dependency conversion to `poetry`. Signed-off-by: Kevin Morris --- docker/git-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index cfd159c9..f07a5577 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -78,7 +78,7 @@ if [ ! -f $GIT_REPO/config ]; then git config --local transfer.hideRefs '^refs/' git config --local --add transfer.hideRefs '!refs/' git config --local --add transfer.hideRefs '!HEAD' - ln -sf /usr/local/bin/aurweb-git-update hooks/update + ln -sf /usr/bin/aurweb-git-update hooks/update cd $curdir chown -R aur:aur $GIT_REPO fi From 0fd31b8d368a4e4a267b5e83d608bc088bb13b4d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 8 Sep 2021 17:14:55 -0700 Subject: [PATCH 0423/1451] refactor(docker): New mariadb_init service Provides a single source of truth for mariadb database initialization. Previously, php-fpm and fastapi were racing against each other; while this wasn't an issue, it was very messy. Signed-off-by: Kevin Morris --- docker-compose.yml | 45 +++++++++++++------------------ docker/fastapi-entrypoint.sh | 19 ++++++++----- docker/mariadb-entrypoint.sh | 2 -- docker/mariadb-init-entrypoint.sh | 20 ++++++++++++++ docker/php-entrypoint.sh | 18 +++++++++---- 5 files changed, 64 insertions(+), 40 deletions(-) create mode 100755 docker/mariadb-init-entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index e4eccb12..309e95fe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,8 +49,6 @@ services: mariadb: image: aurweb:latest init: true - environment: - - DB_HOST="%" entrypoint: /docker/mariadb-entrypoint.sh command: /usr/bin/mysqld_safe --datadir=/var/lib/mysql ports: @@ -63,11 +61,23 @@ services: healthcheck: test: "bash /docker/health/mariadb.sh" + mariadb_init: + image: aurweb:latest + init: true + environment: + - DB_HOST=mariadb + entrypoint: /docker/mariadb-init-entrypoint.sh + command: echo "MariaDB tables initialized." + depends_on: + mariadb: + condition: service_healthy + git: image: aurweb:latest init: true environment: - AUR_CONFIG=/aurweb/conf/config + - DB_HOST=mariadb entrypoint: /docker/git-entrypoint.sh command: /docker/scripts/run-sshd.sh ports: @@ -75,11 +85,9 @@ services: healthcheck: test: "bash /docker/health/sshd.sh" depends_on: - mariadb: - condition: service_healthy + mariadb_init: + condition: service_started volumes: - - mariadb_run:/var/run/mysqld - - mariadb_data:/var/lib/mysql - git_data:/aurweb/aur.git - ./cache:/cache @@ -96,8 +104,6 @@ services: mariadb: condition: service_healthy volumes: - - mariadb_run:/var/run/mysqld - - mariadb_data:/var/lib/mysql - git_data:/aurweb/aur.git - ./cache:/cache - smartgit_run:/var/run/smartgit @@ -114,8 +120,6 @@ services: depends_on: git: condition: service_healthy - php-fpm: - condition: service_healthy volumes: - git_data:/aurweb/aur.git @@ -131,8 +135,6 @@ services: depends_on: git: condition: service_healthy - fastapi: - condition: service_healthy volumes: - git_data:/aurweb/aur.git @@ -151,13 +153,9 @@ services: condition: service_started git: condition: service_healthy - mariadb: - condition: service_healthy memcached: condition: service_healthy volumes: - - mariadb_run:/var/run/mysqld # Bind socket in this volume. - - mariadb_data:/var/lib/mysql - ./cache:/cache - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations @@ -186,11 +184,7 @@ services: condition: service_healthy redis: condition: service_healthy - mariadb: - condition: service_healthy volumes: - - mariadb_run:/var/run/mysqld # Bind socket in this volume. - - mariadb_data:/var/lib/mysql - ./cache:/cache - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations @@ -268,10 +262,9 @@ services: stdin_open: true tty: true depends_on: - mariadb: - condition: service_healthy + mariadb_init: + condition: service_started volumes: - - mariadb_run:/var/run/mysqld - git_data:/aurweb/aur.git - ./cache:/cache - ./aurweb:/aurweb/aurweb @@ -292,7 +285,6 @@ services: stdin_open: true tty: true volumes: - - mariadb_run:/var/run/mysqld - git_data:/aurweb/aur.git - ./cache:/cache - ./aurweb:/aurweb/aurweb @@ -314,10 +306,9 @@ services: stdin_open: true tty: true depends_on: - mariadb: - condition: service_healthy + mariadb_init: + condition: service_started volumes: - - mariadb_run:/var/run/mysqld - git_data:/aurweb/aur.git - ./cache:/cache - ./aurweb:/aurweb/aurweb diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index 83a2cda8..3829b0bf 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -1,11 +1,21 @@ #!/bin/bash set -eou pipefail -dir="$(dirname $0)" -bash $dir/test-mysql-entrypoint.sh +[[ -z "$DB_HOST" ]] && echo 'Error: $DB_HOST required but missing.' && exit 1 + +DB_NAME="aurweb" +DB_USER="aur" +DB_PASS="aur" + +# Setup a config for our mysql db. +cp -vf conf/config.dev conf/config +sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config +sed -ri "s/^(name) = .+/\1 = ${DB_NAME}/" conf/config +sed -ri "s/^(host) = .+/\1 = ${DB_HOST}/" conf/config +sed -ri "s/^(user) = .+/\1 = ${DB_USER}/" conf/config +sed -ri "s/^;?(password) = .+/\1 = ${DB_PASS}/" conf/config sed -ri "s;^(aur_location) = .+;\1 = https://localhost:8444;" conf/config -sed -ri 's/^(name) = .+/\1 = aurweb/' conf/config # Setup Redis for FastAPI. sed -ri 's/^(cache) = .+/\1 = redis/' conf/config @@ -14,7 +24,4 @@ sed -ri 's|^(redis_address) = .+|\1 = redis://redis|' conf/config sed -ri "s|^(git_clone_uri_anon) = .+|\1 = https://localhost:8444/%s.git|" conf/config.defaults sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ssh://aur@localhost:2222/%s.git|" conf/config.defaults -# Initialize the new database; ignore errors. -python -m aurweb.initdb 2>/dev/null || /bin/true - exec "$@" diff --git a/docker/mariadb-entrypoint.sh b/docker/mariadb-entrypoint.sh index 945a4b82..e1ebfa6a 100755 --- a/docker/mariadb-entrypoint.sh +++ b/docker/mariadb-entrypoint.sh @@ -3,8 +3,6 @@ set -eou pipefail MYSQL_DATA=/var/lib/mysql -[[ -z "$DB_HOST" ]] && DB_HOST="localhost" - mariadb-install-db --user=mysql --basedir=/usr --datadir=$MYSQL_DATA # Start it up. diff --git a/docker/mariadb-init-entrypoint.sh b/docker/mariadb-init-entrypoint.sh new file mode 100755 index 00000000..4cd6f46c --- /dev/null +++ b/docker/mariadb-init-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -eou pipefail + +[[ -z "$DB_HOST" ]] && echo 'Error: $DB_HOST required but missing.' && exit 1 + +DB_NAME="aurweb" +DB_USER="aur" +DB_PASS="aur" + +# Setup a config for our mysql db. +cp -vf conf/config.dev conf/config +sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config +sed -ri "s/^(name) = .+/\1 = ${DB_NAME}/" conf/config +sed -ri "s/^(host) = .+/\1 = ${DB_HOST}/" conf/config +sed -ri "s/^(user) = .+/\1 = ${DB_USER}/" conf/config +sed -ri "s/^;?(password) = .+/\1 = ${DB_PASS}/" conf/config + +python -m aurweb.initdb 2>/dev/null || /bin/true + +exec "$@" diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index 1f3ed82b..8fda1830 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -1,11 +1,21 @@ #!/bin/bash set -eou pipefail -dir="$(dirname $0)" -bash $dir/test-mysql-entrypoint.sh +[[ -z "$DB_HOST" ]] && echo 'Error: $DB_HOST required but missing.' && exit 1 + +DB_NAME="aurweb" +DB_USER="aur" +DB_PASS="aur" + +# Setup a config for our mysql db. +cp -vf conf/config.dev conf/config +sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config +sed -ri "s/^(name) = .+/\1 = ${DB_NAME}/" conf/config +sed -ri "s/^(host) = .+/\1 = ${DB_HOST}/" conf/config +sed -ri "s/^(user) = .+/\1 = ${DB_USER}/" conf/config +sed -ri "s/^;?(password) = .+/\1 = ${DB_PASS}/" conf/config sed -ri "s;^(aur_location) = .+;\1 = https://localhost:8443;" conf/config -sed -ri 's/^(name) = .+/\1 = aurweb/' conf/config # Enable memcached. sed -ri 's/^(cache) = .+$/\1 = memcache/' conf/config @@ -27,6 +37,4 @@ sed -ri 's/^;?(open_basedir).*$/\1 = \//' /etc/php/php.ini # Use the sqlite3 extension line for memcached. sed -ri 's/^;(extension)=sqlite3$/\1=memcached/' /etc/php/php.ini -python -m aurweb.initdb 2>/dev/null || /bin/true - exec "$@" From ad3016ef4f98a3131af2680d8d7a80cf0ce6ac74 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 8 Sep 2021 17:36:37 -0700 Subject: [PATCH 0424/1451] fix: /account/{name}/edit Account Type selection The "Account Type" selection was not properly being rendered due to an incorrect equality. This has been fixed in templates/partials/account_form.html. Signed-off-by: Kevin Morris --- templates/partials/account_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 6374fd5e..f166c230 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -59,7 +59,7 @@ {% if request.user.is_authenticated() %} diff --git a/test/test_packages_util.py b/test/test_packages_util.py index 754e3b8d..1396734b 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -1,3 +1,5 @@ +from datetime import datetime + import pytest from fastapi.testclient import TestClient @@ -7,6 +9,8 @@ from aurweb.models.account_type import USER_ID, AccountType from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_vote import PackageVote from aurweb.models.user import User from aurweb.packages import util from aurweb.redis import kill_redis @@ -19,6 +23,8 @@ def setup(): User.__tablename__, Package.__tablename__, PackageBase.__tablename__, + PackageVote.__tablename__, + PackageNotification.__tablename__, OfficialProvider.__tablename__ ) @@ -71,3 +77,24 @@ def test_updated_packages(maintainer: User, package: Package): assert util.updated_packages(1, 0) == [expected] assert util.updated_packages(1, 600) == [expected] kill_redis() # Kill it again, in case other tests use a real instance. + + +def test_query_voted(maintainer: User, package: Package): + now = int(datetime.utcnow().timestamp()) + with db.begin(): + db.create(PackageVote, User=maintainer, VoteTS=now, + PackageBase=package.PackageBase) + + query = db.query(Package).filter(Package.ID == package.ID).all() + query_voted = util.query_voted(query, maintainer) + assert query_voted[package.PackageBase.ID] + + +def test_query_notified(maintainer: User, package: Package): + with db.begin(): + db.create(PackageNotification, User=maintainer, + PackageBase=package.PackageBase) + + query = db.query(Package).filter(Package.ID == package.ID).all() + query_notified = util.query_notified(query, maintainer) + assert query_notified[package.PackageBase.ID] From aee1390e2c0aab59b2008d575c5f1750ca664733 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 19 Sep 2021 11:48:19 -0700 Subject: [PATCH 0437/1451] fix(FastAPI): registration sends WelcomeNotification Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index ef4b99af..3c799938 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -22,7 +22,7 @@ from aurweb.models.ban import Ban from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.term import Term from aurweb.models.user import User -from aurweb.scripts.notify import ResetKeyNotification +from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification from aurweb.templates import make_context, make_variable_context, render_template router = APIRouter() @@ -414,7 +414,7 @@ async def account_register_post(request: Request, # Send a reset key notification to the new user. executor = db.ConnectionExecutor(db.get_engine().raw_connection()) - ResetKeyNotification(executor, user.ID).send() + WelcomeNotification(executor, user.ID).send() context["complete"] = True context["user"] = user From b59601a8b7af137c0f31cd92dbff10f52afe82f6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Sep 2021 19:14:47 -0700 Subject: [PATCH 0438/1451] feat(poetry): add paginate==0.5.6 With upstream at https://github.com/Pylons/paginate, this module helps us deal with pagination without reinventing the wheel. Signed-off-by: Kevin Morris --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 3cc84361..322e250f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -513,6 +513,14 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pluggy" version = "0.13.1" @@ -921,7 +929,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = "*" -content-hash = "96112731ca21a6ff5d0657c6c40979642bb992ae660ba8d6135421718737c6b0" +content-hash = "c262ac1160b83593377fb7520d35c4b8ad81e5acff9d0a2060b2b048e3865b78" [metadata.files] aiofiles = [ @@ -1328,6 +1336,9 @@ packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] +paginate = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, diff --git a/pyproject.toml b/pyproject.toml index 8cb276ce..4b530493 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ python-multipart = { version = "0.0.5", python = "^3.9" } redis = { version = "3.5.3", python = "^3.9" } requests = { version = "2.26.0", python = "^3.9" } werkzeug = { version = "2.0.1", python = "^3.9" } +paginate = { version = "0.5.6", python = "^3.9" } # SQL alembic = { version = "1.6.5", python = "^3.9" } From c006386079b3f1dff893ba359eb978132800627c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 28 Aug 2021 16:14:07 -0700 Subject: [PATCH 0439/1451] add User.is_elevated() This one returns true if the user is either a Trusted User or a Developer. Signed-off-by: Kevin Morris --- aurweb/models/user.py | 9 +++++++++ test/test_user.py | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 70d15f88..28aa613e 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -165,6 +165,15 @@ class User(Base): aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID } + def is_elevated(self): + """ A User is 'elevated' when they have either a + Trusted User or Developer AccountType. """ + return self.AccountType.ID in { + aurweb.models.account_type.TRUSTED_USER_ID, + aurweb.models.account_type.DEVELOPER_ID, + aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID, + } + def can_edit_user(self, user): """ Can this account record edit the target user? It must either be the target user or a user with enough permissions to do so. diff --git a/test/test_user.py b/test/test_user.py index 70eac079..43cbf58a 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -214,6 +214,11 @@ def test_user_credential_types(): assert aurweb.auth.developer(user) assert aurweb.auth.trusted_user_or_dev(user) + # Some model authorization checks. + assert user.is_elevated() + assert user.is_trusted_user() + assert user.is_developer() + def test_user_json(): data = json.loads(user.json()) From 741cbfaa4e4d08dc02544140faf7bd1470ca7692 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 28 Aug 2021 16:15:48 -0700 Subject: [PATCH 0440/1451] auth: add several AnonymousUser method stubs We'll need to use these, so this commit implements them here with tests for coverage. Signed-off-by: Kevin Morris --- aurweb/auth.py | 20 ++++++++++++++++++++ test/test_auth.py | 27 ++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 26e4073d..2e6674b0 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -53,10 +53,30 @@ class AnonymousUser: def is_authenticated(): return False + @staticmethod + def is_trusted_user(): + return False + + @staticmethod + def is_developer(): + return False + + @staticmethod + def is_elevated(): + return False + @staticmethod def has_credential(credential): return False + @staticmethod + def voted_for(package): + return False + + @staticmethod + def notified(package): + return False + class BasicAuthBackend(AuthenticationBackend): async def authenticate(self, conn: HTTPConnection): diff --git a/test/test_auth.py b/test/test_auth.py index caa39468..ced64064 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -5,7 +5,7 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.auth import BasicAuthBackend, account_type_required, has_credential +from aurweb.auth import AnonymousUser, BasicAuthBackend, account_type_required, has_credential from aurweb.db import create, query from aurweb.models.account_type import USER, USER_ID, AccountType from aurweb.models.session import Session @@ -92,3 +92,28 @@ def test_account_type_required(): # But this one should! We have no "FAKE" key. with pytest.raises(KeyError): account_type_required({'FAKE'}) + + +def test_is_trusted_user(): + user_ = AnonymousUser() + assert not user_.is_trusted_user() + + +def test_is_developer(): + user_ = AnonymousUser() + assert not user_.is_developer() + + +def test_is_elevated(): + user_ = AnonymousUser() + assert not user_.is_elevated() + + +def test_voted_for(): + user_ = AnonymousUser() + assert not user_.voted_for(None) + + +def test_notified(): + user_ = AnonymousUser() + assert not user_.notified(None) From 6298b1228a7313de4375a567d79a7ab046bc2a26 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 15 Sep 2021 11:31:55 -0700 Subject: [PATCH 0441/1451] feat(FastAPI): add templates/partials/widgets/pager.html A pager that can be used for paginated result tables. Signed-off-by: Kevin Morris --- aurweb/filters.py | 50 +++++++++++++++++++++++++++ templates/partials/widgets/pager.html | 26 ++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 aurweb/filters.py create mode 100644 templates/partials/widgets/pager.html diff --git a/aurweb/filters.py b/aurweb/filters.py new file mode 100644 index 00000000..bb56c656 --- /dev/null +++ b/aurweb/filters.py @@ -0,0 +1,50 @@ +from typing import Any, Dict + +import paginate + +from jinja2 import pass_context + +from aurweb import util +from aurweb.templates import register_filter + + +@register_filter("pager_nav") +@pass_context +def pager_nav(context: Dict[str, Any], + page: int, total: int, prefix: str) -> str: + page = int(page) # Make sure this is an int. + + pp = context.get("PP", 50) + + # Setup a local query string dict, optionally passed by caller. + q = context.get("q", dict()) + + search_by = context.get("SeB", None) + if search_by: + q["SeB"] = search_by + + sort_by = context.get("SB", None) + if sort_by: + q["SB"] = sort_by + + def create_url(page: int): + nonlocal q + offset = max(page * pp - pp, 0) + qs = util.to_qs(util.extend_query(q, ["O", offset])) + return f"{prefix}?{qs}" + + # Use the paginate module to produce our linkage. + pager = paginate.Page([], page=page + 1, + items_per_page=pp, + item_count=total, + url_maker=create_url) + + return pager.pager( + link_attr={"class": "page"}, + curpage_attr={"class": "page"}, + separator=" ", + format="$link_first $link_previous ~5~ $link_next $link_last", + symbol_first="« First", + symbol_previous="‹ Previous", + symbol_next="Next ›", + symbol_last="Last »") diff --git a/templates/partials/widgets/pager.html b/templates/partials/widgets/pager.html new file mode 100644 index 00000000..4809accf --- /dev/null +++ b/templates/partials/widgets/pager.html @@ -0,0 +1,26 @@ +{# A pager widget that can be used for navigation of a number of results. + +Inputs required: + + prefix: Request URI prefix used to produce navigation offsets + singular: Singular sentence to be translated via tn + plural: Plural sentence to be translated via tn + PP: The number of results per page + O: The current offset value + total: The total number of results +#} + +{% set page = ((O / PP) | int) %} +{% set pages = ((total / PP) | ceil) %} + +
    +

    + {{ total | tn(singular, plural) | format(total) }} + {{ "Page %d of %d." | tr | format(page + 1, pages) }} +

    + {% if pages > 1 %} +

    + {{ page | pager_nav(total, prefix) | safe }} +

    + {% endif %} +

    From 5cf70620921848050ed3ac92dff7f84df1dbf979 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 29 Aug 2021 22:21:39 -0700 Subject: [PATCH 0442/1451] feat(FastAPI): add /packages (get) search In terms of performance, most queries on this page win over PHP in query times, with the exception of sorting by Voted or Notify (https://gitlab.archlinux.org/archlinux/aurweb/-/issues/102). Otherwise, there are a few modifications: described below. * Pagination * The `paginate` Python module has been used in the FastAPI project here to implement paging on the packages search page. This changes how pagination is displayed, however it serves the same purpose. We'll take advantage of this module in other places as well. * Form action * The form action for actions now use `POST /packages` to perform. This is currently implemented and will be addressed in a follow-up commit. * Input names and values * Input names and values have been modified to satisfy the snake_case naming convention we'd like to use as much as possible. * Some input names and values were modified to comply with FastAPI Forms: (IDs[]) -> (IDs, ). Signed-off-by: Kevin Morris --- aurweb/packages/search.py | 195 +++++++ aurweb/routers/packages.py | 83 ++- aurweb/templates.py | 2 + conf/config.defaults | 1 + setup.cfg | 2 + templates/packages.html | 84 +++ templates/partials/packages/search.html | 58 +- .../partials/packages/search_actions.html | 25 + .../partials/packages/search_results.html | 114 ++++ test/test_packages_routes.py | 540 +++++++++++++++++- web/html/css/aurweb.css | 7 + 11 files changed, 1081 insertions(+), 30 deletions(-) create mode 100644 aurweb/packages/search.py create mode 100644 templates/packages.html create mode 100644 templates/partials/packages/search_actions.html create mode 100644 templates/partials/packages/search_results.html diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py new file mode 100644 index 00000000..854834ee --- /dev/null +++ b/aurweb/packages/search.py @@ -0,0 +1,195 @@ +from sqlalchemy import and_, case, or_, orm + +from aurweb import config, db +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_keyword import PackageKeyword +from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_vote import PackageVote +from aurweb.models.user import User + +DEFAULT_MAX_RESULTS = 2500 + + +class PackageSearch: + """ A Package search query builder. """ + + # A constant mapping of short to full name sort orderings. + FULL_SORT_ORDER = {"d": "desc", "a": "asc"} + + def __init__(self, user: User): + """ Construct an instance of PackageSearch. + + This constructors performs several steps during initialization: + 1. Setup self.query: an ORM query of Package joined by PackageBase. + """ + self.user = user + self.query = db.query(Package).join(PackageBase).join( + PackageVote, + and_(PackageVote.PackageBaseID == PackageBase.ID, + PackageVote.UsersID == self.user.ID), + isouter=True + ).join( + PackageNotification, + and_(PackageNotification.PackageBaseID == PackageBase.ID, + PackageNotification.UserID == self.user.ID), + isouter=True + ) + self.ordering = "d" + + # Setup SeB (Search By) callbacks. + self.search_by_cb = { + "nd": self._search_by_namedesc, + "n": self._search_by_name, + "b": self._search_by_pkgbase, + "N": self._search_by_exact_name, + "B": self._search_by_exact_pkgbase, + "k": self._search_by_keywords, + "m": self._search_by_maintainer, + "c": self._search_by_comaintainer, + "M": self._search_by_co_or_maintainer, + "s": self._search_by_submitter + } + + # Setup SB (Sort By) callbacks. + self.sort_by_cb = { + "n": self._sort_by_name, + "v": self._sort_by_votes, + "p": self._sort_by_popularity, + "w": self._sort_by_voted, + "o": self._sort_by_notify, + "m": self._sort_by_maintainer, + "l": self._sort_by_last_modified + } + + def _search_by_namedesc(self, keywords: str) -> orm.Query: + self.query = self.query.filter( + or_(Package.Name.like(f"%{keywords}%"), + Package.Description.like(f"%{keywords}%")) + ) + return self + + def _search_by_name(self, keywords: str) -> orm.Query: + self.query = self.query.filter(Package.Name.like(f"%{keywords}%")) + return self + + def _search_by_exact_name(self, keywords: str) -> orm.Query: + self.query = self.query.filter(Package.Name == keywords) + return self + + def _search_by_pkgbase(self, keywords: str) -> orm.Query: + self.query = self.query.filter(PackageBase.Name.like(f"%{keywords}%")) + return self + + def _search_by_exact_pkgbase(self, keywords: str) -> orm.Query: + self.query = self.query.filter(PackageBase.Name == keywords) + return self + + def _search_by_keywords(self, keywords: str) -> orm.Query: + self.query = self.query.join(PackageKeyword).filter( + PackageKeyword.Keyword == keywords + ) + return self + + def _search_by_maintainer(self, keywords: str) -> orm.Query: + self.query = self.query.join( + User, User.ID == PackageBase.MaintainerUID + ).filter(User.Username == keywords) + return self + + def _search_by_comaintainer(self, keywords: str) -> orm.Query: + self.query = self.query.join(PackageComaintainer).join( + User, User.ID == PackageComaintainer.UsersID + ).filter(User.Username == keywords) + return self + + def _search_by_co_or_maintainer(self, keywords: str) -> orm.Query: + self.query = self.query.join( + PackageComaintainer, + isouter=True + ).join( + User, or_(User.ID == PackageBase.MaintainerUID, + User.ID == PackageComaintainer.UsersID) + ).filter(User.Username == keywords) + return self + + def _search_by_submitter(self, keywords: str) -> orm.Query: + self.query = self.query.join( + User, User.ID == PackageBase.SubmitterUID + ).filter(User.Username == keywords) + return self + + def search_by(self, search_by: str, keywords: str) -> orm.Query: + if search_by not in self.search_by_cb: + search_by = "nd" # Default: Name, Description + callback = self.search_by_cb.get(search_by) + result = callback(keywords) + return result + + def _sort_by_name(self, order: str): + column = getattr(Package.Name, order) + self.query = self.query.order_by(column()) + return self + + def _sort_by_votes(self, order: str): + column = getattr(PackageBase.NumVotes, order) + self.query = self.query.order_by(column()) + return self + + def _sort_by_popularity(self, order: str): + column = getattr(PackageBase.Popularity, order) + self.query = self.query.order_by(column()) + return self + + def _sort_by_voted(self, order: str): + # FIXME: Currently, PHP is destroying this implementation + # in terms of performance. We should improve this; there's no + # reason it should take _longer_. + column = getattr( + case([(PackageVote.UsersID == self.user.ID, 1)], else_=0), + order + ) + self.query = self.query.order_by(column(), Package.Name.desc()) + return self + + def _sort_by_notify(self, order: str): + # FIXME: Currently, PHP is destroying this implementation + # in terms of performance. We should improve this; there's no + # reason it should take _longer_. + column = getattr( + case([(PackageNotification.UserID == self.user.ID, 1)], else_=0), + order + ) + self.query = self.query.order_by(column(), Package.Name.desc()) + return self + + def _sort_by_maintainer(self, order: str): + column = getattr(User.Username, order) + self.query = self.query.join( + User, User.ID == PackageBase.MaintainerUID, isouter=True + ).order_by(column()) + return self + + def _sort_by_last_modified(self, order: str): + column = getattr(PackageBase.ModifiedTS, order) + self.query = self.query.order_by(column()) + return self + + def sort_by(self, sort_by: str, ordering: str = "d") -> orm.Query: + if sort_by not in self.sort_by_cb: + sort_by = "n" # Default: Name. + callback = self.sort_by_cb.get(sort_by) + if ordering not in self.FULL_SORT_ORDER: + ordering = "d" # Default: Descending. + ordering = self.FULL_SORT_ORDER.get(ordering) + return callback(ordering) + + def results(self) -> orm.Query: + # Store the total count of all records found up to limit. + limit = (config.getint("options", "max_search_results") + or DEFAULT_MAX_RESULTS) + self.total_count = self.query.limit(limit).count() + + # Return the query to the user. + return self.query diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index a20c97b1..3eda2539 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Request, Response from fastapi.responses import RedirectResponse from sqlalchemy import and_ +import aurweb.filters import aurweb.models.package_comment import aurweb.models.package_keyword import aurweb.packages.util @@ -21,12 +22,92 @@ from aurweb.models.package_request import PackageRequest from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID -from aurweb.packages.util import get_pkgbase +from aurweb.packages.search import PackageSearch +from aurweb.packages.util import get_pkgbase, query_notified, query_voted from aurweb.templates import make_context, render_template router = APIRouter() +async def packages_get(request: Request, context: Dict[str, Any]): + # Query parameters used in this request. + context["q"] = dict(request.query_params) + + # Per page and offset. + per_page = context["PP"] = int(request.query_params.get("PP", 50)) + offset = context["O"] = int(request.query_params.get("O", 0)) + + # Query search by. + search_by = context["SeB"] = request.query_params.get("SeB", "nd") + + # Query sort by. + sort_by = context["SB"] = request.query_params.get("SB", "n") + + # Query sort order. + sort_order = request.query_params.get("SO", None) + + # Apply ordering, limit and offset. + search = PackageSearch(request.user) + + # For each keyword found in K, apply a search_by filter. + # This means that for any sentences separated by spaces, + # they are used as if they were ANDed. + keywords = context["K"] = request.query_params.get("K", str()) + keywords = keywords.split(" ") + for keyword in keywords: + search.search_by(search_by, keyword) + + flagged = request.query_params.get("outdated", None) + if flagged: + # If outdated was given, set it up in the context. + context["outdated"] = flagged + + # When outdated is set to "on," we filter records which do have + # an OutOfDateTS. When it's set to "off," we filter out any which + # do **not** have OutOfDateTS. + criteria = None + if flagged == "on": + criteria = PackageBase.OutOfDateTS.isnot + else: + criteria = PackageBase.OutOfDateTS.is_ + + # Apply the flag criteria to our PackageSearch.query. + search.query = search.query.filter(criteria(None)) + + submit = request.query_params.get("submit", "Go") + if submit == "Orphans": + # If the user clicked the "Orphans" button, we only want + # orphaned packages. + search.query = search.query.filter(PackageBase.MaintainerUID.is_(None)) + + # Apply user-specified specified sort column and ordering. + search.sort_by(sort_by, sort_order) + + # If no SO was given, default the context SO to 'a' (Ascending). + # By default, if no SO is given, the search should sort by 'd' + # (Descending), but display "Ascending" for the Sort order select. + if sort_order is None: + sort_order = "a" + context["SO"] = sort_order + + # Insert search results into the context. + results = search.results() + context["packages"] = results.limit(per_page).offset(offset) + context["packages_voted"] = query_voted( + context.get("packages"), request.user) + context["packages_notified"] = query_notified( + context.get("packages"), request.user) + context["packages_count"] = search.total_count + + return render_template(request, "packages.html", context) + + +@router.get("/packages") +async def packages(request: Request) -> Response: + context = make_context(request, "Packages") + return await packages_get(request, context) + + async def make_single_context(request: Request, pkgbase: PackageBase) -> Dict[str, Any]: """ Make a basic context for package or pkgbase. diff --git a/aurweb/templates.py b/aurweb/templates.py index 6a1b6a1c..09be049c 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -1,5 +1,6 @@ import copy import functools +import math import os import zoneinfo @@ -35,6 +36,7 @@ _env.filters["urlencode"] = util.to_qs _env.filters["quote_plus"] = quote_plus _env.filters["get_vote"] = util.get_vote _env.filters["number_format"] = util.number_format +_env.filters["ceil"] = math.ceil # Add captcha filters. _env.filters["captcha_salt"] = captcha.captcha_salt_filter diff --git a/conf/config.defaults b/conf/config.defaults index 1c96a55d..988859a0 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -22,6 +22,7 @@ aur_location = https://aur.archlinux.org git_clone_uri_anon = https://aur.archlinux.org/%s.git git_clone_uri_priv = ssh://aur@aur.archlinux.org/%s.git max_rpc_results = 5000 +max_search_results = 2500 max_depends = 1000 aur_request_ml = aur-requests@lists.archlinux.org request_idle_time = 1209600 diff --git a/setup.cfg b/setup.cfg index 1d67ca96..4f2bdf7d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [pycodestyle] max-line-length = 127 +ignore = E741, W503 [flake8] max-line-length = 127 @@ -25,6 +26,7 @@ max-complexity = 10 per-file-ignores = aurweb/routers/accounts.py:E741,C901 test/test_ssh_pub_key.py:E501 + aurweb/routers/packages.py:E741 [isort] line_length = 127 diff --git a/templates/packages.html b/templates/packages.html new file mode 100644 index 00000000..8b5b06d1 --- /dev/null +++ b/templates/packages.html @@ -0,0 +1,84 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% if errors %} + +
      + {% for error in errors %} +
    • {{ error | tr }}
    • + {% endfor %} +
    + {% include "partials/packages/search.html" %} + + {% else %} + + {% set pages = (packages_count / PP) | ceil %} + {% set page = O / PP %} + + {% if success %} +
      + {% for message in success %} +
    • {{ message | tr }}
    • + {% endfor %} +
    + {% endif %} + + {# Search form #} + {% include "partials/packages/search.html" %} +
    + + {# /packages does things a bit roundabout-wise: + + If SeB is not given, "nd" is the default. + If SB is not given, "n" is the default. + If SO is not given, "d" is the default. + + However, we depend on flipping SO for column sorting. + + This section sets those defaults for the context if + they are not already setup. #} + {% if not SeB %} + {% set SeB = "nd" %} + {% endif %} + {% if not SB %} + {% set SB = "n" %} + {% endif %} + {% if not SO %} + {% set SO = "d" %} + {% endif %} + + {# Pagination widget #} + {% with total = packages_count, + singular = "%d package found.", + plural = "%d packages found.", + prefix = "/packages" %} + {% include "partials/widgets/pager.html" %} + {% endwith %} + + {# Package action form #} +
    + + {# Search results #} + {% with voted = packages_voted, notified = packages_notified %} + {% include "partials/packages/search_results.html" %} + {% endwith %} + + {# Pagination widget #} + {% with total = packages_count, + singular = "%d package found.", + plural = "%d packages found.", + prefix = "/packages" %} + {% include "partials/widgets/pager.html" %} + {% endwith %} + + {% if request.user.is_authenticated() %} + {# Package actions #} + {% include "partials/packages/search_actions.html" %} + {% endif %} + +
    + + {% endif %} +{% endblock %} diff --git a/templates/partials/packages/search.html b/templates/partials/packages/search.html index c4488b95..bb6fdb50 100644 --- a/templates/partials/packages/search.html +++ b/templates/partials/packages/search.html @@ -8,61 +8,65 @@
    - +
    - - + +
    diff --git a/templates/partials/packages/search_actions.html b/templates/partials/packages/search_actions.html new file mode 100644 index 00000000..2f5fe2e7 --- /dev/null +++ b/templates/partials/packages/search_actions.html @@ -0,0 +1,25 @@ +

    + + + {% if request.user.is_trusted_user() or request.user.is_developer() %} + + + {% endif %} + + + + +

    diff --git a/templates/partials/packages/search_results.html b/templates/partials/packages/search_results.html new file mode 100644 index 00000000..28cf0b48 --- /dev/null +++ b/templates/partials/packages/search_results.html @@ -0,0 +1,114 @@ +
    {{ "Git Clone URL" | tr }}:
    {{ "Description" | tr }}:{{ pkgbase.packages.first().Description }}{{ pkg.Description }}
    {{ "Upstream URL" | tr }}: - {% set pkg = pkgbase.packages.first() %} {% if pkg.URL %} {{ pkg.URL }} {% else %} @@ -33,7 +33,7 @@
    {{ "Keywords" | tr }}:
    {{ "Licenses" | tr }}: {{ licenses | join(', ', attribute='Name') | default('None' | tr) }}
    {{ "Conflicts" | tr }}: From ae0f69a5e463b5cdb502aa3777eb5da011112eba Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 16 Aug 2021 17:18:29 -0700 Subject: [PATCH 0377/1451] Docker: remove intervals and timeouts These weren't needed at all and provided false negatives in general. Removed them to let Docker deal with them. Additionally. 'exit 0' -> 'echo' for ca's command; 'exit 0' happens to depend on the shell running Docker (it seems). echo is quite a bit more agnostic. Moreso, added mariadb deps to php-fpm and fastapi. Signed-off-by: Kevin Morris --- docker-compose.yml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ab8d7c41..3500b8e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: image: aurweb:latest init: true entrypoint: /docker/ca-entrypoint.sh - command: exit 0 + command: echo volumes: - ./cache:/cache @@ -45,8 +45,6 @@ services: - mariadb_data:/var/lib/mysql healthcheck: test: "bash /docker/health/mariadb.sh" - interval: 2s - timeout: 60s git: image: aurweb:latest @@ -59,8 +57,6 @@ services: - "2222:2222" healthcheck: test: "bash /docker/health/sshd.sh" - interval: 2s - timeout: 30s depends_on: mariadb: condition: service_healthy @@ -79,8 +75,6 @@ services: command: /docker/scripts/run-smartgit.sh healthcheck: test: "bash /docker/health/smartgit.sh" - interval: 2s - timeout: 30s depends_on: mariadb: condition: service_healthy @@ -100,11 +94,11 @@ services: command: /docker/scripts/run-cgit.sh 3000 "https://localhost:8443/cgit" healthcheck: test: "bash /docker/health/cgit.sh 3000" - interval: 2s - timeout: 30s depends_on: git: condition: service_healthy + php-fpm: + condition: service_healthy volumes: - git_data:/aurweb/aur.git @@ -117,11 +111,11 @@ services: command: /docker/scripts/run-cgit.sh 3000 "https://localhost:8444/cgit" healthcheck: test: "bash /docker/health/cgit.sh 3000" - interval: 2s - timeout: 30s depends_on: git: condition: service_healthy + fastapi: + condition: service_healthy volumes: - git_data:/aurweb/aur.git @@ -135,8 +129,6 @@ services: command: /docker/scripts/run-php.sh healthcheck: test: "bash /docker/health/php.sh" - interval: 2s - timeout: 30s depends_on: ca: condition: service_started @@ -166,8 +158,6 @@ services: command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" healthcheck: test: "bash /docker/health/fastapi.sh ${FASTAPI_BACKEND}" - interval: 2s - timeout: 30s depends_on: ca: condition: service_started @@ -199,8 +189,6 @@ services: - "8444:8444" # FastAPI healthcheck: test: "bash /docker/health/nginx.sh" - interval: 2s - timeout: 30s depends_on: cgit-php: condition: service_healthy From 35851d553348383903c17d98cd049ac1f07bfaab Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 8 Aug 2021 18:35:49 -0700 Subject: [PATCH 0378/1451] Docker: add service 'memcached' Additionally, setup memcached for php-fpm. Signed-off-by: Kevin Morris --- conf/config.dev | 4 ++++ docker-compose.yml | 9 +++++++++ docker/health/memcached.sh | 2 ++ docker/php-entrypoint.sh | 6 ++++++ docker/scripts/install-deps.sh | 2 +- docker/scripts/run-memcached.sh | 2 ++ 6 files changed, 24 insertions(+), 1 deletion(-) create mode 100755 docker/health/memcached.sh create mode 100755 docker/scripts/run-memcached.sh diff --git a/conf/config.dev b/conf/config.dev index fc3bde91..566b655e 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -28,6 +28,10 @@ enable-maintenance = 0 localedir = YOUR_AUR_ROOT/web/locale ; In production, salt_rounds should be higher; suggested: 12. salt_rounds = 4 +cache = none +; In docker, the memcached host is available. On a user's system, +; this should be set to localhost (most likely). +memcache_servers = memcached:11211 [notifications] ; For development/testing, use /usr/bin/sendmail diff --git a/docker-compose.yml b/docker-compose.yml index 3500b8e9..56eff570 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,13 @@ services: volumes: - ./cache:/cache + memcached: + image: aurweb:latest + init: true + command: /docker/scripts/run-memcached.sh + healthcheck: + test: "bash /docker/health/memcached.sh" + mariadb: image: aurweb:latest init: true @@ -136,6 +143,8 @@ services: condition: service_healthy mariadb: condition: service_healthy + memcached: + condition: service_healthy volumes: - mariadb_run:/var/run/mysqld # Bind socket in this volume. - mariadb_data:/var/lib/mysql diff --git a/docker/health/memcached.sh b/docker/health/memcached.sh new file mode 100755 index 00000000..00f8cd98 --- /dev/null +++ b/docker/health/memcached.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec pgrep memcached diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index b4f6c631..1f3ed82b 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -7,6 +7,9 @@ bash $dir/test-mysql-entrypoint.sh sed -ri "s;^(aur_location) = .+;\1 = https://localhost:8443;" conf/config sed -ri 's/^(name) = .+/\1 = aurweb/' conf/config +# Enable memcached. +sed -ri 's/^(cache) = .+$/\1 = memcache/' conf/config + sed -ri "s|^(git_clone_uri_anon) = .+|\1 = https://localhost:8443/%s.git|" conf/config.defaults sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ssh://aur@localhost:2222/%s.git|" conf/config.defaults @@ -21,6 +24,9 @@ sed -ri 's|^;?(access\.log) = .*$|\1 = /proc/self/fd/2|g' \ sed -ri 's/^;?(extension=pdo_mysql)/\1/' /etc/php/php.ini sed -ri 's/^;?(open_basedir).*$/\1 = \//' /etc/php/php.ini +# Use the sqlite3 extension line for memcached. +sed -ri 's/^;(extension)=sqlite3$/\1=memcached/' /etc/php/php.ini + python -m aurweb.initdb 2>/dev/null || /bin/true exec "$@" diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 8d4525de..6edbff5a 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -14,6 +14,6 @@ pacman -Syu --noconfirm --noprogressbar \ python-pytest-asyncio python-coverage hypercorn python-bcrypt \ python-email-validator openssh python-lxml mariadb mariadb-libs \ python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ - python-asgiref uvicorn python-feedgen + python-asgiref uvicorn python-feedgen memcached php-memcached exec "$@" diff --git a/docker/scripts/run-memcached.sh b/docker/scripts/run-memcached.sh new file mode 100755 index 00000000..90784b0f --- /dev/null +++ b/docker/scripts/run-memcached.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/memcached -u memcached -m 64 -c 1024 -l 0.0.0.0 From 96d1af936381a959090d5362ee2ee49b4c91eced Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 08:30:12 -0700 Subject: [PATCH 0379/1451] docker-compose: add redis service Now, the fastapi docker-compose service uses the new redis service for a cache option. Signed-off-by: Kevin Morris --- INSTALL | 5 +++++ docker-compose.yml | 12 ++++++++++++ docker/fastapi-entrypoint.sh | 4 ++++ docker/health/redis.sh | 2 ++ docker/redis-entrypoint.sh | 6 ++++++ docker/scripts/install-deps.sh | 3 ++- docker/scripts/run-redis.sh | 2 ++ 7 files changed, 33 insertions(+), 1 deletion(-) create mode 100755 docker/health/redis.sh create mode 100755 docker/redis-entrypoint.sh create mode 100755 docker/scripts/run-redis.sh diff --git a/INSTALL b/INSTALL index f192f9f5..c41a5c8e 100644 --- a/INSTALL +++ b/INSTALL @@ -55,6 +55,11 @@ read the instructions below. python-lxml python-feedgen # python3 setup.py install +(FastAPI-Specific) + + # pacman -S redis python-redis + # systemctl enable --now redis + 5) Create a new MySQL database and a user and import the aurweb SQL schema: $ python -m aurweb.initdb diff --git a/docker-compose.yml b/docker-compose.yml index 56eff570..0e91d6eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,16 @@ services: healthcheck: test: "bash /docker/health/memcached.sh" + redis: + image: aurweb:latest + init: true + entrypoint: /docker/redis-entrypoint.sh + command: /docker/scripts/run-redis.sh + healthcheck: + test: "bash /docker/health/redis.sh" + ports: + - "16379:6379" + mariadb: image: aurweb:latest init: true @@ -172,6 +182,8 @@ services: condition: service_started git: condition: service_healthy + redis: + condition: service_healthy mariadb: condition: service_healthy volumes: diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index c46a33eb..41a88206 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -7,6 +7,10 @@ bash $dir/test-mysql-entrypoint.sh sed -ri "s;^(aur_location) = .+;\1 = https://localhost:8444;" conf/config sed -ri 's/^(name) = .+/\1 = aurweb/' conf/config +# Setup Redis for FastAPI. +sed -ri 's/^(cache) = .+/\1 = redis/' conf/config +sed -ri 's|^(redis_address) = .+|\1 = redis://redis|' conf/config + sed -ri "s|^(git_clone_uri_anon) = .+|\1 = https://localhost:8444/%s.git|" conf/config.defaults sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ssh://aur@localhost:2222/%s.git|" conf/config.defaults diff --git a/docker/health/redis.sh b/docker/health/redis.sh new file mode 100755 index 00000000..b5b442e8 --- /dev/null +++ b/docker/health/redis.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec pgrep redis-server diff --git a/docker/redis-entrypoint.sh b/docker/redis-entrypoint.sh new file mode 100755 index 00000000..e92be6c5 --- /dev/null +++ b/docker/redis-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eou pipefail + +sed -ri 's/^bind .*$/bind 0.0.0.0 -::1/g' /etc/redis/redis.conf + +exec "$@" diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 6edbff5a..6b0ec48b 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -14,6 +14,7 @@ pacman -Syu --noconfirm --noprogressbar \ python-pytest-asyncio python-coverage hypercorn python-bcrypt \ python-email-validator openssh python-lxml mariadb mariadb-libs \ python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ - python-asgiref uvicorn python-feedgen memcached php-memcached + python-asgiref uvicorn python-feedgen memcached php-memcached \ + python-redis redis exec "$@" diff --git a/docker/scripts/run-redis.sh b/docker/scripts/run-redis.sh new file mode 100755 index 00000000..8dc98b10 --- /dev/null +++ b/docker/scripts/run-redis.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/redis-server /etc/redis/redis.conf From 91e769f6033867daaa951334ff51380275de5c84 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 28 Jun 2021 08:49:02 -0700 Subject: [PATCH 0380/1451] FastAPI: add redis integration This includes the addition of the python-fakeredis package, used for stubbing python-redis when a user does not have a configured cache. Signed-off-by: Kevin Morris --- INSTALL | 2 +- aurweb/redis.py | 57 ++++++++++++++++++++++++++++++++++ conf/config.defaults | 4 ++- conf/config.dev | 3 ++ docker/scripts/install-deps.sh | 2 +- test/test_asgi.py | 18 +++++++++++ test/test_redis.py | 40 ++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 aurweb/redis.py create mode 100644 test/test_redis.py diff --git a/INSTALL b/INSTALL index c41a5c8e..fdeb64ca 100644 --- a/INSTALL +++ b/INSTALL @@ -57,7 +57,7 @@ read the instructions below. (FastAPI-Specific) - # pacman -S redis python-redis + # pacman -S redis python-redis python-fakeredis # systemctl enable --now redis 5) Create a new MySQL database and a user and import the aurweb SQL schema: diff --git a/aurweb/redis.py b/aurweb/redis.py new file mode 100644 index 00000000..6b8dede4 --- /dev/null +++ b/aurweb/redis.py @@ -0,0 +1,57 @@ +import logging + +import fakeredis + +from redis import ConnectionPool, Redis + +import aurweb.config + +logger = logging.getLogger(__name__) +pool = None + + +class FakeConnectionPool: + """ A fake ConnectionPool class which holds an internal reference + to a fakeredis handle. + + We normally deal with Redis by keeping its ConnectionPool globally + referenced so we can persist connection state through different calls + to redis_connection(), and since FakeRedis does not offer a ConnectionPool, + we craft one up here to hang onto the same handle instance as long as the + same instance is alive; this allows us to use a similar flow from the + redis_connection() user's perspective. + """ + + def __init__(self): + self.handle = fakeredis.FakeStrictRedis() + + def disconnect(self): + pass + + +def redis_connection(): # pragma: no cover + global pool + + disabled = aurweb.config.get("options", "cache") != "redis" + + # If we haven't initialized redis yet, construct a pool. + if disabled: + logger.debug("Initializing fake Redis instance.") + if pool is None: + pool = FakeConnectionPool() + return pool.handle + else: + logger.debug("Initializing real Redis instance.") + if pool is None: + redis_addr = aurweb.config.get("options", "redis_address") + pool = ConnectionPool.from_url(redis_addr) + + # Create a connection to the pool. + return Redis(connection_pool=pool) + + +def kill_redis(): + global pool + if pool: + pool.disconnect() + pool = None diff --git a/conf/config.defaults b/conf/config.defaults index ebc21e51..1b4c3a74 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -36,11 +36,13 @@ enable-maintenance = 1 maintenance-exceptions = 127.0.0.1 render-comment-cmd = /usr/local/bin/aurweb-rendercomment localedir = /srv/http/aurweb/aur.git/web/locale/ -# memcache or apc +; memcache, apc, or redis +; memcache/apc are supported in PHP, redis is supported in Python. cache = none cache_pkginfo_ttl = 86400 memcache_servers = 127.0.0.1:11211 salt_rounds = 12 +redis_address = redis://localhost [ratelimit] request_limit = 4000 diff --git a/conf/config.dev b/conf/config.dev index 566b655e..94a9630b 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -28,10 +28,13 @@ enable-maintenance = 0 localedir = YOUR_AUR_ROOT/web/locale ; In production, salt_rounds should be higher; suggested: 12. salt_rounds = 4 +; See config.defaults comment about cache. cache = none ; In docker, the memcached host is available. On a user's system, ; this should be set to localhost (most likely). memcache_servers = memcached:11211 +; If cache = 'redis' this address is used to connect to Redis. +redis_address = redis://127.0.0.1 [notifications] ; For development/testing, use /usr/bin/sendmail diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 6b0ec48b..0405f29b 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -15,6 +15,6 @@ pacman -Syu --noconfirm --noprogressbar \ python-email-validator openssh python-lxml mariadb mariadb-libs \ python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ python-asgiref uvicorn python-feedgen memcached php-memcached \ - python-redis redis + python-redis redis python-fakeredis exec "$@" diff --git a/test/test_asgi.py b/test/test_asgi.py index 79b34daf..b8856741 100644 --- a/test/test_asgi.py +++ b/test/test_asgi.py @@ -9,6 +9,24 @@ from fastapi import HTTPException import aurweb.asgi import aurweb.config +import aurweb.redis + + +@pytest.mark.asyncio +async def test_asgi_startup_session_secret_exception(monkeypatch): + """ Test that we get an IOError on app_startup when we cannot + connect to options.redis_address. """ + + redis_addr = aurweb.config.get("options", "redis_address") + + def mock_get(section: str, key: str): + if section == "fastapi" and key == "session_secret": + return None + return redis_addr + + with mock.patch("aurweb.config.get", side_effect=mock_get): + with pytest.raises(Exception): + await aurweb.asgi.app_startup() @pytest.mark.asyncio diff --git a/test/test_redis.py b/test/test_redis.py new file mode 100644 index 00000000..82aebb57 --- /dev/null +++ b/test/test_redis.py @@ -0,0 +1,40 @@ +from unittest import mock + +import pytest + +import aurweb.config + +from aurweb.redis import redis_connection + + +@pytest.fixture +def rediss(): + """ Create a RedisStub. """ + def mock_get(section, key): + return "none" + + with mock.patch("aurweb.config.get", side_effect=mock_get): + aurweb.config.rehash() + redis = redis_connection() + aurweb.config.rehash() + + yield redis + + +def test_redis_stub(rediss): + # We don't yet have a test key set. + assert rediss.get("test") is None + + # Set the test key to abc. + rediss.set("test", "abc") + assert rediss.get("test").decode() == "abc" + + # Test expire. + rediss.expire("test", 0) + assert rediss.get("test") is None + + # Now, set the test key again and use delete() on it. + rediss.set("test", "abc") + assert rediss.get("test").decode() == "abc" + rediss.delete("test") + assert rediss.get("test") is None From 968ed736c16f92de6ebca1a77fe1e7b2d78ec4e5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 8 Aug 2021 20:42:00 -0700 Subject: [PATCH 0381/1451] add python-orjson dependency python-orjson speeds up a lot of JSON serialization steps, so we choose to use it over the standard library json module. Signed-off-by: Kevin Morris --- INSTALL | 2 +- docker/scripts/install-deps.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/INSTALL b/INSTALL index fdeb64ca..4df59bd2 100644 --- a/INSTALL +++ b/INSTALL @@ -57,7 +57,7 @@ read the instructions below. (FastAPI-Specific) - # pacman -S redis python-redis python-fakeredis + # pacman -S redis python-redis python-fakeredis python-orjson # systemctl enable --now redis 5) Create a new MySQL database and a user and import the aurweb SQL schema: diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 0405f29b..a532a6b2 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -15,6 +15,6 @@ pacman -Syu --noconfirm --noprogressbar \ python-email-validator openssh python-lxml mariadb mariadb-libs \ python-isort flake8 cgit uwsgi uwsgi-plugin-cgi php php-fpm \ python-asgiref uvicorn python-feedgen memcached php-memcached \ - python-redis redis python-fakeredis + python-redis redis python-fakeredis python-orjson exec "$@" From 9e73936c4e52a862f392947005d6ed29668969de Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 6 Aug 2021 22:44:19 -0700 Subject: [PATCH 0382/1451] add aurweb.cache, a redis caching utility module Signed-off-by: Kevin Morris --- aurweb/cache.py | 20 +++++++++++++ test/test_cache.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 aurweb/cache.py create mode 100644 test/test_cache.py diff --git a/aurweb/cache.py b/aurweb/cache.py new file mode 100644 index 00000000..697473b8 --- /dev/null +++ b/aurweb/cache.py @@ -0,0 +1,20 @@ +from redis import Redis +from sqlalchemy import orm + + +async def db_count_cache(redis: Redis, key: str, query: orm.Query, + expire: int = None) -> int: + """ Store and retrieve a query.count() via redis cache. + + :param redis: Redis handle + :param key: Redis key + :param query: SQLAlchemy ORM query + :param expire: Optional expiration in seconds + :return: query.count() + """ + result = redis.get(key) + if result is None: + redis.set(key, (result := int(query.count()))) + if expire: + redis.expire(key, expire) + return int(result) diff --git a/test/test_cache.py b/test/test_cache.py new file mode 100644 index 00000000..35346e52 --- /dev/null +++ b/test/test_cache.py @@ -0,0 +1,74 @@ +import pytest + +from aurweb import cache, db +from aurweb.models.account_type import USER_ID +from aurweb.models.user import User +from aurweb.testing import setup_test_db + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db( + User.__tablename__ + ) + + +class StubRedis: + """ A class which acts as a RedisConnection without using Redis. """ + + cache = dict() + expires = dict() + + def get(self, key, *args): + if "key" not in self.cache: + self.cache[key] = None + return self.cache[key] + + def set(self, key, *args): + self.cache[key] = list(args)[0] + + def expire(self, key, *args): + self.expires[key] = list(args)[0] + + async def execute(self, command, key, *args): + f = getattr(self, command) + return f(key, *args) + + +@pytest.fixture +def redis(): + yield StubRedis() + + +@pytest.mark.asyncio +async def test_db_count_cache(redis): + db.create(User, Username="user1", + Email="user1@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID) + + query = db.query(User) + + # Now, perform several checks that db_count_cache matches query.count(). + + # We have no cached value yet. + assert await cache.db_count_cache(redis, "key1", query) == query.count() + + # It's cached now. + assert await cache.db_count_cache(redis, "key1", query) == query.count() + + +@pytest.mark.asyncio +async def test_db_count_cache_expires(redis): + db.create(User, Username="user1", + Email="user1@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID) + + query = db.query(User) + + # Cache a query with an expire. + value = await cache.db_count_cache(redis, "key1", query, 100) + assert value == query.count() + + assert redis.expires["key1"] == 100 From d9cdd5faeff608f4191872cc808be5c3ce0ce4b4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 28 Jul 2021 13:28:17 -0700 Subject: [PATCH 0383/1451] [FastAPI] Modularize homepage and add side panel This puts one more toward completion of the homepage overall; we'll need to still implement the authenticated user dashboard after this. Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 55 ++++++++- aurweb/routers/html.py | 74 ++++++++++- templates/home.html | 101 +++++++++++++++ templates/index.html | 107 ++-------------- .../partials/packages/widgets/search.html | 14 +++ .../partials/packages/widgets/statistics.html | 55 +++++++++ .../partials/packages/widgets/updates.html | 35 ++++++ templates/partials/widgets/statistics.html | 27 ++++ test/test_homepage.py | 115 ++++++++++++++++++ test/test_packages_util.py | 19 ++- 10 files changed, 500 insertions(+), 102 deletions(-) create mode 100644 templates/home.html create mode 100644 templates/partials/packages/widgets/search.html create mode 100644 templates/partials/packages/widgets/statistics.html create mode 100644 templates/partials/packages/widgets/updates.html create mode 100644 templates/partials/widgets/statistics.html diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 60db2962..036e3441 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -1,7 +1,10 @@ from http import HTTPStatus +from typing import List + +import orjson from fastapi import HTTPException -from sqlalchemy import and_ +from sqlalchemy import and_, orm from aurweb import db from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider @@ -10,6 +13,7 @@ from aurweb.models.package_base import PackageBase from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_relation import PackageRelation from aurweb.models.relation_type import PROVIDES_ID, RelationType +from aurweb.redis import redis_connection from aurweb.templates import register_filter @@ -111,3 +115,52 @@ def get_pkgbase(name: str) -> PackageBase: raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) return pkgbase + + +@register_filter("out_of_date") +def out_of_date(packages: orm.Query) -> orm.Query: + return packages.filter(PackageBase.OutOfDateTS.isnot(None)) + + +def updated_packages(limit: int = 0, cache_ttl: int = 600) -> List[Package]: + """ Return a list of valid Package objects ordered by their + ModifiedTS column in descending order from cache, after setting + the cache when no key yet exists. + + :param limit: Optional record limit + :param cache_ttl: Cache expiration time (in seconds) + :return: A list of Packages + """ + redis = redis_connection() + packages = redis.get("package_updates") + if packages: + # If we already have a cache, deserialize it and return. + return orjson.loads(packages) + + query = db.query(Package).join(PackageBase).filter( + PackageBase.PackagerUID.isnot(None) + ).order_by( + PackageBase.ModifiedTS.desc() + ) + + if limit: + query = query.limit(limit) + + packages = [] + for pkg in query: + # For each Package returned by the query, append a dict + # containing Package columns we're interested in. + packages.append({ + "Name": pkg.Name, + "Version": pkg.Version, + "PackageBase": { + "ModifiedTS": pkg.PackageBase.ModifiedTS + } + }) + + # Store the JSON serialization of the package_updates key into Redis. + redis.set("package_updates", orjson.dumps(packages)) + redis.expire("package_updates", cache_ttl) + + # Return the deserialized list of packages. + return packages diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index f6f1a54e..ae012901 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -1,14 +1,21 @@ """ AURWeb's primary routing module. Define all routes via @app.app.{get,post} decorators in some way; more complex routes should be defined in their own modules and imported here. """ +from datetime import datetime from http import HTTPStatus from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse +from sqlalchemy import and_, or_ import aurweb.config -from aurweb import util +from aurweb import db, util +from aurweb.cache import db_count_cache +from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID +from aurweb.models.package_base import PackageBase +from aurweb.models.user import User +from aurweb.packages.util import updated_packages from aurweb.templates import make_context, render_template router = APIRouter() @@ -60,6 +67,71 @@ async def index(request: Request): context = make_context(request, "Home") context['ssh_fingerprints'] = util.get_ssh_fingerprints() + bases = db.query(PackageBase) + + redis = aurweb.redis.redis_connection() + stats_expire = 300 # Five minutes. + updates_expire = 600 # Ten minutes. + + # Package statistics. + query = bases.filter(PackageBase.PackagerUID.isnot(None)) + context["package_count"] = await db_count_cache( + redis, "package_count", query, expire=stats_expire) + + query = bases.filter( + and_(PackageBase.MaintainerUID.is_(None), + PackageBase.PackagerUID.isnot(None)) + ) + context["orphan_count"] = await db_count_cache( + redis, "orphan_count", query, expire=stats_expire) + + query = db.query(User) + context["user_count"] = await db_count_cache( + redis, "user_count", query, expire=stats_expire) + + query = query.filter( + or_(User.AccountTypeID == TRUSTED_USER_ID, + User.AccountTypeID == TRUSTED_USER_AND_DEV_ID)) + context["trusted_user_count"] = await db_count_cache( + redis, "trusted_user_count", query, expire=stats_expire) + + # Current timestamp. + now = int(datetime.utcnow().timestamp()) + + seven_days = 86400 * 7 # Seven days worth of seconds. + seven_days_ago = now - seven_days + + one_hour = 3600 + updated = bases.filter( + and_(PackageBase.ModifiedTS - PackageBase.SubmittedTS >= one_hour, + PackageBase.PackagerUID.isnot(None)) + ) + + query = bases.filter( + and_(PackageBase.SubmittedTS >= seven_days_ago, + PackageBase.PackagerUID.isnot(None)) + ) + context["seven_days_old_added"] = await db_count_cache( + redis, "seven_days_old_added", query, expire=stats_expire) + + query = updated.filter(PackageBase.ModifiedTS >= seven_days_ago) + context["seven_days_old_updated"] = await db_count_cache( + redis, "seven_days_old_updated", query, expire=stats_expire) + + year = seven_days * 52 # Fifty two weeks worth: one year. + year_ago = now - year + query = updated.filter(PackageBase.ModifiedTS >= year_ago) + context["year_old_updated"] = await db_count_cache( + redis, "year_old_updated", query, expire=stats_expire) + + query = bases.filter( + PackageBase.ModifiedTS - PackageBase.SubmittedTS < 3600) + context["never_updated"] = await db_count_cache( + redis, "never_updated", query, expire=stats_expire) + + # Get the 15 most recently updated packages. + context["package_updates"] = updated_packages(15, updates_expire) + return render_template(request, "index.html", context) diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 00000000..a8cae5b8 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,101 @@ +
    +

    AUR {% trans %}Home{% endtrans %}

    +

    + {{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information." + | tr + | format('', "", + '', "") + | safe + }} + {{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!" + | tr + | format("", "", + '', + "") + | safe + }} + {% trans %}Remember to vote for your favourite packages!{% endtrans %} + {% trans %}Some packages may be provided as binaries in [community].{% endtrans %} +

    +

    + {% trans %}DISCLAIMER{% endtrans %}: + {% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %} +

    +

    {% trans %}Learn more...{% endtrans %}

    +
    +
    +

    {% trans %}Support{% endtrans %}

    +

    {% trans %}Package Requests{% endtrans %}

    +
    +

    + {{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:" + | tr + | format("", "") + | safe + }} +

    +
      +
    • {% trans %}Orphan Request{% endtrans %}: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}
    • +
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}
    • +
    • {% trans %}Merge Request{% endtrans %}: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}
    • +
    +

    + {{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests." + | tr + | format('', "") + | safe + }} +

    +
    +

    {% trans %}Submitting Packages{% endtrans %}

    +
    +

    + {{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details." + | tr + | format('', "") + | safe + }} +

    + {% if ssh_fingerprints %} +

    + {% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %} +

    +

      + {% for keytype in ssh_fingerprints %} +
    • {{ keytype }}: {{ ssh_fingerprints[keytype] }} + {% endfor %} +
    + {% endif %} +
    +

    {% trans %}Discussion{% endtrans %}

    +
    +

    + {{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." + | tr + | format('', "", + '', "") + | safe + }} +

    +

    +

    {% trans %}Bug Reporting{% endtrans %}

    +
    +

    + {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page." + | tr + | format('', "", + "", "") + | safe + }} +

    +
    +
    + + + + + + diff --git a/templates/index.html b/templates/index.html index f8745f33..e50a99cd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,106 +1,15 @@ {% extends 'partials/layout.html' %} {% block pageContent %} -
    -

    AUR {% trans %}Home{% endtrans %}

    -

    - {{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information." - | tr - | format('', "", - '', "") - | safe - }} - {{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!" - | tr - | format("", "", - '', - "") - | safe - }} - {% trans %}Remember to vote for your favourite packages!{% endtrans %} - {% trans %}Some packages may be provided as binaries in [community].{% endtrans %} -

    - {% trans %}DISCLAIMER{% endtrans %}: - {% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %} -

    -

    {% trans %}Learn more...{% endtrans %}

    -

    -
    -
    -

    {% trans %}Support{% endtrans %}

    -

    {% trans %}Package Requests{% endtrans %}

    -
    -

    - {{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:" - | tr - | format("", "") - | safe - }} -

    -
      -
    • {% trans %}Orphan Request{% endtrans %}: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}
    • -
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}
    • -
    • {% trans %}Merge Request{% endtrans %}: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}
    • -
    -

    - {{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests." - | tr - | format('', "") - | safe - }} -

    +
    +
    + {% include 'home.html' %} +
    -

    {% trans %}Submitting Packages{% endtrans %}

    -
    -

    - {{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details." - | tr - | format('', "") - | safe - }} -

    - {% if ssh_fingerprints %} -

    - {% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %} -

    -

      - {% for keytype in ssh_fingerprints %} -
    • {{ keytype }}: {{ ssh_fingerprints[keytype] }} - {% endfor %} -
    - {% endif %} +
    + {% include 'partials/packages/widgets/search.html' %} + {% include 'partials/packages/widgets/updates.html' %} + {% include 'partials/packages/widgets/statistics.html' %}
    -

    {% trans %}Discussion{% endtrans %}

    -
    -

    - {{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." - | tr - | format('', "", - '', "") - | safe - }} -

    -

    -

    {% trans %}Bug Reporting{% endtrans %}

    -
    -

    - {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page." - | tr - | format('', "", - "", "") - | safe - }} -

    -
    -
    - - - - - - {% endblock %} diff --git a/templates/partials/packages/widgets/search.html b/templates/partials/packages/widgets/search.html new file mode 100644 index 00000000..106b93ea --- /dev/null +++ b/templates/partials/packages/widgets/search.html @@ -0,0 +1,14 @@ +
    +
    +
    + + + +
    +
    +
    diff --git a/templates/partials/packages/widgets/statistics.html b/templates/partials/packages/widgets/statistics.html new file mode 100644 index 00000000..f841ae0e --- /dev/null +++ b/templates/partials/packages/widgets/statistics.html @@ -0,0 +1,55 @@ +
    +

    {{ "Statistics" | tr }}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{ "Packages" | tr }}{{ package_count }}
    {{ "Orphan Packages" | tr }}{{ orphan_count }}
    + {{ "Packages added in the past 7 days" | tr }} + {{ seven_days_old_added }}
    + {{ "Packages updated in the past 7 days" | tr }} + {{ seven_days_old_updated }}
    + {{ "Packages updated in the past year" | tr }} + {{ year_old_updated }}
    + {{ "Packages never updated" | tr }} + {{ never_updated }}
    + {{ "Registered Users" | tr }} + {{ user_count }}
    + {{ "Trusted Users" | tr }} + {{ trusted_user_count }}
    +
    + +{% if request.user.is_authenticated() %} + + {% include 'partials/widgets/statistics.html' %} +{% endif %} diff --git a/templates/partials/packages/widgets/updates.html b/templates/partials/packages/widgets/updates.html new file mode 100644 index 00000000..3ee1b98e --- /dev/null +++ b/templates/partials/packages/widgets/updates.html @@ -0,0 +1,35 @@ +
    +

    + {{ "Recent Updates" | tr }} + + ({{ "more" | tr }}) + +

    + + RSS Feed + + + RSS Feed + + + + + {% for pkg in package_updates %} + + + + + {% endfor %} + +
    + + {{ pkg.Name }} {{ pkg.Version }} + + + {% set modified = pkg.PackageBase.ModifiedTS | dt | as_timezone(timezone) %} + {{ modified.strftime("%Y-%m-%d %H:%M") }} +
    + +
    diff --git a/templates/partials/widgets/statistics.html b/templates/partials/widgets/statistics.html new file mode 100644 index 00000000..0bf844b6 --- /dev/null +++ b/templates/partials/widgets/statistics.html @@ -0,0 +1,27 @@ +
    +

    {{ "My Statistics" | tr }}

    + + {% set bases = request.user.maintained_bases %} + + + + + + + {% set out_of_date_packages = bases | out_of_date %} + + + + + +
    + + {{ "Packages" | tr }} + + {{ bases.count() }}
    + + {{ "Out of Date" | tr }} + + {{ out_of_date_packages.count() }}
    + +
    diff --git a/test/test_homepage.py b/test/test_homepage.py index 23d7185f..a629b98c 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -1,13 +1,82 @@ +import re + +from datetime import datetime from http import HTTPStatus from unittest.mock import patch +import pytest + from fastapi.testclient import TestClient +from aurweb import db from aurweb.asgi import app +from aurweb.models.account_type import USER_ID +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.user import User +from aurweb.redis import redis_connection +from aurweb.testing import setup_test_db +from aurweb.testing.html import parse_root client = TestClient(app) +@pytest.fixture(autouse=True) +def setup(): + yield setup_test_db( + User.__tablename__, + Package.__tablename__, + PackageBase.__tablename__ + ) + + +@pytest.fixture +def user(): + yield db.create(User, Username="test", Email="test@example.org", + Passwd="testPassword", AccountTypeID=USER_ID) + + +@pytest.fixture +def redis(): + redis = redis_connection() + + def delete_keys(): + # Cleanup keys if they exist. + for key in ("package_count", "orphan_count", "user_count", + "trusted_user_count", "seven_days_old_added", + "seven_days_old_updated", "year_old_updated", + "never_updated", "package_updates"): + if redis.get(key) is not None: + redis.delete(key) + + delete_keys() + yield redis + delete_keys() + + +@pytest.fixture +def packages(user): + """ Yield a list of num_packages Package objects maintained by user. """ + num_packages = 50 # Tunable + + # For i..num_packages, create a package named pkg_{i}. + pkgs = [] + now = int(datetime.utcnow().timestamp()) + for i in range(num_packages): + pkgbase = db.create(PackageBase, Name=f"pkg_{i}", + Maintainer=user, Packager=user, + autocommit=False, SubmittedTS=now, + ModifiedTS=now) + pkg = db.create(Package, PackageBase=pkgbase, + Name=pkgbase.Name, autocommit=False) + pkgs.append(pkg) + now += 1 + + db.commit() + + yield pkgs + + def test_homepage(): with client as request: response = request.get("/") @@ -34,3 +103,49 @@ def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock): response = request.get("/") assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode() + + +def test_homepage_stats(redis, packages): + with client as request: + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + expectations = [ + ("Packages", r'\d+'), + ("Orphan Packages", r'\d+'), + ("Packages added in the past 7 days", r'\d+'), + ("Packages updated in the past 7 days", r'\d+'), + ("Packages updated in the past year", r'\d+'), + ("Packages never updated", r'\d+'), + ("Registered Users", r'\d+'), + ("Trusted Users", r'\d+') + ] + + stats = root.xpath('//div[@id="pkg-stats"]//tr') + for i, expected in enumerate(expectations): + expected_key, expected_regex = expected + key, value = stats[i].xpath('./td') + assert key.text.strip() == expected_key + assert re.match(expected_regex, value.text.strip()) + + +def test_homepage_updates(redis, packages): + with client as request: + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + # Run the request a second time to exercise the Redis path. + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + # We expect to see the latest 15 packages, which happens to be + # pkg_49 .. pkg_34. So, create a list of expectations using a range + # starting at 49, stepping down to 49 - 15, -1 step at a time. + expectations = [f"pkg_{i}" for i in range(50 - 1, 50 - 1 - 15, -1)] + updates = root.xpath('//div[@id="pkg-updates"]/table/tbody/tr') + for i, expected in enumerate(expectations): + pkgname = updates[i].xpath('./td/a').pop(0) + assert pkgname.text.strip() == expected diff --git a/test/test_packages_util.py b/test/test_packages_util.py index 17978490..bc6a941c 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -9,6 +9,7 @@ from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.user import User from aurweb.packages import util +from aurweb.redis import kill_redis from aurweb.testing import setup_test_db @@ -33,7 +34,8 @@ def maintainer() -> User: @pytest.fixture def package(maintainer: User) -> Package: - pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=maintainer) + pkgbase = db.create(PackageBase, Name="test-pkg", + Packager=maintainer, Maintainer=maintainer) yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) @@ -49,3 +51,18 @@ def test_package_link(client: TestClient, maintainer: User, package: Package): Provides=package.Name) expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}" assert util.package_link(package) == expected + + +def test_updated_packages(maintainer: User, package: Package): + expected = { + "Name": package.Name, + "Version": package.Version, + "PackageBase": { + "ModifiedTS": package.PackageBase.ModifiedTS + } + } + + kill_redis() # Kill it here to ensure we're on a fake instance. + assert util.updated_packages(1, 0) == [expected] + assert util.updated_packages(1, 600) == [expected] + kill_redis() # Kill it again, in case other tests use a real instance. From 469c141f6b541ea4b6d6aadbbd32fdb9822f6975 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 17 Aug 2021 20:58:41 -0700 Subject: [PATCH 0384/1451] [FastAPI] bugfix: remove use of scalar() in plural context Anything where we can have more than one of something, scalar() cannot be used. Signed-off-by: Kevin Morris --- templates/partials/packages/comments.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index 051849b0..39cfb363 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -49,7 +49,7 @@
    {% endif %} -{% if comments.scalar() %} +{% if comments %}

    From eb8ea53a4483d3a9d42b9b5cb0bfcfcc87d2cb4e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 7 Aug 2021 14:57:24 -0700 Subject: [PATCH 0385/1451] PackageRequest: add status_display() A helper function which provides a textual string conversion of a particular Status column. In a PackageRequest, Status is split up into four different types: - PENDING : "Pending", PENDING_ID: 0 - CLOSED : "Closed", CLOSED_ID: 1 - ACCEPTED : "Accepted", ACCEPTED_ID: 2 - REJECTED : "Rejected", REJECTED_ID: 3 This commit adds constants for the textual strings and the IDs. It also adds a PackageRequest.status_display() function which grabs the proper display string for a particular Status ID. Signed-off-by: Kevin Morris --- aurweb/models/package_request.py | 22 +++++++++++++++++++++ test/test_package_request.py | 34 ++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/aurweb/models/package_request.py b/aurweb/models/package_request.py index 00f46ce2..a5125cee 100644 --- a/aurweb/models/package_request.py +++ b/aurweb/models/package_request.py @@ -8,6 +8,17 @@ import aurweb.models.user from aurweb.models.declarative import Base +PENDING = "Pending" +CLOSED = "Closed" +ACCEPTED = "Accepted" +REJECTED = "Rejected" + +# Integer values used for the Status column of PackageRequest. +PENDING_ID = 0 +CLOSED_ID = 1 +ACCEPTED_ID = 2 +REJECTED_ID = 3 + class PackageRequest(Base): __tablename__ = "PackageRequests" @@ -40,6 +51,13 @@ class PackageRequest(Base): __mapper_args__ = {"primary_key": [ID]} + STATUS_DISPLAY = { + PENDING_ID: PENDING, + CLOSED_ID: CLOSED, + ACCEPTED_ID: ACCEPTED, + REJECTED_ID: REJECTED + } + def __init__(self, RequestType: aurweb.models.request_type.RequestType = None, PackageBase: aurweb.models.package_base.PackageBase = None, @@ -91,3 +109,7 @@ class PackageRequest(Base): statement="Column ClosureComment cannot be null.", orig="PackageRequests.ClosureComment", params=("NULL")) + + def status_display(self) -> str: + """ Return a display string for the Status column. """ + return self.STATUS_DISPLAY[self.Status] diff --git a/test/test_package_request.py b/test/test_package_request.py index fc839836..c28af6bd 100644 --- a/test/test_package_request.py +++ b/test/test_package_request.py @@ -4,9 +4,10 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query, rollback +from aurweb.db import commit, create, query, rollback from aurweb.models.package_base import PackageBase -from aurweb.models.package_request import PackageRequest +from aurweb.models.package_request import (ACCEPTED, ACCEPTED_ID, CLOSED, CLOSED_ID, PENDING, PENDING_ID, REJECTED, + REJECTED_ID, PackageRequest) from aurweb.models.request_type import RequestType from aurweb.models.user import User from aurweb.testing import setup_test_db @@ -117,3 +118,32 @@ def test_package_request_null_closure_comment_raises_exception(): User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, Comments=str()) rollback() + + +def test_package_request_status_display(): + """ Test status_display() based on the Status column value. """ + request_type = query(RequestType, RequestType.Name == "merge").first() + + pkgreq = create(PackageRequest, RequestType=request_type, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str(), + Status=PENDING_ID) + assert pkgreq.status_display() == PENDING + + pkgreq.Status = CLOSED_ID + commit() + assert pkgreq.status_display() == CLOSED + + pkgreq.Status = ACCEPTED_ID + commit() + assert pkgreq.status_display() == ACCEPTED + + pkgreq.Status = REJECTED_ID + commit() + assert pkgreq.status_display() == REJECTED + + pkgreq.Status = 124 + commit() + with pytest.raises(KeyError): + pkgreq.status_display() From 5bd3a7bbabd06bf846991a9dd4c74d61c9a8d0a0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 7 Aug 2021 15:43:44 -0700 Subject: [PATCH 0386/1451] RequestType: add name_display() and record constants Just like some of the other tables, we have some constant records that we use to denote types of things. This commit adds constants which correlate with these record constants. Signed-off-by: Kevin Morris --- aurweb/models/request_type.py | 15 +++++++++++++++ test/test_request_type.py | 15 +++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/aurweb/models/request_type.py b/aurweb/models/request_type.py index 2c8276e8..a26dcf9a 100644 --- a/aurweb/models/request_type.py +++ b/aurweb/models/request_type.py @@ -1,7 +1,12 @@ from sqlalchemy import Column, Integer +from aurweb import db from aurweb.models.declarative import Base +DELETION = "deletion" +ORPHAN = "orphan" +MERGE = "merge" + class RequestType(Base): __tablename__ = "RequestTypes" @@ -9,3 +14,13 @@ class RequestType(Base): ID = Column(Integer, primary_key=True) __mapper_args__ = {"primary_key": [ID]} + + def name_display(self) -> str: + """ Return the Name column with its first char capitalized. """ + name = self.Name + return name[0].upper() + name[1:] + + +DELETION_ID = db.query(RequestType, RequestType.Name == DELETION).first().ID +ORPHAN_ID = db.query(RequestType, RequestType.Name == ORPHAN).first().ID +MERGE_ID = db.query(RequestType, RequestType.Name == MERGE).first().ID diff --git a/test/test_request_type.py b/test/test_request_type.py index a470a60b..a3b3ccb8 100644 --- a/test/test_request_type.py +++ b/test/test_request_type.py @@ -1,7 +1,7 @@ import pytest -from aurweb.db import create, delete -from aurweb.models.request_type import RequestType +from aurweb.db import create, delete, query +from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID, RequestType from aurweb.testing import setup_test_db @@ -22,3 +22,14 @@ def test_request_type_null_name_returns_empty_string(): assert bool(request_type.ID) assert request_type.Name == str() delete(RequestType, RequestType.ID == request_type.ID) + + +def test_request_type_name_display(): + deletion = query(RequestType, RequestType.ID == DELETION_ID).first() + assert deletion.name_display() == "Deletion" + + orphan = query(RequestType, RequestType.ID == ORPHAN_ID).first() + assert orphan.name_display() == "Orphan" + + merge = query(RequestType, RequestType.ID == MERGE_ID).first() + assert merge.name_display() == "Merge" From af51b5c4604636408d78d015b49bfbc0e8d7de27 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 7 Aug 2021 19:35:50 -0700 Subject: [PATCH 0387/1451] User: add several utility methods Added: - User.voted_for(package) - Has a user voted for a particular package? - User.notified(package) - Is a user being notified about a particular package? - User.packages() - Entire collection of Package objects related to User. Signed-off-by: Kevin Morris --- aurweb/models/user.py | 36 +++++++++++++++++++++++++++++++++++- test/test_user.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 4705c050..0ccf7329 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -5,13 +5,14 @@ from datetime import datetime import bcrypt from fastapi import Request -from sqlalchemy import Column, ForeignKey, Integer, String, text +from sqlalchemy import Column, ForeignKey, Integer, String, or_, text from sqlalchemy.orm import backref, relationship import aurweb.config import aurweb.models.account_type import aurweb.schema +from aurweb import db from aurweb.models.ban import is_banned from aurweb.models.declarative import Base @@ -177,6 +178,39 @@ class User(Base): """ return self == user or self.is_trusted_user() or self.is_developer() + def voted_for(self, package) -> bool: + """ Has this User voted for package? """ + from aurweb.models.package_vote import PackageVote + return bool(package.PackageBase.package_votes.filter( + PackageVote.UsersID == self.ID + ).scalar()) + + def notified(self, package) -> bool: + """ Is this User being notified about package? """ + from aurweb.models.package_notification import PackageNotification + return bool(package.PackageBase.package_notifications.filter( + PackageNotification.UserID == self.ID + ).scalar()) + + def packages(self): + """ Returns an ORM query to Package objects owned by this user. + + This should really be replaced with an internal ORM join + configured for the User model. This has not been done yet + due to issues I've been encountering in the process, so + sticking with this function until we can properly implement it. + + :return: ORM query of User-packaged or maintained Package objects + """ + from aurweb.models.package import Package + from aurweb.models.package_base import PackageBase + return db.query(Package).join(PackageBase).filter( + or_( + PackageBase.PackagerUID == self.ID, + PackageBase.MaintainerUID == self.ID + ) + ) + def __repr__(self): return "" % ( self.ID, str(self.AccountType), self.Username) diff --git a/test/test_user.py b/test/test_user.py index 9ab40801..7756cff3 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -12,6 +12,10 @@ import aurweb.config from aurweb.db import commit, create, query from aurweb.models.account_type import AccountType from aurweb.models.ban import Ban +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_vote import PackageVote from aurweb.models.session import Session from aurweb.models.ssh_pub_key import SSHPubKey from aurweb.models.user import User @@ -25,7 +29,16 @@ account_type = user = None def setup(): global account_type, user - setup_test_db("Users", "Sessions", "Bans", "SSHPubKeys") + setup_test_db( + User.__tablename__, + Session.__tablename__, + Ban.__tablename__, + SSHPubKey.__tablename__, + Package.__tablename__, + PackageBase.__tablename__, + PackageVote.__tablename__, + PackageNotification.__tablename__ + ) account_type = query(AccountType, AccountType.AccountType == "User").first() @@ -249,3 +262,24 @@ def test_user_is_developer(): user.AccountType = dev_type commit() assert user.is_developer() is True + + +def test_user_voted_for(): + now = int(datetime.utcnow().timestamp()) + pkgbase = create(PackageBase, Name="pkg1", Maintainer=user) + pkg = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + create(PackageVote, PackageBase=pkgbase, User=user, VoteTS=now) + assert user.voted_for(pkg) + + +def test_user_notified(): + pkgbase = create(PackageBase, Name="pkg1", Maintainer=user) + pkg = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + create(PackageNotification, PackageBase=pkgbase, User=user) + assert user.notified(pkg) + + +def test_user_packages(): + pkgbase = create(PackageBase, Name="pkg1", Maintainer=user) + pkg = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + assert pkg in user.packages() From 5a175bd92a791b45ad1d224cca133645abfcaa0d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 9 Aug 2021 23:43:48 -0700 Subject: [PATCH 0388/1451] routers.html: add authenticated dashboard to homepage Signed-off-by: Kevin Morris --- aurweb/routers/html.py | 39 +++++++++++ templates/dashboard.html | 54 ++++++++++++++++ templates/index.html | 6 +- templates/partials/packages/requests.html | 38 +++++++++++ templates/partials/packages/results.html | 55 ++++++++++++++++ test/test_homepage.py | 79 ++++++++++++++++++++++- 6 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 templates/dashboard.html create mode 100644 templates/partials/packages/requests.html create mode 100644 templates/partials/packages/results.html diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index ae012901..c2375f69 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -9,11 +9,15 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, or_ import aurweb.config +import aurweb.models.package_request from aurweb import db, util from aurweb.cache import db_count_cache from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID +from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_request import PackageRequest from aurweb.models.user import User from aurweb.packages.util import updated_packages from aurweb.templates import make_context, render_template @@ -132,6 +136,41 @@ async def index(request: Request): # Get the 15 most recently updated packages. context["package_updates"] = updated_packages(15, updates_expire) + if request.user.is_authenticated(): + # Authenticated users get a few extra pieces of data for + # the dashboard display. + packages = db.query(Package).join(PackageBase) + + maintained = packages.join( + User, PackageBase.MaintainerUID == User.ID + ).filter( + PackageBase.MaintainerUID == request.user.ID + ) + + context["flagged_packages"] = maintained.filter( + PackageBase.OutOfDateTS.isnot(None) + ).order_by( + PackageBase.ModifiedTS.desc(), Package.Name.asc() + ).limit(50).all() + + archive_time = aurweb.config.getint('options', 'request_archive_time') + start = now - archive_time + context["package_requests"] = request.user.package_requests.filter( + PackageRequest.RequestTS >= start + ).limit(50).all() + + # Packages that the request user maintains or comaintains. + context["packages"] = maintained.order_by( + PackageBase.ModifiedTS.desc(), Package.Name.desc() + ).limit(50).all() + + # Any packages that the request user comaintains. + context["comaintained"] = packages.join( + PackageComaintainer).filter( + PackageComaintainer.UsersID == request.user.ID).order_by( + PackageBase.ModifiedTS.desc(), Package.Name.desc() + ).limit(50).all() + return render_template(request, "index.html", context) diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 00000000..5ad89992 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,54 @@ +
    +

    {{ "Dashboard" | tr }}

    + +

    {{ "My Flagged Packages" | tr }}

    + {% if not flagged_packages %} +

    {{ "No packages matched your search criteria." | tr }}

    + {% else %} + {% with table_id = "flagged-packages", packages = flagged_packages %} + {% include 'partials/packages/results.html' %} + {% endwith %} + {% endif %} + +

    {{ "My Requests" | tr }}

    + {% if not package_requests %} +

    {{ "No requests matched your search criteria." | tr }}

    + {% else %} + {% with requests = package_requests %} + {% include 'partials/packages/requests.html' %} + {% endwith %} + {% endif %} +
    + +
    +

    {{ "My Packages" | tr }}

    +

    + + {{ "Search for packages I maintain" | tr }} + +

    + {% if not packages %} +

    {{ "No packages matched your search criteria." | tr }}

    + {% else %} + {% with table_id = "my-packages" %} + {% include 'partials/packages/results.html' %} + {% endwith %} + {% endif %} +
    + +
    +

    {{ "Co-Maintained Packages" | tr }}

    +

    + + {{ "Search for packages I co-maintain" | tr }} + +

    + {% if not comaintained %} +

    {{ "No packages matched your search criteria." | tr }}

    + {% else %} + {% with table_id = "comaintained-packages", packages = comaintained %} + {% include 'partials/packages/results.html' %} + {% endwith %} + {% endif %} +
    + diff --git a/templates/index.html b/templates/index.html index e50a99cd..0b6eda50 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,7 +3,11 @@ {% block pageContent %}
    - {% include 'home.html' %} + {% if request.user.is_authenticated() %} + {% include 'dashboard.html' %} + {% else %} + {% include 'home.html' %} + {% endif %}
    diff --git a/templates/partials/packages/requests.html b/templates/partials/packages/requests.html new file mode 100644 index 00000000..5239ca72 --- /dev/null +++ b/templates/partials/packages/requests.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + {% for request in requests %} + {% set requested = request.RequestTS | dt | as_timezone(timezone) %} + + + + + + + + + {% endfor %} + + + +
    {{ "Package" | tr }}{{ "Type" | tr }}{{ "Comments" | tr }}{{ "Filed by" | tr }}{{ "Date" | tr }}{{ "Status" | tr }}
    + + {{ request.PackageBase.Name }} + + {{ request.RequestType.name_display() | tr }}{{ request.Comments }} + + {{ request.User.Username }} + + {{ requested.strftime("%Y-%m-%d %H:%M") }}{{ request.status_display() | tr }}
    diff --git a/templates/partials/packages/results.html b/templates/partials/packages/results.html new file mode 100644 index 00000000..005bd5a9 --- /dev/null +++ b/templates/partials/packages/results.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + {% for pkg in packages %} + {% set flagged = pkg.PackageBase.OutOfDateTS %} + + + {% if flagged %} + + {% else %} + + {% endif %} + + + + + + + + {% endfor %} + +
    {{ "Name" | tr }}{{ "Version" | tr }}{{ "Votes" | tr }}{{ "Popularity" | tr }}{{ "Voted" | tr }}{{ "Notify" | tr }}{{ "Description" | tr }}{{ "Maintainer" | tr }}
    + + {{ pkg.Name }} + + {{ pkg.Version }}{{ pkg.Version }}{{ pkg.PackageBase.NumVotes }} + {{ pkg.PackageBase.Popularity | number_format(2) }} + + + {% if request.user.voted_for(pkg) %} + {{ "Yes" | tr }} + {% endif %} + + + {% if request.user.notified(pkg) %} + {{ "Yes" | tr }} + {% endif %} + {{ pkg.Description or '' }} + {% set maintainer = pkg.PackageBase.Maintainer %} + + {{ maintainer.Username }} + +
    diff --git a/test/test_homepage.py b/test/test_homepage.py index a629b98c..2cd6682f 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -13,10 +13,14 @@ from aurweb.asgi import app from aurweb.models.account_type import USER_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_request import PackageRequest +from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User from aurweb.redis import redis_connection from aurweb.testing import setup_test_db from aurweb.testing.html import parse_root +from aurweb.testing.requests import Request client = TestClient(app) @@ -26,7 +30,9 @@ def setup(): yield setup_test_db( User.__tablename__, Package.__tablename__, - PackageBase.__tablename__ + PackageBase.__tablename__, + PackageComaintainer.__tablename__, + PackageRequest.__tablename__ ) @@ -149,3 +155,74 @@ def test_homepage_updates(redis, packages): for i, expected in enumerate(expectations): pkgname = updates[i].xpath('./td/a').pop(0) assert pkgname.text.strip() == expected + + +def test_homepage_dashboard(redis, packages, user): + # Create Comaintainer records for all of the packages. + for pkg in packages: + db.create(PackageComaintainer, PackageBase=pkg.PackageBase, + User=user, Priority=1, autocommit=False) + db.commit() + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + response = request.get("/", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + # Assert some expectations that we end up getting all fifty + # packages in the "My Packages" table. + expectations = [f"pkg_{i}" for i in range(50 - 1, 0, -1)] + my_packages = root.xpath('//table[@id="my-packages"]/tbody/tr') + for i, expected in enumerate(expectations): + name, version, votes, pop, voted, notify, desc, maint \ + = my_packages[i].xpath('./td') + assert name.xpath('./a').pop(0).text.strip() == expected + + # Do the same for the Comaintained Packages table. + my_packages = root.xpath('//table[@id="comaintained-packages"]/tbody/tr') + for i, expected in enumerate(expectations): + name, version, votes, pop, voted, notify, desc, maint \ + = my_packages[i].xpath('./td') + assert name.xpath('./a').pop(0).text.strip() == expected + + +def test_homepage_dashboard_requests(redis, packages, user): + now = int(datetime.utcnow().timestamp()) + + pkg = packages[0] + reqtype = db.query(RequestType, RequestType.ID == DELETION_ID).first() + pkgreq = db.create(PackageRequest, PackageBase=pkg.PackageBase, + PackageBaseName=pkg.PackageBase.Name, + User=user, Comments=str(), + ClosureComment=str(), RequestTS=now, + RequestType=reqtype) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + response = request.get("/", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + request = root.xpath('//table[@id="pkgreq-results"]/tbody/tr').pop(0) + pkgname = request.xpath('./td/a').pop(0) + assert pkgname.text.strip() == pkgreq.PackageBaseName + + +def test_homepage_dashboard_flagged_packages(redis, packages, user): + # Set the first Package flagged by setting its OutOfDateTS column. + pkg = packages[0] + pkg.PackageBase.OutOfDateTS = int(datetime.utcnow().timestamp()) + db.commit() + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + response = request.get("/", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Check to see that the package showed up in the Flagged Packages table. + root = parse_root(response.text) + flagged_pkg = root.xpath('//table[@id="flagged-packages"]/tbody/tr').pop(0) + flagged_name = flagged_pkg.xpath('./td/a').pop(0) + assert flagged_name.text.strip() == pkg.Name From f086457741574867f30109dc58ae656684968e2e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 17 Aug 2021 21:52:59 -0700 Subject: [PATCH 0389/1451] aurweb.redis: Reduce logging Signed-off-by: Kevin Morris --- aurweb/redis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/redis.py b/aurweb/redis.py index 6b8dede4..6d3cff38 100644 --- a/aurweb/redis.py +++ b/aurweb/redis.py @@ -36,13 +36,13 @@ def redis_connection(): # pragma: no cover # If we haven't initialized redis yet, construct a pool. if disabled: - logger.debug("Initializing fake Redis instance.") if pool is None: + logger.debug("Initializing fake Redis instance.") pool = FakeConnectionPool() return pool.handle else: - logger.debug("Initializing real Redis instance.") if pool is None: + logger.debug("Initializing real Redis instance.") redis_addr = aurweb.config.get("options", "redis_address") pool = ConnectionPool.from_url(redis_addr) From 6eafb457ec18cefd9341e27d84b7be76abbcca29 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 20 Aug 2021 16:36:10 -0700 Subject: [PATCH 0390/1451] aurweb.util: fix code style violation Signed-off-by: Kevin Morris --- aurweb/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/util.py b/aurweb/util.py index d4a0b221..860bdd12 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -20,8 +20,8 @@ import aurweb.config def make_random_string(length): - return ''.join(random.choices(string.ascii_lowercase + - string.digits, k=length)) + return ''.join(random.choices(string.ascii_lowercase + + string.digits, k=length)) def make_nonce(length: int = 8): From a72ab61902961562048f487c2e102249b4a33964 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 25 Aug 2021 16:34:37 -0700 Subject: [PATCH 0391/1451] [FastAPI] fix dashboard template Some columns should only be shown when a user is authenticated. Signed-off-by: Kevin Morris --- templates/partials/packages/results.html | 52 ++++++++++++------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/templates/partials/packages/results.html b/templates/partials/packages/results.html index 005bd5a9..80029d10 100644 --- a/templates/partials/packages/results.html +++ b/templates/partials/packages/results.html @@ -6,8 +6,10 @@

    {{ "Version" | tr }} {{ "Votes" | tr }} {{ "Popularity" | tr }}{{ "Voted" | tr }}{{ "Notify" | tr }}{{ "Voted" | tr }}{{ "Notify" | tr }}{{ "Description" | tr }} {{ "Maintainer" | tr }}
    {{ pkg.Version }}{{ pkg.Version }}{{ pkg.PackageBase.NumVotes }} - {{ pkg.PackageBase.Popularity | number_format(2) }} - - - {% if request.user.voted_for(pkg) %} - {{ "Yes" | tr }} - {% endif %} - - - {% if request.user.notified(pkg) %} - {{ "Yes" | tr }} - {% endif %} - {{ pkg.PackageBase.Popularity | number_format(2) }} + + {% if request.user.voted_for(pkg) %} + {{ "Yes" | tr }} + {% endif %} + + + {% if request.user.notified(pkg) %} + {{ "Yes" | tr }} + {% endif %} + {{ pkg.Description or '' }} {% set maintainer = pkg.PackageBase.Maintainer %} - - {{ maintainer.Username }} - + {% if maintainer %} + + {{ maintainer.Username }} + + {% else %} + {{ "orphan" | tr }} + {% endif %}
    {{ "Proposal" | tr }} {% set off_qs = "%s=%d" | format(off_param, off) %} - {% set by_qs = "%s=%s" | format(by_param, by_next | urlencode) %} + {% set by_qs = "%s=%s" | format(by_param, by_next | quote_plus) %} {{ "Start" | tr }} @@ -95,7 +95,7 @@ {% if off > 0 %} {% set off_qs = "%s=%d" | format(off_param, off - 10) %} - {% set by_qs = "%s=%s" | format(by_param, by | urlencode) %} + {% set by_qs = "%s=%s" | format(by_param, by | quote_plus) %} ‹ Back @@ -104,7 +104,7 @@ {% if off < total_votes - pp %} {% set off_qs = "%s=%d" | format(off_param, off + 10) %} - {% set by_qs = "%s=%s" | format(by_param, by | urlencode) %} + {% set by_qs = "%s=%s" | format(by_param, by | quote_plus) %} Next › From a114bd3e16e5df00cc2542e37cedb82bf99e9a84 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 28 Aug 2021 16:33:33 -0700 Subject: [PATCH 0407/1451] aurweb.util: add extend_query and to_qs helpers The first addition, extend_query, can be used to take an existing query parameter dictionary and inject an *additions as replacement key/value pairs. The second, to_qs, converts a query parameter dictionary to a query string in the form "a=b&c=d". These two functions simplify and make dedupe_qs and quote_plus more efficient in terms of constructing custom query string overrides. Signed-off-by: Kevin Morris --- aurweb/util.py | 14 +++++++++++++- test/test_util.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/aurweb/util.py b/aurweb/util.py index 494a988d..e993c440 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -8,7 +8,8 @@ import string from collections import OrderedDict from datetime import datetime -from urllib.parse import quote_plus, urlparse +from typing import Any, Dict +from urllib.parse import quote_plus, urlencode, urlparse from zoneinfo import ZoneInfo import fastapi @@ -148,6 +149,17 @@ def dedupe_qs(query_string: str, *additions): return '&'.join([f"{k}={quote_plus(v)}" for k, v in reversed(qs.items())]) +def extend_query(query: Dict[str, Any], *additions) -> Dict[str, Any]: + """ Add additionally key value pairs to query. """ + for k, v in list(additions): + query[k] = v + return query + + +def to_qs(query: Dict[str, Any]) -> str: + return urlencode(query, doseq=True) + + def get_vote(voteinfo, request: fastapi.Request): from aurweb.models.tu_vote import TUVote return voteinfo.tu_votes.filter(TUVote.User == request.user).first() diff --git a/test/test_util.py b/test/test_util.py index f54a98a0..06fc08d3 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -35,3 +35,18 @@ def test_dedupe_qs(): def test_number_format(): assert util.number_format(0.222, 2) == "0.22" assert util.number_format(0.226, 2) == "0.23" + + +def test_extend_query(): + """ Test extension of a query via extend_query. """ + query = {"a": "b"} + extended = util.extend_query(query, ("a", "c"), ("b", "d")) + assert extended.get("a") == "c" + assert extended.get("b") == "d" + + +def test_to_qs(): + """ Test conversion from a query dictionary to a query string. """ + query = {"a": "b", "c": [1, 2, 3]} + qs = util.to_qs(query) + assert qs == "a=b&c=1&c=2&c=3" From c9374732c013ba336cd1b3d6b3991fece9b2c934 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 31 Aug 2021 14:25:58 -0700 Subject: [PATCH 0408/1451] add filters for extend_query, to_qs New jinja2 filters: * `extend_query` -> `aurweb.util.extend_query` * `urlencode` -> `aurweb.util.to_qs` Signed-off-by: Kevin Morris --- aurweb/templates.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aurweb/templates.py b/aurweb/templates.py index a648d5a1..4a9927fb 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -31,6 +31,8 @@ _env.filters["tn"] = l10n.tn _env.filters["dt"] = util.timestamp_to_datetime _env.filters["as_timezone"] = util.as_timezone _env.filters["dedupe_qs"] = util.dedupe_qs +_env.filters["extend_query"] = util.extend_query +_env.filters["urlencode"] = util.to_qs _env.filters["quote_plus"] = quote_plus _env.filters["get_vote"] = util.get_vote _env.filters["number_format"] = util.number_format From 210e459ba90e274e585a6928585aa2362d168944 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 31 Aug 2021 14:27:16 -0700 Subject: [PATCH 0409/1451] Eradicate the `dedupe_qs` filter The new `extend_query` and `urlencode` filters are way cleaner ways to achieve what we did with `dedupe_qs`. Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 13 ++++++------- aurweb/templates.py | 1 - aurweb/util.py | 28 ++-------------------------- templates/partials/tu/proposals.html | 6 +++--- test/test_util.py | 16 ---------------- 5 files changed, 11 insertions(+), 53 deletions(-) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index fd5ebb04..61cfec6c 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -5,7 +5,6 @@ import typing from datetime import datetime from http import HTTPStatus -from urllib.parse import quote_plus from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import RedirectResponse, Response @@ -106,12 +105,12 @@ async def trusted_user(request: Request, context["current_by_next"] = "asc" if current_by == "desc" else "desc" context["past_by_next"] = "asc" if past_by == "desc" else "desc" - context["q"] = '&'.join([ - f"coff={current_off}", - f"cby={quote_plus(current_by)}", - f"poff={past_off}", - f"pby={quote_plus(past_by)}" - ]) + context["q"] = { + "coff": current_off, + "cby": current_by, + "poff": past_off, + "pby": past_by + } return render_template(request, "tu/index.html", context) diff --git a/aurweb/templates.py b/aurweb/templates.py index 4a9927fb..6a1b6a1c 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -30,7 +30,6 @@ _env.filters["tn"] = l10n.tn # Utility filters. _env.filters["dt"] = util.timestamp_to_datetime _env.filters["as_timezone"] = util.as_timezone -_env.filters["dedupe_qs"] = util.dedupe_qs _env.filters["extend_query"] = util.extend_query _env.filters["urlencode"] = util.to_qs _env.filters["quote_plus"] = quote_plus diff --git a/aurweb/util.py b/aurweb/util.py index e993c440..f9181811 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -6,10 +6,9 @@ import re import secrets import string -from collections import OrderedDict from datetime import datetime from typing import Any, Dict -from urllib.parse import quote_plus, urlencode, urlparse +from urllib.parse import urlencode, urlparse from zoneinfo import ZoneInfo import fastapi @@ -126,31 +125,8 @@ def as_timezone(dt: datetime, timezone: str): return dt.astimezone(tz=ZoneInfo(timezone)) -def dedupe_qs(query_string: str, *additions): - """ Dedupe keys found in a query string by rewriting it without - duplicates found while iterating from the end to the beginning, - using an ordered memo to track keys found and persist locations. - - That is, query string 'a=1&b=1&a=2' will be deduped and converted - to 'b=1&a=2'. - - :param query_string: An HTTP URL query string. - :param *additions: Optional additional fields to add to the query string. - :return: Deduped query string, including *additions at the tail. - """ - for addition in list(additions): - query_string += f"&{addition}" - - qs = OrderedDict() - for item in reversed(query_string.split('&')): - key, value = item.split('=') - if key not in qs: - qs[key] = value - return '&'.join([f"{k}={quote_plus(v)}" for k, v in reversed(qs.items())]) - - def extend_query(query: Dict[str, Any], *additions) -> Dict[str, Any]: - """ Add additionally key value pairs to query. """ + """ Add additional key value pairs to query. """ for k, v in list(additions): query[k] = v return query diff --git a/templates/partials/tu/proposals.html b/templates/partials/tu/proposals.html index ab90444e..40eba22b 100644 --- a/templates/partials/tu/proposals.html +++ b/templates/partials/tu/proposals.html @@ -24,7 +24,7 @@ {% set off_qs = "%s=%d" | format(off_param, off) %} {% set by_qs = "%s=%s" | format(by_param, by_next | quote_plus) %} - + {{ "Start" | tr }} {{ pkg.PackageBase.Popularity | number_format(2) }} - - {% if request.user.voted_for(pkg) %} + {# If I voted, display "Yes". #} + {% if pkg.PackageBase.ID in votes %} {{ "Yes" | tr }} {% endif %} - - {% if request.user.notified(pkg) %} + {# If I'm being notified, display "Yes". #} + {% if pkg.PackageBase.ID in notified %} {{ "Yes" | tr }} {% endif %}
    + + + {% if request.user.is_authenticated() %} + + {% endif %} + + + + + {% if request.user.is_authenticated() %} + + + {% endif %} + + + + + + {% for pkg in packages %} + {% set flagged = pkg.PackageBase.OutOfDateTS %} + + {% if request.user.is_authenticated() %} + + {% endif %} + + {% if flagged %} + + {% else %} + + {% endif %} + + + {% if request.user.is_authenticated() %} + + + {% endif %} + + + + {% endfor %} + +
    + {% set order = SO %} + {% if SB == "n" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Name" | tr }} + + {{ "Version" | tr }} + {% set order = SO %} + {% if SB == "v" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Votes" | tr }} + + + {% set order = SO %} + {% if SB == "p" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + {{ "Popularity" | tr }}? + + {% set order = SO %} + {% if SB == "w" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Voted" | tr }} + + + {% set order = SO %} + {% if SB == "o" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Notify" | tr }} + + {{ "Description" | tr }} + {% set order = SO %} + {% if SB == "m" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Maintainer" | tr }} + +
    + + + + {{ pkg.Name }} + + {{ pkg.Version }}{{ pkg.Version }}{{ pkg.PackageBase.NumVotes }} + {{ pkg.PackageBase.Popularity | number_format(2) }} + + {% if pkg.PackageBase.ID in voted %} + {{ "Yes" | tr }} + {% endif %} + + {% if pkg.PackageBase.ID in notified %} + {{ "Yes" | tr }} + {% endif %} + {{ pkg.Description or '' }} + {% set maintainer = pkg.PackageBase.Maintainer %} + {% if maintainer %} + + {{ maintainer.Username }} + + {% else %} + {{ "orphan" | tr }} + {% endif %} +
    diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 8a468c15..fb45af88 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1,5 +1,8 @@ +import re + from datetime import datetime from http import HTTPStatus +from typing import List import pytest @@ -11,11 +14,14 @@ from aurweb.models.dependency_type import DependencyType from aurweb.models.official_provider import OfficialProvider from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comment import PackageComment from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_keyword import PackageKeyword +from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation from aurweb.models.package_request import PackageRequest +from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import PROVIDES_ID, RelationType from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User @@ -64,6 +70,9 @@ def setup(): PackageDependency.__tablename__, PackageRelation.__tablename__, PackageKeyword.__tablename__, + PackageVote.__tablename__, + PackageNotification.__tablename__, + PackageComaintainer.__tablename__, OfficialProvider.__tablename__ ) @@ -101,16 +110,41 @@ def maintainer() -> User: @pytest.fixture def package(maintainer: User) -> Package: """ Yield a Package created by user. """ + now = int(datetime.utcnow().timestamp()) with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", - Maintainer=maintainer) + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now) package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield package +@pytest.fixture +def packages(maintainer: User) -> List[Package]: + """ Yield 55 packages named pkg_0 .. pkg_54. """ + packages_ = [] + now = int(datetime.utcnow().timestamp()) + with db.begin(): + for i in range(55): + pkgbase = db.create(PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now) + package = db.create(Package, + PackageBase=pkgbase, + Name=f"pkg_{i}") + packages_.append(package) + + yield packages_ + + def test_package_not_found(client: TestClient): with client as request: resp = request.get("/packages/not_found") @@ -133,7 +167,7 @@ def test_package_official_not_found(client: TestClient, package: Package): def test_package(client: TestClient, package: Package): - """ Test a single /packages/{name} route. """ + """ Test a single / packages / {name} route. """ with client as request: resp = request.get(package_endpoint(package)) @@ -376,3 +410,505 @@ def test_pkgbase(client: TestClient, package: Package): pkgs = root.findall('.//div[@id="pkgs"]/ul/li/a') for i, name in enumerate(expected): assert pkgs[i].text.strip() == name + + +def test_packages(client: TestClient, packages: List[Package]): + """ Test the / packages route with defaults. + + Defaults: + 50 results per page + offset of 0 + """ + with client as request: + response = request.get("/packages", params={ + "SeB": "X" # "X" isn't valid, defaults to "nd" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + stats = root.xpath('//div[@class="pkglist-stats"]/p')[0] + pager_text = re.sub(r'\s+', " ", stats.text.replace("\n", "").strip()) + assert pager_text == "55 packages found. Page 1 of 2." + + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 # Default per-page + + +def test_packages_search_by_name(client: TestClient, packages: List[Package]): + with client as request: + response = request.get("/packages", params={ + "SeB": "n", + "K": "pkg_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 # Default per-page + + +def test_packages_search_by_exact_name(client: TestClient, + packages: List[Package]): + with client as request: + response = request.get("/packages", params={ + "SeB": "N", + "K": "pkg_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + + # There is no package named exactly 'pkg_', we get 0 results. + assert len(rows) == 0 + + with client as request: + response = request.get("/packages", params={ + "SeB": "N", + "K": "pkg_1" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + + # There's just one package named 'pkg_1', we get 1 result. + assert len(rows) == 1 + + +def test_packages_search_by_pkgbase(client: TestClient, + packages: List[Package]): + with client as request: + response = request.get("/packages", params={ + "SeB": "b", + "K": "pkg_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + +def test_packages_search_by_exact_pkgbase(client: TestClient, + packages: List[Package]): + with client as request: + response = request.get("/packages", params={ + "SeB": "B", + "K": "pkg_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + with client as request: + response = request.get("/packages", params={ + "SeB": "B", + "K": "pkg_1" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_packages_search_by_keywords(client: TestClient, + packages: List[Package]): + # None of our packages have keywords, so this query should return nothing. + with client as request: + response = request.get("/packages", params={ + "SeB": "k", + "K": "testKeyword" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + # But now, let's create the keyword for the first package. + package = packages[0] + with db.begin(): + db.create(PackageKeyword, + PackageBase=package.PackageBase, + Keyword="testKeyword") + + # And request packages with that keyword, we should get 1 result. + with client as request: + response = request.get("/packages", params={ + "SeB": "k", + "K": "testKeyword" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_packages_search_by_maintainer(client: TestClient, + maintainer: User, + package: Package): + with client as request: + response = request.get("/packages", params={ + "SeB": "m", + "K": maintainer.Username + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_packages_search_by_comaintainer(client: TestClient, + maintainer: User, + package: Package): + # Nobody's a comaintainer yet. + with client as request: + response = request.get("/packages", params={ + "SeB": "c", + "K": maintainer.Username + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + # Now, we create a comaintainer. + with db.begin(): + db.create(PackageComaintainer, + PackageBase=package.PackageBase, + User=maintainer, + Priority=1) + + # Then test that it's returned by our search. + with client as request: + response = request.get("/packages", params={ + "SeB": "c", + "K": maintainer.Username + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_packages_search_by_co_or_maintainer(client: TestClient, + maintainer: User, + package: Package): + with client as request: + response = request.get("/packages", params={ + "SeB": "M", + "SB": "BLAH", # Invalid SB; gets reset to default "n". + "K": maintainer.Username + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + with db.begin(): + user = db.create(User, Username="comaintainer", + Email="comaintainer@example.org", + Passwd="testPassword") + db.create(PackageComaintainer, + PackageBase=package.PackageBase, + User=user, + Priority=1) + + with client as request: + response = request.get("/packages", params={ + "SeB": "M", + "K": user.Username + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_packages_search_by_submitter(client: TestClient, + maintainer: User, + package: Package): + with client as request: + response = request.get("/packages", params={ + "SeB": "s", + "K": maintainer.Username + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_packages_sort_by_votes(client: TestClient, + maintainer: User, + packages: List[Package]): + # Set the first package's NumVotes to 1. + with db.begin(): + packages[0].PackageBase.NumVotes = 1 + + # Test that, by default, the first result is what we just set above. + with client as request: + response = request.get("/packages", params={ + "SB": "v" # Votes. + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + votes = rows[0].xpath('./td')[2] # The third column of the first row. + assert votes.text.strip() == "1" + + # Now, test that with an ascending order, the last result is + # the one we set, since the default (above) is descending. + with client as request: + response = request.get("/packages", params={ + "SB": "v", # Votes. + "SO": "a", # Ascending. + "O": "50" # Second page. + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + votes = rows[-1].xpath('./td')[2] # The third column of the last row. + assert votes.text.strip() == "1" + + +def test_packages_sort_by_popularity(client: TestClient, + maintainer: User, + packages: List[Package]): + # Set the first package's Popularity to 0.50. + with db.begin(): + packages[0].PackageBase.Popularity = "0.50" + + # Test that, by default, the first result is what we just set above. + with client as request: + response = request.get("/packages", params={ + "SB": "p" # Popularity + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + pop = rows[0].xpath('./td')[3] # The fourth column of the first row. + assert pop.text.strip() == "0.50" + + +def test_packages_sort_by_voted(client: TestClient, + maintainer: User, + packages: List[Package]): + now = int(datetime.utcnow().timestamp()) + with db.begin(): + db.create(PackageVote, PackageBase=packages[0].PackageBase, + User=maintainer, VoteTS=now) + + # Test that, by default, the first result is what we just set above. + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + response = request.get("/packages", params={ + "SB": "w", # Voted + "SO": "d" # Descending, Voted first. + }, cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + voted = rows[0].xpath('./td')[5] # The sixth column of the first row. + assert voted.text.strip() == "Yes" + + # Conversely, everything else was not voted on. + voted = rows[1].xpath('./td')[5] # The sixth column of the second row. + assert voted.text.strip() == str() # Empty. + + +def test_packages_sort_by_notify(client: TestClient, + maintainer: User, + packages: List[Package]): + db.create(PackageNotification, + PackageBase=packages[0].PackageBase, + User=maintainer) + + # Test that, by default, the first result is what we just set above. + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + response = request.get("/packages", params={ + "SB": "o", # Voted + "SO": "d" # Descending, Voted first. + }, cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + notify = rows[0].xpath('./td')[6] # The sixth column of the first row. + assert notify.text.strip() == "Yes" + + # Conversely, everything else was not voted on. + notify = rows[1].xpath('./td')[6] # The sixth column of the second row. + assert notify.text.strip() == str() # Empty. + + +def test_packages_sort_by_maintainer(client: TestClient, + maintainer: User, + package: Package): + """ Sort a package search by the maintainer column. """ + + # Create a second package, so the two can be ordered and checked. + with db.begin(): + maintainer2 = db.create(User, Username="maintainer2", + Email="maintainer2@example.org", + Passwd="testPassword") + base2 = db.create(PackageBase, Name="pkg_2", Maintainer=maintainer2, + Submitter=maintainer2, Packager=maintainer2) + db.create(Package, Name="pkg_2", PackageBase=base2) + + # Check the descending order route. + with client as request: + response = request.get("/packages", params={ + "SB": "m", + "SO": "d" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + col = rows[0].xpath('./td')[5].xpath('./a')[0] # Last column. + + assert col.text.strip() == maintainer.Username + + # On the other hand, with ascending, we should get reverse ordering. + with client as request: + response = request.get("/packages", params={ + "SB": "m", + "SO": "a" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + col = rows[0].xpath('./td')[5].xpath('./a')[0] # Last column. + + assert col.text.strip() == maintainer2.Username + + +def test_packages_sort_by_last_modified(client: TestClient, + packages: List[Package]): + now = int(datetime.utcnow().timestamp()) + # Set the first package's ModifiedTS to be 1000 seconds before now. + package = packages[0] + with db.begin(): + package.PackageBase.ModifiedTS = now - 1000 + + with client as request: + response = request.get("/packages", params={ + "SB": "l", + "SO": "a" # Ascending; oldest modification first. + }) + assert response.status_code == int(HTTPStatus.OK) + + # We should have 50 (default per page) results. + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + # Let's assert that the first item returned was the one we modified above. + row = rows[0] + col = row.xpath('./td')[0].xpath('./a')[0] + assert col.text.strip() == package.Name + + +def test_packages_flagged(client: TestClient, maintainer: User, + packages: List[Package]): + package = packages[0] + + now = int(datetime.utcnow().timestamp()) + + with db.begin(): + package.PackageBase.OutOfDateTS = now + package.PackageBase.Flagger = maintainer + + with client as request: + response = request.get("/packages", params={ + "outdated": "on" + }) + assert response.status_code == int(HTTPStatus.OK) + + # We should only get one result from this query; the package we flagged. + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + with client as request: + response = request.get("/packages", params={ + "outdated": "off" + }) + assert response.status_code == int(HTTPStatus.OK) + + # In this case, we should get 54 results, which means that the first + # page will have 50 results (55 packages - 1 outdated = 54 not outdated). + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + +def test_packages_orphans(client: TestClient, packages: List[Package]): + package = packages[0] + with db.begin(): + package.PackageBase.Maintainer = None + + with client as request: + response = request.get("/packages", params={"submit": "Orphans"}) + assert response.status_code == int(HTTPStatus.OK) + + # We only have one orphan. Let's make sure that's what is returned. + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_packages_per_page(client: TestClient, maintainer: User): + """ Test the ability for /packages to deal with the PP query + argument specifications (50, 100, 250; default: 50). """ + with db.begin(): + for i in range(255): + base = db.create(PackageBase, Name=f"pkg_{i}", + Maintainer=maintainer, + Submitter=maintainer, + Packager=maintainer) + db.create(Package, PackageBase=base, Name=base.Name) + + # Test default case, PP of 50. + with client as request: + response = request.get("/packages", params={"PP": 50}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + # Alright, test the next case, PP of 100. + with client as request: + response = request.get("/packages", params={"PP": 100}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 100 + + # And finally, the last case, a PP of 250. + with client as request: + response = request.get("/packages", params={"PP": 250}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 250 diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index b36dbd4d..62179769 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -222,3 +222,10 @@ button[type="reset"] { text-align: right; } +input#search-action-submit { + width: 80px; +} + +.success { + color: green; +} From 7e589863561a0777f8feb9f4b11f505715d80841 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 20 Sep 2021 01:30:12 -0700 Subject: [PATCH 0443/1451] feat: add util/adduser.py database tooling script We'll need to add tests for these things at some point. However, I'd like to include this script in here immediately for ease of testing or administration in general. Signed-off-by: Kevin Morris --- util/adduser.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 util/adduser.py diff --git a/util/adduser.py b/util/adduser.py new file mode 100644 index 00000000..7e35d13d --- /dev/null +++ b/util/adduser.py @@ -0,0 +1,67 @@ +import argparse +import sys +import traceback + +from aurweb import db +from aurweb.models.account_type import AccountType +from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint +from aurweb.models.user import User + + +def parse_args(): + parser = argparse.ArgumentParser(description="aurweb-adduser options") + + parser.add_argument("-u", "--username", help="Username", required=True) + parser.add_argument("-e", "--email", help="Email", required=True) + parser.add_argument("-p", "--password", help="Password", required=True) + parser.add_argument("-r", "--realname", help="Real Name") + parser.add_argument("-i", "--ircnick", help="IRC Nick") + parser.add_argument("--pgp-key", help="PGP Key Fingerprint") + parser.add_argument("--ssh-pubkey", help="SSH PubKey") + + parser.add_argument("-t", "--type", help="Account Type", + choices=[ + "User", + "Trusted User", + "Developer", + "Trusted User & Developer" + ], default="User") + + return parser.parse_args() + + +def main(): + args = parse_args() + + type = db.query(AccountType, + AccountType.AccountType == args.type).first() + with db.begin(): + user = db.create(User, Username=args.username, + Email=args.email, Passwd=args.password, + RealName=args.realname, IRCNick=args.ircnick, + PGPKey=args.pgp_key, AccountType=type) + + if args.ssh_pubkey: + pubkey = args.ssh_pubkey.strip() + + # Remove host from the pubkey if it's there. + pubkey = ' '.join(pubkey.split(' ')[:2]) + + with db.begin(): + db.create(SSHPubKey, + User=user, + PubKey=pubkey, + Fingerprint=get_fingerprint(pubkey)) + + print(user.json()) + return 0 + + +if __name__ == "__main__": + e = 1 + try: + e = main() + except Exception: + traceback.print_exc() + e = 1 + sys.exit(e) From dc5dc233ec120ace46e870a057b632b4d5c8149c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 21 Sep 2021 00:01:39 -0700 Subject: [PATCH 0444/1451] .gitlab-ci.yml: add coverage regex This was required for the GitLab coverage badge to get the % of coverage. Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ffea5308..a8ddf08f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,6 +36,7 @@ test: - isort --check-only aurweb # Assert no isort violations in aurweb. - isort --check-only test # Assert no flake8 violations in test. - isort --check-only migrations # Assert no flake8 violations in migrations. + coverage: '/TOTAL.*\s+(\d+\%)/' artifacts: reports: cobertura: coverage.xml From fbd91f346a69e41f44407fc568343c469ea59460 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 22 Sep 2021 18:33:58 -0700 Subject: [PATCH 0445/1451] feat(FastAPI): add /pkgbase/{name}/voters (get) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 9 +++++++++ templates/pkgbase/voters.html | 27 +++++++++++++++++++++++++++ test/test_packages_routes.py | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 templates/pkgbase/voters.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 3eda2539..72cd8c99 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -195,3 +195,12 @@ async def package_base(request: Request, name: str) -> Response: context["packages"] = pkgbase.packages.all() return render_template(request, "pkgbase.html", context) + + +@router.get("/pkgbase/{name}/voters") +async def package_base_voters(request: Request, name: str) -> Response: + # Get the PackageBase. + pkgbase = get_pkgbase(name) + context = make_context(request, "Voters") + context["pkgbase"] = pkgbase + return render_template(request, "pkgbase/voters.html", context) diff --git a/templates/pkgbase/voters.html b/templates/pkgbase/voters.html new file mode 100644 index 00000000..be86f01f --- /dev/null +++ b/templates/pkgbase/voters.html @@ -0,0 +1,27 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    + {{ "Votes" | tr }} for + + {{ pkgbase.Name }} + +

    + +
    +
      + {% for pkg_vote in pkgbase.package_votes %} +
    • + + {{ pkg_vote.User.Username }} + + + {% set voted_at = pkg_vote.VoteTS | dt | as_timezone(timezone) %} + ({{ voted_at.strftime("%Y-%m-%d %H:%M") }}) +
    • + {% endfor %} +
    +
    +
    +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index fb45af88..2190dc18 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -912,3 +912,21 @@ def test_packages_per_page(client: TestClient, maintainer: User): root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') assert len(rows) == 250 + + +def test_pkgbase_voters(client: TestClient, maintainer: User, package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/voters" + + now = int(datetime.utcnow().timestamp()) + with db.begin(): + db.create(PackageVote, User=maintainer, PackageBase=pkgbase, + VoteTS=now) + + with client as request: + resp = request.get(endpoint) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//div[@class="box"]//ul/li') + assert len(rows) == 1 From ad9997c48ff39d4412e888470b980330ea28e3ea Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 2 Oct 2021 12:59:49 -0700 Subject: [PATCH 0446/1451] feat(Docker): build aurweb:latest via docker-compose build Users can now build the required image by running (in the aurweb root): $ docker-compose build Signed-off-by: Kevin Morris --- docker-compose.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 309e95fe..8e2d91d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ version: "3.8" services: ca: + build: . image: aurweb:latest init: true entrypoint: /docker/ca-entrypoint.sh @@ -30,6 +31,7 @@ services: - ./cache:/cache memcached: + build: . image: aurweb:latest init: true command: /docker/scripts/run-memcached.sh @@ -37,6 +39,7 @@ services: test: "bash /docker/health/memcached.sh" redis: + build: . image: aurweb:latest init: true entrypoint: /docker/redis-entrypoint.sh @@ -47,6 +50,7 @@ services: - "16379:6379" mariadb: + build: . image: aurweb:latest init: true entrypoint: /docker/mariadb-entrypoint.sh @@ -62,6 +66,7 @@ services: test: "bash /docker/health/mariadb.sh" mariadb_init: + build: . image: aurweb:latest init: true environment: @@ -73,6 +78,7 @@ services: condition: service_healthy git: + build: . image: aurweb:latest init: true environment: @@ -92,6 +98,7 @@ services: - ./cache:/cache smartgit: + build: . image: aurweb:latest init: true environment: @@ -109,6 +116,7 @@ services: - smartgit_run:/var/run/smartgit cgit-php: + build: . image: aurweb:latest init: true environment: @@ -124,6 +132,7 @@ services: - git_data:/aurweb/aur.git cgit-fastapi: + build: . image: aurweb:latest init: true environment: @@ -139,6 +148,7 @@ services: - git_data:/aurweb/aur.git php-fpm: + build: . image: aurweb:latest init: true environment: @@ -168,6 +178,7 @@ services: - "19000:9000" fastapi: + build: . image: aurweb:latest init: true environment: @@ -197,6 +208,7 @@ services: - "18000:8000" nginx: + build: . image: aurweb:latest init: true environment: @@ -229,6 +241,7 @@ services: - smartgit_run:/var/run/smartgit sharness: + build: . image: aurweb:latest init: true environment: @@ -252,6 +265,7 @@ services: - ./templates:/aurweb/templates pytest-mysql: + build: . image: aurweb:latest init: true environment: @@ -276,6 +290,7 @@ services: - ./templates:/aurweb/templates pytest-sqlite: + build: . image: aurweb:latest init: true environment: @@ -296,6 +311,7 @@ services: - ./templates:/aurweb/templates test: + build: . image: aurweb:latest init: true environment: From 3b1809e2ea8d7adcafaff09664569c75c35791e3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 2 Oct 2021 13:26:05 -0700 Subject: [PATCH 0447/1451] feat(Docker): allow custom certificates for fastapi/nginx Now, when a `./cache/production.{cert,key}.pem` pair is found, it is used in place of any certificates generated by the `ca` service. This allows users to customize the certificate that the FastAPI ASGI server uses as well as the front-end nginx certificates. Optional: - ./cache/production.cert.pem - ./cache/production.key.pem Fallback: - ./cache/localhost.cert.pem + ./cache/root.ca.pem (chain) - ./cache/localhost.key.pem Signed-off-by: Kevin Morris --- docker/config/nginx.conf | 8 ++++---- docker/nginx-entrypoint.sh | 20 +++++++++++++++++--- docker/scripts/run-fastapi.sh | 20 ++++++++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index 3a8de801..4288a57d 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -43,8 +43,8 @@ http { listen 8443 ssl http2; server_name localhost default_server; - ssl_certificate /etc/ssl/certs/localhost.cert.pem; - ssl_certificate_key /etc/ssl/private/localhost.key.pem; + ssl_certificate /etc/ssl/certs/web.cert.pem; + ssl_certificate_key /etc/ssl/private/web.key.pem; root /aurweb/web/html; index index.php; @@ -91,8 +91,8 @@ http { listen 8444 ssl http2; server_name localhost default_server; - ssl_certificate /etc/ssl/certs/localhost.cert.pem; - ssl_certificate_key /etc/ssl/private/localhost.key.pem; + ssl_certificate /etc/ssl/certs/web.cert.pem; + ssl_certificate_key /etc/ssl/private/web.key.pem; root /aurweb/web/html; diff --git a/docker/nginx-entrypoint.sh b/docker/nginx-entrypoint.sh index 226ded8f..63307948 100755 --- a/docker/nginx-entrypoint.sh +++ b/docker/nginx-entrypoint.sh @@ -1,6 +1,16 @@ #!/bin/bash set -eou pipefail +# If production.{cert,key}.pem exists, prefer them. This allows +# user customization of the certificates that FastAPI uses. +# Otherwise, fallback to localhost.{cert,key}.pem, generated by `ca`. + +CERT=/cache/production.cert.pem +KEY=/cache/production.key.pem + +DEST_CERT=/etc/ssl/certs/web.cert.pem +DEST_KEY=/etc/ssl/private/web.key.pem + # Setup a config for our mysql db. cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config @@ -12,9 +22,13 @@ sed -ri 's/^;?(password) = .+/\1 = aur/' conf/config sed -ri "s|^(aur_location) = .+|\1 = https://localhost:8444|" conf/config sed -ri 's/^(disable_http_login) = .+/\1 = 1/' conf/config -cat /cache/localhost.cert.pem /cache/ca.root.pem \ - > /etc/ssl/certs/localhost.cert.pem -cp -vf /cache/localhost.key.pem /etc/ssl/private/localhost.key.pem +if [ -f "$CERT" ]; then + cp -vf "$CERT" "$DEST_CERT" + cp -vf "$KEY" "$DEST_KEY" +else + cat /cache/localhost.cert.pem /cache/ca.root.pem > "$DEST_CERT" + cp -vf /cache/localhost.key.pem "$DEST_KEY" +fi cp -vf /docker/config/nginx.conf /etc/nginx/nginx.conf diff --git a/docker/scripts/run-fastapi.sh b/docker/scripts/run-fastapi.sh index bb1a01a7..4dcc1d96 100755 --- a/docker/scripts/run-fastapi.sh +++ b/docker/scripts/run-fastapi.sh @@ -1,17 +1,29 @@ #!/bin/bash +CERT=/cache/localhost.cert.pem +KEY=/cache/localhost.key.pem + +# If production.{cert,key}.pem exists, prefer them. This allows +# user customization of the certificates that FastAPI uses. +if [ -f /cache/production.cert.pem ]; then + CERT=/cache/production.cert.pem +fi +if [ -f /cache/production.key.pem ]; then + KEY=/cache/production.key.pem +fi + if [ "$1" == "uvicorn" ] || [ "$1" == "" ]; then exec uvicorn --reload \ - --ssl-certfile /cache/localhost.cert.pem \ - --ssl-keyfile /cache/localhost.key.pem \ + --ssl-certfile "$CERT" \ + --ssl-keyfile "$KEY" \ --log-config /docker/logging.conf \ --host "0.0.0.0" \ --port 8000 \ aurweb.asgi:app else exec hypercorn --reload \ - --certfile /cache/localhost.cert.pem \ - --keyfile /cache/localhost.key.pem \ + --certfile "$CERT" \ + --keyfile "$KEY" \ --log-config /docker/logging.conf \ -b "0.0.0.0:8000" \ aurweb.asgi:app From ef0c2d5a285f185143ec8d5c0632f53f28c230a6 Mon Sep 17 00:00:00 2001 From: Kristian Klausen Date: Sat, 2 Oct 2021 23:54:10 +0200 Subject: [PATCH 0448/1451] magic --- docker-compose.override.yml | 47 +++++++++++++++++++++++++++++++++++++ docker-compose.prod.yml | 40 +++++++++++++++++++++++++++++++ docker-compose.yml | 39 ++++-------------------------- 3 files changed, 91 insertions(+), 35 deletions(-) create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.prod.yml diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..c0bee88c --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,47 @@ +services: + ca: + volumes: + - ./cache:/cache + + git: + volumes: + - git_data:/aurweb/aur.git + - ./cache:/cache + + smartgit: + volumes: + - git_data:/aurweb/aur.git + - ./cache:/cache + - smartgit_run:/var/run/smartgit + + php-fpm: + volumes: + - ./cache:/cache + - ./aurweb:/aurweb/aurweb + - ./migrations:/aurweb/migrations + - ./test:/aurweb/test + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib + - ./templates:/aurweb/templates + + fastapi: + volumes: + - ./cache:/cache + - ./aurweb:/aurweb/aurweb + - ./migrations:/aurweb/migrations + - ./test:/aurweb/test + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib + - ./templates:/aurweb/templates + + nginx: + volumes: + - git_data:/aurweb/aur.git + - ./cache:/cache + - ./logs:/var/log/nginx + - ./web/html:/aurweb/web/html + - ./web/template:/aurweb/web/template + - ./web/lib:/aurweb/web/lib + - smartgit_run:/var/run/smartgit diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..1addecb2 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,40 @@ +version: "3.8" + +services: + ca: + volumes: + - cache:/cache + + git: + volumes: + - git_data:/aurweb/aur.git + - cache:/cache + + smartgit: + volumes: + - git_data:/aurweb/aur.git + - cache:/cache + - smartgit_run:/var/run/smartgit + + php-fpm: + volumes: + - cache:/cache + + fastapi: + volumes: + - cache:/cache + + nginx: + volumes: + - git_data:/aurweb/aur.git + - cache:/cache + - logs:/var/log/nginx + - smartgit_run:/var/run/smartgit + +volumes: + mariadb_run: {} # Share /var/run/mysqld/mysqld.sock + mariadb_data: {} # Share /var/lib/mysql + git_data: {} # Share aurweb/aur.git + smartgit_run: {} + cache: {} + logs: {} diff --git a/docker-compose.yml b/docker-compose.yml index 8e2d91d8..f19485e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,8 +27,6 @@ services: init: true entrypoint: /docker/ca-entrypoint.sh command: echo - volumes: - - ./cache:/cache memcached: build: . @@ -93,9 +91,6 @@ services: depends_on: mariadb_init: condition: service_started - volumes: - - git_data:/aurweb/aur.git - - ./cache:/cache smartgit: build: . @@ -110,10 +105,6 @@ services: depends_on: mariadb: condition: service_healthy - volumes: - - git_data:/aurweb/aur.git - - ./cache:/cache - - smartgit_run:/var/run/smartgit cgit-php: build: . @@ -165,15 +156,6 @@ services: condition: service_healthy memcached: condition: service_healthy - volumes: - - ./cache:/cache - - ./aurweb:/aurweb/aurweb - - ./migrations:/aurweb/migrations - - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - - ./templates:/aurweb/templates ports: - "19000:9000" @@ -195,15 +177,6 @@ services: condition: service_healthy redis: condition: service_healthy - volumes: - - ./cache:/cache - - ./aurweb:/aurweb/aurweb - - ./migrations:/aurweb/migrations - - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - - ./templates:/aurweb/templates ports: - "18000:8000" @@ -231,18 +204,11 @@ services: condition: service_healthy php-fpm: condition: service_healthy - volumes: - - git_data:/aurweb/aur.git - - ./cache:/cache - - ./logs:/var/log/nginx - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - - smartgit_run:/var/run/smartgit sharness: build: . image: aurweb:latest + profiles: ["dev"] init: true environment: - AUR_CONFIG=conf/config.sqlite @@ -267,6 +233,7 @@ services: pytest-mysql: build: . image: aurweb:latest + profiles: ["dev"] init: true environment: - AUR_CONFIG=conf/config @@ -292,6 +259,7 @@ services: pytest-sqlite: build: . image: aurweb:latest + profiles: ["dev"] init: true environment: - AUR_CONFIG=conf/config.sqlite @@ -313,6 +281,7 @@ services: test: build: . image: aurweb:latest + profiles: ["dev"] init: true environment: - AUR_CONFIG=conf/config From 438080827ab7e3700ad982b91d177ec6cf20dcc9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 2 Oct 2021 15:13:14 -0700 Subject: [PATCH 0449/1451] fix(Docker): add production config overrides Additionally, `up -d` will no longer run tests unless `--profile dev` is specified by the Docker user. People should now be running docker with two files: $ docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d nginx $ docker-compose -f docker-compose.yml -f docker-compose.dev.yml run test Contributed by @klausenbusk. Thanks! From a3cb81962f6ad29e28f15102cff4aaba4f4b21db Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 2 Oct 2021 16:18:18 -0700 Subject: [PATCH 0450/1451] add: added aur_request_ml setting to config.dev For the dev environment, we use a no-op address. We don't want to be spamming aur-requests with development notifications. Signed-off-by: Kevin Morris --- conf/config.dev | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/config.dev b/conf/config.dev index 94a9630b..9f837171 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -35,6 +35,7 @@ cache = none memcache_servers = memcached:11211 ; If cache = 'redis' this address is used to connect to Redis. redis_address = redis://127.0.0.1 +aur_request_ml = aur-requests@example-noop.org [notifications] ; For development/testing, use /usr/bin/sendmail From 4abbf9a917a381141d7adce142df5f158325354c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 2 Oct 2021 16:34:34 -0700 Subject: [PATCH 0451/1451] fix: use @localhost for dev email addresses Signed-off-by: Kevin Morris --- conf/config.dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/config.dev b/conf/config.dev index 9f837171..ec0b33dc 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -35,7 +35,7 @@ cache = none memcache_servers = memcached:11211 ; If cache = 'redis' this address is used to connect to Redis. redis_address = redis://127.0.0.1 -aur_request_ml = aur-requests@example-noop.org +aur_request_ml = aur-requests@localhost [notifications] ; For development/testing, use /usr/bin/sendmail From f849e8b696416933282185df2d7581890e748f5a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 27 Sep 2021 13:49:33 -0700 Subject: [PATCH 0452/1451] change(FastAPI): allow User.notified to accept a Package OR PackageBase In addition, shorten the `package_notifications` relationship to `notifications`. Signed-off-by: Kevin Morris --- aurweb/auth.py | 2 +- aurweb/models/package_notification.py | 4 ++-- aurweb/models/user.py | 22 +++++++++++++++++++--- aurweb/routers/packages.py | 4 +--- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 2e6674b0..19c3a276 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -40,7 +40,7 @@ class AnonymousUser: ssh_pub_key = None # Add stubbed relationship backrefs. - package_notifications = StubQuery() + notifications = StubQuery() package_votes = StubQuery() # A nonce attribute, needed for all browser sessions; set in __init__. diff --git a/aurweb/models/package_notification.py b/aurweb/models/package_notification.py index ab23a212..803c0496 100644 --- a/aurweb/models/package_notification.py +++ b/aurweb/models/package_notification.py @@ -15,7 +15,7 @@ class PackageNotification(Base): Integer, ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False) User = relationship( - "User", backref=backref("package_notifications", lazy="dynamic"), + "User", backref=backref("notifications", lazy="dynamic"), foreign_keys=[UserID]) PackageBaseID = Column( @@ -23,7 +23,7 @@ class PackageNotification(Base): nullable=False) PackageBase = relationship( "PackageBase", - backref=backref("package_notifications", lazy="dynamic"), + backref=backref("notifications", lazy="dynamic"), foreign_keys=[PackageBaseID]) __mapper_args__ = {"primary_key": [UserID, PackageBaseID]} diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 28aa613e..5f848304 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -191,10 +191,26 @@ class User(Base): ).scalar()) def notified(self, package) -> bool: - """ Is this User being notified about package? """ + """ Is this User being notified about package (or package base)? + + :param package: Package or PackageBase instance + :return: Boolean indicating state of package notification + in relation to this User + """ + from aurweb.models.package import Package + from aurweb.models.package_base import PackageBase from aurweb.models.package_notification import PackageNotification - return bool(package.PackageBase.package_notifications.filter( - PackageNotification.UserID == self.ID + + query = None + if isinstance(package, Package): + query = package.PackageBase.notifications + elif isinstance(package, PackageBase): + query = package.notifications + + # Run an exists() query where a pkgbase-related + # PackageNotification exists for self (a user). + return bool(db.query( + query.filter(PackageNotification.UserID == self.ID).exists() ).scalar()) def packages(self): diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 72cd8c99..aa20e5fa 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -127,9 +127,7 @@ async def make_single_context(request: Request, context["comments"] = pkgbase.comments context["is_maintainer"] = (request.user.is_authenticated() and request.user.ID == pkgbase.MaintainerUID) - context["notified"] = request.user.package_notifications.filter( - PackageNotification.PackageBaseID == pkgbase.ID - ).scalar() + context["notified"] = request.user.notified(pkgbase) context["out_of_date"] = bool(pkgbase.OutOfDateTS) From 5e95cfbc8a844c11682db57186ac01d9732e631d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 27 Sep 2021 14:58:07 -0700 Subject: [PATCH 0453/1451] fix(FastAPI): get_pkgbase -> get_pkg_or_base `get_pkgbase` has been replaced with `get_pkg_or_base`, which is quite similar, but it does take a new `cls` keyword argument which is to be the model class which we search for via its `Name` column. Additionally, this change fixes a bug in the `/packages/{name}` route by supplying the Package object in question to the context and modifying the template to use it instead of a hacky through-base workaround. Examples: pkgbase = get_pkg_or_base("some_pkgbase_name", PackageBase) pkg = get_pkg_or_base("some_package_name", Package) Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 22 +++++++++++----------- aurweb/routers/packages.py | 11 +++++++---- templates/packages/show.html | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 18ac7a5a..696c158f 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -1,6 +1,6 @@ from collections import defaultdict from http import HTTPStatus -from typing import Dict, List +from typing import Dict, List, Union import orjson @@ -101,24 +101,24 @@ def provides_list(package: Package, depname: str) -> list: return string -def get_pkgbase(name: str) -> PackageBase: +def get_pkg_or_base(name: str, cls: Union[Package, PackageBase] = PackageBase): """ Get a PackageBase instance by its name or raise a 404 if - it can't be foudn in the database. + it can't be found in the database. - :param name: PackageBase.Name - :raises HTTPException: With status code 404 if PackageBase doesn't exist - :return: PackageBase instance + :param name: {Package,PackageBase}.Name + :raises HTTPException: With status code 404 if record doesn't exist + :return: {Package,PackageBase} instance """ - pkgbase = db.query(PackageBase).filter(PackageBase.Name == name).first() - if not pkgbase: - raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) - provider = db.query(OfficialProvider).filter( OfficialProvider.Name == name).first() if provider: raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) - return pkgbase + instance = db.query(cls).filter(cls.Name == name).first() + if cls == PackageBase and not instance: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + + return instance @register_filter("out_of_date") diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index aa20e5fa..8fd7717b 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -23,7 +23,7 @@ from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID from aurweb.packages.search import PackageSearch -from aurweb.packages.util import get_pkgbase, query_notified, query_voted +from aurweb.packages.util import get_pkg_or_base, query_notified, query_voted from aurweb.templates import make_context, render_template router = APIRouter() @@ -143,11 +143,14 @@ async def make_single_context(request: Request, @router.get("/packages/{name}") async def package(request: Request, name: str) -> Response: - # Get the PackageBase. - pkgbase = get_pkgbase(name) + # Get the Package. + pkg = get_pkg_or_base(name, Package) + pkgbase = (get_pkg_or_base(name, PackageBase) + if not pkg else pkg.PackageBase) # Add our base information. context = await make_single_context(request, pkgbase) + context["package"] = pkg # Package sources. context["sources"] = db.query(PackageSource).join(Package).join( @@ -181,7 +184,7 @@ async def package(request: Request, name: str) -> Response: @router.get("/pkgbase/{name}") async def package_base(request: Request, name: str) -> Response: # Get the PackageBase. - pkgbase = get_pkgbase(name) + pkgbase = get_pkg_or_base(name, PackageBase) # If this is not a split package, redirect to /packages/{name}. if pkgbase.packages.count() == 1: diff --git a/templates/packages/show.html b/templates/packages/show.html index 7480f573..0bf8d9fd 100644 --- a/templates/packages/show.html +++ b/templates/packages/show.html @@ -3,7 +3,7 @@ {% block pageContent %} {% include "partials/packages/search.html" %}
    -

    {{ 'Package Details' | tr }}: {{ pkgbase.Name }} {{ pkgbase.packages.first().Version }}

    +

    {{ 'Package Details' | tr }}: {{ package.Name }} {{ package.Version }}

    {% set result = pkgbase %} {% include "partials/packages/actions.html" %} From 7961fa932a92b3743411a3b5307e5ee6636d6904 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 27 Sep 2021 15:01:45 -0700 Subject: [PATCH 0454/1451] feat(FastAPI): add templates.render_raw_template This function is now used as `render_template`'s underlying implementation of rendering a template, and uses that render in its HTMLResponse path. This separation allows users to directly render a template without producing a Response object. Signed-off-by: Kevin Morris --- aurweb/templates.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/aurweb/templates.py b/aurweb/templates.py index 09be049c..ef020bdf 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -102,12 +102,8 @@ async def make_variable_context(request: Request, title: str, next: str = None): return context -def render_template(request: Request, - path: str, - context: dict, - status_code: HTTPStatus = HTTPStatus.OK): +def render_raw_template(request: Request, path: str, context: dict): """ Render a Jinja2 multi-lingual template with some context. """ - # Create a deep copy of our jinja2 _environment. The _environment in # total by itself is 48 bytes large (according to sys.getsizeof). # This is done so we can install gettext translations on the template @@ -119,8 +115,15 @@ def render_template(request: Request, templates.install_gettext_translations(translator) template = templates.get_template(path) - rendered = template.render(context) + return template.render(context) + +def render_template(request: Request, + path: str, + context: dict, + status_code: HTTPStatus = HTTPStatus.OK): + """ Render a template as an HTMLResponse. """ + rendered = render_raw_template(request, path, context) response = HTMLResponse(rendered, status_code=status_code) secure_cookies = aurweb.config.getboolean("options", "disable_http_login") response.set_cookie("AURLANG", context.get("language"), From 0d8216e8eabc96faa48d855a596597a510145cd7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 1 Oct 2021 17:50:23 -0700 Subject: [PATCH 0455/1451] change(FastAPI): decouple rendercomment logic from main This commit decouples most of the rendercomment.py logic into a function, `update_comment_render`, which can be used by other Python modules to perform comment rendering. In addition, we silence some deprecation warnings from python-markdown by removing `md_globals` parameters from python-markdown callbacks. Signed-off-by: Kevin Morris --- aurweb/scripts/rendercomment.py | 43 +++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index f6dfd058..dd5da4f9 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import logging import sys import bleach @@ -9,6 +10,7 @@ import pygit2 import aurweb.config import aurweb.db +logger = logging.getLogger(__name__) repo_path = aurweb.config.get('serve', 'repo-path') commit_uri = aurweb.config.get('options', 'commit_uri') @@ -24,7 +26,7 @@ class LinkifyExtension(markdown.extensions.Extension): _urlre = (r'(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' r'(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))') - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md): processor = markdown.inlinepatterns.AutolinkInlineProcessor(self._urlre, md) # Register it right after the default <>-link processor (priority 120). md.inlinePatterns.register(processor, 'linkify', 119) @@ -46,7 +48,7 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): class FlysprayLinksExtension(markdown.extensions.Extension): - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md): processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b', md) md.inlinePatterns.register(processor, 'flyspray-links', 118) @@ -90,9 +92,12 @@ class GitCommitsExtension(markdown.extensions.Extension): self._head = head super(markdown.extensions.Extension, self).__init__() - def extendMarkdown(self, md, md_globals): - processor = GitCommitsInlineProcessor(md, self._head) - md.inlinePatterns.register(processor, 'git-commits', 117) + def extendMarkdown(self, md): + try: + processor = GitCommitsInlineProcessor(md, self._head) + md.inlinePatterns.register(processor, 'git-commits', 117) + except pygit2.GitError: + logger.error(f"No git repository found for '{self._head}'.") class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): @@ -105,7 +110,7 @@ class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): class HeadingExtension(markdown.extensions.Extension): - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md): # Priority doesn't matter since we don't conflict with other processors. md.treeprocessors.register(HeadingTreeprocessor(md), 'heading', 30) @@ -123,19 +128,20 @@ def save_rendered_comment(conn, commentid, html): [html, commentid]) -def main(): - commentid = int(sys.argv[1]) - +def update_comment_render(commentid): conn = aurweb.db.Connection() text, pkgbase = get_comment(conn, commentid) - html = markdown.markdown(text, extensions=['fenced_code', - LinkifyExtension(), - FlysprayLinksExtension(), - GitCommitsExtension(pkgbase), - HeadingExtension()]) - allowed_tags = (bleach.sanitizer.ALLOWED_TAGS + - ['p', 'pre', 'h4', 'h5', 'h6', 'br', 'hr']) + html = markdown.markdown(text, extensions=[ + 'fenced_code', + LinkifyExtension(), + FlysprayLinksExtension(), + GitCommitsExtension(pkgbase), + HeadingExtension() + ]) + + allowed_tags = (bleach.sanitizer.ALLOWED_TAGS + + ['p', 'pre', 'h4', 'h5', 'h6', 'br', 'hr']) html = bleach.clean(html, tags=allowed_tags) save_rendered_comment(conn, commentid, html) @@ -143,5 +149,10 @@ def main(): conn.close() +def main(): + commentid = int(sys.argv[1]) + update_comment_render(commentid) + + if __name__ == '__main__': main() From fc28aad245a0350ec3190fc484543bae4612883d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 27 Sep 2021 18:46:20 -0700 Subject: [PATCH 0456/1451] feat(FastAPI): add pkgbase comments (new, edit) In PHP, this was implemented using an /rpc type 'get-comment-form'. With FastAPI, we've decided to reorganize this into a non-RPC route: `/pkgbase/{name}/comments/{id}/form`, rendered via the new `templates/partials/packages/comment_form.html` template. When the comment_form.html template is provided a `comment` object, it will produce an edit comment form. Otherwise, it will produce a new comment form. A few new FastAPI routes have been introduced: - GET `/pkgbase/{name}/comments/{id}/form` - Produces a JSON response based on {"form": ""}. - POST `/pkgbase/{name}/comments' - Creates a new comment. - POST `/pkgbase/{name}/comments/{id}` - Edits an existing comment. In addition, some Javascript has been modified for our new routes. Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 8 ++ aurweb/routers/packages.py | 105 +++++++++++++- templates/partials/packages/comment.html | 50 ++++--- templates/partials/packages/comment_form.html | 46 ++++++ templates/partials/packages/comments.html | 70 +++------ test/test_packages_routes.py | 133 ++++++++++++++++++ 6 files changed, 333 insertions(+), 79 deletions(-) create mode 100644 templates/partials/packages/comment_form.html diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 696c158f..55149127 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -11,6 +11,7 @@ from aurweb import db from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comment import PackageComment from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation @@ -121,6 +122,13 @@ def get_pkg_or_base(name: str, cls: Union[Package, PackageBase] = PackageBase): return instance +def get_pkgbase_comment(pkgbase: PackageBase, id: int) -> PackageComment: + comment = pkgbase.comments.filter(PackageComment.ID == id).first() + if not comment: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + return comment + + @register_filter("out_of_date") def out_of_date(packages: orm.Query) -> orm.Query: return packages.filter(PackageBase.OutOfDateTS.isnot(None)) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 8fd7717b..d5c99e8d 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -1,8 +1,9 @@ +from datetime import datetime from http import HTTPStatus from typing import Any, Dict -from fastapi import APIRouter, Request, Response -from fastapi.responses import RedirectResponse +from fastapi import APIRouter, Form, HTTPException, Request, Response +from fastapi.responses import JSONResponse, RedirectResponse from sqlalchemy import and_ import aurweb.filters @@ -11,9 +12,11 @@ import aurweb.models.package_keyword import aurweb.packages.util from aurweb import db +from aurweb.auth import auth_required from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comment import PackageComment from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_license import PackageLicense from aurweb.models.package_notification import PackageNotification @@ -23,8 +26,9 @@ from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID from aurweb.packages.search import PackageSearch -from aurweb.packages.util import get_pkg_or_base, query_notified, query_voted -from aurweb.templates import make_context, render_template +from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted +from aurweb.scripts.rendercomment import update_comment_render +from aurweb.templates import make_context, render_raw_template, render_template router = APIRouter() @@ -124,7 +128,9 @@ async def make_single_context(request: Request, context["pkgbase"] = pkgbase context["packages_count"] = pkgbase.packages.count() context["keywords"] = pkgbase.keywords - context["comments"] = pkgbase.comments + context["comments"] = pkgbase.comments.order_by( + PackageComment.CommentTS.desc() + ) context["is_maintainer"] = (request.user.is_authenticated() and request.user.ID == pkgbase.MaintainerUID) context["notified"] = request.user.notified(pkgbase) @@ -201,7 +207,94 @@ async def package_base(request: Request, name: str) -> Response: @router.get("/pkgbase/{name}/voters") async def package_base_voters(request: Request, name: str) -> Response: # Get the PackageBase. - pkgbase = get_pkgbase(name) + pkgbase = get_pkg_or_base(name, PackageBase) context = make_context(request, "Voters") context["pkgbase"] = pkgbase return render_template(request, "pkgbase/voters.html", context) + + +@router.post("/pkgbase/{name}/comments") +@auth_required(True) +async def pkgbase_comments_post( + request: Request, name: str, + comment: str = Form(default=str()), + enable_notifications: bool = Form(default=False)): + """ Add a new comment. """ + pkgbase = get_pkg_or_base(name, PackageBase) + + if not comment: + raise HTTPException(status_code=int(HTTPStatus.EXPECTATION_FAILED)) + + # If the provided comment is different than the record's version, + # update the db record. + now = int(datetime.utcnow().timestamp()) + with db.begin(): + comment = db.create(PackageComment, User=request.user, + PackageBase=pkgbase, + Comments=comment, RenderedComment=str(), + CommentTS=now) + + if enable_notifications and not request.user.notified(pkgbase): + db.create(PackageNotification, + User=request.user, + PackageBase=pkgbase) + update_comment_render(comment.ID) + + # Redirect to the pkgbase page. + return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}", + status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.get("/pkgbase/{name}/comments/{id}/form") +@auth_required(True) +async def pkgbase_comment_form(request: Request, name: str, id: int): + """ Produce a comment form for comment {id}. """ + pkgbase = get_pkg_or_base(name, PackageBase) + comment = pkgbase.comments.filter(PackageComment.ID == id).first() + if not comment: + return JSONResponse({}, status_code=int(HTTPStatus.NOT_FOUND)) + + if not request.user.is_elevated() and request.user != comment.User: + return JSONResponse({}, status_code=int(HTTPStatus.UNAUTHORIZED)) + + context = await make_single_context(request, pkgbase) + context["comment"] = comment + + form = render_raw_template( + request, "partials/packages/comment_form.html", context) + return JSONResponse({"form": form}) + + +@router.post("/pkgbase/{name}/comments/{id}") +@auth_required(True) +async def pkgbase_comment_post( + request: Request, name: str, id: int, + comment: str = Form(default=str()), + enable_notifications: bool = Form(default=False)): + pkgbase = get_pkg_or_base(name, PackageBase) + db_comment = get_pkgbase_comment(pkgbase, id) + + if not comment: + raise HTTPException(status_code=int(HTTPStatus.EXPECTATION_FAILED)) + + # If the provided comment is different than the record's version, + # update the db record. + now = int(datetime.utcnow().timestamp()) + if db_comment.Comments != comment: + with db.begin(): + db_comment.Comments = comment + db_comment.Editor = request.user + db_comment.EditedTS = now + + db_notif = request.user.notifications.filter( + PackageNotification.PackageBaseID == pkgbase.ID + ).first() + if enable_notifications and not db_notif: + db.create(PackageNotification, + User=request.user, + PackageBase=pkgbase) + update_comment_render(db_comment.ID) + + # Redirect to the pkgbase page anchored to the updated comment. + return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{db_comment.ID}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index 6cf5f319..36696215 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -16,26 +16,38 @@ ) | safe }} - {% if is_maintainer %} -
    -
    - - - - -
    -
    - Edit comment + {% if comment.Editor %} + {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} + + ({{ "edited on %s by %s" | tr + | format(edited_on.strftime('%Y-%m-%d %H:%M'), + '%s' | format( + comment.Editor.Username, comment.Editor.Username)) + | safe + }}) + + {% endif %} + {% if request.user.is_elevated() or pkgbase.Maintainer == request.user %} +
    +
    + + + + +
    +
    + Edit comment + +
    +
    + + + + + +
    +
    {% endif %} -
    -
    - - - - - -
    -

    diff --git a/templates/partials/packages/comment_form.html b/templates/partials/packages/comment_form.html new file mode 100644 index 00000000..c1c25f87 --- /dev/null +++ b/templates/partials/packages/comment_form.html @@ -0,0 +1,46 @@ +{# `action` is assigned the proper route to use for the form action. +When `comment` is provided (PackageComment), we display an edit form +for the comment. Otherwise, we display a new form. + +Routes: + new comment - /pkgbase/{name}/comments + edit comment - /pkgbase/{name}/comments/{id} +#} +{% set action = "/pkgbase/%s/comments" | format(pkgbase.Name) %} +{% if comment %} + {% set action = "/pkgbase/%s/comments/%d" | format(pkgbase.Name, comment.ID) %} +{% endif %} + +
    +
    +

    + {{ "Git commit identifiers referencing commits in the AUR package " + "repository and URLs are converted to links automatically." | tr }} + {{ "%sMarkdown syntax%s is partially supported." | tr + | format('', + "") + | safe }} +

    +

    + +

    +

    + + {% if comment and not request.user.notified(pkgbase) %} + + + + + {% endif %} +

    +
    +
    diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index 39cfb363..7c8a32e5 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -8,44 +8,7 @@ {% if request.user.is_authenticated() %}

    Add Comment

    -
    -
    -
    - - -
    -

    - {{ - "Git commit identifiers referencing commits in the AUR package" - " repository and URLs are converted to links automatically." - | tr - }} - {{ - "%sMarkdown Syntax%s is partially supported." - | tr - | format('', '') - | safe - }} -

    -

    - -

    -

    - - {% if not notifications_enabled %} - - - - - {% endif %} -

    -
    -
    + {% include "partials/packages/comment_form.html" %}
    {% endif %} @@ -99,29 +62,28 @@ function handleEditCommentClick(event) { // The div class="article-content" which contains the comment const edit_form = parent_element.nextElementSibling; - const params = new URLSearchParams({ - type: "get-comment-form", - arg: comment_id, - base_id: {{ pkgbase.ID }}, - pkgbase_name: {{ pkgbase.Name }} - }); - - const url = '/rpc?' + params.toString(); + const url = "/pkgbase/{{ pkgbase.Name }}/comments/" + comment_id + "/form"; add_busy_indicator(event.target); fetch(url, { - method: 'GET' + method: 'GET', + credentials: 'same-origin' + }) + .then(function(response) { + if (!response.ok) { + throw Error(response.statusText); + } + return response.json(); }) - .then(function(response) { return response.json(); }) .then(function(data) { remove_busy_indicator(event.target); - if (data.success) { - edit_form.innerHTML = data.form; - edit_form.querySelector('textarea').focus(); - } else { - alert(data.error); - } + edit_form.innerHTML = data.form; + edit_form.querySelector('textarea').focus(); + }) + .catch(function(error) { + remove_busy_indicator(event.target); + console.error(error); }); } diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 2190dc18..1bfa5fc0 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -73,6 +73,7 @@ def setup(): PackageVote.__tablename__, PackageNotification.__tablename__, PackageComaintainer.__tablename__, + PackageComment.__tablename__, OfficialProvider.__tablename__ ) @@ -930,3 +931,135 @@ def test_pkgbase_voters(client: TestClient, maintainer: User, package: Package): root = parse_root(resp.text) rows = root.xpath('//div[@class="box"]//ul/li') assert len(rows) == 1 + + +def test_pkgbase_comment_not_found(client: TestClient, maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + comment_id = 12345 # A non-existing comment. + endpoint = f"/pkgbase/{package.PackageBase.Name}/comments/{comment_id}" + with client as request: + resp = request.post(endpoint, data={ + "comment": "Failure" + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_comment_form_unauthorized(client: TestClient, user: User, + maintainer: User, package: Package): + now = int(datetime.utcnow().timestamp()) + with db.begin(): + comment = db.create(PackageComment, PackageBase=package.PackageBase, + User=maintainer, Comments="Test", + RenderedComment=str(), CommentTS=now) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/form" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +def test_pkgbase_comment_form_not_found(client: TestClient, maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + comment_id = 12345 # A non-existing comment. + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/form" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_comments_missing_comment(client: TestClient, maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{package.PackageBase.Name}/comments" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.EXPECTATION_FAILED) + + +def test_pkgbase_comments(client: TestClient, maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments" + with client as request: + resp = request.post(endpoint, data={ + "comment": "Test comment.", + "enable_notifications": True + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + expected_prefix = f"/pkgbase/{pkgbasename}" + prefix_len = len(expected_prefix) + assert resp.headers.get("location")[:prefix_len] == expected_prefix + + with client as request: + resp = request.get(resp.headers.get("location")) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + headers = root.xpath('//h4[@class="comment-header"]') + bodies = root.xpath('//div[@class="article-content"]/div/p') + + assert len(headers) == 1 + assert len(bodies) == 1 + + assert bodies[0].text.strip() == "Test comment." + + # Clear up the PackageNotification. This doubles as testing + # that the notification was created and clears it up so we can + # test enabling it during edit. + pkgbase = package.PackageBase + db_notif = pkgbase.notifications.filter( + PackageNotification.UserID == maintainer.ID + ).first() + with db.begin(): + db.session.delete(db_notif) + + # Now, let's edit the comment we just created. + comment_id = int(headers[0].attrib["id"].split("-")[-1]) + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}" + with client as request: + resp = request.post(endpoint, data={ + "comment": "Edited comment.", + "enable_notifications": True + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + with client as request: + resp = request.get(resp.headers.get("location")) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + headers = root.xpath('//h4[@class="comment-header"]') + bodies = root.xpath('//div[@class="article-content"]/div/p') + + assert len(headers) == 1 + assert len(bodies) == 1 + + assert bodies[0].text.strip() == "Edited comment." + + # Ensure that a notification was created. + db_notif = pkgbase.notifications.filter( + PackageNotification.UserID == maintainer.ID + ).first() + assert db_notif is not None + + # Don't supply a comment; should return EXPECTATION_FAILED. + with client as request: + fail_resp = request.post(endpoint, cookies=cookies) + assert fail_resp.status_code == int(HTTPStatus.EXPECTATION_FAILED) + + # Now, test the form route, which should return form markup + # via JSON. + endpoint = f"{endpoint}/form" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + data = resp.json() + assert "form" in data From 59d04d6e0c50d3b0443dae29715de63f197be890 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 30 Sep 2021 13:53:31 -0700 Subject: [PATCH 0457/1451] fix(FastAPI): comment.html template rendering Deleters and edits were not previously taken into account. This fix addresses that issue using User.has_credential. Signed-off-by: Kevin Morris --- templates/partials/packages/comment.html | 148 ++++++++++++++--------- 1 file changed, 89 insertions(+), 59 deletions(-) diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index 36696215..6af5cd9e 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -1,60 +1,90 @@ -

    - {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} - {% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %} - {{ - "%s commented on %s" | tr | format( - ('%s' | format( - comment.User.Username, - view_account_info, - comment.User.Username - )) if request.user.is_authenticated() else - (comment.User.Username), - '%s' | format( - comment.ID, - commented_at.strftime("%Y-%m-%d %H:%M") - ) - ) - | safe - }} - {% if comment.Editor %} - {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} - - ({{ "edited on %s by %s" | tr - | format(edited_on.strftime('%Y-%m-%d %H:%M'), - '%s' | format( - comment.Editor.Username, comment.Editor.Username)) - | safe - }}) - - {% endif %} - {% if request.user.is_elevated() or pkgbase.Maintainer == request.user %} -
    -
    - - - - -
    -
    - Edit comment +{% set header_cls = "comment-header" %} +{% set article_cls = "article-content" %} +{% if comment.Deleter %} + {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} + {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} +{% endif %} -
    -
    - - - - - -
    -
    - {% endif %} -

    -
    -
    - {% if comment.RenderedComment %} - {{ comment.RenderedComment | safe }} - {% else %} - {{ comment.Comments }} - {% endif %} -
    -
    +{% if not comment.Deleter or request.user.has_credential("CRED_COMMENT_VIEW_DELETED", approved=[comment.Deleter]) %} +

    + {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} + {% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %} + {{ + "%s commented on %s" | tr | format( + ('%s' | format( + comment.User.Username, + view_account_info, + comment.User.Username + )) if request.user.is_authenticated() else + (comment.User.Username), + '%s' | format( + comment.ID, + commented_at.strftime("%Y-%m-%d %H:%M") + ) + ) + | safe + }} + {% if comment.Editor %} + {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} + + ({{ "edited on %s by %s" | tr + | format(edited_on.strftime('%Y-%m-%d %H:%M'), + '%s' | format( + comment.Editor.Username, comment.Editor.Username)) + | safe + }}) + + {% endif %} + {% if not comment.Deleter %} + {% if request.user.has_credential('CRED_COMMENT_DELETE', approved=[comment.User]) %} +
    +
    + +
    +
    + {% endif %} + + {% if request.user.has_credential('CRED_COMMENT_EDIT', approved=[comment.User]) %} + Edit comment + {% endif %} + + {% if request.user.has_credential("CRED_COMMENT_PIN", approved=[pkgbase.Maintainer]) %} +
    +
    + + + + + +
    +
    + {% endif %} + {% else %} + {% if request.user.has_credential("CRED_COMMENT_UNDELETE", approved=[comment.User]) %} +
    +
    + +
    +
    + {% endif %} + {% endif %} +

    +
    +
    + {% if comment.RenderedComment %} + {{ comment.RenderedComment | safe }} + {% else %} + {{ comment.Comments }} + {% endif %} +
    +
    +{% endif %} From 6644c42922ad4645104bef114e1685973ea1a92d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 30 Sep 2021 13:56:14 -0700 Subject: [PATCH 0458/1451] fix(FastAPI): AnonymousUser.has_credential also takes kwargs Signed-off-by: Kevin Morris --- aurweb/auth.py | 2 +- test/test_auth.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 19c3a276..d1a9d9cb 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -66,7 +66,7 @@ class AnonymousUser: return False @staticmethod - def has_credential(credential): + def has_credential(credential, **kwargs): return False @staticmethod diff --git a/test/test_auth.py b/test/test_auth.py index ced64064..7aea17a0 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -117,3 +117,8 @@ def test_voted_for(): def test_notified(): user_ = AnonymousUser() assert not user_.notified(None) + + +def test_has_credential(): + user_ = AnonymousUser() + assert not user_.has_credential("FAKE_CREDENTIAL") From d3be30744ccfe6556f0299d08a1bf8bc63b2ae44 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 1 Oct 2021 15:44:24 -0700 Subject: [PATCH 0459/1451] add(FeatAPI): comment pytest.fixture Signed-off-by: Kevin Morris --- test/test_packages_routes.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 1bfa5fc0..93a7f524 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -125,6 +125,20 @@ def package(maintainer: User) -> Package: yield package +@pytest.fixture +def comment(user: User, package: Package) -> PackageComment: + pkgbase = package.PackageBase + now = int(datetime.utcnow().timestamp()) + with db.begin(): + comment = db.create(PackageComment, + User=user, + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment=str(), + CommentTS=now) + yield comment + + @pytest.fixture def packages(maintainer: User) -> List[Package]: """ Yield 55 packages named pkg_0 .. pkg_54. """ From 40cd1b9029cc50e41c21d69e502947996862b7b4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 30 Sep 2021 13:58:37 -0700 Subject: [PATCH 0460/1451] feat(FastAPI): add /pkgbase/{name}/comments/{id}/delete (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 25 ++++++++++++++++++++- test/test_packages_routes.py | 43 +++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index d5c99e8d..3a5ca047 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -11,7 +11,7 @@ import aurweb.models.package_comment import aurweb.models.package_keyword import aurweb.packages.util -from aurweb import db +from aurweb import db, l10n from aurweb.auth import auth_required from aurweb.models.license import License from aurweb.models.package import Package @@ -298,3 +298,26 @@ async def pkgbase_comment_post( # Redirect to the pkgbase page anchored to the updated comment. return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{db_comment.ID}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/comments/{id}/delete") +@auth_required(True) +async def pkgbase_comment_delete(request: Request, name: str, id: int): + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + authorized = request.user.has_credential("CRED_COMMENT_DELETE", + [comment.User]) + if not authorized: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + status_code=int(HTTPStatus.UNAUTHORIZED), + detail=_("You are not allowed to delete this comment.")) + + now = int(datetime.utcnow().timestamp()) + with db.begin(): + comment.Deleter = request.user + comment.DelTS = now + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 93a7f524..eb3da41a 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -995,7 +995,7 @@ def test_pkgbase_comments_missing_comment(client: TestClient, maintainer: User, assert resp.status_code == int(HTTPStatus.EXPECTATION_FAILED) -def test_pkgbase_comments(client: TestClient, maintainer: User, +def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, package: Package): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name @@ -1077,3 +1077,44 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, data = resp.json() assert "form" in data + + +def test_pkgbase_comment_delete(client: TestClient, + user: User, + package: Package, + comment: PackageComment): + # Test the unauthorized case of comment deletion. + cookies = {"AURSID": user.login(Request(), "testPassword")} + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/delete" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + expected = f"/pkgbase/{pkgbasename}" + assert resp.headers.get("location") == expected + + +def test_pkgbase_comment_delete_unauthorized(client: TestClient, + maintainer: User, + package: Package, + comment: PackageComment): + # Test the unauthorized case of comment deletion. + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/delete" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +def test_pkgbase_comment_delete_not_found(client: TestClient, + maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + comment_id = 12345 # Non-existing comment. + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/delete" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) From bb45ae7ac3f8da424c51051981bc6af91742e70e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 30 Sep 2021 19:48:25 -0700 Subject: [PATCH 0461/1451] feat(FastAPI): add /pkgbase/{name}/comments/{id}/undelete (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 22 ++++++++++++++++++++++ test/test_packages_routes.py | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 3a5ca047..92fc9361 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -321,3 +321,25 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int): return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/comments/{id}/undelete") +@auth_required(True) +async def pkgbase_comment_undelete(request: Request, name: str, id: int): + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + has_cred = request.user.has_credential("CRED_COMMENT_UNDELETE", + approved=[comment.User]) + if not has_cred: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + status_code=int(HTTPStatus.UNAUTHORIZED), + detail=_("You are not allowed to undelete this comment.")) + + with db.begin(): + comment.Deleter = None + comment.DelTS = None + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index eb3da41a..47b5ed81 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1080,6 +1080,7 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, def test_pkgbase_comment_delete(client: TestClient, + maintainer: User, user: User, package: Package, comment: PackageComment): @@ -1094,6 +1095,18 @@ def test_pkgbase_comment_delete(client: TestClient, expected = f"/pkgbase/{pkgbasename}" assert resp.headers.get("location") == expected + # Test the unauthorized case of comment undeletion. + maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/undelete" + with client as request: + resp = request.post(endpoint, cookies=maint_cookies) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + # And move on to undeleting it. + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + def test_pkgbase_comment_delete_unauthorized(client: TestClient, maintainer: User, @@ -1118,3 +1131,15 @@ def test_pkgbase_comment_delete_not_found(client: TestClient, with client as request: resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_comment_undelete_not_found(client: TestClient, + maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + comment_id = 12345 # Non-existing comment. + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/undelete" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) From 0895dd07ee621d24007a30b4f3d076d7b55c23f0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 1 Oct 2021 15:47:16 -0700 Subject: [PATCH 0462/1451] feat(FastAPI): add /pkgbase/{name}/comments/{id}/pin (post) In addition, fix up some templates to display pinned comments, and include the unpin form input for pinned comments, which is not yet implemented. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 26 ++++++++++++++ templates/packages/show.html | 4 +-- templates/partials/packages/comment.html | 42 +++++++++++++++++------ templates/partials/packages/comments.html | 16 ++++++++- test/test_packages_routes.py | 26 ++++++++++++++ 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 92fc9361..681cde4f 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -131,6 +131,10 @@ async def make_single_context(request: Request, context["comments"] = pkgbase.comments.order_by( PackageComment.CommentTS.desc() ) + context["pinned_comments"] = pkgbase.comments.filter( + PackageComment.PinnedTS != 0 + ).order_by(PackageComment.CommentTS.desc()) + context["is_maintainer"] = (request.user.is_authenticated() and request.user.ID == pkgbase.MaintainerUID) context["notified"] = request.user.notified(pkgbase) @@ -343,3 +347,25 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int): return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/comments/{id}/pin") +@auth_required(True) +async def pkgbase_comment_pin(request: Request, name: str, id: int): + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + has_cred = request.user.has_credential("CRED_COMMENT_PIN", + approved=[pkgbase.Maintainer]) + if not has_cred: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + status_code=int(HTTPStatus.UNAUTHORIZED), + detail=_("You are not allowed to pin this comment.")) + + now = int(datetime.utcnow().timestamp()) + with db.begin(): + comment.PinnedTS = now + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/packages/show.html b/templates/packages/show.html index 0bf8d9fd..ba531fc8 100644 --- a/templates/packages/show.html +++ b/templates/packages/show.html @@ -18,7 +18,5 @@ {% set pkgname = result.Name %} {% set pkgbase_id = result.ID %} - {% if comments.count() %} - {% include "partials/packages/comments.html" %} - {% endif %} + {% include "partials/packages/comments.html" %} {% endblock %} diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index 6af5cd9e..97f11723 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -49,16 +49,38 @@ Edit comment {% endif %} - {% if request.user.has_credential("CRED_COMMENT_PIN", approved=[pkgbase.Maintainer]) %} -
    -
    - - - - - -
    -
    + {% if request.user.has_credential("CRED_COMMENT_PIN", approved=[pkgbase.Maintainer]) %} + {% if comment.PinnedTS %} +
    +
    + +
    +
    + {% else %} +
    +
    + +
    +
    + {% endif %} {% endif %} {% else %} {% if request.user.has_credential("CRED_COMMENT_UNDELETE", approved=[comment.User]) %} diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index 7c8a32e5..56b5ab03 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -12,7 +12,21 @@
    {% endif %} -{% if comments %} +{% if pinned_comments.count() %} +
    +
    +

    + {{ "Pinned Comments" | tr }} + +

    +
    + {% for comment in pinned_comments.all() %} + {% include "partials/packages/comment.html" %} + {% endfor %} +
    +{% endif %} + +{% if comments.count() %}

    diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 47b5ed81..6bf4b975 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1143,3 +1143,29 @@ def test_pkgbase_comment_undelete_not_found(client: TestClient, with client as request: resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_comment_pin(client: TestClient, + maintainer: User, + package: Package, + comment: PackageComment): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + comment_id = comment.ID + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_pkgbase_comment_pin_unauthorized(client: TestClient, + user: User, + package: Package, + comment: PackageComment): + cookies = {"AURSID": user.login(Request(), "testPassword")} + comment_id = comment.ID + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) From 2efd254974fd2db0253ebc4aec725a08ba525d67 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 1 Oct 2021 17:51:39 -0700 Subject: [PATCH 0463/1451] feat(FastAPI): add /pkgbase/{name}/comments/{id}/unpin (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 21 +++++++++++++++++++++ test/test_packages_routes.py | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 681cde4f..5ae19d07 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -369,3 +369,24 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int): return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/comments/{id}/unpin") +@auth_required(True) +async def pkgbase_comment_unpin(request: Request, name: str, id: int): + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + has_cred = request.user.has_credential("CRED_COMMENT_PIN", + approved=[pkgbase.Maintainer]) + if not has_cred: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + status_code=int(HTTPStatus.UNAUTHORIZED), + detail=_("You are not allowed to unpin this comment.")) + + with db.begin(): + comment.PinnedTS = 0 + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 6bf4b975..1c7d5d3e 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1152,11 +1152,25 @@ def test_pkgbase_comment_pin(client: TestClient, cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = comment.ID pkgbasename = package.PackageBase.Name + + # Pin the comment. endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" with client as request: resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) + # Assert that PinnedTS got set. + assert comment.PinnedTS > 0 + + # Unpin the comment we just pinned. + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/unpin" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Let's assert that PinnedTS was unset. + assert comment.PinnedTS == 0 + def test_pkgbase_comment_pin_unauthorized(client: TestClient, user: User, @@ -1169,3 +1183,16 @@ def test_pkgbase_comment_pin_unauthorized(client: TestClient, with client as request: resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +def test_pkgbase_comment_unpin_unauthorized(client: TestClient, + user: User, + package: Package, + comment: PackageComment): + cookies = {"AURSID": user.login(Request(), "testPassword")} + comment_id = comment.ID + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/unpin" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) From 986fa9ee305ed113172f7f214d451a7af071ecc2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 26 Sep 2021 20:26:24 -0700 Subject: [PATCH 0464/1451] feat(PHP): add aurweb Prometheus metrics Along with this initial requests metric implementation, we also now serve the `/metrics` route, which grabs request metrics out of cache and renders them properly for Prometheus. **NOTE** Metrics are only enabled when the aurweb system admin has enabled caching by configuring `options.cache` correctly in `$AUR_CONFIG`. Otherwise, an error is logged about no cache being configured. New dependencies have been added which require the use of `composer`. See `INSTALL` for the dependency section in regards to composer dependencies and how to install them properly for aurweb. Metrics are in the following forms: aurweb_http_requests_count(method="GET",route="/some_route") aurweb_api_requests_count(method="GET",route="/rpc",type="search") This should allow us to search through the requests for specific routes and queries. Signed-off-by: Kevin Morris --- INSTALL | 8 ++- web/html/index.php | 29 ++++++++ web/html/metrics.php | 16 +++++ web/lib/metricfuncs.inc.php | 129 ++++++++++++++++++++++++++++++++++++ web/lib/routing.inc.php | 3 +- 5 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 web/html/metrics.php create mode 100644 web/lib/metricfuncs.inc.php diff --git a/INSTALL b/INSTALL index 9bcd0759..b161edd2 100644 --- a/INSTALL +++ b/INSTALL @@ -49,9 +49,15 @@ read the instructions below. # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ python-bleach python-markdown python-alembic python-jinja \ - python-itsdangerous python-authlib python-httpx hypercorn + python-itsdangerous python-authlib python-httpx hypercorn \ + composer # python3 setup.py install +4a) Install `composer` dependencies while inside of aurweb's root: + + $ cd /path/to/aurweb + /path/to/aurweb $ composer require promphp/prometheus_client_php + 5) Create a new MySQL database and a user and import the aurweb SQL schema: $ python -m aurweb.initdb diff --git a/web/html/index.php b/web/html/index.php index e57e7708..82a44c55 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -3,10 +3,39 @@ set_include_path(get_include_path() . PATH_SEPARATOR . '../lib'); include_once("aur.inc.php"); include_once("pkgfuncs.inc.php"); +include_once("cachefuncs.inc.php"); +include_once("metricfuncs.inc.php"); $path = $_SERVER['PATH_INFO']; $tokens = explode('/', $path); +$query_string = $_SERVER['QUERY_STRING']; + +// If no options.cache is configured, we no-op metric storage operations. +$is_cached = defined('EXTENSION_LOADED_APC') || defined('EXTENSION_LOADED_MEMCACHE'); +if ($is_cached) { + $method = $_SERVER['REQUEST_METHOD']; + // We'll always add +1 to our total request count to this $path, + // unless this path == /metrics. + if ($path !== "/metrics") + add_metric("http_requests_count", $method, $path); + + // Extract $type out of $query_string, if we can. + $type = null; + $query = array(); + if ($query_string) + parse_str($query_string, $query); + $type = $query['type']; + + // Only store RPC metrics for valid types. + $good_types = [ + "info", "multiinfo", "search", "msearch", + "suggest", "suggest-pkgbase", "get-comment-form" + ]; + if ($path === "/rpc" && in_array($type, $good_types)) + add_metric("api_requests_count", $method, $path, $type); +} + if (config_get_bool('options', 'enable-maintenance') && (empty($tokens[1]) || ($tokens[1] != "css" && $tokens[1] != "images"))) { if (!in_array($_SERVER['REMOTE_ADDR'], explode(" ", config_get('options', 'maintenance-exceptions')))) { header("HTTP/1.0 503 Service Unavailable"); diff --git a/web/html/metrics.php b/web/html/metrics.php new file mode 100644 index 00000000..dfa860ed --- /dev/null +++ b/web/html/metrics.php @@ -0,0 +1,16 @@ + diff --git a/web/lib/metricfuncs.inc.php b/web/lib/metricfuncs.inc.php new file mode 100644 index 00000000..acfc30d7 --- /dev/null +++ b/web/lib/metricfuncs.inc.php @@ -0,0 +1,129 @@ +, 'query_string': }. + $metrics = get_cache_value("prometheus_metrics"); + $metrics = $metrics ? json_decode($metrics) : array(); + + $key = "$path:$type"; + + // If the current request $path isn't yet in $metrics create + // a new assoc array for it and push it into $metrics. + if (!in_array($key, $metrics)) { + $data = array( + 'anchor' => $anchor, + 'method' => $method, + 'path' => $path, + 'type' => $type + ); + array_push($metrics, json_encode($data)); + } + + // Cache-wise, we also store the count values of each route + // through the "prometheus:" key. Grab the cache value + // representing the current $path we're on (defaulted to 1). + $count = get_cache_value("prometheus:$key"); + $count = $count ? $count + 1 : 1; + + $labels = ["method", "route"]; + if ($type) + array_push($labels, "type"); + + $gauge = $registry->getOrRegisterGauge( + 'aurweb', + $anchor, + 'A metric count for the aurweb platform.', + $labels + ); + + $label_values = [$data['method'], $data['path']]; + if ($type) + array_push($label_values, $type); + + $gauge->set($count, $label_values); + + // Update cache values. + set_cache_value("prometheus:$key", $count, 0); + set_cache_value("prometheus_metrics", json_encode($metrics), 0); + +} + +function render_metrics() { + if (!defined('EXTENSION_LOADED_APC') && !defined('EXTENSION_LOADED_MEMCACHE')) { + error_log("The /metrics route requires a valid 'options.cache' " + . "configuration; no cache is configured."); + return http_response_code(417); // EXPECTATION_FAILED + } + + global $registry; + + // First, we grab the set of metrics we're interested in in the + // form of a cached JSON list, if we can. + $metrics = get_cache_value("prometheus_metrics"); + if (!$metrics) + $metrics = array(); + else + $metrics = json_decode($metrics); + + // Now, we walk through each of those list values one by one, + // which happen to be JSON-serialized associative arrays, + // and process each metric via its associative array's contents: + // The route path and the query string. + // See web/html/index.php for the creation of such metrics. + foreach ($metrics as $metric) { + $data = json_decode($metric, true); + + $anchor = $data['anchor']; + $path = $data['path']; + $type = $data['type']; + $key = "$path:$type"; + + $labels = ["method", "route"]; + if ($type) + array_push($labels, "type"); + + $count = get_cache_value("prometheus:$key"); + $gauge = $registry->getOrRegisterGauge( + 'aurweb', + $anchor, + 'A metric count for the aurweb platform.', + $labels + ); + + $label_values = [$data['method'], $data['path']]; + if ($type) + array_push($label_values, $type); + + $gauge->set($count, $label_values); + } + + // Construct the results from RenderTextFormat renderer and + // registry's samples. + $renderer = new RenderTextFormat(); + $result = $renderer->render($registry->getMetricFamilySamples()); + + // Output the results with the right content type header. + http_response_code(200); // OK + header('Content-Type: ' . RenderTextFormat::MIME_TYPE); + echo $result; +} + +?> diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php index 73c667d2..0f452f22 100644 --- a/web/lib/routing.inc.php +++ b/web/lib/routing.inc.php @@ -19,7 +19,8 @@ $ROUTES = array( '/rss' => 'rss.php', '/tos' => 'tos.php', '/tu' => 'tu.php', - '/addvote' => 'addvote.php', + '/addvote' => 'addvote.php', + '/metrics' => 'metrics.php' // Prometheus Metrics ); $PKG_PATH = '/packages'; From 4d191b51f9d5b9a4d365c2b2f806c257f0a720e3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 16 Sep 2021 19:42:09 -0700 Subject: [PATCH 0465/1451] feat(FastAPI): add /pkgbase/{name}/comaintainers (get, post) Changes from PHP: - Form action now points to `/pkgbase/{name}/comaintainers`. - When an error occurs, users are sent back to `/pkgbase/{name}/comaintainers` with an error at the top of the page. (PHP used to send people to /pkgbase/, which ended up at a blank search page). Closes: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/51 Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 145 +++++++++++++++++++++++ templates/partials/packages/actions.html | 4 +- templates/pkgbase/comaintainers.html | 40 +++++++ test/test_packages_routes.py | 108 +++++++++++++++++ 4 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 templates/pkgbase/comaintainers.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 5ae19d07..385d91db 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -16,6 +16,7 @@ from aurweb.auth import auth_required from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comment import PackageComment from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_license import PackageLicense @@ -25,8 +26,10 @@ from aurweb.models.package_request import PackageRequest from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID +from aurweb.models.user import User from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted +from aurweb.scripts import notify from aurweb.scripts.rendercomment import update_comment_render from aurweb.templates import make_context, render_raw_template, render_template @@ -390,3 +393,145 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int): return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.get("/pkgbase/{name}/comaintainers") +@auth_required(True) +async def package_base_comaintainers(request: Request, name: str) -> Response: + # Get the PackageBase. + pkgbase = get_pkg_or_base(name, PackageBase) + + # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) + # get redirected to the package base's page. + has_creds = request.user.has_credential("CRED_PKGBASE_EDIT_COMAINTAINERS", + approved=[pkgbase.Maintainer]) + if not has_creds: + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + # Add our base information. + context = make_context(request, "Manage Co-maintainers") + context["pkgbase"] = pkgbase + + context["comaintainers"] = [ + c.User.Username for c in pkgbase.comaintainers + ] + + return render_template(request, "pkgbase/comaintainers.html", context) + + +def remove_users(pkgbase, usernames): + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + notifications = [] + with db.begin(): + for username in usernames: + # We know that the users we passed here are in the DB. + # No need to check for their existence. + comaintainer = pkgbase.comaintainers.join(User).filter( + User.Username == username + ).first() + notifications.append( + notify.ComaintainerRemoveNotification( + conn, comaintainer.User.ID, pkgbase.ID + ) + ) + db.session.delete(comaintainer) + + # Send out notifications if need be. + for notify_ in notifications: + notify_.send() + + +@router.post("/pkgbase/{name}/comaintainers") +@auth_required(True) +async def package_base_comaintainers_post( + request: Request, name: str, + users: str = Form(default=str())) -> Response: + # Get the PackageBase. + pkgbase = get_pkg_or_base(name, PackageBase) + + # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) + # get redirected to the package base's page. + has_creds = request.user.has_credential("CRED_PKGBASE_EDIT_COMAINTAINERS", + approved=[pkgbase.Maintainer]) + if not has_creds: + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + users = set(users.split("\n")) + users.remove(str()) # Remove any empty strings from the set. + records = {c.User.Username for c in pkgbase.comaintainers} + + remove_users(pkgbase, records.difference(users)) + + # Default priority (lowest value; most preferred). + priority = 1 + + # Get the highest priority in the comaintainer set. + last_priority = pkgbase.comaintainers.order_by( + PackageComaintainer.Priority.desc() + ).limit(1).first() + + # If that record exists, we use a priority which is 1 higher. + # TODO: This needs to ensure that it wraps around and preserves + # ordering in the case where we hit the max number allowed by + # the Priority column type. + if last_priority: + priority = last_priority.Priority + 1 + + def add_users(usernames): + """ Add users as comaintainers to pkgbase. + + :param usernames: An iterable of username strings + :return: None on success, an error string on failure. """ + nonlocal request, pkgbase, priority + + # First, perform a check against all usernames given; for each + # username, add its related User object to memo. + _ = l10n.get_translator_for_request(request) + memo = {} + for username in usernames: + user = db.query(User).filter(User.Username == username).first() + if not user: + return _("Invalid user name: %s") % username + memo[username] = user + + # Alright, now that we got past the check, add them all to the DB. + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + notifications = [] + with db.begin(): + for username in usernames: + user = memo.get(username) + if pkgbase.Maintainer == user: + # Already a maintainer. Move along. + continue + + # If we get here, our user model object is in the memo. + comaintainer = db.create( + PackageComaintainer, + PackageBase=pkgbase, + User=user, + Priority=priority) + priority += 1 + + notifications.append( + notify.ComaintainerAddNotification( + conn, comaintainer.User.ID, pkgbase.ID) + ) + + # Send out notifications. + for notify_ in notifications: + notify_.send() + + error = add_users(users.difference(records)) + if error: + context = make_context(request, "Manage Co-maintainers") + context["pkgbase"] = pkgbase + context["comaintainers"] = [ + c.User.Username for c in pkgbase.comaintainers + ] + context["errors"] = [error] + return render_template(request, "pkgbase/comaintainers.html", context) + + return RedirectResponse(f"/pkgbase/{pkgbase.Name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index d552f2dd..6c30153c 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -117,9 +117,9 @@ {% endif %} - {% if is_maintainer %} + {% if request.user.has_credential('CRED_PKGBASE_EDIT_COMAINTAINERS', approved=[pkgbase.Maintainer]) %}
  • - + {{ "Manage Co-Maintainers" | tr }}
  • diff --git a/templates/pkgbase/comaintainers.html b/templates/pkgbase/comaintainers.html new file mode 100644 index 00000000..06e8b9d7 --- /dev/null +++ b/templates/pkgbase/comaintainers.html @@ -0,0 +1,40 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% if errors %} +
      + {% for error in errors %} +
    • {{ error | tr }}
    • + {% endfor %} +
    + {% endif %} + +
    +

    {{ "Manage Co-maintainers" | tr }}:

    +

    + {{ + "Use this form to add co-maintainers for %s%s%s " + "(one user name per line):" + | tr | format("", pkgbase.Name, "") + | safe + }} +

    + +
    +
    +

    + + +

    + +

    + +

    +
    +
    + +
    +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 1c7d5d3e..f1c20067 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1196,3 +1196,111 @@ def test_pkgbase_comment_unpin_unauthorized(client: TestClient, with client as request: resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +def test_pkgbase_comaintainers_not_found(client: TestClient, maintainer: User): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + endpoint = "/pkgbase/fake/comaintainers" + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_comaintainers_post_not_found(client: TestClient, + maintainer: User): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + endpoint = "/pkgbase/fake/comaintainers" + with client as request: + resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_comaintainers_unauthorized(client: TestClient, user: User, + package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + +def test_pkgbase_comaintainers_post_unauthorized(client: TestClient, + user: User, + package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + +def test_pkgbase_comaintainers_post_invalid_user(client: TestClient, + maintainer: User, + package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "users": "\nfake\n" + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + assert error.text.strip() == "Invalid user name: fake" + + +def test_pkgbase_comaintainers(client: TestClient, user: User, + maintainer: User, package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + + # Start off by adding user as a comaintainer to package. + # The maintainer username given should be ignored. + with client as request: + resp = request.post(endpoint, data={ + "users": f"\n{user.Username}\n{maintainer.Username}\n" + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + # Do it again to exercise the last_priority bump path. + with client as request: + resp = request.post(endpoint, data={ + "users": f"\n{user.Username}\n{maintainer.Username}\n" + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + # Now that we've added a comaintainer to the pkgbase, + # let's perform a GET request to make sure that the backend produces + # the user we added in the users textarea. + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + users = root.xpath('//textarea[@id="id_users"]')[0] + assert users.text.strip() == user.Username + + # Finish off by removing all the comaintainers. + with client as request: + resp = request.post(endpoint, data={ + "users": str() + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + users = root.xpath('//textarea[@id="id_users"]')[0] + assert users is not None and users.text is None From c164abe256e2c1ee71be1ab3815b365fcb3f80de Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 1 Sep 2021 13:55:41 -0700 Subject: [PATCH 0466/1451] feat(FastAPI): add Requests navigation item Along with this, created a new test suite at test/test_html.py, which has the responsibility of testing various HTML things that are not suitable for another test suite. Signed-off-by: Kevin Morris --- templates/partials/archdev-navbar.html | 5 ++ test/test_html.py | 99 ++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 test/test_html.py diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index 13459e1a..9a3ba780 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -7,6 +7,11 @@ {% endif %}
  • {% trans %}Packages{% endtrans %}
  • {% if request.user.is_authenticated() %} +
  • + + {% trans %}Requests{% endtrans %} + +
  • {% if request.user.is_trusted_user() or request.user.is_developer() %}
  • diff --git a/test/test_html.py b/test/test_html.py new file mode 100644 index 00000000..562d6a63 --- /dev/null +++ b/test/test_html.py @@ -0,0 +1,99 @@ +""" A test suite used to test HTML renders in different cases. """ +from http import HTTPStatus + +import pytest + +from fastapi.testclient import TestClient + +from aurweb import asgi, db +from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID, AccountType +from aurweb.models.user import User +from aurweb.testing import setup_test_db +from aurweb.testing.html import parse_root +from aurweb.testing.requests import Request + + +@pytest.fixture(autouse=True) +def setup(): + setup_test_db(User.__tablename__) + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) + + +@pytest.fixture +def user() -> User: + user_type = db.query(AccountType, AccountType.ID == USER_ID).first() + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd="testPassword", AccountType=user_type) + yield user + + +@pytest.fixture +def trusted_user(user: User) -> User: + tu_type = db.query(AccountType, + AccountType.ID == TRUSTED_USER_ID).first() + with db.begin(): + user.AccountType = tu_type + yield user + + +def test_archdev_navbar(client: TestClient): + expected = [ + "AUR Home", + "Packages", + "Register", + "Login" + ] + with client as request: + resp = request.get("/") + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + items = root.xpath('//div[@id="archdev-navbar"]/ul/li/a') + for i, item in enumerate(items): + assert item.text.strip() == expected[i] + + +def test_archdev_navbar_authenticated(client: TestClient, user: User): + expected = [ + "Dashboard", + "Packages", + "Requests", + "My Account", + "Logout" + ] + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/", cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + items = root.xpath('//div[@id="archdev-navbar"]/ul/li/a') + for i, item in enumerate(items): + assert item.text.strip() == expected[i] + + +def test_archdev_navbar_authenticated_tu(client: TestClient, + trusted_user: User): + expected = [ + "Dashboard", + "Packages", + "Requests", + "Accounts", + "My Account", + "Trusted User", + "Logout" + ] + cookies = {"AURSID": trusted_user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/", cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + items = root.xpath('//div[@id="archdev-navbar"]/ul/li/a') + for i, item in enumerate(items): + assert item.text.strip() == expected[i] From 99482f9962de96de0d5b248f0ee99cdd15a6a740 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 10 Sep 2021 13:28:11 -0700 Subject: [PATCH 0467/1451] feat(FastAPI): added /requests (get) route Introduces `aurweb.defaults` and `aurweb.filters`. `aurweb.filters` is a location developers can put their additional Jinja2 filters and/or functions. We should slowly move all of our filters over here, where it makes sense. `aurweb.defaults` is a new module which hosts some default constants and utility functions, starting with offsets (O) and per page values (PP). As far as the new GET /requests is concerned, we match up here to PHP's implementation, with some minor improvements: Improvements: * PP on this page is now configurable: 50 (default), 100, or 250. * Example: `https://localhost:8444/requests?PP=250` Modifications: * The pagination is a bit different, but serves the exact same purpose. * "Last" no longer goes to an empty page. * Closes: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/14 Signed-off-by: Kevin Morris --- aurweb/auth.py | 15 +++++ aurweb/defaults.py | 18 ++++++ aurweb/filters.py | 14 ++++- aurweb/routers/packages.py | 40 ++++++++++-- aurweb/templates.py | 15 +++++ setup.cfg | 19 +++--- templates/requests.html | 115 +++++++++++++++++++++++++++++++++++ test/test_defaults.py | 14 +++++ test/test_packages_routes.py | 84 ++++++++++++++++++++++++- test/test_templates.py | 16 ++++- test/test_util.py | 8 ++- 11 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 aurweb/defaults.py create mode 100644 templates/requests.html create mode 100644 test/test_defaults.py diff --git a/aurweb/auth.py b/aurweb/auth.py index d1a9d9cb..21d31081 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -31,8 +31,23 @@ class StubQuery: class AnonymousUser: + """ A stubbed User class used when an unauthenticated User + makes a request against FastAPI. """ # Stub attributes used to mimic a real user. ID = 0 + + class AccountType: + """ A stubbed AccountType static class. In here, we use an ID + and AccountType which do not exist in our constant records. + All records primary keys (AccountType.ID) should be non-zero, + so using a zero here means that we'll never match against a + real AccountType. """ + ID = 0 + AccountType = "Anonymous" + + # AccountTypeID == AccountType.ID; assign a stubbed column. + AccountTypeID = AccountType.ID + LangPreference = aurweb.config.get("options", "default_lang") Timezone = aurweb.config.get("options", "default_timezone") diff --git a/aurweb/defaults.py b/aurweb/defaults.py new file mode 100644 index 00000000..c2568d05 --- /dev/null +++ b/aurweb/defaults.py @@ -0,0 +1,18 @@ +""" Constant default values centralized in one place. """ + +# Default [O]ffset +O = 0 + +# Default [P]er [P]age +PP = 50 + +# A whitelist of valid PP values +PP_WHITELIST = {50, 100, 250} + + +def fallback_pp(per_page: int) -> int: + """ If `per_page` is a valid value in PP_WHITELIST, return it. + Otherwise, return defaults.PP. """ + if per_page not in PP_WHITELIST: + return PP + return per_page diff --git a/aurweb/filters.py b/aurweb/filters.py index bb56c656..f9f56b5d 100644 --- a/aurweb/filters.py +++ b/aurweb/filters.py @@ -4,8 +4,8 @@ import paginate from jinja2 import pass_context -from aurweb import util -from aurweb.templates import register_filter +from aurweb import config, util +from aurweb.templates import register_filter, register_function @register_filter("pager_nav") @@ -48,3 +48,13 @@ def pager_nav(context: Dict[str, Any], symbol_previous="‹ Previous", symbol_next="Next ›", symbol_last="Last »") + + +@register_function("config_getint") +def config_getint(section: str, key: str) -> int: + return config.getint(section, key) + + +@register_function("round") +def do_round(f: float) -> int: + return round(f) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 385d91db..5751a3ee 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -2,17 +2,18 @@ from datetime import datetime from http import HTTPStatus from typing import Any, Dict -from fastapi import APIRouter, Form, HTTPException, Request, Response +from fastapi import APIRouter, Form, HTTPException, Query, Request, Response from fastapi.responses import JSONResponse, RedirectResponse -from sqlalchemy import and_ +from sqlalchemy import and_, case import aurweb.filters import aurweb.models.package_comment import aurweb.models.package_keyword import aurweb.packages.util -from aurweb import db, l10n -from aurweb.auth import auth_required +from aurweb import db, defaults, l10n +from aurweb.auth import account_type_required, auth_required +from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND_DEV from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -22,10 +23,11 @@ from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_license import PackageLicense from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation -from aurweb.models.package_request import PackageRequest +from aurweb.models.package_request import PENDING_ID, PackageRequest from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID +from aurweb.models.request_type import RequestType from aurweb.models.user import User from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted @@ -535,3 +537,31 @@ async def package_base_comaintainers_post( return RedirectResponse(f"/pkgbase/{pkgbase.Name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.get("/requests") +@account_type_required({TRUSTED_USER, DEVELOPER, TRUSTED_USER_AND_DEV}) +@auth_required(True, redirect="/") +async def requests(request: Request, + O: int = Query(default=defaults.O), + PP: int = Query(default=defaults.PP)): + context = make_context(request, "Requests") + + context["q"] = dict(request.query_params) + context["O"] = O + context["PP"] = PP + + # A PackageRequest query, with left inner joined User and RequestType. + query = db.query(PackageRequest).join( + User, PackageRequest.UsersID == User.ID + ).join(RequestType) + + context["total"] = query.count() + context["results"] = query.order_by( + # Order primarily by the Status column being PENDING_ID, + # and secondarily by RequestTS; both in descending order. + case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), + PackageRequest.RequestTS.desc() + ).limit(PP).offset(O).all() + + return render_template(request, "requests.html", context) diff --git a/aurweb/templates.py b/aurweb/templates.py index ef020bdf..2301cfe2 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -71,6 +71,20 @@ def register_filter(name: str) -> Callable: return decorator +def register_function(name: str) -> Callable: + """ A decorator that can be used to register a function. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + if name in _env.globals: + raise KeyError(f"Jinja already has a function named '{name}'") + _env.globals[name] = wrapper + return wrapper + return decorator + + def make_context(request: Request, title: str, next: str = None): """ Create a context for a jinja2 TemplateResponse. """ @@ -83,6 +97,7 @@ def make_context(request: Request, title: str, next: str = None): "timezones": time.SUPPORTED_TIMEZONES, "title": title, "now": datetime.now(tz=zoneinfo.ZoneInfo(timezone)), + "utcnow": int(datetime.utcnow().timestamp()), "config": aurweb.config, "next": next if next else request.url.path } diff --git a/setup.cfg b/setup.cfg index 4f2bdf7d..cec1bcf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,17 +6,22 @@ ignore = E741, W503 max-line-length = 127 max-complexity = 10 -# aurweb/routers/accounts.py # Ignore some unavoidable flake8 warnings; we know this is against -# pycodestyle, but some of the existing codebase uses `I` variables, +# PEP8, but some of the existing codebase uses `I` variables, # so specifically silence warnings about it in pre-defined files. +# # In E741, the 'I', 'O', 'l' are ambiguous variable names. # Our current implementation uses these variables through HTTP # and the FastAPI form specification wants them named as such. -# In C901's case, our process_account_form function is way too -# complex for PEP (too many if statements). However, we need to -# process these anyways, and making it any more complex would -# just add confusion to the implementation. +# +# With {W503,W504}, PEP8 does not want us to break lines before +# or after a binary operator. We have many scripts that already +# do this, so we're ignoring it here. +ignore = E741, W503, W504 + +# aurweb/routers/accounts.py +# Ignore over-reaching complexity. +# TODO: This should actually be addressed so we do not ignore C901. # # test/test_ssh_pub_key.py # E501 is detected due to our >127 width test constant. Ignore it. @@ -24,7 +29,7 @@ max-complexity = 10 # Anything like this should be questioned. # per-file-ignores = - aurweb/routers/accounts.py:E741,C901 + aurweb/routers/accounts.py:C901 test/test_ssh_pub_key.py:E501 aurweb/routers/packages.py:E741 diff --git a/templates/requests.html b/templates/requests.html new file mode 100644 index 00000000..a9017e2f --- /dev/null +++ b/templates/requests.html @@ -0,0 +1,115 @@ +{% extends "partials/layout.html" %} + +{% set singular = "%d package request found." %} +{% set plural = "%d package requests found." %} + +{% block pageContent %} +
    + {% if not total %} +

    {{ "No requests matched your search criteria." | tr }}

    + {% else %} + {% include "partials/widgets/pager.html" %} + + + + + + + + + + + + + {% for result in results %} + + + {# Type #} + + {# Comments #} + + + {% set idle_time = config_getint("options", "request_idle_time") %} + {% set time_delta = (utcnow - result.RequestTS) | int %} + + {% set due = result.Status == 0 and time_delta > idle_time %} + + + + {% endfor %} + +
    {{ "Package" | tr }}{{ "Type" | tr }}{{ "Comments" | tr }}{{ "Filed by" | tr }}{{ "Date" | tr }}{{ "Status" | tr }}
    + {# Package #} + + {{ result.PackageBaseName }} + + + {{ result.RequestType.name_display() }} + {# If the RequestType is a merge and request.MergeBaseName is valid... #} + {% if result.RequestType.ID == 3 and result.MergeBaseName %} + ({{ result.MergeBaseName }}) + {% endif %} + {{ result.Comments }} + {# Filed by #} + + {{ result.User.Username }} + + + {# Date #} + {% set date = result.RequestTS | dt | as_timezone(timezone) %} + {{ date.strftime("%Y-%m-%d %H:%M") }} + + {# Status #} + {% if result.Status == 0 %} + {% set temp_q = { "next": "/requests" } %} + + {% if result.RequestType.ID == 1 %} + {% set action = "delete" %} + {% elif result.RequestType.ID == 2 %} + {% set action = "disown" %} + {% elif result.RequestType.ID == 3 %} + {% set action = "merge" %} + {# Add the 'via' url query parameter. #} + {% set temp_q = temp_q | extend_query( + ["via", result.ID], + ["into", result.MergeBaseName] + ) %} + {% endif %} + + {% if request.user.is_elevated() %} + {% if result.RequestType.ID == 2 and not due %} + {% set time_left = idle_time - time_delta %} + {% if time_left > 48 * 3600 %} + {% set n = round(time_left / (24 * 3600)) %} + {% set time_left_fmt = (n | tn("~%d day left", "~%d days left") | format(n)) %} + {% elif time_left > 3600 %} + {% set n = round(time_left / 3600) %} + {% set time_left_fmt = (n | tn("~%d hour left", "~%d hours left") | format(n)) %} + {% else %} + {% set time_left_fmt = ("<1 hour left" | tr) %} + {% endif %} + {{ "Locked" | tr }} + ({{ time_left_fmt }}) + {% else %} + {# Only elevated users (TU or Dev) are allowed to accept requests. #} + + {{ "Accept" | tr }} + + {% endif %} +
    + {% endif %} + + {{ "Close" | tr }} + + {% else %} + {{ result.status_display() }} + {% endif %} +
    + {% include "partials/widgets/pager.html" %} + {% endif %} +
    +{% endblock %} diff --git a/test/test_defaults.py b/test/test_defaults.py new file mode 100644 index 00000000..4803fb5a --- /dev/null +++ b/test/test_defaults.py @@ -0,0 +1,14 @@ +from aurweb import defaults + + +def test_fallback_pp(): + assert defaults.fallback_pp(75) == defaults.PP + assert defaults.fallback_pp(100) == 100 + + +def test_pp(): + assert defaults.PP == 50 + + +def test_o(): + assert defaults.O == 0 diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index f1c20067..a25fcb7e 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -8,7 +8,7 @@ import pytest from fastapi.testclient import TestClient -from aurweb import asgi, db +from aurweb import asgi, db, defaults from aurweb.models.account_type import USER_ID, AccountType from aurweb.models.dependency_type import DependencyType from aurweb.models.official_provider import OfficialProvider @@ -74,6 +74,7 @@ def setup(): PackageNotification.__tablename__, PackageComaintainer.__tablename__, PackageComment.__tablename__, + PackageRequest.__tablename__, OfficialProvider.__tablename__ ) @@ -108,6 +109,18 @@ def maintainer() -> User: yield maintainer +@pytest.fixture +def tu_user(): + tu_type = db.query(AccountType, + AccountType.AccountType == "Trusted User").first() + with db.begin(): + tu_user = db.create(User, Username="test_tu", + Email="test_tu@example.org", + RealName="Test TU", Passwd="testPassword", + AccountType=tu_type) + yield tu_user + + @pytest.fixture def package(maintainer: User) -> Package: """ Yield a Package created by user. """ @@ -160,6 +173,25 @@ def packages(maintainer: User) -> List[Package]: yield packages_ +@pytest.fixture +def requests(user: User, packages: List[Package]) -> List[PackageRequest]: + pkgreqs = [] + deletion_type = db.query(RequestType).filter( + RequestType.ID == DELETION_ID + ).first() + with db.begin(): + for i in range(55): + pkgreq = db.create(PackageRequest, + RequestType=deletion_type, + User=user, + PackageBase=packages[i].PackageBase, + PackageBaseName=packages[i].Name, + Comments=f"Deletion request for pkg_{i}", + ClosureComment=str()) + pkgreqs.append(pkgreq) + yield pkgreqs + + def test_package_not_found(client: TestClient): with client as request: resp = request.get("/packages/not_found") @@ -1304,3 +1336,53 @@ def test_pkgbase_comaintainers(client: TestClient, user: User, root = parse_root(resp.text) users = root.xpath('//textarea[@id="id_users"]')[0] assert users is not None and users.text is None + + +def test_requests_unauthorized(client: TestClient, + maintainer: User, + tu_user: User, + packages: List[Package], + requests: List[PackageRequest]): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + resp = request.get("/requests", cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_requests(client: TestClient, + maintainer: User, + tu_user: User, + packages: List[Package], + requests: List[PackageRequest]): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/requests", params={ + # Pass in url query parameters O, SeB and SB to exercise + # their paths inside of the pager_nav used in this request. + "O": 0, # Page 1 + "SeB": "nd", + "SB": "n" + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + assert "Next ›" in resp.text + assert "Last »" in resp.text + + root = parse_root(resp.text) + # We have 55 requests, our defaults.PP is 50, so expect we have 50 rows. + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == defaults.PP + + # Request page 2 of the requests page. + with client as request: + resp = request.get("/requests", params={ + "O": 50 # Page 2 + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + assert "‹ Previous" in resp.text + assert "« First" in resp.text + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 5 # There are five records left on the second page. diff --git a/test/test_templates.py b/test/test_templates.py index b6aa2055..86fbf611 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -1,6 +1,6 @@ import pytest -from aurweb.templates import register_filter +from aurweb.templates import register_filter, register_function @register_filter("func") @@ -8,6 +8,11 @@ def func(): pass +@register_function("function") +def function(): + pass + + def test_register_filter_exists_key_error(): """ Most instances of register_filter are tested through module imports or template renders, so we only test failures here. """ @@ -15,3 +20,12 @@ def test_register_filter_exists_key_error(): @register_filter("func") def some_func(): pass + + +def test_register_function_exists_key_error(): + """ Most instances of register_filter are tested through module + imports or template renders, so we only test failures here. """ + with pytest.raises(KeyError): + @register_function("function") + def some_func(): + pass diff --git a/test/test_util.py b/test/test_util.py index 0cc45409..99b77a78 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,7 +1,7 @@ from datetime import datetime from zoneinfo import ZoneInfo -from aurweb import util +from aurweb import filters, util def test_timestamp_to_datetime(): @@ -34,3 +34,9 @@ def test_to_qs(): query = {"a": "b", "c": [1, 2, 3]} qs = util.to_qs(query) assert qs == "a=b&c=1&c=2&c=3" + + +def test_round(): + assert filters.do_round(1.3) == 1 + assert filters.do_round(1.5) == 2 + assert filters.do_round(2.0) == 2 From 1cf9420997bc6788b4ae3264d8e019fd0ec13d56 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 12 Sep 2021 20:05:49 -0700 Subject: [PATCH 0468/1451] feat(FastAPI): allow reporters to cancel their own requests (1/2) This change required a slight modification of how we handle the Requests page. It is now available to all users. This commit provides 1/2 of the implementation which actually satisfies this feature. 2/2 will contain the actual implementation of closures of requests, which will also allow users who created the request to decide to close it. Issue: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/20 Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 9 ++++++--- test/test_packages_routes.py | 28 +++++++++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 5751a3ee..2b350478 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -12,8 +12,7 @@ import aurweb.models.package_keyword import aurweb.packages.util from aurweb import db, defaults, l10n -from aurweb.auth import account_type_required, auth_required -from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND_DEV +from aurweb.auth import auth_required from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -540,7 +539,6 @@ async def package_base_comaintainers_post( @router.get("/requests") -@account_type_required({TRUSTED_USER, DEVELOPER, TRUSTED_USER_AND_DEV}) @auth_required(True, redirect="/") async def requests(request: Request, O: int = Query(default=defaults.O), @@ -556,6 +554,11 @@ async def requests(request: Request, User, PackageRequest.UsersID == User.ID ).join(RequestType) + # If the request user is not elevated (TU or Dev), then + # filter PackageRequests which are owned by the request user. + if not request.user.is_elevated(): + query = query.filter(PackageRequest.UsersID == request.user.ID) + context["total"] = query.count() context["results"] = query.order_by( # Order primarily by the Status column being PENDING_ID, diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index a25fcb7e..9867ce42 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1338,14 +1338,9 @@ def test_pkgbase_comaintainers(client: TestClient, user: User, assert users is not None and users.text is None -def test_requests_unauthorized(client: TestClient, - maintainer: User, - tu_user: User, - packages: List[Package], - requests: List[PackageRequest]): - cookies = {"AURSID": maintainer.login(Request(), "testPassword")} +def test_requests_unauthorized(client: TestClient): with client as request: - resp = request.get("/requests", cookies=cookies, allow_redirects=False) + resp = request.get("/requests", allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -1386,3 +1381,22 @@ def test_requests(client: TestClient, root = parse_root(resp.text) rows = root.xpath('//table[@class="results"]/tbody/tr') assert len(rows) == 5 # There are five records left on the second page. + + +def test_requests_selfmade(client: TestClient, user: User, + requests: List[PackageRequest]): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/requests", cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # As the user who creates all of the requests, we should see all of them. + # However, we are not allowed to accept any of them ourselves. + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == defaults.PP + + # Our first and only link in the last row should be "Close". + for row in rows: + last_row = row.xpath('./td')[-1].xpath('./a')[0] + assert last_row.text.strip() == "Close" From ad8369395e323d99bd4b3cae430269ac8dd19491 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 12 Sep 2021 21:51:20 -0700 Subject: [PATCH 0469/1451] feat(FastAPI): add /pkgbase/{name}/request (get) This change brings in the package base request form for new submissions. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 14 ++++ templates/pkgbase/request.html | 87 ++++++++++++++++++++++++ test/test_packages_routes.py | 20 ++++++ web/html/js/typeahead-pkgbase-request.js | 36 ++++++++++ 4 files changed, 157 insertions(+) create mode 100644 templates/pkgbase/request.html create mode 100644 web/html/js/typeahead-pkgbase-request.js diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 2b350478..9c9a41e3 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -568,3 +568,17 @@ async def requests(request: Request, ).limit(PP).offset(O).all() return render_template(request, "requests.html", context) + + +@router.get("/pkgbase/{name}/request") +@auth_required(True) +async def package_request(request: Request, name: str): + context = make_context(request, "Submit Request") + + pkgbase = db.query(PackageBase).filter(PackageBase.Name == name).first() + + if not pkgbase: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + + context["pkgbase"] = pkgbase + return render_template(request, "pkgbase/request.html", context) diff --git a/templates/pkgbase/request.html b/templates/pkgbase/request.html new file mode 100644 index 00000000..66d69f07 --- /dev/null +++ b/templates/pkgbase/request.html @@ -0,0 +1,87 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {{ "Submit Request" | tr }}: {{ pkgbase.Name }}

    +

    + {{ "Use this form to file a request against package base " + "%s%s%s which includes the following packages:" + | tr | format("", pkgbase.Name, "") | safe }} +

    +
      + {% for package in pkgbase.packages %} +
    • {{ package.Name }}
    • + {% endfor %} +
    + + {# Request form #} +
    +
    +

    + + +

    + + {# Javascript included for HTML-changing triggers depending + on the selected type (above). #} + + + + +

    + + +

    + +

    + {{ + "By submitting a deletion request, you ask a Trusted " + "User to delete the package base. This type of " + "request should be used for duplicates, software " + "abandoned by upstream, as well as illegal and " + "irreparably broken packages." | tr + }} +

    + + + + + +

    + +

    + +
    +
    + +
    +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 9867ce42..8704d702 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1400,3 +1400,23 @@ def test_requests_selfmade(client: TestClient, user: User, for row in rows: last_row = row.xpath('./td')[-1].xpath('./a')[0] assert last_row.text.strip() == "Close" + + +def test_pkgbase_request_not_found(client: TestClient, user: User): + pkgbase_name = "fake" + endpoint = f"/pkgbase/{pkgbase_name}/request" + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_request(client: TestClient, user: User, package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/request" + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) diff --git a/web/html/js/typeahead-pkgbase-request.js b/web/html/js/typeahead-pkgbase-request.js new file mode 100644 index 00000000..e012d55f --- /dev/null +++ b/web/html/js/typeahead-pkgbase-request.js @@ -0,0 +1,36 @@ +function showHideMergeSection() { + const elem = document.getElementById('id_type'); + const merge_section = document.getElementById('merge_section'); + if (elem.value == 'merge') { + merge_section.style.display = ''; + } else { + merge_section.style.display = 'none'; + } +} + +function showHideRequestHints() { + document.getElementById('deletion_hint').style.display = 'none'; + document.getElementById('merge_hint').style.display = 'none'; + document.getElementById('orphan_hint').style.display = 'none'; + + const elem = document.getElementById('id_type'); + document.getElementById(elem.value + '_hint').style.display = ''; +} + +document.addEventListener('DOMContentLoaded', function() { + showHideMergeSection(); + showHideRequestHints(); + + const input = document.getElementById('id_merge_into'); + const form = document.getElementById('request-form'); + const type = "suggest-pkgbase"; + + typeahead.init(type, input, form, false); +}); + +// Bind the change event here, otherwise we have to inline javascript, +// which angers CSP (Content Security Policy). +document.getElementById("id_type").addEventListener("change", function() { + showHideMergeSection(); + showHideRequestHints(); +}); From 1c031638c65588ef5c219adffdaf1a7b695d0d02 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 13 Sep 2021 17:26:25 -0700 Subject: [PATCH 0470/1451] feat(FastAPI): add /pkgbase/{name}/request (post) This change implements the FastAPI version of the /pkgbase/{name}/request form's action. Changes from PHP: - Additional errors are now displayed for the **merge_into** field, which are only displayed when the Merge type is selected. - If the **merge_into** field is empty, a new error is displayed: 'The "Merge into" field must not be empty.' - If the **merge_into** field is given the name of a package base which does not exist, a new error is displayed: "The package base you want to merge into does not exist." - If the **merge_into** field is given the name of the package base that a request is being created for, a new error is displayed: "You cannot merge a package base into itself." - When an error is encountered, users are now brought back to the request form which they submitted and an error is displayed at the top of the page. - If an invalid type is provided, users are returned to a BAD_REQUEST status rendering of the request form. Signed-off-by: Kevin Morris --- aurweb/models/request_type.py | 6 ++ aurweb/routers/packages.py | 69 ++++++++++++++ templates/pkgbase/request.html | 12 ++- test/test_packages_routes.py | 161 +++++++++++++++++++++++++++++++++ 4 files changed, 247 insertions(+), 1 deletion(-) diff --git a/aurweb/models/request_type.py b/aurweb/models/request_type.py index a26dcf9a..48ace3a3 100644 --- a/aurweb/models/request_type.py +++ b/aurweb/models/request_type.py @@ -20,6 +20,12 @@ class RequestType(Base): name = self.Name return name[0].upper() + name[1:] + def title(self) -> str: + return self.name_display() + + def __getitem__(self, n: int) -> str: + return self.Name[n] + DELETION_ID = db.query(RequestType, RequestType.Name == DELETION).first().ID ORPHAN_ID = db.query(RequestType, RequestType.Name == ORPHAN).first().ID diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 9c9a41e3..231f953b 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -582,3 +582,72 @@ async def package_request(request: Request, name: str): context["pkgbase"] = pkgbase return render_template(request, "pkgbase/request.html", context) + + +@router.post("/pkgbase/{name}/request") +@auth_required(True) +async def pkgbase_request_post(request: Request, name: str, + type: str = Form(...), + merge_into: str = Form(default=None), + comments: str = Form(default=str())): + pkgbase = get_pkg_or_base(name, PackageBase) + + # Create our render context. + context = make_context(request, "Submit Request") + context["pkgbase"] = pkgbase + if type not in {"deletion", "merge", "orphan"}: + # In the case that someone crafted a POST request with an invalid + # type, just return them to the request form with BAD_REQUEST status. + return render_template(request, "pkgbase/request.html", context, + status_code=HTTPStatus.BAD_REQUEST) + + if not comments: + context["errors"] = ["The comment field must not be empty."] + return render_template(request, "pkgbase/request.html", context) + + if type == "merge": + # Perform merge-related checks. + if not merge_into: + # TODO: This error needs to be translated. + context["errors"] = ['The "Merge into" field must not be empty.'] + return render_template(request, "pkgbase/request.html", context) + + target = db.query(PackageBase).filter( + PackageBase.Name == merge_into + ).first() + if not target: + # TODO: This error needs to be translated. + context["errors"] = [ + "The package base you want to merge into does not exist." + ] + return render_template(request, "pkgbase/request.html", context) + + if target.ID == pkgbase.ID: + # TODO: This error needs to be translated. + context["errors"] = [ + "You cannot merge a package base into itself." + ] + return render_template(request, "pkgbase/request.html", context) + + # All good. Create a new PackageRequest based on the given type. + now = int(datetime.utcnow().timestamp()) + reqtype = db.query(RequestType, RequestType.Name == type).first() + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + notify_ = None + with db.begin(): + pkgreq = db.create(PackageRequest, RequestType=reqtype, RequestTS=now, + PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + MergeBaseName=merge_into, User=request.user, + Comments=comments, ClosureComment=str()) + + # Prepare notification object. + notify_ = notify.RequestOpenNotification( + conn, request.user.ID, pkgreq.ID, reqtype, + pkgreq.PackageBase.ID, merge_into=merge_into or None) + + # Send the notification now that we're out of the DB scope. + notify_.send() + + # Redirect the submitting user to /packages. + return RedirectResponse("/packages", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/pkgbase/request.html b/templates/pkgbase/request.html index 66d69f07..bb9b5aba 100644 --- a/templates/pkgbase/request.html +++ b/templates/pkgbase/request.html @@ -1,8 +1,17 @@ {% extends "partials/layout.html" %} {% block pageContent %} + {% if errors %} +
      + {% for error in errors %} +
    • {{ error | tr }}
    • + {% endfor %} +
    + {% endif %} +

    {{ "Submit Request" | tr }}: {{ pkgbase.Name }}

    +

    {{ "Use this form to file a request against package base " "%s%s%s which includes the following packages:" @@ -15,7 +24,8 @@ {# Request form #} -

    +

    diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 8704d702..5353d3bf 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1420,3 +1420,164 @@ def test_pkgbase_request(client: TestClient, user: User, package: Package): with client as request: resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) + + +def test_pkgbase_request_post_deletion(client: TestClient, user: User, + package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "deletion", + "comments": "We want to delete this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = db.query(PackageRequest).filter( + PackageRequest.PackageBaseID == package.PackageBase.ID + ).first() + assert pkgreq is not None + assert pkgreq.RequestType.Name == "deletion" + assert pkgreq.PackageBaseName == package.PackageBase.Name + assert pkgreq.Comments == "We want to delete this." + + +def test_pkgbase_request_post_orphan(client: TestClient, user: User, + package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "orphan", + "comments": "We want to disown this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = db.query(PackageRequest).filter( + PackageRequest.PackageBaseID == package.PackageBase.ID + ).first() + assert pkgreq is not None + assert pkgreq.RequestType.Name == "orphan" + assert pkgreq.PackageBaseName == package.PackageBase.Name + assert pkgreq.Comments == "We want to disown this." + + +def test_pkgbase_request_post_merge(client: TestClient, user: User, + package: Package): + with db.begin(): + pkgbase2 = db.create(PackageBase, Name="new-pkgbase", + Submitter=user, Maintainer=user, Packager=user) + target = db.create(Package, PackageBase=pkgbase2, + Name=pkgbase2.Name, Version="1.0.0") + + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "merge", + "merge_into": target.PackageBase.Name, + "comments": "We want to merge this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = db.query(PackageRequest).filter( + PackageRequest.PackageBaseID == package.PackageBase.ID + ).first() + assert pkgreq is not None + assert pkgreq.RequestType.Name == "merge" + assert pkgreq.PackageBaseName == package.PackageBase.Name + assert pkgreq.MergeBaseName == target.PackageBase.Name + assert pkgreq.Comments == "We want to merge this." + + +def test_pkgbase_request_post_not_found(client: TestClient, user: User): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post("/pkgbase/fake/request", data={ + "type": "fake" + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_request_post_invalid_type(client: TestClient, + user: User, + package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={"type": "fake"}, cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + +def test_pkgbase_request_post_no_comment_error(client: TestClient, + user: User, + package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "deletion", + "comments": "" # An empty comment field causes an error. + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = "The comment field must not be empty." + assert error.text.strip() == expected + + +def test_pkgbase_request_post_merge_not_found_error(client: TestClient, + user: User, + package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "merge", + "merge_into": "fake", # There is no PackageBase.Name "fake" + "comments": "We want to merge this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = "The package base you want to merge into does not exist." + assert error.text.strip() == expected + + +def test_pkgbase_request_post_merge_no_merge_into_error(client: TestClient, + user: User, + package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "merge", + "merge_into": "", # There is no PackageBase.Name "fake" + "comments": "We want to merge this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = 'The "Merge into" field must not be empty.' + assert error.text.strip() == expected + + +def test_pkgbase_request_post_merge_self_error(client: TestClient, user: User, + package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "merge", + "merge_into": package.PackageBase.Name, + "comments": "We want to merge this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = "You cannot merge a package base into itself." + assert error.text.strip() == expected From f6141ff1778e8d1376a0db3b92e7a7d7fa2f9097 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 14 Sep 2021 21:37:35 -0700 Subject: [PATCH 0471/1451] feat(FastAPI): add /requests/{id}/close (get, post) Changes from PHP: - If a user submits a POST request with an invalid reason, they are returned back to the closure form with a BAD_REQUEST status. - Now, users which created a PackageRequest have the ability to close their own. - Form action has been changed to `/requests/{id}/close`. Closes https://gitlab.archlinux.org/archlinux/aurweb/-/issues/20 Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 52 ++++++++++++++++++++- templates/requests/close.html | 60 ++++++++++++++++++++++++ test/test_packages_routes.py | 86 ++++++++++++++++++++++++++++++++++- 3 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 templates/requests/close.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 231f953b..a3effb36 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -22,7 +22,7 @@ from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_license import PackageLicense from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation -from aurweb.models.package_request import PENDING_ID, PackageRequest +from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID, PackageRequest from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID @@ -651,3 +651,53 @@ async def pkgbase_request_post(request: Request, name: str, # Redirect the submitting user to /packages. return RedirectResponse("/packages", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.get("/requests/{id}/close") +@auth_required(True) +async def requests_close(request: Request, id: int): + pkgreq = db.query(PackageRequest).filter(PackageRequest.ID == id).first() + if not request.user.is_elevated() and request.user != pkgreq.User: + # Request user doesn't have permission here: redirect to '/'. + return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER)) + + context = make_context(request, "Close Request") + context["pkgreq"] = pkgreq + return render_template(request, "requests/close.html", context) + + +@router.post("/requests/{id}/close") +@auth_required(True) +async def requests_close_post(request: Request, id: int, + reason: int = Form(default=0), + comments: str = Form(default=str())): + pkgreq = db.query(PackageRequest).filter(PackageRequest.ID == id).first() + if not request.user.is_elevated() and request.user != pkgreq.User: + # Request user doesn't have permission here: redirect to '/'. + return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER)) + + context = make_context(request, "Close Request") + context["pkgreq"] = pkgreq + + if reason not in {ACCEPTED_ID, REJECTED_ID}: + # If the provided reason is not valid, send the user back to + # the closure form with a BAD_REQUEST status. + return render_template(request, "requests/close.html", context, + status_code=HTTPStatus.BAD_REQUEST) + + if not request.user.is_elevated(): + # If we're closing the request as the user who created it, + # the reason should just be a REJECTION. + reason = REJECTED_ID + + with db.begin(): + pkgreq.Closer = request.user + pkgreq.Status = reason + pkgreq.ClosureComment = comments + + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + notify_ = notify.RequestCloseNotification( + conn, request.user.ID, pkgreq.ID, pkgreq.status_display()) + notify_.send() + + return RedirectResponse("/requests", status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/requests/close.html b/templates/requests/close.html new file mode 100644 index 00000000..7862064a --- /dev/null +++ b/templates/requests/close.html @@ -0,0 +1,60 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +

    +

    {{ "Close Request" | tr }}: {{ pkgreq.PackageBaseName }}

    + +

    + {{ + "Use this form to close the request for package base %s%s%s." + | tr | format("", pkgreq.PackageBaseName, "") + | safe + }} +

    + +

    + {{ "Note" | tr }}: + {{ + "The comments field can be left empty. However, it is highly " + "recommended to add a comment when rejecting a request." + | tr + }} +

    + + +
    +

    + + +

    + +

    + + +

    + +

    + +

    + +
    + + +
    +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 5353d3bf..5afe011a 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -20,7 +20,7 @@ from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation -from aurweb.models.package_request import PackageRequest +from aurweb.models.package_request import ACCEPTED_ID, REJECTED_ID, PackageRequest from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import PROVIDES_ID, RelationType from aurweb.models.request_type import DELETION_ID, RequestType @@ -1581,3 +1581,87 @@ def test_pkgbase_request_post_merge_self_error(client: TestClient, user: User, error = root.xpath('//ul[@class="errorlist"]/li')[0] expected = "You cannot merge a package base into itself." assert error.text.strip() == expected + + +@pytest.fixture +def pkgreq(user: User, package: Package) -> PackageRequest: + reqtype = db.query(RequestType).filter( + RequestType.ID == DELETION_ID + ).first() + with db.begin(): + pkgreq = db.create(PackageRequest, + RequestType=reqtype, + User=user, + PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + Comments=str(), + ClosureComment=str()) + yield pkgreq + + +def test_requests_close(client: TestClient, user: User, + pkgreq: PackageRequest): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies, + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + +def test_requests_close_unauthorized(client: TestClient, maintainer: User, + pkgreq: PackageRequest): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies, + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/" + + +def test_requests_close_post_invalid_reason(client: TestClient, user: User, + pkgreq: PackageRequest): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(f"/requests/{pkgreq.ID}/close", data={ + "reason": 0 + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + +def test_requests_close_post_unauthorized(client: TestClient, maintainer: User, + pkgreq: PackageRequest): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + resp = request.post(f"/requests/{pkgreq.ID}/close", data={ + "reason": ACCEPTED_ID + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/" + + +def test_requests_close_post(client: TestClient, user: User, + pkgreq: PackageRequest): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(f"/requests/{pkgreq.ID}/close", data={ + "reason": REJECTED_ID + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + assert pkgreq.Status == REJECTED_ID + assert pkgreq.Closer == user + assert pkgreq.ClosureComment == str() + + +def test_requests_close_post_rejected(client: TestClient, user: User, + pkgreq: PackageRequest): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(f"/requests/{pkgreq.ID}/close", data={ + "reason": REJECTED_ID + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + assert pkgreq.Status == REJECTED_ID + assert pkgreq.Closer == user + assert pkgreq.ClosureComment == str() From b5f8e69b8aaefc093eabb3163eb0dd6445682a8b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 3 Oct 2021 10:22:34 -0700 Subject: [PATCH 0472/1451] feat(FastAPI): use SQLAlchemy's scoped_session Closes #113 Signed-off-by: Kevin Morris --- aurweb/db.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aurweb/db.py b/aurweb/db.py index ea6b6918..2b934300 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -3,6 +3,7 @@ import math import re from sqlalchemy import event +from sqlalchemy.orm import scoped_session import aurweb.config import aurweb.util @@ -167,7 +168,8 @@ def get_engine(echo: bool = False): connect_args=connect_args, echo=echo) - Session = sessionmaker(autocommit=True, autoflush=False, bind=engine) + Session = scoped_session( + sessionmaker(autocommit=True, autoflush=False, bind=engine)) session = Session() if db_backend == "sqlite": From 7bfc2bf9b44ba13526b20160531aea208f694c89 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 3 Oct 2021 15:11:42 -0700 Subject: [PATCH 0473/1451] fix(FastAPI): Improve sqlite testing speed This commit adds a new Arch dependency: `libeatmydata`, which provides the `eatmydata` executable that stubs out fsync() operations. We use `eatmydata` to run our sharness and pytests in Docker now. With `autocommit=True`, required by SQLAlchemy to keep the session up to date with external DB modifications, many fsync calls are used in the SQLite backend; especially because we're wiping and creating records in every DB-bound test. **Before:** - mysql: 1m42s (elapsed during pytest run) - sqlite: 3m06s (elapsed during pytest run) **After:** - mysql: 1m40s (elapsed during pytest run) - sqlite: 1m50s (elapsed during pytest run) Shout out to @klausenbusk, who suggested this as a possible fix, and it was. Thanks, Kristian! Closes #120 Signed-off-by: Kevin Morris --- docker/scripts/install-deps.sh | 2 +- docker/scripts/run-pytests.sh | 2 +- docker/scripts/run-sharness.sh | 2 +- test/README.md | 10 ++++++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index f8881d05..fc068b06 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -8,7 +8,7 @@ pacman -Syu --noconfirm --noprogressbar \ --cachedir .pkg-cache git gpgme nginx redis openssh \ mariadb mariadb-libs cgit uwsgi uwsgi-plugin-cgi \ php php-fpm memcached php-memcached python-pip pyalpm \ - python-srcinfo curl + python-srcinfo curl libeatmydata # https://python-poetry.org/docs/ Installation section. curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - diff --git a/docker/scripts/run-pytests.sh b/docker/scripts/run-pytests.sh index c6baa939..ef8a2318 100755 --- a/docker/scripts/run-pytests.sh +++ b/docker/scripts/run-pytests.sh @@ -27,7 +27,7 @@ python -m aurweb.initdb 2>/dev/null || \ (echo "Error: aurweb.initdb failed; already initialized?" && /bin/true) # Run pytest with optional targets in front of it. -make -C test "${PARAMS[@]}" pytest +eatmydata -- make -C test "${PARAMS[@]}" pytest # By default, report coverage and move it into cache. if [ $COVERAGE -eq 1 ]; then diff --git a/docker/scripts/run-sharness.sh b/docker/scripts/run-sharness.sh index 8e928b3f..fe16751c 100755 --- a/docker/scripts/run-sharness.sh +++ b/docker/scripts/run-sharness.sh @@ -4,4 +4,4 @@ set -eou pipefail # Initialize the new database; ignore errors. python -m aurweb.initdb 2>/dev/null || /bin/true -make -C test sh +eatmydata -- make -C test sh diff --git a/test/README.md b/test/README.md index ef8a08f4..13fb0a0c 100644 --- a/test/README.md +++ b/test/README.md @@ -31,6 +31,10 @@ For all the test to run, the following Arch packages should be installed: - postfix - openssh +Optional (faster testing) + +- libeatmydata + Test Configuration ------------------ @@ -115,6 +119,12 @@ To run `pytest` Python test suites: $ make -C test pytest +**Note:** For SQLite tests, users may want to use `eatmydata` +to improve speed: + + $ eatmydata -- make -C test sh + $ eatmydata -- make -C test pytest + To produce coverage reports related to Python when running tests manually, use the following method: From 08068e0a5c70ef8d5ef94c20952d9aa15ba6c8dc Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Mon, 4 Oct 2021 13:30:25 -0400 Subject: [PATCH 0474/1451] fix(FastAPI): use configured letter case for SSH fingerprints Currently, the config parser converts all keys to lowercase which is inconsistent with the old PHP behavior. This has been fixed and relevant fingerprint-getting functions have been simplified without changes in behavior. Signed-off-by: Steven Guikal --- aurweb/config.py | 8 ++++---- aurweb/util.py | 8 +------- test/test_homepage.py | 4 +++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 52fadda2..aa111f15 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -17,6 +17,7 @@ def _get_parser(): defaults = os.environ.get('AUR_CONFIG_DEFAULTS', path + '.defaults') _parser = configparser.RawConfigParser() + _parser.optionxform = lambda option: option if os.path.isfile(defaults): with open(defaults) as f: _parser.read_file(f) @@ -48,7 +49,6 @@ def getint(section, option, fallback=None): return _get_parser().getint(section, option, fallback=fallback) -def get_section(section_name): - for section in _get_parser().sections(): - if section == section_name: - return _get_parser()[section] +def get_section(section): + if section in _get_parser().sections(): + return _get_parser()[section] diff --git a/aurweb/util.py b/aurweb/util.py index f9181811..08e6d7c6 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -166,10 +166,4 @@ def add_samesite_fields(response: Response, value: str): def get_ssh_fingerprints(): - fingerprints = {} - fingerprint_section = aurweb.config.get_section("fingerprints") - - if fingerprint_section: - fingerprints = {key: fingerprint_section[key] for key in fingerprint_section.keys()} - - return fingerprints + return aurweb.config.get_section("fingerprints") or {} diff --git a/test/test_homepage.py b/test/test_homepage.py index fef3532d..5c678b71 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -96,7 +96,9 @@ def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock): with client as request: response = request.get("/") - assert list(fingerprints.values())[0] in response.content.decode() + for key, value in fingerprints.items(): + assert key in response.content.decode() + assert value in response.content.decode() assert 'The following SSH fingerprints are used for the AUR' in response.content.decode() From 5c179dc4d35b8c2bbacce61d0664fc7285d99e56 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Mon, 4 Oct 2021 17:04:23 -0400 Subject: [PATCH 0475/1451] fix(FastAPI): use consistent ordering on dashboard and request page Signed-off-by: Steven Guikal --- aurweb/routers/html.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 3d44cf87..6e7697e4 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -6,7 +6,7 @@ from http import HTTPStatus from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse -from sqlalchemy import and_, or_ +from sqlalchemy import and_, case, or_ import aurweb.config import aurweb.models.package_request @@ -17,7 +17,7 @@ from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_comaintainer import PackageComaintainer -from aurweb.models.package_request import PackageRequest +from aurweb.models.package_request import PENDING_ID, PackageRequest from aurweb.models.user import User from aurweb.packages.util import query_notified, query_voted, updated_packages from aurweb.templates import make_context, render_template @@ -166,6 +166,11 @@ async def index(request: Request): # Package requests created by request.user. context["package_requests"] = request.user.package_requests.filter( PackageRequest.RequestTS >= start + ).order_by( + # Order primarily by the Status column being PENDING_ID, + # and secondarily by RequestTS; both in descending order. + case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), + PackageRequest.RequestTS.desc() ).limit(50).all() # Packages that the request user maintains or comaintains. From 9af76a73a331b889815e42866e3df4bbe8ddc5d0 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Mon, 4 Oct 2021 16:32:10 -0400 Subject: [PATCH 0476/1451] fix(FastAPI): include MergeBaseName in merge request type This was done on the dedicated requests page, but missed on the dashboard. Signed-off-by: Steven Guikal --- templates/partials/packages/requests.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/templates/partials/packages/requests.html b/templates/partials/packages/requests.html index 5239ca72..5188a476 100644 --- a/templates/partials/packages/requests.html +++ b/templates/partials/packages/requests.html @@ -20,7 +20,13 @@ {{ request.PackageBase.Name }}
    - {{ request.RequestType.name_display() | tr }} + + {{ request.RequestType.name_display() | tr }} + {# If the RequestType is a merge and request.MergeBaseName is valid... #} + {% if request.RequestType.ID == 3 and request.MergeBaseName %} + ({{ request.MergeBaseName }}) + {% endif %} + {{ request.Comments }} Date: Mon, 4 Oct 2021 17:37:25 -0400 Subject: [PATCH 0477/1451] fix(FastAPI): add missing translation filter for request type Signed-off-by: Steven Guikal --- templates/requests.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/requests.html b/templates/requests.html index a9017e2f..74ea6416 100644 --- a/templates/requests.html +++ b/templates/requests.html @@ -31,7 +31,7 @@ {# Type #} - {{ result.RequestType.name_display() }} + {{ result.RequestType.name_display() | tr }} {# If the RequestType is a merge and request.MergeBaseName is valid... #} {% if result.RequestType.ID == 3 and result.MergeBaseName %} ({{ result.MergeBaseName }}) From 1956be0f469e5870d957a8dec2fa48100a247273 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Tue, 5 Oct 2021 14:00:12 -0400 Subject: [PATCH 0478/1451] fix(FastAPI): prefill login fields with entered data --- aurweb/routers/auth.py | 16 ++++++++-------- templates/login.html | 9 +++++++-- test/test_auth_routes.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 8f37fe27..a985281e 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -9,14 +9,14 @@ import aurweb.config from aurweb import util from aurweb.auth import auth_required from aurweb.models.user import User -from aurweb.templates import make_context, render_template +from aurweb.templates import make_variable_context, render_template router = APIRouter() -def login_template(request: Request, next: str, errors: list = None): +async def login_template(request: Request, next: str, errors: list = None): """ Provide login-specific template context to render_template. """ - context = make_context(request, "Login", next) + context = await make_variable_context(request, "Login", next) context["errors"] = errors context["url_base"] = f"{request.url.scheme}://{request.url.netloc}" return render_template(request, "login.html", context) @@ -25,7 +25,7 @@ def login_template(request: Request, next: str, errors: list = None): @router.get("/login", response_class=HTMLResponse) @auth_required(False) async def login_get(request: Request, next: str = "/"): - return login_template(request, next) + return await login_template(request, next) @router.post("/login", response_class=HTMLResponse) @@ -39,8 +39,8 @@ async def login_post(request: Request, user = session.query(User).filter(User.Username == user).first() if not user: - return login_template(request, next, - errors=["Bad username or password."]) + return await login_template(request, next, + errors=["Bad username or password."]) cookie_timeout = 0 @@ -50,8 +50,8 @@ async def login_post(request: Request, sid = user.login(request, passwd, cookie_timeout) if not sid: - return login_template(request, next, - errors=["Bad username or password."]) + return await login_template(request, next, + errors=["Bad username or password."]) login_timeout = aurweb.config.getint("options", "login_timeout") diff --git a/templates/login.html b/templates/login.html index da7bd722..3c4f945f 100644 --- a/templates/login.html +++ b/templates/login.html @@ -45,7 +45,8 @@ + maxlength="254" autofocus="autofocus" + value="{{ user or '' }}">

    @@ -57,7 +58,11 @@

    - + diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 1d8f9cbe..313f9927 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -160,6 +160,11 @@ def test_login_missing_username(): response = request.post("/login", data=post_data) assert "AURSID" not in response.cookies + # Make sure password isn't prefilled and remember_me isn't checked. + content = response.content.decode() + assert post_data["passwd"] not in content + assert "checked" not in content + def test_login_remember_me(): post_data = { @@ -188,6 +193,26 @@ def test_login_remember_me(): assert _session.LastUpdateTS < expected_ts + 5 +def test_login_incorrect_password_remember_me(): + post_data = { + "user": "test", + "passwd": "badPassword", + "next": "/", + "remember_me": "on" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + # Make sure username is prefilled, password isn't prefilled, and remember_me + # is checked. + content = response.content.decode() + assert post_data["user"] in content + assert post_data["passwd"] not in content + assert "checked" in content + + def test_login_missing_password(): post_data = { "user": "test", @@ -198,6 +223,11 @@ def test_login_missing_password(): response = request.post("/login", data=post_data) assert "AURSID" not in response.cookies + # Make sure username is prefilled and remember_me isn't checked. + content = response.content.decode() + assert post_data["user"] in content + assert "checked" not in content + def test_login_incorrect_password(): post_data = { @@ -209,3 +239,10 @@ def test_login_incorrect_password(): with client as request: response = request.post("/login", data=post_data) assert "AURSID" not in response.cookies + + # Make sure username is prefilled, password isn't prefilled and remember_me + # isn't checked. + content = response.content.decode() + assert post_data["user"] in content + assert post_data["passwd"] not in content + assert "checked" not in content From 1bce53bbb7343fc861f253e97be171403bb930f4 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Tue, 5 Oct 2021 14:36:46 -0400 Subject: [PATCH 0479/1451] fix(FastAPI): mark user and passwd as required fields --- templates/login.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/login.html b/templates/login.html index 3c4f945f..45fc1645 100644 --- a/templates/login.html +++ b/templates/login.html @@ -46,7 +46,7 @@ + required="required" value="{{ user or '' }}">

    @@ -54,7 +54,7 @@ {% trans %}Password{% endtrans %}: + size="30" required="required">

    From a54a09f61d0495789b22724c5f83bf883af83b45 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Tue, 5 Oct 2021 12:38:31 -0400 Subject: [PATCH 0480/1451] fix(FastAPI): fix padding on email inputs Signed-off-by: Steven Guikal --- web/html/css/archweb.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/html/css/archweb.css b/web/html/css/archweb.css index b935d7db..45b9bff0 100644 --- a/web/html/css/archweb.css +++ b/web/html/css/archweb.css @@ -329,6 +329,7 @@ label { input[type=text], input[type=password], +input[type=email], textarea { padding: 0.10em; } From 889c5b1e21788a1f3126dfa121b6253ea9497501 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 6 Oct 2021 22:08:17 -0700 Subject: [PATCH 0481/1451] fix(FastAPI): pkgbase actions template Display Delete, Merge and Disown actions based on user credentials. Signed-off-by: Kevin Morris --- templates/partials/packages/actions.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 6c30153c..a54d4c90 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -142,17 +142,21 @@ {% endif %}

  • - {% if is_maintainer %} + {% if request.user.has_credential("CRED_PKGBASE_DELETE") %}
  • {{ "Delete Package" | tr }}
  • + {% endif %} + {% if request.user.has_credential("CRED_PKGBASE_MERGE") %}
  • {{ "Merge Package" | tr }}
  • + {% endif %} + {% if request.user.has_credential("CRED_PKGBASE_DISOWN", approved=[pkgbase.Maintainer]) %}
  • Date: Wed, 6 Oct 2021 22:29:53 -0700 Subject: [PATCH 0482/1451] feat(FastAPI): add CRED_PKGBASE_MERGE Signed-off-by: Kevin Morris --- aurweb/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aurweb/auth.py b/aurweb/auth.py index 21d31081..fb062eab 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -268,6 +268,7 @@ CRED_PKGREQ_LIST = 18 CRED_TU_ADD_VOTE = 19 CRED_TU_LIST_VOTES = 20 CRED_TU_VOTE = 21 +CRED_PKGBASE_MERGE = 29 def has_any(user, *account_types): @@ -321,6 +322,7 @@ cred_filters = { CRED_TU_LIST_VOTES: trusted_user, CRED_TU_VOTE: trusted_user, CRED_ACCOUNT_EDIT_DEV: developer, + CRED_PKGBASE_MERGE: trusted_user_or_dev, } From e5299b5ed4c9c9041217f196f5721d8b49bfbf00 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 6 Oct 2021 23:17:08 -0700 Subject: [PATCH 0483/1451] fix(FastAPI): pkgbase/package tests Signed-off-by: Kevin Morris --- test/test_packages_routes.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 5afe011a..4118744a 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -335,6 +335,30 @@ def test_package_authenticated_maintainer(client: TestClient, resp = request.get(package_endpoint(package), cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) + expected = [ + "View PKGBUILD", + "View Changes", + "Download snapshot", + "Search wiki", + "Flag package out-of-date", + "Vote for this package", + "Enable notifications", + "Manage Co-Maintainers", + "Submit Request", + "Disown Package" + ] + for expected_text in expected: + assert expected_text in resp.text + + +def test_package_authenticated_tu(client: TestClient, + tu_user: User, + package: Package): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.get(package_endpoint(package), cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + expected = [ "View PKGBUILD", "View Changes", From 75c49e4f8ada4cae1c5c5fd02ddd3ce73e7ac06a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 00:03:24 -0700 Subject: [PATCH 0484/1451] feat(FastAPI): support {named} fmt in auth_required redirect Signed-off-by: Kevin Morris --- aurweb/auth.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index fb062eab..d44d4ded 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -1,4 +1,5 @@ import functools +import re from datetime import datetime from http import HTTPStatus @@ -121,6 +122,7 @@ class BasicAuthBackend(AuthenticationBackend): def auth_required(is_required: bool = True, + login: bool = False, redirect: str = "/", template: tuple = None, status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): @@ -162,8 +164,16 @@ def auth_required(is_required: bool = True, async def wrapper(request, *args, **kwargs): if request.user.is_authenticated() != is_required: url = "/" + if redirect: - url = redirect + path_params_expr = re.compile(r'\{(\w+)\}') + match = re.findall(path_params_expr, redirect) + args = {k: request.path_params.get(k) for k in match} + url = redirect.format(**args) + + if login: + url = "/login?" + util.urlencode({"next": url}) + if template: # template=("template.html", # ["Some Title", "someFormatted {}"], From 8bc1fab74df9f14a47ad1923a718633702ae82eb Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 00:26:57 -0700 Subject: [PATCH 0485/1451] change(FastAPI): automate request login requirement Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 2 +- templates/partials/packages/actions.html | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index a3effb36..539f9526 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -571,7 +571,7 @@ async def requests(request: Request, @router.get("/pkgbase/{name}/request") -@auth_required(True) +@auth_required(True, login=True, redirect="/pkgbase/{name}") async def package_request(request: Request, name: str): context = make_context(request, "Submit Request") diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index a54d4c90..7355420c 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -132,15 +132,9 @@
  • {% endif %}
  • - {% if not request.user.is_authenticated() %} - - {{ "Submit Request" | tr }} - - {% else %} {{ "Submit Request" | tr }} - {% endif %}
  • {% if request.user.has_credential("CRED_PKGBASE_DELETE") %}
  • From dc11a88ed35f9cfc8c253e23f5814867448f3ac0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 00:39:25 -0700 Subject: [PATCH 0486/1451] change(FastAPI): depend on auth_required redirect for pkgbase actions Signed-off-by: Kevin Morris --- templates/partials/packages/actions.html | 133 +++++++++-------------- 1 file changed, 51 insertions(+), 82 deletions(-) diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 7355420c..f1863663 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -23,99 +23,68 @@ {{ "Search wiki" | tr }}
  • - {% if not request.user.is_authenticated() %} - {% if not out_of_date %} + {% if not out_of_date %}
  • {{ "Flag package out-of-date" | tr }}
  • - {% else %} -
  • - - {% set ood_ts = result.OutOfDateTS | dt | as_timezone(timezone) %} - {{ - "Flagged out-of-date (%s)" - | tr | format(ood_ts.strftime("%Y-%m-%d")) - }} - -
  • - {% endif %} -
  • - - {{ "Vote for this package" | tr }} - -
  • -
  • - - {{ "Enable notifications" | tr }} - -
  • {% else %} - {% if not out_of_date %} -
  • - - {{ "Flag package out-of-date" | tr }} - -
  • - {% else %} -
  • - - {% set ood_ts = result.OutOfDateTS | dt | as_timezone(timezone) %} - {{ - "Flagged out-of-date (%s)" - | tr | format(ood_ts.strftime("%Y-%m-%d")) - }} - -
  • -
  • - - + + {% set ood_ts = result.OutOfDateTS | dt | as_timezone(timezone) %} + {{ + "Flagged out-of-date (%s)" + | tr | format(ood_ts.strftime("%Y-%m-%d")) + }} + +
  • +
  • + + + +
  • + {% endif %} +
  • + {% if not voted %} +
    + +
    + {% else %} +
    + +
    + {% endif %} +
  • +
  • + {% if notified %} +
    +
    -
  • - {% endif %} -
  • - {% if not voted %} -
    + {% else %} + + name="do_Notify" + value="{{ 'Enable notifications' | tr }}" + />
    - {% else %} -
    - -
    - {% endif %} -
  • -
  • - {% if notified %} -
    - -
    - {% else %} -
    - -
    - {% endif %} -
  • - - {% endif %} + {% endif %} + {% if request.user.has_credential('CRED_PKGBASE_EDIT_COMAINTAINERS', approved=[pkgbase.Maintainer]) %}
  • From a756691d08408b2098557ebc6a50cf205ffe0084 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 10:00:46 -0700 Subject: [PATCH 0487/1451] change(FastAPI): user_developer_or_trusted_user always True Signed-off-by: Kevin Morris --- aurweb/auth.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index fb062eab..9f56f90f 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -276,8 +276,7 @@ def has_any(user, *account_types): def user_developer_or_trusted_user(user): - return has_any(user, "User", "Trusted User", "Developer", - "Trusted User & Developer") + return True def trusted_user(user): From 2e6f8cb9f40869198bd6aaba34597467a5951476 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 09:43:47 -0700 Subject: [PATCH 0488/1451] change(FastAPI): @auth_required login kwarg defaulted to True We pretty much want @auth_required to send users to login if we enforce auth requirements but don't otherwise specify a way to deal with it. Signed-off-by: Kevin Morris --- aurweb/auth.py | 3 ++- aurweb/routers/accounts.py | 20 ++++++++++---------- aurweb/routers/packages.py | 28 ++++++++++++++-------------- aurweb/routers/trusted_user.py | 10 +++++----- test/test_trusted_user_routes.py | 6 ++++-- 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index d44d4ded..fb52fade 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -122,7 +122,7 @@ class BasicAuthBackend(AuthenticationBackend): def auth_required(is_required: bool = True, - login: bool = False, + login: bool = True, redirect: str = "/", template: tuple = None, status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): @@ -152,6 +152,7 @@ def auth_required(is_required: bool = True, applying any format operations. :param is_required: A boolean indicating whether the function requires auth + :param login: Redirect to `/login`, passing `next=` :param redirect: Path to redirect to if is_required isn't True :param template: A three-element template tuple: (path, title_iterable, variable_iterable) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 3c799938..fc1c5242 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -30,14 +30,14 @@ logger = logging.getLogger(__name__) @router.get("/passreset", response_class=HTMLResponse) -@auth_required(False) +@auth_required(False, login=False) async def passreset(request: Request): context = await make_variable_context(request, "Password Reset") return render_template(request, "passreset.html", context) @router.post("/passreset", response_class=HTMLResponse) -@auth_required(False) +@auth_required(False, login=False) async def passreset_post(request: Request, user: str = Form(...), resetkey: str = Form(default=None), @@ -315,7 +315,7 @@ def make_account_form_context(context: dict, @router.get("/register", response_class=HTMLResponse) -@auth_required(False) +@auth_required(False, login=False) async def account_register(request: Request, U: str = Form(default=str()), # Username E: str = Form(default=str()), # Email @@ -341,7 +341,7 @@ async def account_register(request: Request, @router.post("/register", response_class=HTMLResponse) -@auth_required(False) +@auth_required(False, login=False) async def account_register_post(request: Request, U: str = Form(default=str()), # Username E: str = Form(default=str()), # Email @@ -432,7 +432,7 @@ def cannot_edit(request, user): @router.get("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True) +@auth_required(True, redirect="/account/{username}") async def account_edit(request: Request, username: str): user = db.query(User, User.Username == username).first() @@ -448,7 +448,7 @@ async def account_edit(request: Request, @router.post("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True) +@auth_required(True, redirect="/account/{username}") async def account_edit_post(request: Request, username: str, U: str = Form(default=str()), # Username @@ -594,7 +594,7 @@ async def account(request: Request, username: str): @router.get("/accounts/") -@auth_required(True) +@auth_required(True, redirect="/accounts/") @account_type_required({TRUSTED_USER, DEVELOPER, TRUSTED_USER_AND_DEV}) async def accounts(request: Request): context = make_context(request, "Accounts") @@ -602,7 +602,7 @@ async def accounts(request: Request): @router.post("/accounts/") -@auth_required(True) +@auth_required(True, redirect="/accounts/") @account_type_required({TRUSTED_USER, DEVELOPER, TRUSTED_USER_AND_DEV}) async def accounts_post(request: Request, O: int = Form(default=0), # Offset @@ -688,7 +688,7 @@ def render_terms_of_service(request: Request, @router.get("/tos") -@auth_required(True, redirect="/") +@auth_required(True, redirect="/tos") async def terms_of_service(request: Request): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. @@ -709,7 +709,7 @@ async def terms_of_service(request: Request): @router.post("/tos") -@auth_required(True, redirect="/") +@auth_required(True, redirect="/tos") async def terms_of_service_post(request: Request, accept: bool = Form(default=False)): # Query the database for terms that were previously accepted, diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 539f9526..ee6d71ba 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -222,7 +222,7 @@ async def package_base_voters(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comments") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comments") async def pkgbase_comments_post( request: Request, name: str, comment: str = Form(default=str()), @@ -254,7 +254,7 @@ async def pkgbase_comments_post( @router.get("/pkgbase/{name}/comments/{id}/form") -@auth_required(True) +@auth_required(True, login=False) async def pkgbase_comment_form(request: Request, name: str, id: int): """ Produce a comment form for comment {id}. """ pkgbase = get_pkg_or_base(name, PackageBase) @@ -274,7 +274,7 @@ async def pkgbase_comment_form(request: Request, name: str, id: int): @router.post("/pkgbase/{name}/comments/{id}") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comments/{id}") async def pkgbase_comment_post( request: Request, name: str, id: int, comment: str = Form(default=str()), @@ -309,7 +309,7 @@ async def pkgbase_comment_post( @router.post("/pkgbase/{name}/comments/{id}/delete") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/delete") async def pkgbase_comment_delete(request: Request, name: str, id: int): pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -332,7 +332,7 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int): @router.post("/pkgbase/{name}/comments/{id}/undelete") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/undelete") async def pkgbase_comment_undelete(request: Request, name: str, id: int): pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -354,7 +354,7 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int): @router.post("/pkgbase/{name}/comments/{id}/pin") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/pin") async def pkgbase_comment_pin(request: Request, name: str, id: int): pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -376,7 +376,7 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int): @router.post("/pkgbase/{name}/comments/{id}/unpin") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/unpin") async def pkgbase_comment_unpin(request: Request, name: str, id: int): pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) @@ -397,7 +397,7 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int): @router.get("/pkgbase/{name}/comaintainers") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comaintainers") async def package_base_comaintainers(request: Request, name: str) -> Response: # Get the PackageBase. pkgbase = get_pkg_or_base(name, PackageBase) @@ -444,7 +444,7 @@ def remove_users(pkgbase, usernames): @router.post("/pkgbase/{name}/comaintainers") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/comaintainers") async def package_base_comaintainers_post( request: Request, name: str, users: str = Form(default=str())) -> Response: @@ -539,7 +539,7 @@ async def package_base_comaintainers_post( @router.get("/requests") -@auth_required(True, redirect="/") +@auth_required(True, redirect="/requests") async def requests(request: Request, O: int = Query(default=defaults.O), PP: int = Query(default=defaults.PP)): @@ -571,7 +571,7 @@ async def requests(request: Request, @router.get("/pkgbase/{name}/request") -@auth_required(True, login=True, redirect="/pkgbase/{name}") +@auth_required(True, redirect="/pkgbase/{name}") async def package_request(request: Request, name: str): context = make_context(request, "Submit Request") @@ -585,7 +585,7 @@ async def package_request(request: Request, name: str): @router.post("/pkgbase/{name}/request") -@auth_required(True) +@auth_required(True, redirect="/pkgbase/{name}/request") async def pkgbase_request_post(request: Request, name: str, type: str = Form(...), merge_into: str = Form(default=None), @@ -654,7 +654,7 @@ async def pkgbase_request_post(request: Request, name: str, @router.get("/requests/{id}/close") -@auth_required(True) +@auth_required(True, redirect="/requests/{id}/close") async def requests_close(request: Request, id: int): pkgreq = db.query(PackageRequest).filter(PackageRequest.ID == id).first() if not request.user.is_elevated() and request.user != pkgreq.User: @@ -667,7 +667,7 @@ async def requests_close(request: Request, id: int): @router.post("/requests/{id}/close") -@auth_required(True) +@auth_required(True, redirect="/requests/{id}/close") async def requests_close_post(request: Request, id: int, reason: int = Form(default=0), comments: str = Form(default=str())): diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index a977b31a..b897a635 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -45,7 +45,7 @@ ADDVOTE_SPECIFICS = { @router.get("/tu") -@auth_required(True, redirect="/") +@auth_required(True, redirect="/tu") @account_type_required(REQUIRED_TYPES) async def trusted_user(request: Request, coff: int = 0, # current offset @@ -149,7 +149,7 @@ def render_proposal(request: Request, @router.get("/tu/{proposal}") -@auth_required(True, redirect="/") +@auth_required(True, redirect="/tu/{proposal}") @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal(request: Request, proposal: int): context = await make_variable_context(request, "Trusted User") @@ -175,7 +175,7 @@ async def trusted_user_proposal(request: Request, proposal: int): @router.post("/tu/{proposal}") -@auth_required(True, redirect="/") +@auth_required(True, redirect="/tu/{proposal}") @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal_post(request: Request, proposal: int, @@ -223,7 +223,7 @@ async def trusted_user_proposal_post(request: Request, @router.get("/addvote") -@auth_required(True) +@auth_required(True, redirect="/addvote") @account_type_required({"Trusted User", "Trusted User & Developer"}) async def trusted_user_addvote(request: Request, user: str = str(), @@ -243,7 +243,7 @@ async def trusted_user_addvote(request: Request, @router.post("/addvote") -@auth_required(True) +@auth_required(True, redirect="/addvote") @account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote_post(request: Request, user: str = Form(default=str()), diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index 67181db3..0579247e 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -9,7 +9,7 @@ import pytest from fastapi.testclient import TestClient -from aurweb import db +from aurweb import db, util from aurweb.models.account_type import AccountType from aurweb.models.tu_vote import TUVote from aurweb.models.tu_voteinfo import TUVoteInfo @@ -128,7 +128,9 @@ def test_tu_index_guest(client): with client as request: response = request.get("/tu", allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) - assert response.headers.get("location") == "/" + + params = util.urlencode({"next": "/tu"}) + assert response.headers.get("location") == f"/login?{params}" def test_tu_index_unauthorized(client, user): From 8eadb4251da6029cb4edb528b222eebb0d3b821c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 5 Oct 2021 16:04:19 -0700 Subject: [PATCH 0489/1451] feat(FastAPI): add /pkgbase/{name}/[un]flag (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 32 ++++++++++++++++++++++++++++++++ test/test_packages_routes.py | 31 +++++++++++++++++++++++++++++++ test/test_user.py | 1 - 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index ee6d71ba..8790327f 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -701,3 +701,35 @@ async def requests_close_post(request: Request, id: int, notify_.send() return RedirectResponse("/requests", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/flag") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_flag(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential("CRED_PKGBASE_FLAG") + if has_cred and not pkgbase.Flagger: + now = int(datetime.utcnow().timestamp()) + with db.begin(): + pkgbase.OutOfDateTS = now + pkgbase.Flagger = request.user + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/unflag") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_unflag(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential( + "CRED_PKGBASE_UNFLAG", approved=[pkgbase.Flagger]) + if has_cred: + with db.begin(): + pkgbase.OutOfDateTS = None + pkgbase.Flagger = None + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 4118744a..db36d1a9 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1689,3 +1689,34 @@ def test_requests_close_post_rejected(client: TestClient, user: User, assert pkgreq.Status == REJECTED_ID assert pkgreq.Closer == user assert pkgreq.ClosureComment == str() + + +def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, + package: Package): + pkgbase = package.PackageBase + + # We shouldn't have flagged the package yet; assert so. + assert pkgbase.Flagger is None + + # Flag it. + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/flag" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert pkgbase.Flagger == user + + # Now, test that the 'maintainer' user can't unflag it, because they + # didn't flag it to begin with. + maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/unflag" + with client as request: + resp = request.post(endpoint, cookies=maint_cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert pkgbase.Flagger == user + + # Now, unflag it for real. + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert pkgbase.Flagger is None diff --git a/test/test_user.py b/test/test_user.py index 43cbf58a..771611d8 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -166,7 +166,6 @@ def test_user_minimum_passwd_length(): def test_user_has_credential(): - assert user.has_credential("CRED_PKGBASE_FLAG") assert not user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") From 0dfff2bcb24d8915f5fd317c79f5e750f0897e5a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 5 Oct 2021 21:13:51 -0700 Subject: [PATCH 0490/1451] feat(FastAPI): add /pkgbase/{name}/[un]notify (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 36 ++++++++++++++++++++++++++++++++++++ test/test_packages_routes.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 8790327f..ced9ea3d 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -733,3 +733,39 @@ async def pkgbase_unflag(request: Request, name: str): return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/notify") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_notify(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + notif = db.query(pkgbase.notifications.filter( + PackageNotification.UserID == request.user.ID + ).exists()).scalar() + has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + if has_cred and not notif: + with db.begin(): + db.create(PackageNotification, + PackageBase=pkgbase, + User=request.user) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/unnotify") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_unnotify(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + notif = pkgbase.notifications.filter( + PackageNotification.UserID == request.user.ID + ).first() + has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + if has_cred and notif: + with db.begin(): + db.session.delete(notif) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index db36d1a9..1d7fe17c 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1720,3 +1720,36 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger is None + + +def test_pkgbase_notify(client: TestClient, user: User, package: Package): + pkgbase = package.PackageBase + + # We have no notif record yet; assert that. + notif = pkgbase.notifications.filter( + PackageNotification.UserID == user.ID + ).first() + assert notif is None + + # Enable notifications. + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/notify" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + notif = pkgbase.notifications.filter( + PackageNotification.UserID == user.ID + ).first() + assert notif is not None + + # Disable notifications. + endpoint = f"/pkgbase/{pkgbase.Name}/unnotify" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + notif = pkgbase.notifications.filter( + PackageNotification.UserID == user.ID + ).first() + assert notif is None From 0a02df363a80e7571c9a0bbb9829b84f18cf7f4c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 5 Oct 2021 21:32:12 -0700 Subject: [PATCH 0491/1451] feat(FastAPI): add /pkgbase/{name}/[un]vote (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 38 ++++++++++++++++++++++++++++++++++++ test/test_packages_routes.py | 27 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index ced9ea3d..8bfb680e 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -769,3 +769,41 @@ async def pkgbase_unnotify(request: Request, name: str): return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/vote") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_vote(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + vote = pkgbase.package_votes.filter( + PackageVote.UsersID == request.user.ID + ).first() + has_cred = request.user.has_credential("CRED_PKGBASE_VOTE") + if has_cred and not vote: + now = int(datetime.utcnow().timestamp()) + with db.begin(): + db.create(PackageVote, + User=request.user, + PackageBase=pkgbase, + VoteTS=now) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.post("/pkgbase/{name}/unvote") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_unvote(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + vote = pkgbase.package_votes.filter( + PackageVote.UsersID == request.user.ID + ).first() + has_cred = request.user.has_credential("CRED_PKGBASE_VOTE") + if has_cred and vote: + with db.begin(): + db.session.delete(vote) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 1d7fe17c..a03c5920 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1753,3 +1753,30 @@ def test_pkgbase_notify(client: TestClient, user: User, package: Package): PackageNotification.UserID == user.ID ).first() assert notif is None + + +def test_pkgbase_vote(client: TestClient, user: User, package: Package): + pkgbase = package.PackageBase + + # We haven't voted yet. + vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() + assert vote is None + + # Vote for the package. + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/vote" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() + assert vote is not None + + # Remove vote. + endpoint = f"/pkgbase/{pkgbase.Name}/unvote" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() + assert vote is None From 16d516c221112d36ead6ce36b5beb6a54015c8a2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 6 Oct 2021 22:07:20 -0700 Subject: [PATCH 0492/1451] feat(FastAPI): add /pkgbase/{name}/disown (get, post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 63 +++++++++++++++++++++++ templates/packages/disown.html | 55 ++++++++++++++++++++ templates/partials/packages/actions.html | 10 ++-- test/test_packages_routes.py | 65 ++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 templates/packages/disown.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 8bfb680e..af1ebe46 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -807,3 +807,66 @@ async def pkgbase_unvote(request: Request, name: str): return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +def disown_pkgbase(pkgbase: PackageBase, disowner: User): + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + notif = notify.DisownNotification(conn, disowner.ID, pkgbase.ID) + + if disowner != pkgbase.Maintainer: + with db.begin(): + pkgbase.Maintainer = None + else: + co = pkgbase.comaintainers.order_by( + PackageComaintainer.Priority.asc() + ).limit(1).first() + + if co: + with db.begin(): + pkgbase.Maintainer = co.User + db.session.delete(co) + else: + pkgbase.Maintainer = None + + notif.send() + + +@router.get("/pkgbase/{name}/disown") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_disown_get(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + approved=[pkgbase.Maintainer]) + if not has_cred: + return RedirectResponse(f"/pkgbase/{name}", + int(HTTPStatus.SEE_OTHER)) + + context = make_context(request, "Disown Package") + context["pkgbase"] = pkgbase + return render_template(request, "packages/disown.html", context) + + +@router.post("/pkgbase/{name}/disown") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_disown_post(request: Request, name: str, + confirm: bool = Form(default=False)): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + approved=[pkgbase.Maintainer]) + if not has_cred: + return RedirectResponse(f"/pkgbase/{name}", + int(HTTPStatus.SEE_OTHER)) + + if not confirm: + context = make_context(request, "Disown Package") + context["pkgbase"] = pkgbase + context["errors"] = [("The selected packages have not been disowned, " + "check the confirmation checkbox.")] + return render_template(request, "packages/disown.html", context, + status_code=int(HTTPStatus.EXPECTATION_FAILED)) + + disown_pkgbase(pkgbase, request.user) + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/packages/disown.html b/templates/packages/disown.html new file mode 100644 index 00000000..8d5a8574 --- /dev/null +++ b/templates/packages/disown.html @@ -0,0 +1,55 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + + {% if errors %} +
      + {% for error in errors %} +
    • {{ error | tr }}
    • + {% endfor %} +
    + {% endif %} + +
    +

    {{ "Disown Package" | tr }}: {{ pkgbase.Name }}

    + +

    + {{ + "Use this form to disown the package base %s%s%s which " + "includes the following packages: " + | tr | format("", pkgbase.Name, "") | safe + }} +

    + +
      + {% for package in pkgbase.packages.all() %} +
    • {{ package.Name }}
    • + {% endfor %} +
    + +

    + {{ + "By selecting the checkbox, you confirm that you want to " + "disown the package." | tr + }} +

    + +
    +
    +

    + +

    +

    + +

    +
    +
    + +
    +{% endblock %} diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index f1863663..2b26144e 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -121,13 +121,9 @@ {% endif %} {% if request.user.has_credential("CRED_PKGBASE_DISOWN", approved=[pkgbase.Maintainer]) %}
  • -
    - -
    + + {{ "Disown Package" | tr }} +
  • {% endif %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index a03c5920..c9622431 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1780,3 +1780,68 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() assert vote is None + + +def test_pkgbase_disown_as_tu(client: TestClient, tu_user: User, + package: Package): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/disown" + + # But we do here. + with client as request: + resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_pkgbase_disown_as_sole_maintainer(client: TestClient, + maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/disown" + + # But we do here. + with client as request: + resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_pkgbase_disown(client: TestClient, user: User, maintainer: User, + package: Package): + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + user_cookies = {"AURSID": user.login(Request(), "testPassword")} + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/disown" + + with db.begin(): + db.create(PackageComaintainer, + User=user, + PackageBase=pkgbase, + Priority=1) + + # GET as a normal user, which is rejected for lack of credentials. + with client as request: + resp = request.get(endpoint, cookies=user_cookies, + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # GET as the maintainer. + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # POST as a normal user, which is rejected for lack of credentials. + with client as request: + resp = request.post(endpoint, cookies=user_cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # POST as the maintainer without "confirm". + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.EXPECTATION_FAILED) + + # POST as the maintainer with "confirm". + with client as request: + resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) From c8d01cc5e8083a6586ae61a6c3371d7ed2428f6a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 19 Sep 2021 19:27:29 -0700 Subject: [PATCH 0493/1451] feat(FastAPI): add aurweb.util.apply_all(iterable, fn) A helper which allows us to apply a specific function to each item in an iterable. Signed-off-by: Kevin Morris --- aurweb/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aurweb/util.py b/aurweb/util.py index 08e6d7c6..44f711f1 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -7,7 +7,7 @@ import secrets import string from datetime import datetime -from typing import Any, Dict +from typing import Any, Callable, Dict, Iterable from urllib.parse import urlencode, urlparse from zoneinfo import ZoneInfo @@ -167,3 +167,8 @@ def add_samesite_fields(response: Response, value: str): def get_ssh_fingerprints(): return aurweb.config.get_section("fingerprints") or {} + + +def apply_all(iterable: Iterable, fn: Callable): + for item in iterable: + fn(item) From ed68fa2b57f7f4cf916fd2e40312e1f64da2c71e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 19 Sep 2021 19:27:03 -0700 Subject: [PATCH 0494/1451] feat(FastAPI): add aurweb.db.delete_all(iterable) Signed-off-by: Kevin Morris --- aurweb/db.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aurweb/db.py b/aurweb/db.py index 2b934300..c1e80751 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -2,6 +2,8 @@ import functools import math import re +from typing import Iterable + from sqlalchemy import event from sqlalchemy.orm import scoped_session @@ -71,6 +73,12 @@ def delete(model, *args, **kwargs): session.delete(record) +def delete_all(iterable: Iterable): + with begin(): + for obj in iterable: + session.delete(obj) + + def rollback(): session.rollback() From 0ddc969bdcd07fe0181e05a8e2abdb2e23301212 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 6 Oct 2021 15:33:23 -0700 Subject: [PATCH 0495/1451] feat(FastAPI-dev): add package_delete helper Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 76 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index af1ebe46..40322785 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -1,6 +1,6 @@ from datetime import datetime from http import HTTPStatus -from typing import Any, Dict +from typing import Any, Dict, List from fastapi import APIRouter, Form, HTTPException, Query, Request, Response from fastapi.responses import JSONResponse, RedirectResponse @@ -11,7 +11,7 @@ import aurweb.models.package_comment import aurweb.models.package_keyword import aurweb.packages.util -from aurweb import db, defaults, l10n +from aurweb import db, defaults, l10n, util from aurweb.auth import auth_required from aurweb.models.license import License from aurweb.models.package import Package @@ -26,7 +26,7 @@ from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID, from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID -from aurweb.models.request_type import RequestType +from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted @@ -116,6 +116,76 @@ async def packages(request: Request) -> Response: return await packages_get(request, context) +def create_request_if_missing(requests: List[PackageRequest], + reqtype: RequestType, + user: User, + package: Package): + now = int(datetime.utcnow().timestamp()) + pkgreq = db.query(PackageRequest).filter( + PackageRequest.PackageBaseName == package.PackageBase.Name + ).first() + if not pkgreq: + # No PackageRequest existed. Create one. + comments = "Automatically generated by aurweb." + closure_comment = "Deleted by aurweb." + pkgreq = db.create(PackageRequest, + RequestType=reqtype, + PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + User=user, + Status=ACCEPTED_ID, + Comments=comments, + ClosureComment=closure_comment, + ClosedTS=now, + Closer=user) + requests.append(pkgreq) + + +def delete_package(deleter: User, + package: Package): + notifications = [] + requests = [] + bases_to_delete = [] + + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + # In all cases, though, just delete the Package in question. + if package.PackageBase.packages.count() == 1: + reqtype = db.query(RequestType).filter( + RequestType.ID == DELETION_ID + ).first() + + with db.begin(): + create_request_if_missing( + requests, reqtype, deleter, package) + + bases_to_delete.append(package.PackageBase) + + # Prepare DeleteNotification. + notifications.append( + notify.DeleteNotification(conn, deleter.ID, package.PackageBase.ID) + ) + + # For each PackageRequest created, mock up an open and close notification. + basename = package.PackageBase.Name + for pkgreq in requests: + notifications.append( + notify.RequestOpenNotification( + conn, deleter.ID, pkgreq.ID, reqtype.Name, + pkgreq.PackageBase.ID, merge_into=basename or None) + ) + notifications.append( + notify.RequestCloseNotification( + conn, deleter.ID, pkgreq.ID, pkgreq.status_display()) + ) + + # Perform all the deletions. + db.delete_all([package]) + db.delete_all(bases_to_delete) + + # Send out all the notifications. + util.apply_all(notifications, lambda n: n.send()) + + async def make_single_context(request: Request, pkgbase: PackageBase) -> Dict[str, Any]: """ Make a basic context for package or pkgbase. From 4e7d2295da657ece2dc0fd1422ecd0e75e4facb7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 6 Oct 2021 20:17:58 -0700 Subject: [PATCH 0496/1451] fix(FastAPI): add package-related missing backref cascades Signed-off-by: Kevin Morris --- aurweb/models/package_comment.py | 3 ++- aurweb/models/package_dependency.py | 3 ++- aurweb/models/package_relation.py | 3 ++- aurweb/models/package_source.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aurweb/models/package_comment.py b/aurweb/models/package_comment.py index c52ee270..92ae8911 100644 --- a/aurweb/models/package_comment.py +++ b/aurweb/models/package_comment.py @@ -17,7 +17,8 @@ class PackageComment(Base): Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), nullable=False) PackageBase = relationship( - "PackageBase", backref=backref("comments", lazy="dynamic"), + "PackageBase", backref=backref("comments", lazy="dynamic", + cascade="all,delete"), foreign_keys=[PackageBaseID]) UsersID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index 9ce0b019..fb66c6f2 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -15,7 +15,8 @@ class PackageDependency(Base): Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False) Package = relationship( - "Package", backref=backref("package_dependencies", lazy="dynamic"), + "Package", backref=backref("package_dependencies", lazy="dynamic", + cascade="all,delete"), foreign_keys=[PackageID]) DepTypeID = Column( diff --git a/aurweb/models/package_relation.py b/aurweb/models/package_relation.py index 1e6c146c..d4921859 100644 --- a/aurweb/models/package_relation.py +++ b/aurweb/models/package_relation.py @@ -16,7 +16,8 @@ class PackageRelation(Base): Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False) Package = relationship( - "Package", backref=backref("package_relations", lazy="dynamic"), + "Package", backref=backref("package_relations", lazy="dynamic", + cascade="all,delete"), foreign_keys=[PackageID]) RelTypeID = Column( diff --git a/aurweb/models/package_source.py b/aurweb/models/package_source.py index 4ffa23df..f016bee0 100644 --- a/aurweb/models/package_source.py +++ b/aurweb/models/package_source.py @@ -13,7 +13,8 @@ class PackageSource(Base): PackageID = Column(Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False) Package = relationship( - "Package", backref=backref("package_sources", lazy="dynamic"), + "Package", backref=backref("package_sources", lazy="dynamic", + cascade="all,delete"), foreign_keys=[PackageID]) __mapper_args__ = {"primary_key": [PackageID]} From d38abd783250d13f93294944f3897a38e1209d31 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 6 Oct 2021 13:54:14 -0700 Subject: [PATCH 0497/1451] feat(FastAPI): add /pkgbase/{name}/delete (get, post) In addition, we've had to add cascade arguments to backref so sqlalchemy treats the relationships as proper cascades. Furthermore, our pkgbase actions template was not rendering actions properly based on TU credentials. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 37 ++++++++++++++++++++++ templates/packages/delete.html | 56 ++++++++++++++++++++++++++++++++++ test/test_packages_routes.py | 46 ++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 templates/packages/delete.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 40322785..4426d0be 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -940,3 +940,40 @@ async def pkgbase_disown_post(request: Request, name: str, disown_pkgbase(pkgbase, request.user) return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) + + +@router.get("/pkgbase/{name}/delete") +@auth_required(True) +async def pkgbase_delete_get(request: Request, name: str): + if not request.user.has_credential("CRED_PKGBASE_DELETE"): + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + context = make_context(request, "Package Deletion") + context["pkgbase"] = get_pkg_or_base(name, PackageBase) + return render_template(request, "packages/delete.html", context) + + +@router.post("/pkgbase/{name}/delete") +@auth_required(True) +async def pkgbase_delete_post(request: Request, name: str, + confirm: bool = Form(default=False)): + pkgbase = get_pkg_or_base(name, PackageBase) + + if not request.user.has_credential("CRED_PKGBASE_DELETE"): + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + if not confirm: + context = make_context(request, "Package Deletion") + context["pkgbase"] = pkgbase + context["errors"] = [("The selected packages have not been deleted, " + "check the confirmation checkbox.")] + return render_template(request, "packages/delete.html", context, + status_code=int(HTTPStatus.EXPECTATION_FAILED)) + + packages = pkgbase.packages.all() + for package in packages: + delete_package(request.user, package) + + return RedirectResponse("/packages", status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/packages/delete.html b/templates/packages/delete.html new file mode 100644 index 00000000..6e882d05 --- /dev/null +++ b/templates/packages/delete.html @@ -0,0 +1,56 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + + {% if errors %} +
      + {% for error in errors %} +
    • {{ error | tr }}
    • + {% endfor %} +
    + {% endif %} + +
    +

    {{ "Delete Package" | tr }}: {{ pkgbase.Name }}

    + +

    + {{ + "Use this form to delete the package base %s%s%s and " + "the following packages from the AUR: " + | tr | format("", pkgbase.Name, "") | safe + }} +

    + +
      + {% for package in pkgbase.packages.all() %} +
    • {{ package.Name }}
    • + {% endfor %} +
    + +

    + {{ + "Deletion of a package is permanent. " + "Select the checkbox to confirm action." | tr + }} +

    + +
    +
    +

    + +

    + +

    + +

    +
    +
    + +
    +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index c9622431..1f258497 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1845,3 +1845,49 @@ def test_pkgbase_disown(client: TestClient, user: User, maintainer: User, with client as request: resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_pkgbase_delete_unauthorized(client: TestClient, user: User, + package: Package): + pkgbase = package.PackageBase + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/delete" + + # Test GET. + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + # Test POST. + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + +def test_pkgbase_delete(client: TestClient, tu_user: User, package: Package): + pkgbase = package.PackageBase + + # Test that the GET request works. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/delete" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # Test that POST works and denies us because we haven't confirmed. + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.EXPECTATION_FAILED) + + # Test that we can actually delete the pkgbase. + with client as request: + resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Let's assert that the package base record got removed. + record = db.query(PackageBase).filter( + PackageBase.Name == pkgbase.Name + ).first() + assert record is None From 01fb42c5d97fe930b63ef8b71d4b2a2b62f32a5e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 22:44:54 -0700 Subject: [PATCH 0498/1451] fix(scripts.popupdate): use forced-utc timestamp Additionally, clean up some controversial PEP-8 warnings by removing the '+' string concatenation. Signed-off-by: Kevin Morris --- aurweb/scripts/popupdate.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index b1e70403..96155eef 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 -import time +from datetime import datetime import aurweb.db def main(): conn = aurweb.db.Connection() - conn.execute("UPDATE PackageBases SET NumVotes = (" + - "SELECT COUNT(*) FROM PackageVotes " + - "WHERE PackageVotes.PackageBaseID = PackageBases.ID)") + conn.execute(("UPDATE PackageBases SET NumVotes = (" + "SELECT COUNT(*) FROM PackageVotes " + "WHERE PackageVotes.PackageBaseID = PackageBases.ID)")) - now = int(time.time()) - conn.execute("UPDATE PackageBases SET Popularity = (" + - "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " + - "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " + - "PackageBases.ID AND NOT VoteTS IS NULL)", [now]) + now = int(datetime.utcnow().timestamp()) + conn.execute(("UPDATE PackageBases SET Popularity = (" + "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " + "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " + "PackageBases.ID AND NOT VoteTS IS NULL)"), [now]) conn.commit() conn.close() From 63498f5edde58d231eba34359ac231ea217a34d9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 22:48:31 -0700 Subject: [PATCH 0499/1451] fix(FastAPI): use popupdate when [un]voting The `aurweb.scripts.popupdate` script is used to maintain the NumVotes and Popularity field. We could do the NumVotes change more simply; however, since this is already a long-term implementation, we're going to use it until we move scripts over to ORM. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 10 +++++++- aurweb/scripts/popupdate.py | 45 ++++++++++++++++++++++++++++-------- test/test_packages_routes.py | 2 ++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 4426d0be..f806f054 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -30,7 +30,7 @@ from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted -from aurweb.scripts import notify +from aurweb.scripts import notify, popupdate from aurweb.scripts.rendercomment import update_comment_render from aurweb.templates import make_context, render_raw_template, render_template @@ -858,6 +858,10 @@ async def pkgbase_vote(request: Request, name: str): PackageBase=pkgbase, VoteTS=now) + # Update NumVotes/Popularity. + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + popupdate.run_single(conn, pkgbase) + return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) @@ -875,6 +879,10 @@ async def pkgbase_unvote(request: Request, name: str): with db.begin(): db.session.delete(vote) + # Update NumVotes/Popularity. + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + popupdate.run_single(conn, pkgbase) + return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index 96155eef..fa82208d 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -5,17 +5,44 @@ from datetime import datetime import aurweb.db -def main(): - conn = aurweb.db.Connection() - conn.execute(("UPDATE PackageBases SET NumVotes = (" - "SELECT COUNT(*) FROM PackageVotes " - "WHERE PackageVotes.PackageBaseID = PackageBases.ID)")) +def run_single(conn, pkgbase): + """ A single popupdate. The given pkgbase instance will be + refreshed after the database update is done. + + NOTE: This function is compatible only with aurweb FastAPI. + + :param conn: db.Connection[Executor] + :param pkgbase: Instance of db.PackageBase + """ + + conn.execute("UPDATE PackageBases SET NumVotes = (" + "SELECT COUNT(*) FROM PackageVotes " + "WHERE PackageVotes.PackageBaseID = PackageBases.ID) " + "WHERE PackageBases.ID = ?", [pkgbase.ID]) now = int(datetime.utcnow().timestamp()) - conn.execute(("UPDATE PackageBases SET Popularity = (" - "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " - "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " - "PackageBases.ID AND NOT VoteTS IS NULL)"), [now]) + conn.execute("UPDATE PackageBases SET Popularity = (" + "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " + "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " + "PackageBases.ID AND NOT VoteTS IS NULL) WHERE " + "PackageBases.ID = ?", [now, pkgbase.ID]) + + conn.commit() + conn.close() + aurweb.db.session.refresh(pkgbase) + + +def main(): + conn = aurweb.db.Connection() + conn.execute("UPDATE PackageBases SET NumVotes = (" + "SELECT COUNT(*) FROM PackageVotes " + "WHERE PackageVotes.PackageBaseID = PackageBases.ID)") + + now = int(datetime.utcnow().timestamp()) + conn.execute("UPDATE PackageBases SET Popularity = (" + "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " + "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " + "PackageBases.ID AND NOT VoteTS IS NULL)", [now]) conn.commit() conn.close() diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 1f258497..7b9c520c 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1771,6 +1771,7 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() assert vote is not None + assert pkgbase.NumVotes == 1 # Remove vote. endpoint = f"/pkgbase/{pkgbase.Name}/unvote" @@ -1780,6 +1781,7 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() assert vote is None + assert pkgbase.NumVotes == 0 def test_pkgbase_disown_as_tu(client: TestClient, tu_user: User, From 305d07797371087b5e04bb49827d619c793a2d63 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 7 Oct 2021 22:01:04 -0700 Subject: [PATCH 0500/1451] feat(FastAPI): add /pkgbase/{name}/adopt (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 17 ++++++++++++ templates/partials/packages/actions.html | 19 +++++++++++--- test/test_packages_routes.py | 33 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index f806f054..b623ca10 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -950,6 +950,23 @@ async def pkgbase_disown_post(request: Request, name: str, status_code=int(HTTPStatus.SEE_OTHER)) +@router.post("/pkgbase/{name}/adopt") +@auth_required(True) +async def pkgbase_adopt_post(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential("CRED_PKGBASE_ADOPT") + if has_cred or not pkgbase.Maintainer: + # If the user has credentials, they'll adopt the package regardless + # of maintainership. Otherwise, we'll promote the user to maintainer + # if no maintainer currently exists. + with db.begin(): + pkgbase.Maintainer = request.user + + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + @router.get("/pkgbase/{name}/delete") @auth_required(True) async def pkgbase_delete_get(request: Request, name: str): diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 2b26144e..dd83c84d 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -119,12 +119,23 @@ {% endif %} - {% if request.user.has_credential("CRED_PKGBASE_DISOWN", approved=[pkgbase.Maintainer]) %} + {% if not result.Maintainer %}
  • - - {{ "Disown Package" | tr }} - +
    + +
  • + {% else %} + {% if request.user.has_credential("CRED_PKGBASE_DISOWN", approved=[pkgbase.Maintainer]) %} +
  • + + {{ "Disown Package" | tr }} + +
  • + {% endif %} {% endif %}

    diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 7b9c520c..86949996 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1849,6 +1849,39 @@ def test_pkgbase_disown(client: TestClient, user: User, maintainer: User, assert resp.status_code == int(HTTPStatus.SEE_OTHER) +def test_pkgbase_adopt(client: TestClient, user: User, tu_user: User, + maintainer: User, package: Package): + # Unset the maintainer as if package is orphaned. + with db.begin(): + package.PackageBase.Maintainer = None + + pkgbasename = package.PackageBase.Name + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbasename}/adopt" + + # Adopt the package base. + with client as request: + resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert package.PackageBase.Maintainer == maintainer + + # Try to adopt it when it already has a maintainer; nothing changes. + user_cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, cookies=user_cookies, + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert package.PackageBase.Maintainer == maintainer + + # Steal the package as a TU. + tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, cookies=tu_cookies, + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert package.PackageBase.Maintainer == tu_user + + def test_pkgbase_delete_unauthorized(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase From 5bbc94f2ef333bbe5d33ee1893067e2864de5eb1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 9 Oct 2021 18:41:32 -0700 Subject: [PATCH 0501/1451] fix(FastAPI): add /pkgbase/{name}/flag (get) This was missed in the [un]flag (post) commit. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 28 +++++++++++++++++- templates/packages/flag.html | 57 ++++++++++++++++++++++++++++++++++++ test/test_packages_routes.py | 20 ++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 templates/packages/flag.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index b623ca10..8f4a7e1f 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -773,17 +773,42 @@ async def requests_close_post(request: Request, id: int, return RedirectResponse("/requests", status_code=int(HTTPStatus.SEE_OTHER)) +@router.get("/pkgbase/{name}/flag") +@auth_required(True, redirect="/pkgbase/{name}") +async def pkgbase_flag_get(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential("CRED_PKGBASE_FLAG") + if not has_cred or pkgbase.Flagger is not None: + return RedirectResponse(f"/pkgbase/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + context = make_context(request, "Flag Package Out-Of-Date") + context["pkgbase"] = pkgbase + return render_template(request, "packages/flag.html", context) + + @router.post("/pkgbase/{name}/flag") @auth_required(True, redirect="/pkgbase/{name}") -async def pkgbase_flag(request: Request, name: str): +async def pkgbase_flag_post(request: Request, name: str, + comments: str = Form(default=str())): pkgbase = get_pkg_or_base(name, PackageBase) + if not comments: + context = make_context(request, "Flag Package Out-Of-Date") + context["pkgbase"] = pkgbase + context["errors"] = ["The selected packages have not been flagged, " + "please enter a comment."] + return render_template(request, "packages/flag.html", context, + status_code=int(HTTPStatus.BAD_REQUEST)) + has_cred = request.user.has_credential("CRED_PKGBASE_FLAG") if has_cred and not pkgbase.Flagger: now = int(datetime.utcnow().timestamp()) with db.begin(): pkgbase.OutOfDateTS = now pkgbase.Flagger = request.user + pkgbase.FlaggerComment = comments return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) @@ -800,6 +825,7 @@ async def pkgbase_unflag(request: Request, name: str): with db.begin(): pkgbase.OutOfDateTS = None pkgbase.Flagger = None + pkgbase.FlaggerComment = str() return RedirectResponse(f"/pkgbase/{name}", status_code=int(HTTPStatus.SEE_OTHER)) diff --git a/templates/packages/flag.html b/templates/packages/flag.html new file mode 100644 index 00000000..4e133acb --- /dev/null +++ b/templates/packages/flag.html @@ -0,0 +1,57 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {{ "Flag Package Out-Of-Date" | tr }}: {{ pkgbase.Name }}

    + +

    + {{ + "Use this form to flag the package base %s%s%s and " + "the following packages out-of-date: " + | tr | format("", pkgbase.Name, "") | safe + }} +

    + +
      + {% for package in pkgbase.packages.all() %} +
    • {{ package.Name }}
    • + {% endfor %} +
    + +

    + {{ + "Please do %snot%s use this form to report bugs. " + "Use the package comments instead." + | tr | format("", "") | safe + }} + {{ + "Enter details on why the package is out-of-date below, " + "preferably including links to the release announcement " + "or the new release tarball." | tr + }} +

    + + {% if errors %} +
      + {% for error in errors %} +
    • {{ error | tr }}
    • + {% endfor %} +
    + {% endif %} + +
    +
    +

    + + +

    +

    + +

    +
    +
    +
    +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 86949996..12d7e33e 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1698,13 +1698,31 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # We shouldn't have flagged the package yet; assert so. assert pkgbase.Flagger is None - # Flag it. cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/flag" + + # Get the flag page. + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # Try to flag it without a comment. with client as request: resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + # Flag it with a valid comment. + with client as request: + resp = request.post(endpoint, {"comments": "Test"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger == user + assert pkgbase.FlaggerComment == "Test" + + # Now try to perform a get; we should be redirected because + # it's already flagged. + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Now, test that the 'maintainer' user can't unflag it, because they # didn't flag it to begin with. From d9ab65cb6f2e0f0985f2463c352acc52621e1026 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 9 Oct 2021 20:46:52 -0700 Subject: [PATCH 0502/1451] add Feedback.md GitLab issue template Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Feedback.md | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .gitlab/issue_templates/Feedback.md diff --git a/.gitlab/issue_templates/Feedback.md b/.gitlab/issue_templates/Feedback.md new file mode 100644 index 00000000..e32120aa --- /dev/null +++ b/.gitlab/issue_templates/Feedback.md @@ -0,0 +1,56 @@ +**NOTE:** This issue template is only applicable to FastAPI implementations +in the code-base, which only exists within the `pu` branch. If you wish to +file an issue for the current PHP implementation of aurweb, please file a +standard issue prefixed with `[Bug]` or `[Feature]`. + + +**Checklist** + +- [ ] I have prefixed the issue title with `[Feedback]` along with a message + pointing to the route or feature tested. + - Example: `[Feedback] /packages/{name}` +- [ ] I have completed the [Changes](#changes) section. +- [ ] I have completed the [Bugs](#bugs) section. +- [ ] I have completed the [Improvements](#improvements) section. +- [ ] I have completed the [Summary](#summary) section. + +### Changes + +Please describe changes in user experience when compared to the PHP +implementation. This section can actually hold a lot of info if you +are up for it -- changes in routes, HTML rendering, back-end behavior, +etc. + +If you cannot see any changes from your standpoint, include a short +statement about that fact. + +### Bugs + +Please describe any bugs you've experienced while testing the route +pertaining to this issue. A "perfect" bug report would include your +specific experience, what you expected to occur, and what happened +otherwise. If you can, please include output of `docker-compose logs fastapi` +with your report; especially if any unintended exceptions occurred. + +### Improvements + +If you've experienced improvements in the route when compared to PHP, +please do include those here. We'd like to know if users are noticing +these improvements and how they feel about them. + +There are multiple routes with no improvements. For these, just include +a short sentence about the fact that you've experienced none. + +### Summary + +First: If you've gotten here and completed the [Changes](#changes), +[Bugs](#bugs), and [Improvements](#improvements) sections, we'd like +to thank you very much for your contribution and willingness to test. +We are not a company, and we are not a large team; any bit of assistance +here helps the project astronomically and moves us closer toward a +new release. + +That being said: please include an overall summary of your experience +and how you felt about the current implementation which you're testing +in comparison with PHP (current aur.archlinux.org, or https://localhost:8443 +through docker). From 34c96ed81b017b1b8273e14d98d69a69f9029e37 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 9 Oct 2021 20:46:52 -0700 Subject: [PATCH 0503/1451] add Feedback.md GitLab issue template Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Feedback.md | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .gitlab/issue_templates/Feedback.md diff --git a/.gitlab/issue_templates/Feedback.md b/.gitlab/issue_templates/Feedback.md new file mode 100644 index 00000000..e32120aa --- /dev/null +++ b/.gitlab/issue_templates/Feedback.md @@ -0,0 +1,56 @@ +**NOTE:** This issue template is only applicable to FastAPI implementations +in the code-base, which only exists within the `pu` branch. If you wish to +file an issue for the current PHP implementation of aurweb, please file a +standard issue prefixed with `[Bug]` or `[Feature]`. + + +**Checklist** + +- [ ] I have prefixed the issue title with `[Feedback]` along with a message + pointing to the route or feature tested. + - Example: `[Feedback] /packages/{name}` +- [ ] I have completed the [Changes](#changes) section. +- [ ] I have completed the [Bugs](#bugs) section. +- [ ] I have completed the [Improvements](#improvements) section. +- [ ] I have completed the [Summary](#summary) section. + +### Changes + +Please describe changes in user experience when compared to the PHP +implementation. This section can actually hold a lot of info if you +are up for it -- changes in routes, HTML rendering, back-end behavior, +etc. + +If you cannot see any changes from your standpoint, include a short +statement about that fact. + +### Bugs + +Please describe any bugs you've experienced while testing the route +pertaining to this issue. A "perfect" bug report would include your +specific experience, what you expected to occur, and what happened +otherwise. If you can, please include output of `docker-compose logs fastapi` +with your report; especially if any unintended exceptions occurred. + +### Improvements + +If you've experienced improvements in the route when compared to PHP, +please do include those here. We'd like to know if users are noticing +these improvements and how they feel about them. + +There are multiple routes with no improvements. For these, just include +a short sentence about the fact that you've experienced none. + +### Summary + +First: If you've gotten here and completed the [Changes](#changes), +[Bugs](#bugs), and [Improvements](#improvements) sections, we'd like +to thank you very much for your contribution and willingness to test. +We are not a company, and we are not a large team; any bit of assistance +here helps the project astronomically and moves us closer toward a +new release. + +That being said: please include an overall summary of your experience +and how you felt about the current implementation which you're testing +in comparison with PHP (current aur.archlinux.org, or https://localhost:8443 +through docker). From 27fbda5e7ba21a43c64ca0324c24f42a484196c0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 9 Oct 2021 22:00:18 -0700 Subject: [PATCH 0504/1451] feat(FastAPI): add get_(errors|successes) testing HTML helpers These functions will allow us to more easily check errors or success messages when testing routes. Signed-off-by: Kevin Morris --- aurweb/testing/html.py | 11 +++++++++++ test/test_html.py | 22 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/aurweb/testing/html.py b/aurweb/testing/html.py index d5f0c256..f01aaf3d 100644 --- a/aurweb/testing/html.py +++ b/aurweb/testing/html.py @@ -1,4 +1,5 @@ from io import StringIO +from typing import List from lxml import etree @@ -12,3 +13,13 @@ def parse_root(html: str) -> etree.Element: :return: etree.Element """ return etree.parse(StringIO(html), parser) + + +def get_errors(content: str) -> List[etree._Element]: + root = parse_root(content) + return root.xpath('//ul[@class="errorlist"]/li') + + +def get_successes(content: str) -> List[etree._Element]: + root = parse_root(content) + return root.xpath('//ul[@class="success"]/li') diff --git a/test/test_html.py b/test/test_html.py index 562d6a63..2018840b 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -9,7 +9,7 @@ from aurweb import asgi, db from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID, AccountType from aurweb.models.user import User from aurweb.testing import setup_test_db -from aurweb.testing.html import parse_root +from aurweb.testing.html import get_errors, get_successes, parse_root from aurweb.testing.requests import Request @@ -97,3 +97,23 @@ def test_archdev_navbar_authenticated_tu(client: TestClient, items = root.xpath('//div[@id="archdev-navbar"]/ul/li/a') for i, item in enumerate(items): assert item.text.strip() == expected[i] + + +def test_get_errors(): + html = """ +
      +
    • Test
    • +
    +""" + errors = get_errors(html) + assert errors[0].text.strip() == "Test" + + +def test_get_successes(): + html = """ +
      +
    • Test
    • +
    +""" + successes = get_successes(html) + assert successes[0].text.strip() == "Test" From 4525a11d923f3669e46204626b7c1115927d4703 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 10 Oct 2021 00:59:08 -0700 Subject: [PATCH 0505/1451] fix(FastAPI): change a deep copy instead of original This was updating offsets and causing unintended behavior. We should be a bit more functional anyway. Signed-off-by: Kevin Morris --- aurweb/util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aurweb/util.py b/aurweb/util.py index 44f711f1..61ed5cfb 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,4 +1,5 @@ import base64 +import copy import logging import math import random @@ -127,9 +128,10 @@ def as_timezone(dt: datetime, timezone: str): def extend_query(query: Dict[str, Any], *additions) -> Dict[str, Any]: """ Add additional key value pairs to query. """ + q = copy.copy(query) for k, v in list(additions): - query[k] = v - return query + q[k] = v + return q def to_qs(query: Dict[str, Any]) -> str: From 68383b79e24b645c0079627149187c06b3dda734 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 11 Oct 2021 14:13:29 -0700 Subject: [PATCH 0506/1451] add Feature.md GitLab issue template Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Feature.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .gitlab/issue_templates/Feature.md diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md new file mode 100644 index 00000000..5b1524b1 --- /dev/null +++ b/.gitlab/issue_templates/Feature.md @@ -0,0 +1,30 @@ +- [ ] I have summed up the feature in concise words in the [Summary](#summary) section. +- [ ] I have completely described the feature in the [Description](#description) section. +- [ ] I have completed the [Blockers](#blockers) section. + +### Summary + +Fill this section out with a concise wording about the feature being +requested. + +Example: _A new `Tyrant` account type for users_. + +### Description + +Describe your feature in full detail. + +Example: _The `Tyrant` account type should be used to allow a user to be +tyrannical. When a user is a `Tyrant`, they should be able to assassinate +users due to not complying with their laws. Laws can be configured by updating +the Tyrant laws page at https://aur.archlinux.org/account/{username}/laws. +More specifics about laws._ + +### Blockers + +Include any blockers in a list. If there are no blockers, this section +should be omitted from the issue. + +Example: + +- [Feature] Do not allow users to be Tyrants + - \<(issue|merge_request)_link\> From 3d971bfc8d70c48f69090b7ec0b0b1899178c03d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 11 Oct 2021 14:48:00 -0700 Subject: [PATCH 0507/1451] add Bug.md GitLab issue template Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Bug.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .gitlab/issue_templates/Bug.md diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 00000000..d84a5181 --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,34 @@ +- [ ] I have described the bug in complete detail in the + [Description](#description) section. +- [ ] I have specified steps in the [Reproduction](#reproduction) section. +- [ ] I have included any logs related to the bug in the + [Logs](#logs) section. +- [ ] I have included the versions which are affected in the + [Version(s)](#versions) section. + +### Description + +Describe the bug in full detail. + +### Reproduction + +Describe a specific set of actions that can be used to reproduce +this bug. + +### Logs + +If you have any logs relevent to the bug, include them here in +quoted or code blocks. + +### Version(s) + +In this section, please include a list of versions you have found +to be affected by this program. This can either come in the form +of `major.minor.patch` (if it affects a release tarball), or a +commit hash if the bug does not directly affect a release version. + +All development is done without modifying version displays in +aurweb's HTML render output. If you're testing locally, use the +commit on which you are experiencing the bug. If you have found +a bug which exists on live aur.archlinux.org, include the version +located at the bottom of the webpage. From 748faca87d314e23557a1e20223db939d8b19192 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 12 Oct 2021 17:55:03 -0700 Subject: [PATCH 0508/1451] fix(FastAPI): translate some untranslated strings Affects: templates/partials/packages/search_actions.html Signed-off-by: Kevin Morris --- templates/partials/packages/search_actions.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/partials/packages/search_actions.html b/templates/partials/packages/search_actions.html index 2f5fe2e7..221189fb 100644 --- a/templates/partials/packages/search_actions.html +++ b/templates/partials/packages/search_actions.html @@ -18,8 +18,8 @@ - +

    From 22b3af61b568732861be17e9759be68715a709fb Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 13 Oct 2021 17:10:16 -0700 Subject: [PATCH 0509/1451] fix(PHP): sanitize and produce metrics at shutdown This change now requires that PHP routes do not return HTTP 404 to be considered for the /metrics population. Additionally, we make a small sanitization here to avoid trailing '/' characters, unless we're on the homepage route. Signed-off-by: Kevin Morris --- web/html/index.php | 24 ++--------------------- web/lib/metricfuncs.inc.php | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/web/html/index.php b/web/html/index.php index 82a44c55..99046930 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -13,28 +13,8 @@ $query_string = $_SERVER['QUERY_STRING']; // If no options.cache is configured, we no-op metric storage operations. $is_cached = defined('EXTENSION_LOADED_APC') || defined('EXTENSION_LOADED_MEMCACHE'); -if ($is_cached) { - $method = $_SERVER['REQUEST_METHOD']; - // We'll always add +1 to our total request count to this $path, - // unless this path == /metrics. - if ($path !== "/metrics") - add_metric("http_requests_count", $method, $path); - - // Extract $type out of $query_string, if we can. - $type = null; - $query = array(); - if ($query_string) - parse_str($query_string, $query); - $type = $query['type']; - - // Only store RPC metrics for valid types. - $good_types = [ - "info", "multiinfo", "search", "msearch", - "suggest", "suggest-pkgbase", "get-comment-form" - ]; - if ($path === "/rpc" && in_array($type, $good_types)) - add_metric("api_requests_count", $method, $path, $type); -} +if ($is_cached) + register_shutdown_function('update_metrics'); if (config_get_bool('options', 'enable-maintenance') && (empty($tokens[1]) || ($tokens[1] != "css" && $tokens[1] != "images"))) { if (!in_array($_SERVER['REMOTE_ADDR'], explode(" ", config_get('options', 'maintenance-exceptions')))) { diff --git a/web/lib/metricfuncs.inc.php b/web/lib/metricfuncs.inc.php index acfc30d7..7ebb59be 100644 --- a/web/lib/metricfuncs.inc.php +++ b/web/lib/metricfuncs.inc.php @@ -13,6 +13,44 @@ use \Prometheus\RenderTextFormat; // and will start again at 0 if it's restarted. $registry = new CollectorRegistry(new InMemory()); +function update_metrics() { + // With no code given to http_response_code, it gets the current + // response code set (via http_response_code or header). + if(http_response_code() == 404) + return; + + $path = $_SERVER['PATH_INFO']; + $method = $_SERVER['REQUEST_METHOD']; + $query_string = $_SERVER['QUERY_STRING']; + + // If $path is at least 1 character, strip / off the end. + // This turns $paths like '/packages/' into '/packages'. + if (strlen($path) > 1) + $path = rtrim($path, "/"); + + // We'll always add +1 to our total request count to this $path, + // unless this path == /metrics. + if ($path !== "/metrics") + add_metric("http_requests_count", $method, $path); + + // Extract $type out of $query_string, if we can. + $type = null; + $query = array(); + if ($query_string) + parse_str($query_string, $query); + + if (array_key_exists("type", $query)) + $type = $query["type"]; + + // Only store RPC metrics for valid types. + $good_types = [ + "info", "multiinfo", "search", "msearch", + "suggest", "suggest-pkgbase", "get-comment-form" + ]; + if ($path === "/rpc" && in_array($type, $good_types)) + add_metric("api_requests_count", $method, $path, $type); +} + function add_metric($anchor, $method, $path, $type = null) { global $registry; From 5bfc1e9094e2a67bad534f7380b1b6b20fe13ef9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 15 Oct 2021 13:18:58 -0700 Subject: [PATCH 0510/1451] Revert "fix(PHP): sanitize and produce metrics at shutdown" This reverts commit 22b3af61b568732861be17e9759be68715a709fb. --- web/html/index.php | 24 +++++++++++++++++++++-- web/lib/metricfuncs.inc.php | 38 ------------------------------------- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/web/html/index.php b/web/html/index.php index 99046930..82a44c55 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -13,8 +13,28 @@ $query_string = $_SERVER['QUERY_STRING']; // If no options.cache is configured, we no-op metric storage operations. $is_cached = defined('EXTENSION_LOADED_APC') || defined('EXTENSION_LOADED_MEMCACHE'); -if ($is_cached) - register_shutdown_function('update_metrics'); +if ($is_cached) { + $method = $_SERVER['REQUEST_METHOD']; + // We'll always add +1 to our total request count to this $path, + // unless this path == /metrics. + if ($path !== "/metrics") + add_metric("http_requests_count", $method, $path); + + // Extract $type out of $query_string, if we can. + $type = null; + $query = array(); + if ($query_string) + parse_str($query_string, $query); + $type = $query['type']; + + // Only store RPC metrics for valid types. + $good_types = [ + "info", "multiinfo", "search", "msearch", + "suggest", "suggest-pkgbase", "get-comment-form" + ]; + if ($path === "/rpc" && in_array($type, $good_types)) + add_metric("api_requests_count", $method, $path, $type); +} if (config_get_bool('options', 'enable-maintenance') && (empty($tokens[1]) || ($tokens[1] != "css" && $tokens[1] != "images"))) { if (!in_array($_SERVER['REMOTE_ADDR'], explode(" ", config_get('options', 'maintenance-exceptions')))) { diff --git a/web/lib/metricfuncs.inc.php b/web/lib/metricfuncs.inc.php index 7ebb59be..acfc30d7 100644 --- a/web/lib/metricfuncs.inc.php +++ b/web/lib/metricfuncs.inc.php @@ -13,44 +13,6 @@ use \Prometheus\RenderTextFormat; // and will start again at 0 if it's restarted. $registry = new CollectorRegistry(new InMemory()); -function update_metrics() { - // With no code given to http_response_code, it gets the current - // response code set (via http_response_code or header). - if(http_response_code() == 404) - return; - - $path = $_SERVER['PATH_INFO']; - $method = $_SERVER['REQUEST_METHOD']; - $query_string = $_SERVER['QUERY_STRING']; - - // If $path is at least 1 character, strip / off the end. - // This turns $paths like '/packages/' into '/packages'. - if (strlen($path) > 1) - $path = rtrim($path, "/"); - - // We'll always add +1 to our total request count to this $path, - // unless this path == /metrics. - if ($path !== "/metrics") - add_metric("http_requests_count", $method, $path); - - // Extract $type out of $query_string, if we can. - $type = null; - $query = array(); - if ($query_string) - parse_str($query_string, $query); - - if (array_key_exists("type", $query)) - $type = $query["type"]; - - // Only store RPC metrics for valid types. - $good_types = [ - "info", "multiinfo", "search", "msearch", - "suggest", "suggest-pkgbase", "get-comment-form" - ]; - if ($path === "/rpc" && in_array($type, $good_types)) - add_metric("api_requests_count", $method, $path, $type); -} - function add_metric($anchor, $method, $path, $type = null) { global $registry; From 040bb0d7f4d43af66126abdc677fcea05afa058a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 15 Oct 2021 13:19:07 -0700 Subject: [PATCH 0511/1451] Revert "feat(PHP): add aurweb Prometheus metrics" This reverts commit 986fa9ee305ed113172f7f214d451a7af071ecc2. --- INSTALL | 8 +-- web/html/index.php | 29 -------- web/html/metrics.php | 16 ----- web/lib/metricfuncs.inc.php | 129 ------------------------------------ web/lib/routing.inc.php | 3 +- 5 files changed, 2 insertions(+), 183 deletions(-) delete mode 100644 web/html/metrics.php delete mode 100644 web/lib/metricfuncs.inc.php diff --git a/INSTALL b/INSTALL index b161edd2..9bcd0759 100644 --- a/INSTALL +++ b/INSTALL @@ -49,15 +49,9 @@ read the instructions below. # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ python-bleach python-markdown python-alembic python-jinja \ - python-itsdangerous python-authlib python-httpx hypercorn \ - composer + python-itsdangerous python-authlib python-httpx hypercorn # python3 setup.py install -4a) Install `composer` dependencies while inside of aurweb's root: - - $ cd /path/to/aurweb - /path/to/aurweb $ composer require promphp/prometheus_client_php - 5) Create a new MySQL database and a user and import the aurweb SQL schema: $ python -m aurweb.initdb diff --git a/web/html/index.php b/web/html/index.php index 82a44c55..e57e7708 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -3,39 +3,10 @@ set_include_path(get_include_path() . PATH_SEPARATOR . '../lib'); include_once("aur.inc.php"); include_once("pkgfuncs.inc.php"); -include_once("cachefuncs.inc.php"); -include_once("metricfuncs.inc.php"); $path = $_SERVER['PATH_INFO']; $tokens = explode('/', $path); -$query_string = $_SERVER['QUERY_STRING']; - -// If no options.cache is configured, we no-op metric storage operations. -$is_cached = defined('EXTENSION_LOADED_APC') || defined('EXTENSION_LOADED_MEMCACHE'); -if ($is_cached) { - $method = $_SERVER['REQUEST_METHOD']; - // We'll always add +1 to our total request count to this $path, - // unless this path == /metrics. - if ($path !== "/metrics") - add_metric("http_requests_count", $method, $path); - - // Extract $type out of $query_string, if we can. - $type = null; - $query = array(); - if ($query_string) - parse_str($query_string, $query); - $type = $query['type']; - - // Only store RPC metrics for valid types. - $good_types = [ - "info", "multiinfo", "search", "msearch", - "suggest", "suggest-pkgbase", "get-comment-form" - ]; - if ($path === "/rpc" && in_array($type, $good_types)) - add_metric("api_requests_count", $method, $path, $type); -} - if (config_get_bool('options', 'enable-maintenance') && (empty($tokens[1]) || ($tokens[1] != "css" && $tokens[1] != "images"))) { if (!in_array($_SERVER['REMOTE_ADDR'], explode(" ", config_get('options', 'maintenance-exceptions')))) { header("HTTP/1.0 503 Service Unavailable"); diff --git a/web/html/metrics.php b/web/html/metrics.php deleted file mode 100644 index dfa860ed..00000000 --- a/web/html/metrics.php +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/web/lib/metricfuncs.inc.php b/web/lib/metricfuncs.inc.php deleted file mode 100644 index acfc30d7..00000000 --- a/web/lib/metricfuncs.inc.php +++ /dev/null @@ -1,129 +0,0 @@ -, 'query_string': }. - $metrics = get_cache_value("prometheus_metrics"); - $metrics = $metrics ? json_decode($metrics) : array(); - - $key = "$path:$type"; - - // If the current request $path isn't yet in $metrics create - // a new assoc array for it and push it into $metrics. - if (!in_array($key, $metrics)) { - $data = array( - 'anchor' => $anchor, - 'method' => $method, - 'path' => $path, - 'type' => $type - ); - array_push($metrics, json_encode($data)); - } - - // Cache-wise, we also store the count values of each route - // through the "prometheus:" key. Grab the cache value - // representing the current $path we're on (defaulted to 1). - $count = get_cache_value("prometheus:$key"); - $count = $count ? $count + 1 : 1; - - $labels = ["method", "route"]; - if ($type) - array_push($labels, "type"); - - $gauge = $registry->getOrRegisterGauge( - 'aurweb', - $anchor, - 'A metric count for the aurweb platform.', - $labels - ); - - $label_values = [$data['method'], $data['path']]; - if ($type) - array_push($label_values, $type); - - $gauge->set($count, $label_values); - - // Update cache values. - set_cache_value("prometheus:$key", $count, 0); - set_cache_value("prometheus_metrics", json_encode($metrics), 0); - -} - -function render_metrics() { - if (!defined('EXTENSION_LOADED_APC') && !defined('EXTENSION_LOADED_MEMCACHE')) { - error_log("The /metrics route requires a valid 'options.cache' " - . "configuration; no cache is configured."); - return http_response_code(417); // EXPECTATION_FAILED - } - - global $registry; - - // First, we grab the set of metrics we're interested in in the - // form of a cached JSON list, if we can. - $metrics = get_cache_value("prometheus_metrics"); - if (!$metrics) - $metrics = array(); - else - $metrics = json_decode($metrics); - - // Now, we walk through each of those list values one by one, - // which happen to be JSON-serialized associative arrays, - // and process each metric via its associative array's contents: - // The route path and the query string. - // See web/html/index.php for the creation of such metrics. - foreach ($metrics as $metric) { - $data = json_decode($metric, true); - - $anchor = $data['anchor']; - $path = $data['path']; - $type = $data['type']; - $key = "$path:$type"; - - $labels = ["method", "route"]; - if ($type) - array_push($labels, "type"); - - $count = get_cache_value("prometheus:$key"); - $gauge = $registry->getOrRegisterGauge( - 'aurweb', - $anchor, - 'A metric count for the aurweb platform.', - $labels - ); - - $label_values = [$data['method'], $data['path']]; - if ($type) - array_push($label_values, $type); - - $gauge->set($count, $label_values); - } - - // Construct the results from RenderTextFormat renderer and - // registry's samples. - $renderer = new RenderTextFormat(); - $result = $renderer->render($registry->getMetricFamilySamples()); - - // Output the results with the right content type header. - http_response_code(200); // OK - header('Content-Type: ' . RenderTextFormat::MIME_TYPE); - echo $result; -} - -?> diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php index 0f452f22..73c667d2 100644 --- a/web/lib/routing.inc.php +++ b/web/lib/routing.inc.php @@ -19,8 +19,7 @@ $ROUTES = array( '/rss' => 'rss.php', '/tos' => 'tos.php', '/tu' => 'tu.php', - '/addvote' => 'addvote.php', - '/metrics' => 'metrics.php' // Prometheus Metrics + '/addvote' => 'addvote.php', ); $PKG_PATH = '/packages'; From dd420f8c4148a7696554499fd99eb8c5c726a994 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 11 Oct 2021 14:13:29 -0700 Subject: [PATCH 0512/1451] add Feature.md GitLab issue template Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Feature.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .gitlab/issue_templates/Feature.md diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md new file mode 100644 index 00000000..5b1524b1 --- /dev/null +++ b/.gitlab/issue_templates/Feature.md @@ -0,0 +1,30 @@ +- [ ] I have summed up the feature in concise words in the [Summary](#summary) section. +- [ ] I have completely described the feature in the [Description](#description) section. +- [ ] I have completed the [Blockers](#blockers) section. + +### Summary + +Fill this section out with a concise wording about the feature being +requested. + +Example: _A new `Tyrant` account type for users_. + +### Description + +Describe your feature in full detail. + +Example: _The `Tyrant` account type should be used to allow a user to be +tyrannical. When a user is a `Tyrant`, they should be able to assassinate +users due to not complying with their laws. Laws can be configured by updating +the Tyrant laws page at https://aur.archlinux.org/account/{username}/laws. +More specifics about laws._ + +### Blockers + +Include any blockers in a list. If there are no blockers, this section +should be omitted from the issue. + +Example: + +- [Feature] Do not allow users to be Tyrants + - \<(issue|merge_request)_link\> From 81c9312606458395c90984be3f4a758636f93c9f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 11 Oct 2021 14:48:00 -0700 Subject: [PATCH 0513/1451] add Bug.md GitLab issue template Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Bug.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .gitlab/issue_templates/Bug.md diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 00000000..d84a5181 --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,34 @@ +- [ ] I have described the bug in complete detail in the + [Description](#description) section. +- [ ] I have specified steps in the [Reproduction](#reproduction) section. +- [ ] I have included any logs related to the bug in the + [Logs](#logs) section. +- [ ] I have included the versions which are affected in the + [Version(s)](#versions) section. + +### Description + +Describe the bug in full detail. + +### Reproduction + +Describe a specific set of actions that can be used to reproduce +this bug. + +### Logs + +If you have any logs relevent to the bug, include them here in +quoted or code blocks. + +### Version(s) + +In this section, please include a list of versions you have found +to be affected by this program. This can either come in the form +of `major.minor.patch` (if it affects a release tarball), or a +commit hash if the bug does not directly affect a release version. + +All development is done without modifying version displays in +aurweb's HTML render output. If you're testing locally, use the +commit on which you are experiencing the bug. If you have found +a bug which exists on live aur.archlinux.org, include the version +located at the bottom of the webpage. From 71b3f781f799f8c9d1d8b3e39682972b89d6c9c2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 15 Oct 2021 15:11:45 -0700 Subject: [PATCH 0514/1451] fix(FastAPI): maintainers are allowed to unflag their packages Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 2 +- test/test_packages_routes.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 8f4a7e1f..15d0591c 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -820,7 +820,7 @@ async def pkgbase_unflag(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) has_cred = request.user.has_credential( - "CRED_PKGBASE_UNFLAG", approved=[pkgbase.Flagger]) + "CRED_PKGBASE_UNFLAG", approved=[pkgbase.Flagger, pkgbase.Maintainer]) if has_cred: with db.begin(): pkgbase.OutOfDateTS = None diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 12d7e33e..e2811a46 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1713,7 +1713,9 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # Flag it with a valid comment. with client as request: - resp = request.post(endpoint, {"comments": "Test"}, cookies=cookies) + resp = request.post(endpoint, data={ + "comments": "Test" + }, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger == user assert pkgbase.FlaggerComment == "Test" @@ -1724,14 +1726,34 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, resp = request.get(endpoint, cookies=cookies, allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - # Now, test that the 'maintainer' user can't unflag it, because they + with db.begin(): + user2 = db.create(User, Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountType=user.AccountType) + + # Now, test that the 'user2' user can't unflag it, because they # didn't flag it to begin with. - maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + user2_cookies = {"AURSID": user2.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/unflag" + with client as request: + resp = request.post(endpoint, cookies=user2_cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert pkgbase.Flagger == user + + # Now, test that the 'maintainer' user can. + maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: resp = request.post(endpoint, cookies=maint_cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - assert pkgbase.Flagger == user + assert pkgbase.Flagger is None + + # Flag it again. + with client as request: + resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data={ + "comments": "Test" + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Now, unflag it for real. with client as request: From 2d46811c45a14b89f0c9251ed25b53c8a7f0e775 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 15 Oct 2021 16:15:53 -0700 Subject: [PATCH 0515/1451] fix(FastAPI): display VCS note when flagging a VCS package Closes: #131 Signed-off-by: Kevin Morris --- po/aurweb.pot | 9 +++++++++ templates/packages/flag.html | 14 ++++++++++++++ test/test_packages_routes.py | 21 +++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/po/aurweb.pot b/po/aurweb.pot index aeed9f02..1f7e8784 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -568,6 +568,15 @@ msgstr "" msgid "Flag Package Out-Of-Date" msgstr "" +#: templates/packages/flag.html +msgid "This seems to be a VCS package. Please do %snot%s flag " +"it out-of-date if the package version in the AUR does " +"not match the most recent commit. Flagging this package " +"should only be done if the sources moved or changes in " +"the PKGBUILD are required because of recent upstream " +"changes." +msgstr "" + #: html/pkgflag.php #, php-format msgid "" diff --git a/templates/packages/flag.html b/templates/packages/flag.html index 4e133acb..0335cf18 100644 --- a/templates/packages/flag.html +++ b/templates/packages/flag.html @@ -18,6 +18,20 @@ {% endfor %} + {% if pkgbase.Name.endswith(('-cvs', '-svn', '-git', '-hg', '-bzr', '-darcs')) %} +

    + {# TODO: This error is not yet translated. #} + {{ + "This seems to be a VCS package. Please do %snot%s flag " + "it out-of-date if the package version in the AUR does " + "not match the most recent commit. Flagging this package " + "should only be done if the sources moved or changes in " + "the PKGBUILD are required because of recent upstream " + "changes." | tr | format("", "") | safe + }} +

    + {% endif %} +

    {{ "Please do %snot%s use this form to report bugs. " diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index e2811a46..7eb4e532 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1762,6 +1762,27 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, assert pkgbase.Flagger is None +def test_pkgbase_flag_vcs(client: TestClient, user: User, package: Package): + # Morph our package fixture into a VCS package (-git). + with db.begin(): + package.PackageBase.Name += "-git" + package.Name += "-git" + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag", + cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + expected = ("This seems to be a VCS package. Please do " + "not flag it out-of-date if the package " + "version in the AUR does not match the most recent commit. " + "Flagging this package should only be done if the sources " + "moved or changes in the PKGBUILD are required because of " + "recent upstream changes.") + assert expected in resp.text + + def test_pkgbase_notify(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase From 8040ef5a9c53048598eb6d0b356923db00467b7e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 15 Oct 2021 19:02:53 -0700 Subject: [PATCH 0516/1451] fix(FastAPI): use pkgbase in package actions Previously, `result` was being used which was directly set to `pkgbase` before rendering the actions.html partial. It didn't make much sense. This commit cleans things up a bit. Signed-off-by: Kevin Morris --- templates/packages/show.html | 5 ++-- templates/partials/packages/actions.html | 36 ++++++++++++------------ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/templates/packages/show.html b/templates/packages/show.html index ba531fc8..fbc9c0ea 100644 --- a/templates/packages/show.html +++ b/templates/packages/show.html @@ -5,7 +5,6 @@

    {{ 'Package Details' | tr }}: {{ package.Name }} {{ package.Version }}

    - {% set result = pkgbase %} {% include "partials/packages/actions.html" %} {% set show_package_details = True %} @@ -16,7 +15,7 @@
    - {% set pkgname = result.Name %} - {% set pkgbase_id = result.ID %} + {% set pkgname = package.Name %} + {% set pkgbase_id = pkgbase.ID %} {% include "partials/packages/comments.html" %} {% endblock %} diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index dd83c84d..81536a3d 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -1,38 +1,38 @@ diff --git a/templates/partials/account/comment.html b/templates/partials/account/comment.html new file mode 100644 index 00000000..bc167cf7 --- /dev/null +++ b/templates/partials/account/comment.html @@ -0,0 +1,40 @@ +{% set header_cls = "comment-header" %} +{% if comment.Deleter %} + {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} +{% endif %} + +{% if not comment.Deleter or request.user.has_credential("CRED_COMMENT_VIEW_DELETED", approved=[comment.Deleter]) %} + + {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} +

    + {{ + "Commented on package %s%s%s on %s%s%s" | tr + | format( + '' | format(comment.PackageBase.Name), + comment.PackageBase.Name, + "", + '' | format( + username, + comment.ID + ), + commented_at.strftime("%Y-%m-%d %H:%M"), + "" + ) | safe + }} + {% if comment.Editor %} + {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} + + ({{ "edited on %s by %s" | tr + | format(edited_on.strftime('%Y-%m-%d %H:%M'), + '%s' | format( + comment.Editor.Username, comment.Editor.Username)) + | safe + }}) + + {% endif %} + + {% include "partials/comment_actions.html" %} +

    + + {% include "partials/comment_content.html" %} +{% endif %} From 7f4c011dc3d4db377c7676bde50b67db9b937c72 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Oct 2021 20:26:57 -0700 Subject: [PATCH 0592/1451] fix(fastapi): sanitize PP/O parameters for package search This definitely leaked through in more areas. We'll need to reuse this new utility function in a few other routes in upcoming commits. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 7 +++++-- aurweb/util.py | 18 ++++++++++++++++-- test/test_packages_routes.py | 10 +++------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index b0da3bf9..14b91221 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -30,8 +30,11 @@ async def packages_get(request: Request, context: Dict[str, Any], context["q"] = dict(request.query_params) # Per page and offset. - per_page = context["PP"] = int(request.query_params.get("PP", 50)) - offset = context["O"] = int(request.query_params.get("O", 0)) + offset, per_page = util.sanitize_params( + request.query_params.get("O", defaults.O), + request.query_params.get("PP", defaults.PP)) + context["O"] = offset + context["PP"] = per_page # Query search by. search_by = context["SeB"] = request.query_params.get("SeB", "nd") diff --git a/aurweb/util.py b/aurweb/util.py index dd7491d3..88142cbc 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -7,7 +7,7 @@ import secrets import string from datetime import datetime -from typing import Any, Callable, Dict, Iterable +from typing import Any, Callable, Dict, Iterable, Tuple from urllib.parse import urlencode, urlparse from zoneinfo import ZoneInfo @@ -18,7 +18,7 @@ from jinja2 import pass_context import aurweb.config -from aurweb import logging +from aurweb import defaults, logging logger = logging.get_logger(__name__) @@ -155,3 +155,17 @@ def get_ssh_fingerprints(): def apply_all(iterable: Iterable, fn: Callable): for item in iterable: fn(item) + + +def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: + try: + offset = int(offset) + except ValueError: + offset = defaults.O + + try: + per_page = int(per_page) + except ValueError: + per_page = defaults.PP + + return (offset, per_page) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index b4a582e3..2ef3f3d8 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -486,15 +486,11 @@ def test_pkgbase(client: TestClient, package: Package): def test_packages(client: TestClient, packages: List[Package]): - """ Test the / packages route with defaults. - - Defaults: - 50 results per page - offset of 0 - """ with client as request: response = request.get("/packages", params={ - "SeB": "X" # "X" isn't valid, defaults to "nd" + "SeB": "X", # "X" isn't valid, defaults to "nd" + "PP": "1 or 1", + "O": "0 or 0" }) assert response.status_code == int(HTTPStatus.OK) From 01e27fa34719b8e68def295c575bfb1f9b0ed362 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Oct 2021 20:29:56 -0700 Subject: [PATCH 0593/1451] fix(fastapi): sanitize /requests params Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 14b91221..27125b60 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -635,6 +635,8 @@ async def requests(request: Request, context = make_context(request, "Requests") context["q"] = dict(request.query_params) + + O, PP = util.sanitize_params(O, PP) context["O"] = O context["PP"] = PP From 9464de108f39e3fe633083ddf3c7a3526426fd98 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Oct 2021 21:37:52 -0700 Subject: [PATCH 0594/1451] feat(fastapi): add /pkgbase/{name}/comments/{id}/edit (get) This is needed so that users can edit comments when they don't have Javascript being used in their browser. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 15 +++++++++ templates/packages/comments/edit.html | 44 +++++++++++++++++++++++++++ test/test_packages_routes.py | 7 +++++ 3 files changed, 66 insertions(+) create mode 100644 templates/packages/comments/edit.html diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 27125b60..c574ec18 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -384,6 +384,21 @@ async def pkgbase_comment_post( status_code=HTTPStatus.SEE_OTHER) +@router.get("/pkgbase/{name}/comments/{id}/edit") +@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/edit") +async def pkgbase_comment_edit(request: Request, name: str, id: int, + next: str = Form(default=None)): + pkgbase = get_pkg_or_base(name, models.PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + if not next: + next = f"/pkgbase/{name}" + + context = await make_variable_context(request, "Edit comment", next=next) + context["comment"] = comment + return render_template(request, "packages/comments/edit.html", context) + + @router.post("/pkgbase/{name}/comments/{id}/delete") @auth_required(True, redirect="/pkgbase/{name}/comments/{id}/delete") async def pkgbase_comment_delete(request: Request, name: str, id: int, diff --git a/templates/packages/comments/edit.html b/templates/packages/comments/edit.html new file mode 100644 index 00000000..f938287e --- /dev/null +++ b/templates/packages/comments/edit.html @@ -0,0 +1,44 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {{ "Edit comment for: %s" | tr | format(comment.PackageBase.Name) }}

    + + +
    +
    + +
    + +

    + {{ + "Git commit identifiers referencing commits in " + "the AUR package repository and URLs are converted " + "to links automatically." | tr + }} + {{ + "%sMarkdown syntax%s is partiaully supported." + | tr | format( + '', + "" + ) | safe + }} +

    + +

    + +

    + +

    + +

    + +
    + + +
    +{% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 2ef3f3d8..207be379 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1084,6 +1084,13 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, assert len(bodies) == 1 assert bodies[0].text.strip() == "Test comment." + comment_id = headers[0].attrib["id"].split("-")[-1] + + # Test the non-javascript version of comment editing by + # visiting the /pkgbase/{name}/comments/{id}/edit route. + with client as request: + resp = request.get(f"{endpoint}/{comment_id}/edit", cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) # Clear up the PackageNotification. This doubles as testing # that the notification was created and clears it up so we can From b3b31394e840b2372134fb23f9217e9c40ef242b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Oct 2021 22:59:40 -0700 Subject: [PATCH 0595/1451] fix(rpc): simplify json generation complexity This simply decouples depends and relations population into their own helper functions. Signed-off-by: Kevin Morris --- aurweb/rpc.py | 60 ++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 84bae53c..e92f9c70 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import List +from typing import Any, Dict, List from sqlalchemy import and_ @@ -95,6 +95,36 @@ class RPC: raise RPCError( f"Request type '{self.type}' is not yet implemented.") + def _update_json_depends(self, package: models.Package, + data: Dict[str, Any]): + # Walk through all related PackageDependencies and produce + # the appropriate dict entries. + depends = package.package_dependencies + for dep in depends: + if dep.DepTypeID in DEP_TYPES: + key = DEP_TYPES.get(dep.DepTypeID) + + display = dep.DepName + if dep.DepCondition: + display += dep.DepCondition + + data[key].append(display) + + def _update_json_relations(self, package: models.Package, + data: Dict[str, Any]): + # Walk through all related PackageRelations and produce + # the appropriate dict entries. + relations = package.package_relations + for rel in relations: + if rel.RelTypeID in REL_TYPES: + key = REL_TYPES.get(rel.RelTypeID) + + display = rel.RelName + if rel.RelCondition: + display += rel.RelCondition + + data[key].append(display) + def _get_json_data(self, package: models.Package): """ Produce dictionary data of one Package that can be JSON-serialized. @@ -137,32 +167,8 @@ class RPC: # We do have a maintainer: set the Maintainer key. data["Maintainer"] = package.PackageBase.Maintainer.Username - # Walk through all related PackageDependencies and produce - # the appropriate dict entries. - if depends := package.package_dependencies: - for dep in depends: - if dep.DepTypeID in DEP_TYPES: - key = DEP_TYPES.get(dep.DepTypeID) - - display = dep.DepName - if dep.DepCondition: - display += dep.DepCondition - - data[key].append(display) - - # Walk through all related PackageRelations and produce - # the appropriate dict entries. - if relations := package.package_relations: - for rel in relations: - if rel.RelTypeID in REL_TYPES: - key = REL_TYPES.get(rel.RelTypeID) - - display = rel.RelName - if rel.RelCondition: - display += rel.RelCondition - - data[key].append(display) - + self._update_json_depends(package, data) + self._update_json_relations(package, data) return data def _handle_multiinfo_type(self, args: List[str] = []): From 0af6a2c32f04f1c0f8b98e1be9fa45983eec9bd5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Oct 2021 23:47:47 -0700 Subject: [PATCH 0596/1451] fix(docker): fix COMMIT_HASH variable check The previous method was super bad. Even if a variable was declared, if it was empty, we would run into a false-positive. Additionally, the previous method did not allow us to not specify the COMMIT_HASH variable; which is problematic for development environments. Signed-off-by: Kevin Morris --- docker/fastapi-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index ec9eb5c1..58fafe56 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -11,7 +11,7 @@ sed -ri "s;^(aur_location) = .+;\1 = ${AURWEB_FASTAPI_PREFIX};" conf/config sed -ri 's/^(cache) = .+/\1 = redis/' conf/config sed -ri 's|^(redis_address) = .+|\1 = redis://redis|' conf/config -if [ "$COMMIT_HASH" ]; then +if [ ! -z ${COMMIT_HASH+x} ]; then sed -ri "s/^;?(commit_hash) =.*$/\1 = $COMMIT_HASH/" conf/config fi From 6d376fed1576e036d8a4ceb687493e647f3d8c0d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 29 Oct 2021 23:10:20 -0700 Subject: [PATCH 0597/1451] feat(rpc): add ETag header with md5 hash content The ETag header can be used for client-side caching. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 25 +++++++++++++++++++++++-- aurweb/rpc.py | 6 ++---- test/test_rpc.py | 8 ++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 0616326b..0c52404c 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -1,8 +1,12 @@ +import hashlib + from http import HTTPStatus from typing import List, Optional from urllib.parse import unquote -from fastapi import APIRouter, Query, Request +import orjson + +from fastapi import APIRouter, Query, Request, Response from fastapi.responses import JSONResponse from aurweb.ratelimit import check_ratelimit @@ -74,4 +78,21 @@ async def rpc(request: Request, # Prepare list of arguments for input. If 'arg' was given, it'll # be a list with one element. arguments = parse_args(request) - return JSONResponse(rpc.handle(arguments)) + data = rpc.handle(arguments) + + # Serialize `data` into JSON in a sorted fashion. This way, our + # ETag header produced below will never end up changed. + output = orjson.dumps(data, option=orjson.OPT_SORT_KEYS) + + # Produce an md5 hash based on `output`. + md5 = hashlib.md5() + md5.update(output) + etag = md5.hexdigest() + + # Finally, return our JSONResponse with the ETag header. + # The ETag header expects quotes to surround any identifier. + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag + return Response(output.decode(), headers={ + "Content-Type": "application/json", + "ETag": f'"{etag}"' + }) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index e92f9c70..87700a2f 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -99,8 +99,7 @@ class RPC: data: Dict[str, Any]): # Walk through all related PackageDependencies and produce # the appropriate dict entries. - depends = package.package_dependencies - for dep in depends: + for dep in package.package_dependencies: if dep.DepTypeID in DEP_TYPES: key = DEP_TYPES.get(dep.DepTypeID) @@ -114,8 +113,7 @@ class RPC: data: Dict[str, Any]): # Walk through all related PackageRelations and produce # the appropriate dict entries. - relations = package.package_relations - for rel in relations: + for rel in package.package_relations: if rel.RelTypeID in REL_TYPES: key = REL_TYPES.get(rel.RelTypeID) diff --git a/test/test_rpc.py b/test/test_rpc.py index 9400ee06..00703c23 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -488,3 +488,11 @@ def test_rpc_ratelimit(getint: mock.MagicMock, pipeline: Pipeline): # The new first request should be good. response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") assert response.status_code == int(HTTPStatus.OK) + + +def test_rpc_etag(): + response1 = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") + response2 = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") + assert response1.headers.get("ETag") is not None + assert response1.headers.get("ETag") != str() + assert response1.headers.get("ETag") == response2.headers.get("ETag") From 9d6dbaf0ecfaf576d0c966ca0c93ba68dbd07544 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Oct 2021 00:36:21 -0700 Subject: [PATCH 0598/1451] feat(rpc): add suggest type handler Signed-off-by: Kevin Morris --- aurweb/rpc.py | 8 ++++++++ test/test_rpc.py | 24 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 87700a2f..5c9df1a7 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -175,6 +175,14 @@ class RPC: models.Package.Name.in_(args)) return [self._get_json_data(pkg) for pkg in packages] + def _handle_suggest_type(self, args: List[str] = []): + arg = args[0] + packages = db.query(models.Package).join(models.PackageBase).filter( + and_(models.PackageBase.PackagerUID.isnot(None), + models.Package.Name.like(f"%{arg}%")) + ).order_by(models.Package.Name.asc()).limit(20) + return [pkg.Name for pkg in packages] + def _handle_suggest_pkgbase_type(self, args: List[str] = []): records = db.query(models.PackageBase).filter( and_(models.PackageBase.PackagerUID.isnot(None), diff --git a/test/test_rpc.py b/test/test_rpc.py index 00703c23..71c7397f 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -98,6 +98,17 @@ def setup(): Maintainer=user1, Packager=user1) + pkgbase4 = create(PackageBase, Name="fugly-chungus", + Maintainer=user1, + Packager=user1) + + desc = "A Package belonging to a PackageBase with another name." + create(Package, + PackageBase=pkgbase4, + Name="other-pkg", + Description=desc, + URL="https://example.com") + create(Package, PackageBase=pkgbase3, Name=pkgbase3.Name, @@ -451,8 +462,19 @@ def test_rpc_suggest_pkgbase(): assert data == ["chungy-chungus"] +def test_rpc_suggest(): + response = make_request("/rpc?v=5&type=suggest&arg=other") + data = response.json() + assert data == ["other-pkg"] + + # Test non-existent Package. + response = make_request("/rpc?v=5&type=suggest&arg=nonexistent") + data = response.json() + assert data == [] + + def test_rpc_unimplemented_types(): - unimplemented = ["search", "msearch", "suggest"] + unimplemented = ["search", "msearch"] for type in unimplemented: response = make_request(f"/rpc?v=5&type={type}&arg=big") data = response.json() From c28f1695edb6d94c038363648c99768e23d7fcf5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Oct 2021 16:22:54 -0700 Subject: [PATCH 0599/1451] fix(fastapi): support `by` maintainer search with no keywords In this case, package search should return orphaned packages. Signed-off-by: Kevin Morris --- aurweb/packages/search.py | 10 +++++++--- test/test_packages_routes.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index e4729d89..0319a2ba 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -90,9 +90,13 @@ class PackageSearch: return self def _search_by_maintainer(self, keywords: str) -> orm.Query: - self.query = self.query.join( - models.User, models.User.ID == models.PackageBase.MaintainerUID - ).filter(models.User.Username == keywords) + if keywords: + self.query = self.query.join( + models.User, models.User.ID == models.PackageBase.MaintainerUID + ).filter(models.User.Username == keywords) + else: + self.query = self.query.filter( + models.PackageBase.MaintainerUID.is_(None)) return self def _search_by_comaintainer(self, keywords: str) -> orm.Query: diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 207be379..c4d9ab1c 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -623,13 +623,36 @@ def test_packages_search_by_keywords(client: TestClient, def test_packages_search_by_maintainer(client: TestClient, maintainer: User, package: Package): + # We should expect that searching by `package`'s maintainer + # returns `package` in the results. with client as request: response = request.get("/packages", params={ "SeB": "m", "K": maintainer.Username }) assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + # Search again by maintainer with no keywords given. + # This kind of search returns all orphans instead. + # In this first case, there are no orphan packages; assert that. + with client as request: + response = request.get("/packages", params={"SeB": "m"}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + # Orphan `package`. + with db.begin(): + package.PackageBase.Maintainer = None + + # This time, we should get `package` returned, since it's now an orphan. + with client as request: + response = request.get("/packages", params={"SeB": "m"}) + assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') assert len(rows) == 1 From af2f3694e7fa59f06ebe1af22ac6592b513ef42f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Oct 2021 16:39:20 -0700 Subject: [PATCH 0600/1451] feat(rpc): add search type handler This commit introduces a PackageSearch-derivative class: `RPCSearch`. This derivative modifies callback behavior of PackageSearch to suit RPC searches, including [make|check|opt]depends `by` types. Signed-off-by: Kevin Morris --- aurweb/defaults.py | 3 + aurweb/packages/search.py | 118 ++++++++++++++++++++++++++++++++------ aurweb/routers/rpc.py | 12 ++-- aurweb/rpc.py | 75 +++++++++++++++++++----- test/test_rpc.py | 74 +++++++++++++++++++++++- 5 files changed, 245 insertions(+), 37 deletions(-) diff --git a/aurweb/defaults.py b/aurweb/defaults.py index c2568d05..51072e8f 100644 --- a/aurweb/defaults.py +++ b/aurweb/defaults.py @@ -9,6 +9,9 @@ PP = 50 # A whitelist of valid PP values PP_WHITELIST = {50, 100, 250} +# Default `by` parameter for RPC search. +RPC_SEARCH_BY = "name-desc" + def fallback_pp(per_page: int) -> int: """ If `per_page` is a valid value in PP_WHITELIST, return it. diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 0319a2ba..a14fe19b 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -1,6 +1,7 @@ from sqlalchemy import and_, case, or_, orm -from aurweb import config, db, models +from aurweb import config, db, models, util +from aurweb.models.dependency_type import CHECKDEPENDS_ID, DEPENDS_ID, MAKEDEPENDS_ID, OPTDEPENDS_ID DEFAULT_MAX_RESULTS = 2500 @@ -11,24 +12,25 @@ class PackageSearch: # A constant mapping of short to full name sort orderings. FULL_SORT_ORDER = {"d": "desc", "a": "asc"} - def __init__(self, user: models.User): - """ Construct an instance of PackageSearch. - - This constructors performs several steps during initialization: - 1. Setup self.query: an ORM query of Package joined by PackageBase. - """ + def __init__(self, user: models.User = None): self.user = user - self.query = db.query(models.Package).join(models.PackageBase).join( - models.PackageVote, - and_(models.PackageVote.PackageBaseID == models.PackageBase.ID, - models.PackageVote.UsersID == self.user.ID), - isouter=True - ).join( - models.PackageNotification, - and_(models.PackageNotification.PackageBaseID == models.PackageBase.ID, - models.PackageNotification.UserID == self.user.ID), - isouter=True - ) + self.query = db.query(models.Package).join(models.PackageBase) + + if self.user: + PackageVote = models.PackageVote + join_vote_on = and_( + PackageVote.PackageBaseID == models.PackageBase.ID, + PackageVote.UsersID == self.user.ID) + + PackageNotification = models.PackageNotification + join_notif_on = and_( + PackageNotification.PackageBaseID == models.PackageBase.ID, + PackageNotification.UserID == self.user.ID) + + self.query = self.query.join( + models.PackageVote, join_vote_on, isouter=True + ).join(models.PackageNotification, join_notif_on, isouter=True) + self.ordering = "d" # Setup SeB (Search By) callbacks. @@ -198,3 +200,83 @@ class PackageSearch: # Return the query to the user. return self.query + + +class RPCSearch(PackageSearch): + """ A PackageSearch-derived RPC package search query builder. + + With RPC search, we need a subset of PackageSearch's handlers, + with a few additional handlers added. So, within the RPCSearch + constructor, we pop unneeded keys out of inherited self.search_by_cb + and add a few more keys to it, namely: depends, makedepends, + optdepends and checkdepends. + + Additionally, some logic within the inherited PackageSearch.search_by + method is not needed, so it is overridden in this class without + sanitization done for the PackageSearch `by` argument. + """ + + keys_removed = ("b", "N", "B", "k", "c", "M", "s") + + def __init__(self) -> "RPCSearch": + super().__init__() + + # Fix-up inherited search_by_cb to reflect RPC-specific by params. + # We keep: "nd", "n" and "m". We also overlay four new by params + # on top: "depends", "makedepends", "optdepends" and "checkdepends". + util.apply_all(RPCSearch.keys_removed, + lambda k: self.search_by_cb.pop(k)) + self.search_by_cb.update({ + "depends": self._search_by_depends, + "makedepends": self._search_by_makedepends, + "optdepends": self._search_by_optdepends, + "checkdepends": self._search_by_checkdepends + }) + + def _join_depends(self, dep_type_id: int) -> orm.Query: + """ Join Package with PackageDependency and filter results + based on `dep_type_id`. + + :param dep_type_id: DependencyType ID + :returns: PackageDependency-joined orm.Query + """ + self.query = self.query.join(models.PackageDependency).filter( + models.PackageDependency.DepTypeID == dep_type_id) + return self.query + + def _search_by_depends(self, keywords: str) -> "RPCSearch": + self.query = self._join_depends(DEPENDS_ID).filter( + models.PackageDependency.DepName == keywords) + return self + + def _search_by_makedepends(self, keywords: str) -> "RPCSearch": + self.query = self._join_depends(MAKEDEPENDS_ID).filter( + models.PackageDependency.DepName == keywords) + return self + + def _search_by_optdepends(self, keywords: str) -> "RPCSearch": + self.query = self._join_depends(OPTDEPENDS_ID).filter( + models.PackageDependency.DepName == keywords) + return self + + def _search_by_checkdepends(self, keywords: str) -> "RPCSearch": + self.query = self._join_depends(CHECKDEPENDS_ID).filter( + models.PackageDependency.DepName == keywords) + return self + + def search_by(self, by: str, keywords: str) -> "RPCSearch": + """ Override inherited search_by. In this override, we reduce the + scope of what we handle within this function. We do not set `by` + to a default of "nd" in the RPC, as the RPC returns an error when + incorrect `by` fields are specified. + + :param by: RPC `by` argument + :param keywords: RPC `arg` argument + :returns: self + """ + callback = self.search_by_cb.get(by) + result = callback(keywords) + return result + + def results(self) -> orm.Query: + return self.query diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 0c52404c..6d3dce54 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -9,6 +9,7 @@ import orjson from fastapi import APIRouter, Query, Request, Response from fastapi.responses import JSONResponse +from aurweb import defaults from aurweb.ratelimit import check_ratelimit from aurweb.rpc import RPC @@ -62,10 +63,11 @@ def parse_args(request: Request): @router.get("/rpc") async def rpc(request: Request, - v: Optional[int] = Query(None), - type: Optional[str] = Query(None), - arg: Optional[str] = Query(None), - args: Optional[List[str]] = Query(None, alias="arg[]")): + v: Optional[int] = Query(default=None), + type: Optional[str] = Query(default=None), + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), + arg: Optional[str] = Query(default=None), + args: Optional[List[str]] = Query(default=[], alias="arg[]")): # Create a handle to our RPC class. rpc = RPC(version=v, type=type) @@ -78,7 +80,7 @@ async def rpc(request: Request, # Prepare list of arguments for input. If 'arg' was given, it'll # be a list with one element. arguments = parse_args(request) - data = rpc.handle(arguments) + data = rpc.handle(by=by, args=arguments) # Serialize `data` into JSON in a sorted fashion. This way, our # ETag header produced below will never end up changed. diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 5c9df1a7..009b1440 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -5,8 +5,9 @@ from sqlalchemy import and_ import aurweb.config as config -from aurweb import db, models, util +from aurweb import db, defaults, models, util from aurweb.models import dependency_type, relation_type +from aurweb.packages.search import RPCSearch # Define dependency type mappings from ID to RPC-compatible keys. DEP_TYPES = { @@ -60,8 +61,16 @@ class RPC: "suggest", "suggest-pkgbase" } - # A mapping of aliases. - ALIASES = {"info": "multiinfo"} + # A mapping of type aliases. + TYPE_ALIASES = {"info": "multiinfo"} + + EXPOSED_BYS = { + "name-desc", "name", "maintainer", + "depends", "makedepends", "optdepends", "checkdepends" + } + + # A mapping of by aliases. + BY_ALIASES = {"name-desc": "nd", "name": "n", "maintainer": "m"} def __init__(self, version: int = 0, type: str = None): self.version = version @@ -76,14 +85,17 @@ class RPC: "error": message } - def _verify_inputs(self, args: List[str] = []): + def _verify_inputs(self, by: str = [], args: List[str] = []): if self.version is None: raise RPCError("Please specify an API version.") if self.version not in RPC.EXPOSED_VERSIONS: raise RPCError("Invalid version specified.") - if self.type is None or not len(args): + if by not in RPC.EXPOSED_BYS: + raise RPCError("Incorrect by field specified.") + + if self.type is None: raise RPCError("No request type/data specified.") if self.type not in RPC.EXPOSED_TYPES: @@ -95,6 +107,10 @@ class RPC: raise RPCError( f"Request type '{self.type}' is not yet implemented.") + def _enforce_args(self, args: List[str]): + if not args: + raise RPCError("No request type/data specified.") + def _update_json_depends(self, package: models.Package, data: Dict[str, Any]): # Walk through all related PackageDependencies and produce @@ -169,13 +185,36 @@ class RPC: self._update_json_relations(package, data) return data - def _handle_multiinfo_type(self, args: List[str] = []): + def _handle_multiinfo_type(self, args: List[str] = [], **kwargs): + self._enforce_args(args) args = set(args) packages = db.query(models.Package).filter( models.Package.Name.in_(args)) return [self._get_json_data(pkg) for pkg in packages] - def _handle_suggest_type(self, args: List[str] = []): + def _handle_search_type(self, by: str = defaults.RPC_SEARCH_BY, + args: List[str] = []): + # If `by` isn't maintainer and we don't have any args, raise an error. + # In maintainer's case, return all orphans if there are no args, + # so we need args to pass through to the handler without errors. + if by != "m" and not len(args): + raise RPCError("No request type/data specified.") + + arg = args[0] + if len(arg) < 2: + raise RPCError("Query arg too small.") + + search = RPCSearch() + search.search_by(by, arg) + + max_results = config.getint("options", "max_rpc_results") + results = search.results().limit(max_results) + return [self._get_json_data(pkg) for pkg in results] + + def _handle_suggest_type(self, args: List[str] = [], **kwargs): + if not args: + return [] + arg = args[0] packages = db.query(models.Package).join(models.PackageBase).filter( and_(models.PackageBase.PackagerUID.isnot(None), @@ -183,14 +222,17 @@ class RPC: ).order_by(models.Package.Name.asc()).limit(20) return [pkg.Name for pkg in packages] - def _handle_suggest_pkgbase_type(self, args: List[str] = []): + def _handle_suggest_pkgbase_type(self, args: List[str] = [], **kwargs): + if not args: + return [] + records = db.query(models.PackageBase).filter( and_(models.PackageBase.PackagerUID.isnot(None), models.PackageBase.Name.like(f"%{args[0]}%")) ).order_by(models.PackageBase.Name.asc()).limit(20) return [record.Name for record in records] - def handle(self, args: List[str] = []): + def handle(self, by: str = defaults.RPC_SEARCH_BY, args: List[str] = []): """ Request entrypoint. A router should pass v, type and args to this function and expect an output dictionary to be returned. @@ -199,22 +241,29 @@ class RPC: :param args: Deciphered list of arguments based on arg/arg[] inputs """ # Convert type aliased types. - if self.type in RPC.ALIASES: - self.type = RPC.ALIASES.get(self.type) + if self.type in RPC.TYPE_ALIASES: + self.type = RPC.TYPE_ALIASES.get(self.type) # Prepare our output data dictionary with some basic keys. data = {"version": self.version, "type": self.type} # Run some verification on our given arguments. try: - self._verify_inputs(args) + self._verify_inputs(by=by, args=args) except RPCError as exc: return self.error(str(exc)) + # Convert by to its aliased value if it has one. + if by in RPC.BY_ALIASES: + by = RPC.BY_ALIASES.get(by) + # Get a handle to our callback and trap an RPCError with # an empty list of results based on callback's execution. callback = getattr(self, f"_handle_{self.type.replace('-', '_')}_type") - results = callback(args) + try: + results = callback(by=by, args=args) + except RPCError as exc: + return self.error(str(exc)) # These types are special: we produce a different kind of # successful JSON output: a list of results. diff --git a/test/test_rpc.py b/test/test_rpc.py index 71c7397f..38b81226 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -461,6 +461,11 @@ def test_rpc_suggest_pkgbase(): data = response.json() assert data == ["chungy-chungus"] + # Test no arg supplied. + response = make_request("/rpc?v=5&type=suggest-pkgbase") + data = response.json() + assert data == [] + def test_rpc_suggest(): response = make_request("/rpc?v=5&type=suggest&arg=other") @@ -472,9 +477,14 @@ def test_rpc_suggest(): data = response.json() assert data == [] + # Test no arg supplied. + response = make_request("/rpc?v=5&type=suggest") + data = response.json() + assert data == [] + def test_rpc_unimplemented_types(): - unimplemented = ["search", "msearch"] + unimplemented = ["msearch"] for type in unimplemented: response = make_request(f"/rpc?v=5&type={type}&arg=big") data = response.json() @@ -518,3 +528,65 @@ def test_rpc_etag(): assert response1.headers.get("ETag") is not None assert response1.headers.get("ETag") != str() assert response1.headers.get("ETag") == response2.headers.get("ETag") + + +def test_rpc_search_arg_too_small(): + response = make_request("/rpc?v=5&type=search&arg=b") + assert response.status_code == int(HTTPStatus.OK) + assert response.json().get("error") == "Query arg too small." + + +def test_rpc_search(): + response = make_request("/rpc?v=5&type=search&arg=big") + assert response.status_code == int(HTTPStatus.OK) + + data = response.json() + assert data.get("resultcount") == 1 + + result = data.get("results")[0] + assert result.get("Name") == "big-chungus" + + # No args on non-m by types return an error. + response = make_request("/rpc?v=5&type=search") + assert response.json().get("error") == "No request type/data specified." + + +def test_rpc_search_depends(): + response = make_request( + "/rpc?v=5&type=search&by=depends&arg=chungus-depends") + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == "big-chungus" + + +def test_rpc_search_makedepends(): + response = make_request( + "/rpc?v=5&type=search&by=makedepends&arg=chungus-makedepends") + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == "big-chungus" + + +def test_rpc_search_optdepends(): + response = make_request( + "/rpc?v=5&type=search&by=optdepends&arg=chungus-optdepends") + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == "big-chungus" + + +def test_rpc_search_checkdepends(): + response = make_request( + "/rpc?v=5&type=search&by=checkdepends&arg=chungus-checkdepends") + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == "big-chungus" + + +def test_rpc_incorrect_by(): + response = make_request("/rpc?v=5&type=search&by=fake&arg=big") + assert response.json().get("error") == "Incorrect by field specified." From 9fef8b06114e011cedab7c25fc6d44f9de99b2ab Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Oct 2021 22:53:30 -0700 Subject: [PATCH 0601/1451] fix(rpc): fix search arg check When by == 'maintainer', we allow an unspecified keyword, resulting in a search of orphan packages. Fix our search check so that when no arg is given, it is set to an empty str(). We already check for valid args when type is not maintainer, so there's no need to worry about other args falling through. Signed-off-by: Kevin Morris --- aurweb/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 009b1440..16985f37 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -200,8 +200,8 @@ class RPC: if by != "m" and not len(args): raise RPCError("No request type/data specified.") - arg = args[0] - if len(arg) < 2: + arg = args[0] if args else str() + if by != "m" and len(arg) < 2: raise RPCError("Query arg too small.") search = RPCSearch() From 05e6cfca62162ffa5ca7e524f08810bc4d0df42a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 30 Oct 2021 22:56:18 -0700 Subject: [PATCH 0602/1451] feat(rpc): add msearch type handler Signed-off-by: Kevin Morris --- aurweb/rpc.py | 9 +++------ test/test_rpc.py | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 16985f37..56f75391 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -101,12 +101,6 @@ class RPC: if self.type not in RPC.EXPOSED_TYPES: raise RPCError("Incorrect request type specified.") - try: - getattr(self, f"_handle_{self.type.replace('-', '_')}_type") - except AttributeError: - raise RPCError( - f"Request type '{self.type}' is not yet implemented.") - def _enforce_args(self, args: List[str]): if not args: raise RPCError("No request type/data specified.") @@ -211,6 +205,9 @@ class RPC: results = search.results().limit(max_results) return [self._get_json_data(pkg) for pkg in results] + def _handle_msearch_type(self, args: List[str] = [], **kwargs): + return self._handle_search_type(by="m", args=args) + def _handle_suggest_type(self, args: List[str] = [], **kwargs): if not args: return [] diff --git a/test/test_rpc.py b/test/test_rpc.py index 38b81226..0636c792 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -483,15 +483,6 @@ def test_rpc_suggest(): assert data == [] -def test_rpc_unimplemented_types(): - unimplemented = ["msearch"] - for type in unimplemented: - response = make_request(f"/rpc?v=5&type={type}&arg=big") - data = response.json() - expected = f"Request type '{type}' is not yet implemented." - assert data.get("error") == expected - - def mock_config_getint(section: str, key: str): if key == "request_limit": return 4 @@ -551,6 +542,35 @@ def test_rpc_search(): assert response.json().get("error") == "No request type/data specified." +def test_rpc_msearch(): + response = make_request("/rpc?v=5&type=msearch&arg=user1") + data = response.json() + + # user1 maintains 4 packages; assert that we got them all. + assert data.get("resultcount") == 4 + names = list(sorted(r.get("Name") for r in data.get("results"))) + expected_results = list(sorted([ + "big-chungus", + "chungy-chungus", + "gluggly-chungus", + "other-pkg" + ])) + assert names == expected_results + + # Search for a non-existent maintainer, giving us zero packages. + response = make_request("/rpc?v=5&type=msearch&arg=blah-blah") + data = response.json() + assert data.get("resultcount") == 0 + + # A missing arg still succeeds, but it returns all orphans. + # Just verify that we receive no error and the orphaned result. + response = make_request("/rpc?v=5&type=msearch") + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == "woogly-chungus" + + def test_rpc_search_depends(): response = make_request( "/rpc?v=5&type=search&by=depends&arg=chungus-depends") From 12b4269ba8c1b4dbe9aa55b9e0541db4f0cdac77 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 00:28:55 -0700 Subject: [PATCH 0603/1451] feat(rpc): support jsonp callbacks This change introduces alternate rendering of text/javascript JSONP-compatible callback content. The `examples/jsonp.html` HTML document can be used to test this functionality against a running aurweb server. Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 26 +++++++++++---- examples/jsonp.html | 74 +++++++++++++++++++++++++++++++++++++++++++ test/test_rpc.py | 14 ++++++++ 3 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 examples/jsonp.html diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 6d3dce54..175e5f0f 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -67,7 +67,8 @@ async def rpc(request: Request, type: Optional[str] = Query(default=None), by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), arg: Optional[str] = Query(default=None), - args: Optional[List[str]] = Query(default=[], alias="arg[]")): + args: Optional[List[str]] = Query(default=[], alias="arg[]"), + callback: Optional[str] = Query(default=None)): # Create a handle to our RPC class. rpc = RPC(version=v, type=type) @@ -84,17 +85,28 @@ async def rpc(request: Request, # Serialize `data` into JSON in a sorted fashion. This way, our # ETag header produced below will never end up changed. - output = orjson.dumps(data, option=orjson.OPT_SORT_KEYS) + content = orjson.dumps(data, option=orjson.OPT_SORT_KEYS) # Produce an md5 hash based on `output`. md5 = hashlib.md5() - md5.update(output) + md5.update(content) etag = md5.hexdigest() - # Finally, return our JSONResponse with the ETag header. + # If `callback` was provided, produce a text/javascript response + # valid for the jsonp callback. Otherwise, by default, return + # application/json containing `output`. + # Note: Being the API hot path, `content` is not defaulted to + # avoid copying the JSON string in the case callback is provided. + content_type = "application/json" + if callback: + print("callback called") + content_type = "text/javascript" + content = f"/**/{callback}({content.decode()})" + # The ETag header expects quotes to surround any identifier. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag - return Response(output.decode(), headers={ - "Content-Type": "application/json", + headers = { + "Content-Type": content_type, "ETag": f'"{etag}"' - }) + } + return Response(content, headers=headers) diff --git a/examples/jsonp.html b/examples/jsonp.html new file mode 100644 index 00000000..d73ec91e --- /dev/null +++ b/examples/jsonp.html @@ -0,0 +1,74 @@ + + + + + + + + JSONP Callback Test + + + + + +
    +
    +

    + Searching with the following form uses a JSONP callback + to log data out to the javascript console. +

    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    + + diff --git a/test/test_rpc.py b/test/test_rpc.py index 0636c792..acf1ae26 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1,3 +1,5 @@ +import re + from http import HTTPStatus from unittest import mock @@ -610,3 +612,15 @@ def test_rpc_search_checkdepends(): def test_rpc_incorrect_by(): response = make_request("/rpc?v=5&type=search&by=fake&arg=big") assert response.json().get("error") == "Incorrect by field specified." + + +def test_rpc_jsonp_callback(): + """ Test the callback parameter. + + For end-to-end verification, the `examples/jsonp.html` file can be + used to submit jsonp callback requests to the RPC. + """ + response = make_request( + "/rpc?v=5&type=search&arg=big&callback=jsonCallback") + assert response.headers.get("content-type") == "text/javascript" + assert re.search(r'^/\*\*/jsonCallback\(.*\)$', response.text) is not None From 2cc44e8f28275c5ccdefc65e6075bd0bec245026 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 01:17:16 -0700 Subject: [PATCH 0604/1451] fix(rpc): perform regex match against callback name Since we're in the hot path, a constant re.compiled JSONP_EXPR is defined for checks against the callback. Additionally, reorganized `content_type` and `content` to avoid performing a DB query when we encounter a regex mismatch. Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 29 ++++++++++++++++++----------- test/test_rpc.py | 6 ++++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 175e5f0f..6abd73d9 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -1,4 +1,5 @@ import hashlib +import re from http import HTTPStatus from typing import List, Optional @@ -61,6 +62,9 @@ def parse_args(request: Request): return args +JSONP_EXPR = re.compile(r'^[a-zA-Z0-9()_.]{1,128}$') + + @router.get("/rpc") async def rpc(request: Request, v: Optional[int] = Query(default=None), @@ -78,6 +82,16 @@ async def rpc(request: Request, return JSONResponse(rpc.error("Rate limit reached"), status_code=int(HTTPStatus.TOO_MANY_REQUESTS)) + # If `callback` was provided, produce a text/javascript response + # valid for the jsonp callback. Otherwise, by default, return + # application/json containing `output`. + content_type = "application/json" + if callback: + if not re.match(JSONP_EXPR, callback): + return rpc.error("Invalid callback name.") + + content_type = "text/javascript" + # Prepare list of arguments for input. If 'arg' was given, it'll # be a list with one element. arguments = parse_args(request) @@ -92,21 +106,14 @@ async def rpc(request: Request, md5.update(content) etag = md5.hexdigest() - # If `callback` was provided, produce a text/javascript response - # valid for the jsonp callback. Otherwise, by default, return - # application/json containing `output`. - # Note: Being the API hot path, `content` is not defaulted to - # avoid copying the JSON string in the case callback is provided. - content_type = "application/json" - if callback: - print("callback called") - content_type = "text/javascript" - content = f"/**/{callback}({content.decode()})" - # The ETag header expects quotes to surround any identifier. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag headers = { "Content-Type": content_type, "ETag": f'"{etag}"' } + + if callback: + content = f"/**/{callback}({content.decode()})" + return Response(content, headers=headers) diff --git a/test/test_rpc.py b/test/test_rpc.py index acf1ae26..f4ce6de8 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -624,3 +624,9 @@ def test_rpc_jsonp_callback(): "/rpc?v=5&type=search&arg=big&callback=jsonCallback") assert response.headers.get("content-type") == "text/javascript" assert re.search(r'^/\*\*/jsonCallback\(.*\)$', response.text) is not None + + # Test an invalid callback name; we get an application/json error. + response = make_request( + "/rpc?v=5&type=search&arg=big&callback=jsonCallback!") + assert response.headers.get("content-type") == "application/json" + assert response.json().get("error") == "Invalid callback name." From 61f3cb938ce601900aba70a1e73bf454689fa156 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 01:22:54 -0700 Subject: [PATCH 0605/1451] feat(rpc): support the If-None-Match request header If the If-None-Match header is supplied with a previously obtained ETag from the same query, a 304 Not Modified is returned with no content. This allows clients to completely leverage the ETag header. Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 5 +++++ test/test_rpc.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 6abd73d9..66376067 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -113,6 +113,11 @@ async def rpc(request: Request, "ETag": f'"{etag}"' } + if_none_match = request.headers.get("If-None-Match", str()) + if if_none_match and if_none_match.strip("\t\n\r\" ") == etag: + return Response(headers=headers, + status_code=int(HTTPStatus.NOT_MODIFIED)) + if callback: content = f"/**/{callback}({content.decode()})" diff --git a/test/test_rpc.py b/test/test_rpc.py index f4ce6de8..055baa33 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1,6 +1,7 @@ import re from http import HTTPStatus +from typing import Dict from unittest import mock import orjson @@ -28,9 +29,9 @@ from aurweb.redis import redis_connection from aurweb.testing import setup_test_db -def make_request(path): +def make_request(path, headers: Dict[str, str] = {}): with TestClient(app) as request: - return request.get(path) + return request.get(path, headers=headers) @pytest.fixture(autouse=True) @@ -539,6 +540,13 @@ def test_rpc_search(): result = data.get("results")[0] assert result.get("Name") == "big-chungus" + # Test the If-None-Match headers. + etag = response.headers.get("ETag").strip('"') + headers = {"If-None-Match": etag} + response = make_request("/rpc?v=5&type=search&arg=big", headers=headers) + assert response.status_code == int(HTTPStatus.NOT_MODIFIED) + assert response.content == b'' + # No args on non-m by types return an error. response = make_request("/rpc?v=5&type=search") assert response.json().get("error") == "No request type/data specified." From b7475a5bd0607200afae508729c53b95c2b40c6c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 04:11:42 -0700 Subject: [PATCH 0606/1451] fix(rpc): fix performance of suggest[-pkgbase] We were selecting the entire record; we should just select the Name column as done in this commit. Signed-off-by: Kevin Morris --- aurweb/rpc.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 56f75391..ca838050 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -213,7 +213,9 @@ class RPC: return [] arg = args[0] - packages = db.query(models.Package).join(models.PackageBase).filter( + packages = db.query(models.Package.Name).join( + models.PackageBase + ).filter( and_(models.PackageBase.PackagerUID.isnot(None), models.Package.Name.like(f"%{arg}%")) ).order_by(models.Package.Name.asc()).limit(20) @@ -223,11 +225,11 @@ class RPC: if not args: return [] - records = db.query(models.PackageBase).filter( + packages = db.query(models.PackageBase.Name).filter( and_(models.PackageBase.PackagerUID.isnot(None), models.PackageBase.Name.like(f"%{args[0]}%")) ).order_by(models.PackageBase.Name.asc()).limit(20) - return [record.Name for record in records] + return [pkg.Name for pkg in packages] def handle(self, by: str = defaults.RPC_SEARCH_BY, args: List[str] = []): """ Request entrypoint. A router should pass v, type and args From cef69b634233b54642c54350d89b6831605f2e81 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 15:47:39 -0700 Subject: [PATCH 0607/1451] fix(gitlab-ci): prune dangling images and build cache Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 156f0abf..e18df8ee 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,6 +65,7 @@ deploy: # Set secure login config for aurweb. - sed -ri "s/^(disable_http_login).*$/\1 = 1/" conf/config.dev - docker-compose build + - docker system prune -f - docker-compose -f docker-compose.yml -f docker-compose.aur-dev.yml up -d environment: name: development From f26cd1e9941d2ac3ffbd852c504251ce2f0d3c40 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 16:13:01 -0700 Subject: [PATCH 0608/1451] fix(gitlab-ci): add `docker` dep to deploy target Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e18df8ee..1590bf34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -58,7 +58,7 @@ deploy: COMMIT_HASH: $CI_COMMIT_SHA GIT_DATA_DIR: git_data script: - - pacman -Syu --noconfirm docker-compose socat openssh + - pacman -Syu --noconfirm docker docker-compose socat openssh - chmod 600 ${SSH_KEY} - socat "UNIX-LISTEN:/tmp/docker.sock,reuseaddr,fork" EXEC:"ssh -o UserKnownHostsFile=${SSH_KNOWN_HOSTS} -Ti ${SSH_KEY} ${SSH_USER}@${SSH_HOST}" & - export DOCKER_HOST="unix:///tmp/docker.sock" From 451eec0c28113425c951861f70ba1fd55ecabf87 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 15:45:41 -0700 Subject: [PATCH 0609/1451] fix(fastapi): remove info-specific fields from search results Signed-off-by: Kevin Morris --- aurweb/rpc.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index ca838050..4ab005af 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -162,7 +162,20 @@ class RPC: "Popularity": pop, "OutOfDate": package.PackageBase.OutOfDateTS, "FirstSubmitted": package.PackageBase.SubmittedTS, - "LastModified": package.PackageBase.ModifiedTS, + "LastModified": package.PackageBase.ModifiedTS + }) + + if package.PackageBase.Maintainer is not None: + # We do have a maintainer: set the Maintainer key. + data["Maintainer"] = package.PackageBase.Maintainer.Username + + return data + + def _get_info_json_data(self, package: models.Package): + data = self._get_json_data(package) + + # Add licenses and keywords to info output. + data.update({ "License": [ lic.License.Name for lic in package.package_licenses ], @@ -171,10 +184,6 @@ class RPC: ] }) - if package.PackageBase.Maintainer is not None: - # We do have a maintainer: set the Maintainer key. - data["Maintainer"] = package.PackageBase.Maintainer.Username - self._update_json_depends(package, data) self._update_json_relations(package, data) return data @@ -184,7 +193,7 @@ class RPC: args = set(args) packages = db.query(models.Package).filter( models.Package.Name.in_(args)) - return [self._get_json_data(pkg) for pkg in packages] + return [self._get_info_json_data(pkg) for pkg in packages] def _handle_search_type(self, by: str = defaults.RPC_SEARCH_BY, args: List[str] = []): From a82879210c9dd04c511a5521d0cd163971db9838 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 19:56:56 -0700 Subject: [PATCH 0610/1451] fix(poetry): add mysql-connector dep This is not used anymore in our FastAPI code, however, for back-compatibility with pre-FastAPI scripts, we need it. Signed-off-by: Kevin Morris --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index eefec89b..4f41307d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -518,6 +518,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mysql-connector" +version = "2.2.9" +description = "MySQL driver written in Python" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "mysqlclient" version = "2.0.3" @@ -962,7 +970,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.10" -content-hash = "eb5ec82957f9fb964ca6f3852e353da51542982923dc6169658bd4bccfa78513" +content-hash = "6a45364297f5a6e88ee62240bb2eb1eaf3b41283b6d8f040ee67db02601f18e7" [metadata.files] aiofiles = [ @@ -1380,6 +1388,9 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +mysql-connector = [ + {file = "mysql-connector-2.2.9.tar.gz", hash = "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"}, +] mysqlclient = [ {file = "mysqlclient-2.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:3381ca1a4f37ff1155fcfde20836b46416d66531add8843f6aa6d968982731c3"}, {file = "mysqlclient-2.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0ac0dd759c4ca02c35a9fedc24bc982cf75171651e8187c2495ec957a87dfff7"}, diff --git a/pyproject.toml b/pyproject.toml index 12812bc8..2f327318 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ SQLAlchemy = "^1.4.26" uvicorn = "^0.15.0" gunicorn = "^20.1.0" Hypercorn = "^0.11.2" +mysql-connector = "^2.2.9" [tool.poetry.dev-dependencies] flake8 = "^4.0.1" From cc45290ec274530d66d77bad5692576876b35446 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 1 Nov 2021 11:41:20 -0700 Subject: [PATCH 0611/1451] feat(poetry): add prometheus-fastapi-instrumentator Signed-off-by: Kevin Morris --- poetry.lock | 33 ++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 4f41307d..9f528d12 100644 --- a/poetry.lock +++ b/poetry.lock @@ -594,6 +594,29 @@ category = "main" optional = false python-versions = ">=3.6.1" +[[package]] +name = "prometheus-client" +version = "0.12.0" +description = "Python client for the Prometheus monitoring system." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "5.7.1" +description = "Instrument your FastAPI with Prometheus metrics" +category = "main" +optional = false +python-versions = ">=3.6.0,<4.0.0" + +[package.dependencies] +fastapi = ">=0.38.1,<1.0.0" +prometheus-client = ">=0.8.0,<1.0.0" + [[package]] name = "protobuf" version = "3.19.0" @@ -970,7 +993,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.10" -content-hash = "6a45364297f5a6e88ee62240bb2eb1eaf3b41283b6d8f040ee67db02601f18e7" +content-hash = "569b0489389b884d269458f8e4252efcf3ebbbaa5fa77b6d09d7f0cdbda53362" [metadata.files] aiofiles = [ @@ -1440,6 +1463,14 @@ priority = [ {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, ] +prometheus-client = [ + {file = "prometheus_client-0.12.0-py2.py3-none-any.whl", hash = "sha256:317453ebabff0a1b02df7f708efbab21e3489e7072b61cb6957230dd004a0af0"}, + {file = "prometheus_client-0.12.0.tar.gz", hash = "sha256:1b12ba48cee33b9b0b9de64a1047cbd3c5f2d0ab6ebcead7ddda613a750ec3c5"}, +] +prometheus-fastapi-instrumentator = [ + {file = "prometheus-fastapi-instrumentator-5.7.1.tar.gz", hash = "sha256:5371f1b494e2b00017a02898d854119b4929025d1a203670b07b3f42dd0b5526"}, + {file = "prometheus_fastapi_instrumentator-5.7.1-py3-none-any.whl", hash = "sha256:da40ea0df14b0e95d584769747fba777522a8df6a8c47cec2edf798f1fff49b5"}, +] protobuf = [ {file = "protobuf-3.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:01a0645ef3acddfbc90237e1cdfae1086130fc7cb480b5874656193afd657083"}, {file = "protobuf-3.19.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d3861c9721a90ba83ee0936a9cfcc4fa1c4b4144ac9658fb6f6343b38558e9b4"}, diff --git a/pyproject.toml b/pyproject.toml index 2f327318..20855fa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ uvicorn = "^0.15.0" gunicorn = "^20.1.0" Hypercorn = "^0.11.2" mysql-connector = "^2.2.9" +prometheus-fastapi-instrumentator = "^5.7.1" [tool.poetry.dev-dependencies] flake8 = "^4.0.1" From f21765bfe467f225825ddeeca48c37dd5220d225 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 02:16:50 -0700 Subject: [PATCH 0612/1451] feat(fastapi): add prometheus /metrics This commit provides custom metrics, so we can group requests into their route paths and not by the arguments given, e.g. /pkgbase/some-package -> /pkgbase/{name}. We also count RPC requests as `http_api_requests_total`, split by the RPC query "type" argument. - `http_api_requests_total` - Labels: ["type", "status"] - `http_requests_total` - Number of HTTP requests in total. - Labels: ["method", "path", "status"] Signed-off-by: Kevin Morris --- LICENSES/starlette_exporter | 201 ++++++++++++++++++++++++++++++++++++ aurweb/asgi.py | 8 ++ aurweb/prometheus.py | 101 ++++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 LICENSES/starlette_exporter create mode 100644 aurweb/prometheus.py diff --git a/LICENSES/starlette_exporter b/LICENSES/starlette_exporter new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSES/starlette_exporter @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 8ebeef29..2ba2afd0 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -19,11 +19,18 @@ import aurweb.logging from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine, query from aurweb.models import AcceptedTerm, Term +from aurweb.prometheus import http_api_requests_total, http_requests_total, instrumentator from aurweb.routers import accounts, auth, errors, html, packages, rpc, rss, sso, trusted_user # Setup the FastAPI app. app = FastAPI(exception_handlers=errors.exceptions) +# Instrument routes with the prometheus-fastapi-instrumentator +# library with custom collectors and expose /metrics. +instrumentator().add(http_api_requests_total()) +instrumentator().add(http_requests_total()) +instrumentator().instrument(app).expose(app) + @app.on_event("startup") async def app_startup(): @@ -67,6 +74,7 @@ async def app_startup(): app.include_router(rss.router) app.include_router(packages.router) app.include_router(rpc.router) + # Initialize the database engine and ORM. get_engine() diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py new file mode 100644 index 00000000..0a3dd173 --- /dev/null +++ b/aurweb/prometheus.py @@ -0,0 +1,101 @@ +from typing import Any, Callable, Dict, List, Optional + +from prometheus_client import Counter +from prometheus_fastapi_instrumentator import Instrumentator +from prometheus_fastapi_instrumentator.metrics import Info +from starlette.routing import Match, Route + +from aurweb import logging + +logger = logging.get_logger(__name__) +_instrumentator = Instrumentator() + + +def instrumentator(): + return _instrumentator + + +# Taken from https://github.com/stephenhillier/starlette_exporter +# Their license is included in LICENSES/starlette_exporter. +# The code has been modified to remove child route checks +# (since we don't have any) and to stay within an 80-width limit. +def get_matching_route_path(scope: Dict[Any, Any], routes: List[Route], + route_name: Optional[str] = None) -> str: + """ + Find a matching route and return its original path string + + Will attempt to enter mounted routes and subrouters. + + Credit to https://github.com/elastic/apm-agent-python + + """ + for route in routes: + match, child_scope = route.matches(scope) + if match == Match.FULL: + route_name = route.path + + ''' + # This path exists in the original function's code, but we + # don't need it (currently), so it's been removed to avoid + # useless test coverage. + child_scope = {**scope, **child_scope} + if isinstance(route, Mount) and route.routes: + child_route_name = get_matching_route_path(child_scope, + route.routes, + route_name) + if child_route_name is None: + route_name = None + else: + route_name += child_route_name + ''' + + return route_name + elif match == Match.PARTIAL and route_name is None: + route_name = route.path + + +def http_requests_total() -> Callable[[Info], None]: + metric = Counter("http_requests_total", + "Number of HTTP requests.", + labelnames=("method", "path", "status")) + + def instrumentation(info: Info) -> None: + scope = info.request.scope + + # Taken from https://github.com/stephenhillier/starlette_exporter + # Their license is included at LICENSES/starlette_exporter. + # The code has been slightly modified: we no longer catch + # exceptions; we expect this collector to always succeed. + # Failures in this collector shall cause test failures. + if not (scope.get("endpoint", None) and scope.get("router", None)): + return None + + base_scope = { + "type": scope.get("type"), + "path": scope.get("root_path", "") + scope.get("path"), + "path_params": scope.get("path_params", {}), + "method": scope.get("method") + } + + method = scope.get("method") + path = get_matching_route_path(base_scope, scope.get("router").routes) + status = str(info.response.status_code)[:1] + "xx" + + metric.labels(method=method, path=path, status=status).inc() + + return instrumentation + + +def http_api_requests_total() -> Callable[[Info], None]: + metric = Counter( + "http_api_requests", + "Number of times an RPC API type has been requested.", + labelnames=("type", "status")) + + def instrumentation(info: Info) -> None: + if info.request.url.path.rstrip("/") == "/rpc": + type = info.request.query_params.get("type", "None") + status = str(info.response.status_code)[:1] + "xx" + metric.labels(type=type, status=status).inc() + + return instrumentation From 1be4ac2fde4f8d867fe476e52332f98ca18341f0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 1 Nov 2021 12:27:33 -0700 Subject: [PATCH 0613/1451] feat(docker): use PROMETHEUS_MULTIPROC_DIR Signed-off-by: Kevin Morris --- docker-compose.aur-dev.yml | 1 + docker-compose.yml | 1 + docker/fastapi-entrypoint.sh | 3 +++ 3 files changed, 5 insertions(+) diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index 3f574d42..1db306cc 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -53,6 +53,7 @@ services: - FASTAPI_WORKERS=${FASTAPI_WORKERS} - AURWEB_FASTAPI_PREFIX=${AURWEB_FASTAPI_PREFIX} - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus volumes: - cache:/cache diff --git a/docker-compose.yml b/docker-compose.yml index 2b25c7d8..6c822e7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -168,6 +168,7 @@ services: - FASTAPI_WORKERS=${FASTAPI_WORKERS} - AURWEB_FASTAPI_PREFIX=${AURWEB_FASTAPI_PREFIX} - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus entrypoint: /docker/fastapi-entrypoint.sh command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" healthcheck: diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index 58fafe56..f4ceaafa 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -18,4 +18,7 @@ fi sed -ri "s|^(git_clone_uri_anon) = .+|\1 = ${AURWEB_FASTAPI_PREFIX}/%s.git|" conf/config.defaults sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ${AURWEB_SSHD_PREFIX}/%s.git|" conf/config.defaults +rm -rf $PROMETHEUS_MULTIPROC_DIR +mkdir -p $PROMETHEUS_MULTIPROC_DIR + exec "$@" From dc397f6bd8d95efcebfa479d897cc00651d3bb20 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 1 Nov 2021 13:17:24 -0700 Subject: [PATCH 0614/1451] fix(fastapi): utilize PROMETHEUS_MULTIPROC_DIR in our own /metrics Signed-off-by: Kevin Morris --- aurweb/asgi.py | 9 ++++++++- aurweb/routers/html.py | 21 +++++++++++++++++++-- test/test_html.py | 7 +++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 2ba2afd0..16de771e 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -9,6 +9,7 @@ from urllib.parse import quote_plus from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles +from prometheus_client import multiprocess from sqlalchemy import and_, or_ from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.sessions import SessionMiddleware @@ -29,7 +30,7 @@ app = FastAPI(exception_handlers=errors.exceptions) # library with custom collectors and expose /metrics. instrumentator().add(http_api_requests_total()) instrumentator().add(http_requests_total()) -instrumentator().instrument(app).expose(app) +instrumentator().instrument(app) @app.on_event("startup") @@ -79,6 +80,12 @@ async def app_startup(): get_engine() +def child_exit(server, worker): # pragma: no cover + """ This function is required for gunicorn customization + of prometheus multiprocessing. """ + multiprocess.mark_process_dead(worker.pid) + + @app.exception_handler(HTTPException) async def http_exception_handler(request, exc): """ diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index c749ca67..4cee5f99 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -1,11 +1,14 @@ """ AURWeb's primary routing module. Define all routes via @app.app.{get,post} decorators in some way; more complex routes should be defined in their own modules and imported here. """ +import os + from datetime import datetime from http import HTTPStatus -from fastapi import APIRouter, Form, HTTPException, Request +from fastapi import APIRouter, Form, HTTPException, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse +from prometheus_client import CONTENT_TYPE_LATEST, CollectorRegistry, generate_latest, multiprocess from sqlalchemy import and_, case, or_ import aurweb.config @@ -203,7 +206,21 @@ async def index(request: Request): return render_template(request, "index.html", context) -# A route that returns a error 503. For testing purposes. +@router.get("/metrics") +async def metrics(request: Request): + registry = CollectorRegistry() + if os.environ.get("FASTAPI_BACKEND", "") == "gunicorn": # pragma: no cover + # This case only ever happens in production, when we are running + # gunicorn. We don't test with gunicorn, so we don't cover this path. + multiprocess.MultiProcessCollector(registry) + data = generate_latest(registry) + headers = { + "Content-Type": CONTENT_TYPE_LATEST, + "Content-Length": str(len(data)) + } + return Response(data, headers=headers) + + @router.get("/raisefivethree", response_class=HTMLResponse) async def raise_service_unavailable(request: Request): raise HTTPException(status_code=503) diff --git a/test/test_html.py b/test/test_html.py index 2018840b..8e7cb2d1 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -117,3 +117,10 @@ def test_get_successes(): """ successes = get_successes(html) assert successes[0].text.strip() == "Test" + + +def test_metrics(client: TestClient): + with client as request: + resp = request.get("/metrics") + assert resp.status_code == int(HTTPStatus.OK) + assert resp.headers.get("Content-Type").startswith("text/plain") From cdb854259af49ff759ce1481cd799612cd657bbc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 1 Nov 2021 13:54:58 -0700 Subject: [PATCH 0615/1451] fix(docker): share FASTAPI_BACKEND with the server Signed-off-by: Kevin Morris --- docker-compose.yml | 1 + docker/scripts/run-fastapi.sh | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6c822e7c..5dffe5d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -165,6 +165,7 @@ services: init: true environment: - AUR_CONFIG=conf/config + - FASTAPI_BACKEND=${FASTAPI_BACKEND} - FASTAPI_WORKERS=${FASTAPI_WORKERS} - AURWEB_FASTAPI_PREFIX=${AURWEB_FASTAPI_PREFIX} - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} diff --git a/docker/scripts/run-fastapi.sh b/docker/scripts/run-fastapi.sh index 16ae3cb9..effc7fe4 100755 --- a/docker/scripts/run-fastapi.sh +++ b/docker/scripts/run-fastapi.sh @@ -14,10 +14,12 @@ fi # By default, set FASTAPI_WORKERS to 2. In production, this should # be configured by the deployer. -if [ -z "$FASTAPI_WORKERS" ]; then +if [ -z ${FASTAPI_WORKERS+x} ]; then FASTAPI_WORKERS=2 fi +export FASTAPI_BACKEND="$1" + echo "FASTAPI_BACKEND: $FASTAPI_BACKEND" echo "FASTAPI_WORKERS: $FASTAPI_WORKERS" From 9aa8decf403c618092b00db0f8b76c9917987b6b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 1 Nov 2021 14:18:19 -0700 Subject: [PATCH 0616/1451] fix(fastapi): use metrics in cases where PROMETHEUS_MULTIPROC_DIR is defined Previously, we restricted this to gunicorn to get it working on aur-dev. This change makes it usable through any backend, and also no-op if PROMETHEUS_MULTIPROC_DIR is not defined. Signed-off-by: Kevin Morris --- aurweb/routers/html.py | 4 +--- docker-compose.yml | 3 +++ docker/scripts/run-pytests.sh | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 4cee5f99..525fb626 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -209,9 +209,7 @@ async def index(request: Request): @router.get("/metrics") async def metrics(request: Request): registry = CollectorRegistry() - if os.environ.get("FASTAPI_BACKEND", "") == "gunicorn": # pragma: no cover - # This case only ever happens in production, when we are running - # gunicorn. We don't test with gunicorn, so we don't cover this path. + if os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): # pragma: no cover multiprocess.MultiProcessCollector(registry) data = generate_latest(registry) headers = { diff --git a/docker-compose.yml b/docker-compose.yml index 5dffe5d3..225e5b9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -241,6 +241,7 @@ services: environment: - AUR_CONFIG=conf/config - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus entrypoint: /docker/test-mysql-entrypoint.sh command: /docker/scripts/run-pytests.sh clean stdin_open: true @@ -267,6 +268,7 @@ services: environment: - AUR_CONFIG=conf/config.sqlite - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus entrypoint: /docker/test-sqlite-entrypoint.sh command: setup-sqlite.sh run-pytests.sh clean stdin_open: true @@ -289,6 +291,7 @@ services: environment: - AUR_CONFIG=conf/config - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus entrypoint: /docker/tests-entrypoint.sh command: setup-sqlite.sh run-tests.sh stdin_open: true diff --git a/docker/scripts/run-pytests.sh b/docker/scripts/run-pytests.sh index d992bf06..ee546fb7 100755 --- a/docker/scripts/run-pytests.sh +++ b/docker/scripts/run-pytests.sh @@ -22,6 +22,9 @@ while [ $# -ne 0 ]; do esac done +rm -rf $PROMETHEUS_MULTIPROC_DIR +mkdir -p $PROMETHEUS_MULTIPROC_DIR + # Initialize the new database; ignore errors. python -m aurweb.initdb 2>/dev/null || \ (echo "Error: aurweb.initdb failed; already initialized?" && /bin/true) From 16e6fa2cdd002fecf6ad5e4251727315d7a3dfc8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 1 Nov 2021 14:23:15 -0700 Subject: [PATCH 0617/1451] fix(fastapi): fix prometheus parsing of HTTPStatus This wasn't actually casting to int. We shouldn't be providing HTTPStatus.CONSTANTS directly anyway, but, in case we do, we now just convert the status to an int before converting it to a string. Signed-off-by: Kevin Morris --- aurweb/prometheus.py | 2 +- aurweb/routers/packages.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index 0a3dd173..a64f6b27 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -79,7 +79,7 @@ def http_requests_total() -> Callable[[Info], None]: method = scope.get("method") path = get_matching_route_path(base_scope, scope.get("router").routes) - status = str(info.response.status_code)[:1] + "xx" + status = str(int(info.response.status_code))[:1] + "xx" metric.labels(method=method, path=path, status=status).inc() diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index c574ec18..bcc0be56 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -269,7 +269,7 @@ async def package_base(request: Request, name: str) -> Response: # If this is not a split package, redirect to /packages/{name}. if pkgbase.packages.count() == 1: return RedirectResponse(f"/packages/{name}", - status_code=HTTPStatus.SEE_OTHER) + status_code=int(HTTPStatus.SEE_OTHER)) # Add our base information. context = await make_single_context(request, pkgbase) From e4a5b7fae968e1d1335b3c03a9b816b2d1b668ae Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 3 Nov 2021 05:39:28 -0700 Subject: [PATCH 0618/1451] fix(docker): use 3s intervals for all healthchecks This'll speed up the docker development and deployment processes significantly. Signed-off-by: Kevin Morris --- docker-compose.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 225e5b9b..038eb65b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,8 @@ services: command: /docker/scripts/run-memcached.sh healthcheck: test: "bash /docker/health/memcached.sh" - + interval: 3s + redis: image: aurweb:latest init: true @@ -45,6 +46,7 @@ services: command: /docker/scripts/run-redis.sh healthcheck: test: "bash /docker/health/redis.sh" + interval: 3s ports: - "16379:6379" @@ -62,6 +64,7 @@ services: - mariadb_data:/var/lib/mysql healthcheck: test: "bash /docker/health/mariadb.sh" + interval: 3s mariadb_init: image: aurweb:latest @@ -85,6 +88,7 @@ services: - "2222:2222" healthcheck: test: "bash /docker/health/sshd.sh" + interval: 3s depends_on: mariadb_init: condition: service_started @@ -100,6 +104,7 @@ services: command: /docker/scripts/run-smartgit.sh healthcheck: test: "bash /docker/health/smartgit.sh" + interval: 3s cgit-php: image: aurweb:latest @@ -111,6 +116,7 @@ services: command: /docker/scripts/run-cgit.sh 3000 healthcheck: test: "bash /docker/health/cgit.sh 3000" + interval: 3s depends_on: git: condition: service_healthy @@ -129,6 +135,7 @@ services: command: /docker/scripts/run-cgit.sh 3000 healthcheck: test: "bash /docker/health/cgit.sh 3000" + interval: 3s depends_on: git: condition: service_healthy @@ -148,6 +155,7 @@ services: command: /docker/scripts/run-php.sh healthcheck: test: "bash /docker/health/php.sh" + interval: 3s depends_on: ca: condition: service_started @@ -174,6 +182,7 @@ services: command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" healthcheck: test: "bash /docker/health/fastapi.sh ${FASTAPI_BACKEND}" + interval: 3s depends_on: ca: condition: service_started @@ -198,6 +207,7 @@ services: - "8444:8444" # FastAPI healthcheck: test: "bash /docker/health/nginx.sh" + interval: 3s depends_on: cgit-php: condition: service_healthy From 020409ef46452bafb0e1bf0d6a9537912059a6dd Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Mon, 1 Nov 2021 17:18:09 -0400 Subject: [PATCH 0619/1451] fix(FastAPI): prevent CSRF forging login requests Signed-off-by: Steven Guikal --- aurweb/routers/auth.py | 12 +++++++++++- po/aurweb.pot | 4 ++++ test/test_auth_routes.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 4e6a416a..b8e83c7d 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -1,13 +1,14 @@ from datetime import datetime from http import HTTPStatus -from fastapi import APIRouter, Form, Request +from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse import aurweb.config from aurweb import cookies from aurweb.auth import auth_required +from aurweb.l10n import get_translator_for_request from aurweb.models import User from aurweb.templates import make_variable_context, render_template @@ -35,6 +36,15 @@ async def login_post(request: Request, user: str = Form(default=str()), passwd: str = Form(default=str()), remember_me: bool = Form(default=False)): + # TODO: Once the Origin header gets broader adoption, this code can be + # slightly simplified to use it. + login_path = aurweb.config.get("options", "aur_location") + "/login" + referer = request.headers.get("Referer") + if not referer or not referer.startswith(login_path): + _ = get_translator_for_request(request) + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, + detail=_("Bad Referer header.")) + from aurweb.db import session user = session.query(User).filter(User.Username == user).first() diff --git a/po/aurweb.pot b/po/aurweb.pot index 721f874e..dd93ca27 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -964,6 +964,10 @@ msgstr "" msgid "Package details could not be found." msgstr "" +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "" + #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." msgstr "" diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 313f9927..39afc6f9 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -18,6 +18,9 @@ from aurweb.testing import setup_test_db # Some test global constants. TEST_USERNAME = "test" TEST_EMAIL = "test@example.org" +TEST_REFERER = { + "referer": aurweb.config.get("options", "aur_location") + "/login", +} # Global mutables. user = client = None @@ -39,6 +42,10 @@ def setup(): client = TestClient(app) + # Necessary for forged login CSRF protection on the login route. Set here + # instead of only on the necessary requests for convenience. + client.headers.update(TEST_REFERER) + def test_login_logout(): post_data = { @@ -92,6 +99,10 @@ def test_secure_login(mock): # Create a local TestClient here since we mocked configuration. client = TestClient(app) + # Necessary for forged login CSRF protection on the login route. Set here + # instead of only on the necessary requests for convenience. + client.headers.update(TEST_REFERER) + # Data used for our upcoming http post request. post_data = { "user": user.Username, @@ -246,3 +257,26 @@ def test_login_incorrect_password(): assert post_data["user"] in content assert post_data["passwd"] not in content assert "checked" not in content + + +def test_login_bad_referer(): + post_data = { + "user": "test", + "passwd": "testPassword", + "next": "/", + } + + # Create new TestClient without a Referer header. + client = TestClient(app) + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + BAD_REFERER = { + "referer": aurweb.config.get("options", "aur_location") + ".mal.local", + } + with client as request: + response = request.post("/login", data=post_data, headers=BAD_REFERER) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + assert "AURSID" not in response.cookies From 69773a5b58ba86bf43a4c4240e5db4842c5dfbe0 Mon Sep 17 00:00:00 2001 From: Kristian Klausen Date: Fri, 15 Oct 2021 20:14:31 +0200 Subject: [PATCH 0620/1451] feat(PHP): Add packages dump file with more metadata --- aurweb/scripts/mkpkglists.py | 10 ++++++++++ conf/config.defaults | 1 + test/setup.sh | 1 + web/html/index.php | 1 + 4 files changed, 13 insertions(+) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 6724141a..c73cc3be 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -2,11 +2,13 @@ import datetime import gzip +import json import aurweb.config import aurweb.db packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') +packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') userfile = aurweb.config.get('mkpkglists', 'userfile') @@ -27,6 +29,14 @@ def main(): "WHERE PackageBases.PackagerUID IS NOT NULL") f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + with gzip.open(packagesmetafile, "wt") as f: + cur = conn.execute("SELECT * FROM Packages") + json.dump({ + "warning": "This is a experimental! It can be removed or modified without warning!", + "columns": [d[0] for d in cur.description], + "data": cur.fetchall() + }, f) + with gzip.open(pkgbasefile, "w") as f: f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) cur = conn.execute("SELECT Name FROM PackageBases " + diff --git a/conf/config.defaults b/conf/config.defaults index b7bc0368..36ea02ef 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -92,5 +92,6 @@ server = ftp://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] packagesfile = /srv/http/aurweb/web/html/packages.gz +packagesmetafile = /srv/http/aurweb/web/html/packages-meta-v1.json.gz pkgbasefile = /srv/http/aurweb/web/html/pkgbase.gz userfile = /srv/http/aurweb/web/html/users.gz diff --git a/test/setup.sh b/test/setup.sh index 764d4518..24bb5f48 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -61,6 +61,7 @@ server = file://$(pwd)/remote/ [mkpkglists] packagesfile = packages.gz +packagesmetafile = packages-meta-v1.json.gz pkgbasefile = pkgbase.gz userfile = users.gz EOF diff --git a/web/html/index.php b/web/html/index.php index e57e7708..3163c3e8 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -189,6 +189,7 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { readfile("./$path"); break; case "/packages.gz": + case "/packages-teapot.json.gz": case "/pkgbase.gz": case "/users.gz": header("Content-Type: text/plain"); From 51fb24ab730f3b09d78e200f020b01974dc9e457 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 16:52:30 -0700 Subject: [PATCH 0621/1451] fix(mkpkglists): improve package meta archive The SQL logic in this file for package metadata now exactly reflects RPC's search logic, without searching for specific packages. Two command line arguments are available: --extended | Include License, Keywords, Groups, relations and dependencies. When --extended is passed, the script will create a packages-meta-ext-v1.json.gz, configured via packagesmetaextfile. Archive JSON is in the following format: line-separated package objects enclosed in a list: [ {...}, {...}, {...} ] Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 +- INSTALL | 3 +- aurweb/scripts/mkpkglists.py | 273 ++++++++++++++++++++++++++++++++--- conf/config.defaults | 1 + test/setup.sh | 2 + web/html/index.php | 3 +- 6 files changed, 258 insertions(+), 26 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aff18a83..ce374082 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ before_script: python-pygit2 python-srcinfo python-bleach python-markdown python-sqlalchemy python-alembic python-pytest python-werkzeug python-pytest-tap python-fastapi hypercorn nginx python-authlib - python-itsdangerous python-httpx + python-itsdangerous python-httpx python-orjson test: script: diff --git a/INSTALL b/INSTALL index 9bcd0759..dc9cc51f 100644 --- a/INSTALL +++ b/INSTALL @@ -49,7 +49,8 @@ read the instructions below. # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ python-bleach python-markdown python-alembic python-jinja \ - python-itsdangerous python-authlib python-httpx hypercorn + python-itsdangerous python-authlib python-httpx hypercorn \ + python-orjson # python3 setup.py install 5) Create a new MySQL database and a user and import the aurweb SQL schema: diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index c73cc3be..f2095a20 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -1,16 +1,192 @@ #!/usr/bin/env python3 +""" +Produces package, package base and user archives for the AUR +database. + +Archives: + + packages.gz | A line-separated list of package names + packages-meta-v1.json | A type=search RPC-formatted JSON dataset + packages-meta-ext-v1.json | An --extended archive + pkgbase.gz | A line-separated list of package base names + users.gz | A line-separated list of user names + +This script takes an optional argument: --extended. Based +on the following, right-hand side fields are added to each item. + + --extended | License, Keywords, Groups, relations and dependencies + +""" import datetime import gzip -import json +import os +import sys + +from collections import defaultdict +from decimal import Decimal +from typing import Tuple + +import orjson import aurweb.config import aurweb.db + +def state_path(archive: str) -> str: + # A hard-coded /tmp state directory. + # TODO: Use Redis cache to store this state after we merge + # FastAPI into master and removed PHP from the tree. + return os.path.join("/tmp", os.path.basename(archive) + ".state") + + packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') +packagesmetaextfile = aurweb.config.get('mkpkglists', 'packagesmetaextfile') +packages_state = state_path(packagesfile) + pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') +pkgbases_state = state_path(pkgbasefile) + userfile = aurweb.config.get('mkpkglists', 'userfile') +users_state = state_path(userfile) + + +def should_update(state: str, tablename: str) -> Tuple[bool, int]: + if aurweb.config.get("database", "backend") != "mysql": + return (False, 0) + + db_name = aurweb.config.get("database", "name") + conn = aurweb.db.Connection() + cur = conn.execute("SELECT auto_increment FROM information_schema.tables " + "WHERE table_schema = ? AND table_name = ?", + (db_name, tablename,)) + update_time = cur.fetchone()[0] + + saved_update_time = 0 + if os.path.exists(state): + with open(state) as f: + saved_update_time = int(f.read().strip()) + + return (saved_update_time == update_time, update_time) + + +def update_state(state: str, update_time: int) -> None: + with open(state, "w") as f: + f.write(str(update_time)) + + +TYPE_MAP = { + "depends": "Depends", + "makedepends": "MakeDepends", + "checkdepends": "CheckDepends", + "optdepends": "OptDepends", + "conflicts": "Conflicts", + "provides": "Provides", + "replaces": "Replaces", +} + + +def get_extended_dict(query: str): + """ + Produce data in the form in a single bulk SQL query: + + { + : { + "Depends": [...], + "Conflicts": [...], + "License": [...] + } + } + + The caller can then use this data to populate a dataset of packages. + + output = produce_base_output_data() + data = get_extended_dict(query) + for i in range(len(output)): + package_id = output[i].get("ID") + output[i].update(data.get(package_id)) + """ + + conn = aurweb.db.Connection() + + cursor = conn.execute(query) + + data = defaultdict(lambda: defaultdict(list)) + + for result in cursor.fetchall(): + + pkgid = result[0] + key = TYPE_MAP.get(result[1]) + output = result[2] + if result[3]: + output += result[3] + + # In all cases, we have at least an empty License list. + if "License" not in data[pkgid]: + data[pkgid]["License"] = [] + + # In all cases, we have at least an empty Keywords list. + if "Keywords" not in data[pkgid]: + data[pkgid]["Keywords"] = [] + + data[pkgid][key].append(output) + + conn.close() + return data + + +def get_extended_fields(): + # Returns: [ID, Type, Name, Cond] + query = """ + SELECT PackageDepends.PackageID AS ID, DependencyTypes.Name AS Type, + PackageDepends.DepName AS Name, PackageDepends.DepCondition AS Cond + FROM PackageDepends + LEFT JOIN DependencyTypes + ON DependencyTypes.ID = PackageDepends.DepTypeID + UNION SELECT PackageRelations.PackageID AS ID, RelationTypes.Name AS Type, + PackageRelations.RelName AS Name, + PackageRelations.RelCondition AS Cond + FROM PackageRelations + LEFT JOIN RelationTypes + ON RelationTypes.ID = PackageRelations.RelTypeID + UNION SELECT PackageGroups.PackageID AS ID, 'Groups' AS Type, + Groups.Name, '' AS Cond + FROM Groups + INNER JOIN PackageGroups ON PackageGroups.GroupID = Groups.ID + UNION SELECT PackageLicenses.PackageID AS ID, 'License' AS Type, + Licenses.Name, '' as Cond + FROM Licenses + INNER JOIN PackageLicenses ON PackageLicenses.LicenseID = Licenses.ID + UNION SELECT Packages.ID AS ID, 'Keywords' AS Type, + PackageKeywords.Keyword AS Name, '' as Cond + FROM PackageKeywords + INNER JOIN Packages ON Packages.PackageBaseID = PackageKeywords.PackageBaseID + """ + return get_extended_dict(query) + + +EXTENDED_FIELD_HANDLERS = { + "--extended": get_extended_fields +} + + +def is_decimal(column): + """ Check if an SQL column is of decimal.Decimal type. """ + if isinstance(column, Decimal): + return float(column) + return column + + +def write_archive(archive: str, output: list): + with gzip.open(archive, "wb") as f: + f.write(b"[\n") + for i, item in enumerate(output): + f.write(orjson.dumps(item)) + if i < len(output) - 1: + f.write(b",") + f.write(b"\n") + f.write(b"]") def main(): @@ -21,32 +197,83 @@ def main(): pkgbaselist_header = "# AUR package base list, generated on " + datestr userlist_header = "# AUR user name list, generated on " + datestr - with gzip.open(packagesfile, "w") as f: - f.write(bytes(pkglist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT Packages.Name FROM Packages " + - "INNER JOIN PackageBases " + - "ON PackageBases.ID = Packages.PackageBaseID " + + updated, update_time = should_update(packages_state, "Packages") + if not updated: + print("Updating Packages...") + + # Query columns; copied from RPC. + columns = ("Packages.ID, Packages.Name, " + "PackageBases.ID AS PackageBaseID, " + "PackageBases.Name AS PackageBase, " + "Version, Description, URL, NumVotes, " + "Popularity, OutOfDateTS AS OutOfDate, " + "Users.UserName AS Maintainer, " + "SubmittedTS AS FirstSubmitted, " + "ModifiedTS AS LastModified") + + # Perform query. + cur = conn.execute(f"SELECT {columns} FROM Packages " + "LEFT JOIN PackageBases " + "ON PackageBases.ID = Packages.PackageBaseID " + "LEFT JOIN Users " + "ON PackageBases.MaintainerUID = Users.ID " "WHERE PackageBases.PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - with gzip.open(packagesmetafile, "wt") as f: - cur = conn.execute("SELECT * FROM Packages") - json.dump({ - "warning": "This is a experimental! It can be removed or modified without warning!", - "columns": [d[0] for d in cur.description], - "data": cur.fetchall() - }, f) + # Produce packages-meta-v1.json.gz + output = list() + snapshot_uri = aurweb.config.get("options", "snapshot_uri") + for result in cur.fetchall(): + item = { + column[0]: is_decimal(result[i]) + for i, column in enumerate(cur.description) + } + item["URLPath"] = snapshot_uri % item.get("Name") + output.append(item) - with gzip.open(pkgbasefile, "w") as f: - f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT Name FROM PackageBases " + - "WHERE PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + write_archive(packagesmetafile, output) - with gzip.open(userfile, "w") as f: - f.write(bytes(userlist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT UserName FROM Users") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + # Produce packages-meta-ext-v1.json.gz + if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: + f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) + data = f() + + default_ = {"Groups": [], "License": [], "Keywords": []} + for i in range(len(output)): + data_ = data.get(output[i].get("ID"), default_) + output[i].update(data_) + + write_archive(packagesmetaextfile, output) + + # Produce packages.gz + with gzip.open(packagesfile, "wb") as f: + f.write(bytes(pkglist_header + "\n", "UTF-8")) + f.writelines([ + bytes(x.get("Name") + "\n", "UTF-8") + for x in output + ]) + + update_state(packages_state, update_time) + + updated, update_time = should_update(pkgbases_state, "PackageBases") + if not updated: + print("Updating PackageBases...") + # Produce pkgbase.gz + with gzip.open(pkgbasefile, "w") as f: + f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT Name FROM PackageBases " + + "WHERE PackagerUID IS NOT NULL") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + update_state(pkgbases_state, update_time) + + updated, update_time = should_update(users_state, "Users") + if not updated: + print("Updating Users...") + # Produce users.gz + with gzip.open(userfile, "w") as f: + f.write(bytes(userlist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT UserName FROM Users") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + update_state(users_state, update_time) conn.close() diff --git a/conf/config.defaults b/conf/config.defaults index 36ea02ef..a04f21bc 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -93,5 +93,6 @@ server = ftp://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] packagesfile = /srv/http/aurweb/web/html/packages.gz packagesmetafile = /srv/http/aurweb/web/html/packages-meta-v1.json.gz +packagesmetaextfile = /srv/http/aurweb/web/html/packages-meta-ext-v1.json.gz pkgbasefile = /srv/http/aurweb/web/html/pkgbase.gz userfile = /srv/http/aurweb/web/html/users.gz diff --git a/test/setup.sh b/test/setup.sh index 24bb5f48..f74cd1b7 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -31,6 +31,7 @@ enable-maintenance = 0 maintenance-exceptions = 127.0.0.1 commit_uri = https://aur.archlinux.org/cgit/aur.git/log/?h=%s&id=%s localedir = $TOPLEVEL/web/locale/ +snapshot_uri = /cgit/aur.git/snapshot/%s.tar.gz [notifications] notify-cmd = $NOTIFY @@ -62,6 +63,7 @@ server = file://$(pwd)/remote/ [mkpkglists] packagesfile = packages.gz packagesmetafile = packages-meta-v1.json.gz +packagesmetaextfile = packages-meta-ext-v1.json.gz pkgbasefile = pkgbase.gz userfile = users.gz EOF diff --git a/web/html/index.php b/web/html/index.php index 3163c3e8..dc435162 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -189,7 +189,8 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { readfile("./$path"); break; case "/packages.gz": - case "/packages-teapot.json.gz": + case "/packages-meta-v1.json.gz": + case "/packages-meta-ext-v1.json.gz": case "/pkgbase.gz": case "/users.gz": header("Content-Type: text/plain"); From cdca8bd2953f3c3aa3a1b4cedb89c3b7b9fd4ddb Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 6 Nov 2021 16:23:08 -0700 Subject: [PATCH 0622/1451] feat(mkpkglists): added metadata archives Two new archives are available: - packages-meta-v1.json.gz - RPC search formatted data for all packages - ~2.1MB at the time of writing. - packages-meta-ext-v1.json.gz (via --extended) - RPC multiinfo formatted data for all packages. - ~9.8MB at the time of writing. New dependencies are required for this update: - `python-orjson` All archives served out by aur.archlinux.org distribute the Last-Modified header and support the If-Modified-Since header, which should be populated with Last-Modified's value. These should be used by clients to avoid redownloading the archive when unnecessary. Additionally, the new meta archives contain a format suitable for streaming the data as the file is retrieved. It is still in JSON format, however, users can parse package objects line by line after the first '[' found in the file, until the last ']'; both contained on their own lines. Note: This commit is a documentation change and commit body. Signed-off-by: Kevin Morris --- doc/maintenance.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/maintenance.txt b/doc/maintenance.txt index d6094545..2c5c9faf 100644 --- a/doc/maintenance.txt +++ b/doc/maintenance.txt @@ -70,7 +70,8 @@ computations and clean up the database: * aurweb-pkgmaint automatically removes empty repositories that were created within the last 24 hours but never populated. -* aurweb-mkpkglists generates the package list files. +* aurweb-mkpkglists generates the package list files; it takes an optional + --extended flag, which additionally produces multiinfo metadata. * aurweb-usermaint removes the last login IP address of all users that did not login within the past seven days. @@ -79,7 +80,7 @@ These scripts can be installed by running `python3 setup.py install` and are usually scheduled using Cron. The current setup is: ---- -*/5 * * * * aurweb-mkpkglists +*/5 * * * * aurweb-mkpkglists [--extended] 1 */2 * * * aurweb-popupdate 2 */2 * * * aurweb-aurblup 3 */2 * * * aurweb-pkgmaint From 9f1f39995740e04d44309c7aed69a9b15d26dac0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 6 Nov 2021 17:13:16 -0700 Subject: [PATCH 0623/1451] fix(mkpkglists): remove caching We really need caching for this; however, our current caching method will cause the script to bypass changes to columns if they have nothing to do with IDs. Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 159 ++++++++++++----------------------- 1 file changed, 54 insertions(+), 105 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index f2095a20..2566a146 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -20,60 +20,23 @@ on the following, right-hand side fields are added to each item. import datetime import gzip -import os import sys from collections import defaultdict from decimal import Decimal -from typing import Tuple import orjson import aurweb.config import aurweb.db - -def state_path(archive: str) -> str: - # A hard-coded /tmp state directory. - # TODO: Use Redis cache to store this state after we merge - # FastAPI into master and removed PHP from the tree. - return os.path.join("/tmp", os.path.basename(archive) + ".state") - - packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') packagesmetaextfile = aurweb.config.get('mkpkglists', 'packagesmetaextfile') -packages_state = state_path(packagesfile) pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') -pkgbases_state = state_path(pkgbasefile) userfile = aurweb.config.get('mkpkglists', 'userfile') -users_state = state_path(userfile) - - -def should_update(state: str, tablename: str) -> Tuple[bool, int]: - if aurweb.config.get("database", "backend") != "mysql": - return (False, 0) - - db_name = aurweb.config.get("database", "name") - conn = aurweb.db.Connection() - cur = conn.execute("SELECT auto_increment FROM information_schema.tables " - "WHERE table_schema = ? AND table_name = ?", - (db_name, tablename,)) - update_time = cur.fetchone()[0] - - saved_update_time = 0 - if os.path.exists(state): - with open(state) as f: - saved_update_time = int(f.read().strip()) - - return (saved_update_time == update_time, update_time) - - -def update_state(state: str, update_time: int) -> None: - with open(state, "w") as f: - f.write(str(update_time)) TYPE_MAP = { @@ -197,83 +160,69 @@ def main(): pkgbaselist_header = "# AUR package base list, generated on " + datestr userlist_header = "# AUR user name list, generated on " + datestr - updated, update_time = should_update(packages_state, "Packages") - if not updated: - print("Updating Packages...") + # Query columns; copied from RPC. + columns = ("Packages.ID, Packages.Name, " + "PackageBases.ID AS PackageBaseID, " + "PackageBases.Name AS PackageBase, " + "Version, Description, URL, NumVotes, " + "Popularity, OutOfDateTS AS OutOfDate, " + "Users.UserName AS Maintainer, " + "SubmittedTS AS FirstSubmitted, " + "ModifiedTS AS LastModified") - # Query columns; copied from RPC. - columns = ("Packages.ID, Packages.Name, " - "PackageBases.ID AS PackageBaseID, " - "PackageBases.Name AS PackageBase, " - "Version, Description, URL, NumVotes, " - "Popularity, OutOfDateTS AS OutOfDate, " - "Users.UserName AS Maintainer, " - "SubmittedTS AS FirstSubmitted, " - "ModifiedTS AS LastModified") + # Perform query. + cur = conn.execute(f"SELECT {columns} FROM Packages " + "LEFT JOIN PackageBases " + "ON PackageBases.ID = Packages.PackageBaseID " + "LEFT JOIN Users " + "ON PackageBases.MaintainerUID = Users.ID " + "WHERE PackageBases.PackagerUID IS NOT NULL") - # Perform query. - cur = conn.execute(f"SELECT {columns} FROM Packages " - "LEFT JOIN PackageBases " - "ON PackageBases.ID = Packages.PackageBaseID " - "LEFT JOIN Users " - "ON PackageBases.MaintainerUID = Users.ID " - "WHERE PackageBases.PackagerUID IS NOT NULL") + # Produce packages-meta-v1.json.gz + output = list() + snapshot_uri = aurweb.config.get("options", "snapshot_uri") + for result in cur.fetchall(): + item = { + column[0]: is_decimal(result[i]) + for i, column in enumerate(cur.description) + } + item["URLPath"] = snapshot_uri % item.get("Name") + output.append(item) - # Produce packages-meta-v1.json.gz - output = list() - snapshot_uri = aurweb.config.get("options", "snapshot_uri") - for result in cur.fetchall(): - item = { - column[0]: is_decimal(result[i]) - for i, column in enumerate(cur.description) - } - item["URLPath"] = snapshot_uri % item.get("Name") - output.append(item) + write_archive(packagesmetafile, output) - write_archive(packagesmetafile, output) + # Produce packages-meta-ext-v1.json.gz + if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: + f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) + data = f() - # Produce packages-meta-ext-v1.json.gz - if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: - f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) - data = f() + default_ = {"Groups": [], "License": [], "Keywords": []} + for i in range(len(output)): + data_ = data.get(output[i].get("ID"), default_) + output[i].update(data_) - default_ = {"Groups": [], "License": [], "Keywords": []} - for i in range(len(output)): - data_ = data.get(output[i].get("ID"), default_) - output[i].update(data_) + write_archive(packagesmetaextfile, output) - write_archive(packagesmetaextfile, output) + # Produce packages.gz + with gzip.open(packagesfile, "wb") as f: + f.write(bytes(pkglist_header + "\n", "UTF-8")) + f.writelines([ + bytes(x.get("Name") + "\n", "UTF-8") + for x in output + ]) - # Produce packages.gz - with gzip.open(packagesfile, "wb") as f: - f.write(bytes(pkglist_header + "\n", "UTF-8")) - f.writelines([ - bytes(x.get("Name") + "\n", "UTF-8") - for x in output - ]) + # Produce pkgbase.gz + with gzip.open(pkgbasefile, "w") as f: + f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT Name FROM PackageBases " + + "WHERE PackagerUID IS NOT NULL") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - update_state(packages_state, update_time) - - updated, update_time = should_update(pkgbases_state, "PackageBases") - if not updated: - print("Updating PackageBases...") - # Produce pkgbase.gz - with gzip.open(pkgbasefile, "w") as f: - f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT Name FROM PackageBases " + - "WHERE PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - update_state(pkgbases_state, update_time) - - updated, update_time = should_update(users_state, "Users") - if not updated: - print("Updating Users...") - # Produce users.gz - with gzip.open(userfile, "w") as f: - f.write(bytes(userlist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT UserName FROM Users") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - update_state(users_state, update_time) + # Produce users.gz + with gzip.open(userfile, "w") as f: + f.write(bytes(userlist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT UserName FROM Users") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) conn.close() From 446a082352bf2bb8d7398afbdbe9dfad42f21fed Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 7 Nov 2021 17:26:05 -0800 Subject: [PATCH 0624/1451] change(fastapi): refactor database ORM model definitions We don't want to depend on the database to load up data about the models we define. We now leverage the existing `aurweb.schema` module for table definitions and set __table_args__["autoload"] to False. Signed-off-by: Kevin Morris --- aurweb/models/accepted_term.py | 16 +++----- aurweb/models/account_type.py | 56 +++++++++++---------------- aurweb/models/api_rate_limit.py | 10 ++--- aurweb/models/ban.py | 10 ++--- aurweb/models/declarative.py | 6 +-- aurweb/models/dependency_type.py | 27 +++++-------- aurweb/models/group.py | 10 ++--- aurweb/models/license.py | 10 ++--- aurweb/models/official_provider.py | 10 ++--- aurweb/models/package.py | 15 +++---- aurweb/models/package_base.py | 21 ++++------ aurweb/models/package_blacklist.py | 10 ++--- aurweb/models/package_comaintainer.py | 20 ++++------ aurweb/models/package_comment.py | 26 ++++--------- aurweb/models/package_dependency.py | 22 ++++------- aurweb/models/package_group.py | 18 ++++----- aurweb/models/package_keyword.py | 19 ++++----- aurweb/models/package_license.py | 20 ++++------ aurweb/models/package_notification.py | 20 ++++------ aurweb/models/package_relation.py | 22 ++++------- aurweb/models/package_request.py | 25 ++++-------- aurweb/models/package_source.py | 14 +++---- aurweb/models/package_vote.py | 20 ++++------ aurweb/models/relation_type.py | 24 ++++-------- aurweb/models/request_type.py | 21 ++++------ aurweb/models/session.py | 13 +++---- aurweb/models/ssh_pub_key.py | 19 +++------ aurweb/models/term.py | 10 ++--- aurweb/models/tu_vote.py | 18 ++++----- aurweb/models/tu_voteinfo.py | 15 +++---- aurweb/models/user.py | 21 ++++------ 31 files changed, 212 insertions(+), 356 deletions(-) diff --git a/aurweb/models/accepted_term.py b/aurweb/models/accepted_term.py index b4dbb410..0f9b187e 100644 --- a/aurweb/models/accepted_term.py +++ b/aurweb/models/accepted_term.py @@ -1,28 +1,24 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.term import Term as _Term from aurweb.models.user import User as _User class AcceptedTerm(Base): - __tablename__ = "AcceptedTerms" + __table__ = schema.AcceptedTerms + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.TermsID]} - UsersID = Column(Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) User = relationship( _User, backref=backref("accepted_terms", lazy="dynamic"), - foreign_keys=[UsersID]) + foreign_keys=[__table__.c.UsersID]) - TermsID = Column(Integer, ForeignKey("Terms.ID", ondelete="CASCADE"), - nullable=False) Term = relationship( _Term, backref=backref("accepted_terms", lazy="dynamic"), - foreign_keys=[TermsID]) - - __mapper_args__ = {"primary_key": [TermsID]} + foreign_keys=[__table__.c.TermsID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py index 7aa7733c..a849df02 100644 --- a/aurweb/models/account_type.py +++ b/aurweb/models/account_type.py @@ -1,6 +1,4 @@ -from sqlalchemy import Column, Integer - -from aurweb import db +from aurweb import schema from aurweb.models.declarative import Base USER = "User" @@ -8,37 +6,10 @@ TRUSTED_USER = "Trusted User" DEVELOPER = "Developer" TRUSTED_USER_AND_DEV = "Trusted User & Developer" - -class AccountType(Base): - """ An ORM model of a single AccountTypes record. """ - __tablename__ = "AccountTypes" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} - - def __init__(self, **kwargs): - self.AccountType = kwargs.pop("AccountType") - - def __str__(self): - return str(self.AccountType) - - def __repr__(self): - return "" % ( - self.ID, str(self)) - - -# Fetch account type IDs from the database for constants. -_account_types = db.query(AccountType) -USER_ID = _account_types.filter( - AccountType.AccountType == USER).first().ID -TRUSTED_USER_ID = _account_types.filter( - AccountType.AccountType == TRUSTED_USER).first().ID -DEVELOPER_ID = _account_types.filter( - AccountType.AccountType == DEVELOPER).first().ID -TRUSTED_USER_AND_DEV_ID = _account_types.filter( - AccountType.AccountType == TRUSTED_USER_AND_DEV).first().ID -_account_types = None # Get rid of the query handle. +USER_ID = 1 +TRUSTED_USER_ID = 2 +DEVELOPER_ID = 3 +TRUSTED_USER_AND_DEV_ID = 4 # Map string constants to integer constants. ACCOUNT_TYPE_ID = { @@ -50,3 +21,20 @@ ACCOUNT_TYPE_ID = { # Reversed ACCOUNT_TYPE_ID mapping. ACCOUNT_TYPE_NAME = {v: k for k, v in ACCOUNT_TYPE_ID.items()} + + +class AccountType(Base): + """ An ORM model of a single AccountTypes record. """ + __table__ = schema.AccountTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, **kwargs): + self.AccountType = kwargs.pop("AccountType") + + def __str__(self): + return str(self.AccountType) + + def __repr__(self): + return "" % ( + self.ID, str(self)) diff --git a/aurweb/models/api_rate_limit.py b/aurweb/models/api_rate_limit.py index f8641896..19b656df 100644 --- a/aurweb/models/api_rate_limit.py +++ b/aurweb/models/api_rate_limit.py @@ -1,15 +1,13 @@ -from sqlalchemy import Column, String from sqlalchemy.exc import IntegrityError +from aurweb import schema from aurweb.models.declarative import Base class ApiRateLimit(Base): - __tablename__ = "ApiRateLimit" - - IP = Column(String(45), primary_key=True, unique=True, default=str()) - - __mapper_args__ = {"primary_key": [IP]} + __table__ = schema.ApiRateLimit + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.IP]} def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/ban.py b/aurweb/models/ban.py index e10087b0..a70be7b9 100644 --- a/aurweb/models/ban.py +++ b/aurweb/models/ban.py @@ -1,15 +1,13 @@ from fastapi import Request -from sqlalchemy import Column, String +from aurweb import schema from aurweb.models.declarative import Base class Ban(Base): - __tablename__ = "Bans" - - IPAddress = Column(String(45), primary_key=True) - - __mapper_args__ = {"primary_key": [IPAddress]} + __table__ = schema.Bans + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.IPAddress]} def __init__(self, **kwargs): self.IPAddress = kwargs.get("IPAddress") diff --git a/aurweb/models/declarative.py b/aurweb/models/declarative.py index 96ee1829..20ddd20c 100644 --- a/aurweb/models/declarative.py +++ b/aurweb/models/declarative.py @@ -2,8 +2,6 @@ import json from sqlalchemy.ext.declarative import declarative_base -import aurweb.db - from aurweb import util @@ -25,12 +23,10 @@ Base = declarative_base() # Setup __table_args__ applicable to every table. Base.__table_args__ = { - "autoload": True, - "autoload_with": aurweb.db.get_engine(), + "autoload": False, "extend_existing": True } - # Setup Base.as_dict and Base.json. # # With this, declarative models can use .as_dict() or .json() diff --git a/aurweb/models/dependency_type.py b/aurweb/models/dependency_type.py index 3b5fafcc..98418802 100644 --- a/aurweb/models/dependency_type.py +++ b/aurweb/models/dependency_type.py @@ -1,6 +1,4 @@ -from sqlalchemy import Column, Integer - -from aurweb import db +from aurweb import schema from aurweb.models.declarative import Base DEPENDS = "depends" @@ -8,23 +6,16 @@ MAKEDEPENDS = "makedepends" CHECKDEPENDS = "checkdepends" OPTDEPENDS = "optdepends" +DEPENDS_ID = 1 +MAKEDEPENDS_ID = 2 +CHECKDEPENDS_ID = 3 +OPTDEPENDS_ID = 4 + class DependencyType(Base): - __tablename__ = "DependencyTypes" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.DependencyTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def __init__(self, Name: str = None): self.Name = Name - - -DEPENDS_ID = db.query(DependencyType).filter( - DependencyType.Name == DEPENDS).first().ID -MAKEDEPENDS_ID = db.query(DependencyType).filter( - DependencyType.Name == MAKEDEPENDS).first().ID -CHECKDEPENDS_ID = db.query(DependencyType).filter( - DependencyType.Name == CHECKDEPENDS).first().ID -OPTDEPENDS_ID = db.query(DependencyType).filter( - DependencyType.Name == OPTDEPENDS).first().ID diff --git a/aurweb/models/group.py b/aurweb/models/group.py index 5493bb7f..0275ed94 100644 --- a/aurweb/models/group.py +++ b/aurweb/models/group.py @@ -1,15 +1,13 @@ -from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError +from aurweb import schema from aurweb.models.declarative import Base class Group(Base): - __tablename__ = "Groups" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.Groups + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/license.py b/aurweb/models/license.py index fa863379..86aeaa86 100644 --- a/aurweb/models/license.py +++ b/aurweb/models/license.py @@ -1,15 +1,13 @@ -from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError +from aurweb import schema from aurweb.models.declarative import Base class License(Base): - __tablename__ = "Licenses" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.Licenses + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/official_provider.py b/aurweb/models/official_provider.py index a273dd06..a8282ff1 100644 --- a/aurweb/models/official_provider.py +++ b/aurweb/models/official_provider.py @@ -1,6 +1,6 @@ -from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError +from aurweb import schema from aurweb.models.declarative import Base # TODO: Fix this! Official packages aren't from aur.archlinux.org... @@ -8,11 +8,9 @@ OFFICIAL_BASE = "https://aur.archlinux.org" class OfficialProvider(Base): - __tablename__ = "OfficialProviders" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.OfficialProviders + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package.py b/aurweb/models/package.py index ef119f3c..8f82dadd 100644 --- a/aurweb/models/package.py +++ b/aurweb/models/package.py @@ -1,24 +1,19 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase class Package(Base): - __tablename__ = "Packages" + __table__ = schema.Packages + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} - ID = Column(Integer, primary_key=True) - - PackageBaseID = Column( - Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), - nullable=False) PackageBase = relationship( _PackageBase, backref=backref("packages", lazy="dynamic"), - foreign_keys=[PackageBaseID]) - - __mapper_args__ = {"primary_key": [ID]} + foreign_keys=[__table__.c.PackageBaseID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_base.py b/aurweb/models/package_base.py index e6f28050..8c88b7b5 100644 --- a/aurweb/models/package_base.py +++ b/aurweb/models/package_base.py @@ -1,38 +1,33 @@ from datetime import datetime -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.user import User as _User class PackageBase(Base): - __tablename__ = "PackageBases" + __table__ = schema.PackageBases + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} - FlaggerUID = Column(Integer, - ForeignKey("Users.ID", ondelete="SET NULL")) Flagger = relationship( _User, backref=backref("flagged_bases", lazy="dynamic"), - foreign_keys=[FlaggerUID]) + foreign_keys=[__table__.c.FlaggerUID]) - SubmitterUID = Column(Integer, - ForeignKey("Users.ID", ondelete="SET NULL")) Submitter = relationship( _User, backref=backref("submitted_bases", lazy="dynamic"), - foreign_keys=[SubmitterUID]) + foreign_keys=[__table__.c.SubmitterUID]) - MaintainerUID = Column(Integer, - ForeignKey("Users.ID", ondelete="SET NULL")) Maintainer = relationship( _User, backref=backref("maintained_bases", lazy="dynamic"), - foreign_keys=[MaintainerUID]) + foreign_keys=[__table__.c.MaintainerUID]) - PackagerUID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) Packager = relationship( _User, backref=backref("package_bases", lazy="dynamic"), - foreign_keys=[PackagerUID]) + foreign_keys=[__table__.c.PackagerUID]) # A set used to check for floatable values. TO_FLOAT = {"Popularity"} diff --git a/aurweb/models/package_blacklist.py b/aurweb/models/package_blacklist.py index 4ba3f308..0f8f0cee 100644 --- a/aurweb/models/package_blacklist.py +++ b/aurweb/models/package_blacklist.py @@ -1,15 +1,13 @@ -from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError +from aurweb import schema from aurweb.models.declarative import Base class PackageBlacklist(Base): - __tablename__ = "PackageBlacklist" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.PackageBlacklist + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_comaintainer.py b/aurweb/models/package_comaintainer.py index 2f77782c..7641fb43 100644 --- a/aurweb/models/package_comaintainer.py +++ b/aurweb/models/package_comaintainer.py @@ -1,30 +1,26 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase from aurweb.models.user import User as _User class PackageComaintainer(Base): - __tablename__ = "PackageComaintainers" + __table__ = schema.PackageComaintainers + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID] + } - UsersID = Column( - Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) User = relationship( _User, backref=backref("comaintained", lazy="dynamic"), - foreign_keys=[UsersID]) + foreign_keys=[__table__.c.UsersID]) - PackageBaseID = Column( - Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), - nullable=False) PackageBase = relationship( _PackageBase, backref=backref("comaintainers", lazy="dynamic"), - foreign_keys=[PackageBaseID]) - - __mapper_args__ = {"primary_key": [UsersID, PackageBaseID]} + foreign_keys=[__table__.c.PackageBaseID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_comment.py b/aurweb/models/package_comment.py index a511df9b..2a529c9c 100644 --- a/aurweb/models/package_comment.py +++ b/aurweb/models/package_comment.py @@ -1,43 +1,33 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase from aurweb.models.user import User as _User class PackageComment(Base): - __tablename__ = "PackageComments" + __table__ = schema.PackageComments + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} - ID = Column(Integer, primary_key=True) - - PackageBaseID = Column( - Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), - nullable=False) PackageBase = relationship( _PackageBase, backref=backref("comments", lazy="dynamic", cascade="all, delete"), - foreign_keys=[PackageBaseID]) + foreign_keys=[__table__.c.PackageBaseID]) - UsersID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) User = relationship( _User, backref=backref("package_comments", lazy="dynamic"), - foreign_keys=[UsersID]) + foreign_keys=[__table__.c.UsersID]) - EditedUsersID = Column( - Integer, ForeignKey("Users.ID", ondelete="SET NULL")) Editor = relationship( _User, backref=backref("edited_comments", lazy="dynamic"), - foreign_keys=[EditedUsersID]) + foreign_keys=[__table__.c.EditedUsersID]) - DelUsersID = Column( - Integer, ForeignKey("Users.ID", ondelete="SET NULL")) Deleter = relationship( _User, backref=backref("deleted_comments", lazy="dynamic"), - foreign_keys=[DelUsersID]) - - __mapper_args__ = {"primary_key": [ID]} + foreign_keys=[__table__.c.DelUsersID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index 3f4e2baa..edaa6538 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -1,34 +1,28 @@ -from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.dependency_type import DependencyType as _DependencyType from aurweb.models.package import Package as _Package class PackageDependency(Base): - __tablename__ = "PackageDepends" + __table__ = schema.PackageDepends + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.PackageID, __table__.c.DepName] + } - PackageID = Column( - Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), - nullable=False) Package = relationship( _Package, backref=backref("package_dependencies", lazy="dynamic", cascade="all, delete"), - foreign_keys=[PackageID]) + foreign_keys=[__table__.c.PackageID]) - DepTypeID = Column( - Integer, ForeignKey("DependencyTypes.ID", ondelete="NO ACTION"), - nullable=False) DependencyType = relationship( _DependencyType, backref=backref("package_dependencies", lazy="dynamic"), - foreign_keys=[DepTypeID]) - - DepName = Column(String(255), nullable=False) - - __mapper_args__ = {"primary_key": [PackageID, DepName]} + foreign_keys=[__table__.c.DepTypeID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_group.py b/aurweb/models/package_group.py index c1d1e4f8..3b6db37d 100644 --- a/aurweb/models/package_group.py +++ b/aurweb/models/package_group.py @@ -1,28 +1,26 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.group import Group as _Group from aurweb.models.package import Package as _Package class PackageGroup(Base): - __tablename__ = "PackageGroups" + __table__ = schema.PackageGroups + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.PackageID, __table__.c.GroupID] + } - PackageID = Column(Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), - primary_key=True, nullable=True) Package = relationship( _Package, backref=backref("package_groups", lazy="dynamic"), - foreign_keys=[PackageID]) + foreign_keys=[__table__.c.PackageID]) - GroupID = Column(Integer, ForeignKey("Groups.ID", ondelete="CASCADE"), - primary_key=True, nullable=True) Group = relationship( _Group, backref=backref("package_groups", lazy="dynamic"), - foreign_keys=[GroupID]) - - __mapper_args__ = {"primary_key": [PackageID, GroupID]} + foreign_keys=[__table__.c.GroupID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py index 25bd340b..581aafdc 100644 --- a/aurweb/models/package_keyword.py +++ b/aurweb/models/package_keyword.py @@ -1,27 +1,22 @@ -from sqlalchemy import Column, ForeignKey, Integer, String, text from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase class PackageKeyword(Base): - __tablename__ = "PackageKeywords" + __table__ = schema.PackageKeywords + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.PackageBaseID, __table__.c.Keyword] + } - PackageBaseID = Column( - Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), - primary_key=True, nullable=True) PackageBase = relationship( _PackageBase, backref=backref("keywords", lazy="dynamic", cascade="all, delete"), - foreign_keys=[PackageBaseID]) - - Keyword = Column( - String(255), primary_key=True, nullable=False, - server_default=text("''")) - - __mapper_args__ = {"primary_key": [PackageBaseID, Keyword]} + foreign_keys=[__table__.c.PackageBaseID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_license.py b/aurweb/models/package_license.py index db12a7c3..43dd0339 100644 --- a/aurweb/models/package_license.py +++ b/aurweb/models/package_license.py @@ -1,32 +1,28 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.license import License as _License from aurweb.models.package import Package as _Package class PackageLicense(Base): - __tablename__ = "PackageLicenses" + __table__ = schema.PackageLicenses + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.PackageID, __table__.c.LicenseID] + } - PackageID = Column( - Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), - primary_key=True, nullable=True) Package = relationship( _Package, backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"), - foreign_keys=[PackageID]) + foreign_keys=[__table__.c.PackageID]) - LicenseID = Column( - Integer, ForeignKey("Licenses.ID", ondelete="CASCADE"), - primary_key=True, nullable=True) License = relationship( _License, backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"), - foreign_keys=[LicenseID]) - - __mapper_args__ = {"primary_key": [PackageID, LicenseID]} + foreign_keys=[__table__.c.LicenseID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_notification.py b/aurweb/models/package_notification.py index 221067e1..97dbe38f 100644 --- a/aurweb/models/package_notification.py +++ b/aurweb/models/package_notification.py @@ -1,31 +1,27 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase from aurweb.models.user import User as _User class PackageNotification(Base): - __tablename__ = "PackageNotifications" + __table__ = schema.PackageNotifications + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.UserID, __table__.c.PackageBaseID] + } - UserID = Column( - Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) User = relationship( _User, backref=backref("notifications", lazy="dynamic"), - foreign_keys=[UserID]) + foreign_keys=[__table__.c.UserID]) - PackageBaseID = Column( - Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), - nullable=False) PackageBase = relationship( _PackageBase, backref=backref("notifications", lazy="dynamic"), - foreign_keys=[PackageBaseID]) - - __mapper_args__ = {"primary_key": [UserID, PackageBaseID]} + foreign_keys=[__table__.c.PackageBaseID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_relation.py b/aurweb/models/package_relation.py index e79a90d6..eb6caa84 100644 --- a/aurweb/models/package_relation.py +++ b/aurweb/models/package_relation.py @@ -1,33 +1,27 @@ -from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package import Package as _Package from aurweb.models.relation_type import RelationType as _RelationType class PackageRelation(Base): - __tablename__ = "PackageRelations" + __table__ = schema.PackageRelations + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.PackageID, __table__.c.RelName] + } - PackageID = Column( - Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), - nullable=False) Package = relationship( _Package, backref=backref("package_relations", lazy="dynamic", cascade="all, delete"), - foreign_keys=[PackageID]) + foreign_keys=[__table__.c.PackageID]) - RelTypeID = Column( - Integer, ForeignKey("RelationTypes.ID", ondelete="CASCADE"), - nullable=False) RelationType = relationship( _RelationType, backref=backref("package_relations", lazy="dynamic"), - foreign_keys=[RelTypeID]) - - RelName = Column(String(255), unique=True) - - __mapper_args__ = {"primary_key": [PackageID, RelName]} + foreign_keys=[__table__.c.RelTypeID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_request.py b/aurweb/models/package_request.py index f600566c..9669ec46 100644 --- a/aurweb/models/package_request.py +++ b/aurweb/models/package_request.py @@ -1,7 +1,7 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase from aurweb.models.request_type import RequestType as _RequestType @@ -20,34 +20,25 @@ REJECTED_ID = 3 class PackageRequest(Base): - __tablename__ = "PackageRequests" + __table__ = schema.PackageRequests + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} - ID = Column(Integer, primary_key=True) - - ReqTypeID = Column( - Integer, ForeignKey("RequestTypes.ID", ondelete="NO ACTION"), - nullable=False) RequestType = relationship( _RequestType, backref=backref("package_requests", lazy="dynamic"), - foreign_keys=[ReqTypeID]) + foreign_keys=[__table__.c.ReqTypeID]) - UsersID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) User = relationship( _User, backref=backref("package_requests", lazy="dynamic"), - foreign_keys=[UsersID]) + foreign_keys=[__table__.c.UsersID]) - PackageBaseID = Column( - Integer, ForeignKey("PackageBases.ID", ondelete="SET NULL")) PackageBase = relationship( _PackageBase, backref=backref("requests", lazy="dynamic"), - foreign_keys=[PackageBaseID]) + foreign_keys=[__table__.c.PackageBaseID]) - ClosedUID = Column(Integer, ForeignKey("Users.ID", ondelete="SET NULL")) Closer = relationship( _User, backref=backref("closed_requests", lazy="dynamic"), - foreign_keys=[ClosedUID]) - - __mapper_args__ = {"primary_key": [ID]} + foreign_keys=[__table__.c.ClosedUID]) STATUS_DISPLAY = { PENDING_ID: PENDING, diff --git a/aurweb/models/package_source.py b/aurweb/models/package_source.py index db983272..59046bbd 100644 --- a/aurweb/models/package_source.py +++ b/aurweb/models/package_source.py @@ -1,22 +1,22 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package import Package as _Package class PackageSource(Base): - __tablename__ = "PackageSources" + __table__ = schema.PackageSources + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.PackageID] + } - PackageID = Column(Integer, ForeignKey("Packages.ID", ondelete="CASCADE"), - nullable=False) Package = relationship( _Package, backref=backref("package_sources", lazy="dynamic", cascade="all, delete"), - foreign_keys=[PackageID]) - - __mapper_args__ = {"primary_key": [PackageID]} + foreign_keys=[__table__.c.PackageID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/package_vote.py b/aurweb/models/package_vote.py index 2d70be16..7221d527 100644 --- a/aurweb/models/package_vote.py +++ b/aurweb/models/package_vote.py @@ -1,30 +1,26 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase from aurweb.models.user import User as _User class PackageVote(Base): - __tablename__ = "PackageVotes" + __table__ = schema.PackageVotes + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID] + } - UsersID = Column( - Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) User = relationship( _User, backref=backref("package_votes", lazy="dynamic"), - foreign_keys=[UsersID]) + foreign_keys=[__table__.c.UsersID]) - PackageBaseID = Column( - Integer, ForeignKey("PackageBases.ID", ondelete="CASCADE"), - nullable=False) PackageBase = relationship( _PackageBase, backref=backref("package_votes", lazy="dynamic"), - foreign_keys=[PackageBaseID]) - - __mapper_args__ = {"primary_key": [UsersID, PackageBaseID]} + foreign_keys=[__table__.c.PackageBaseID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/relation_type.py b/aurweb/models/relation_type.py index 71b6adbb..b52c91ec 100644 --- a/aurweb/models/relation_type.py +++ b/aurweb/models/relation_type.py @@ -1,27 +1,19 @@ -from sqlalchemy import Column, Integer - -from aurweb import db +from aurweb import schema from aurweb.models.declarative import Base CONFLICTS = "conflicts" PROVIDES = "provides" REPLACES = "replaces" +CONFLICTS_ID = 1 +PROVIDES_ID = 2 +REPLACES_ID = 3 + class RelationType(Base): - __tablename__ = "RelationTypes" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.RelationTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def __init__(self, Name: str = None): self.Name = Name - - -CONFLICTS_ID = db.query(RelationType).filter( - RelationType.Name == CONFLICTS).first().ID -PROVIDES_ID = db.query(RelationType).filter( - RelationType.Name == PROVIDES).first().ID -REPLACES_ID = db.query(RelationType).filter( - RelationType.Name == REPLACES).first().ID diff --git a/aurweb/models/request_type.py b/aurweb/models/request_type.py index 4578464c..cabab3d2 100644 --- a/aurweb/models/request_type.py +++ b/aurweb/models/request_type.py @@ -1,25 +1,20 @@ -from sqlalchemy import Column, Integer - -from aurweb import db +from aurweb import schema from aurweb.models.declarative import Base DELETION = "deletion" ORPHAN = "orphan" MERGE = "merge" +DELETION_ID = 1 +ORPHAN_ID = 2 +MERGE_ID = 3 + class RequestType(Base): - __tablename__ = "RequestTypes" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.RequestTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def name_display(self) -> str: """ Return the Name column with its first char capitalized. """ return self.Name.title() - - -DELETION_ID = db.query(RequestType, RequestType.Name == DELETION).first().ID -ORPHAN_ID = db.query(RequestType, RequestType.Name == ORPHAN).first().ID -MERGE_ID = db.query(RequestType, RequestType.Name == MERGE).first().ID diff --git a/aurweb/models/session.py b/aurweb/models/session.py index a4034678..96f88d85 100644 --- a/aurweb/models/session.py +++ b/aurweb/models/session.py @@ -1,23 +1,20 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.db import make_random_value, query from aurweb.models.declarative import Base from aurweb.models.user import User as _User class Session(Base): - __tablename__ = "Sessions" + __table__ = schema.Sessions + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.UsersID]} - UsersID = Column( - Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) User = relationship( _User, backref=backref("session", uselist=False), - foreign_keys=[UsersID]) - - __mapper_args__ = {"primary_key": [UsersID]} + foreign_keys=[__table__.c.UsersID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/ssh_pub_key.py b/aurweb/models/ssh_pub_key.py index 268a585b..789be629 100644 --- a/aurweb/models/ssh_pub_key.py +++ b/aurweb/models/ssh_pub_key.py @@ -3,30 +3,23 @@ import tempfile from subprocess import PIPE, Popen -from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base class SSHPubKey(Base): - __tablename__ = "SSHPubKeys" + __table__ = schema.SSHPubKeys + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.Fingerprint]} - UserID = Column( - Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) User = relationship( "User", backref=backref("ssh_pub_key", uselist=False), - foreign_keys=[UserID]) - - Fingerprint = Column(String(44), primary_key=True) - - __mapper_args__ = {"primary_key": Fingerprint} + foreign_keys=[__table__.c.UserID]) def __init__(self, **kwargs): - self.UserID = kwargs.get("UserID") - self.Fingerprint = kwargs.get("Fingerprint") - self.PubKey = kwargs.get("PubKey") + super().__init__(**kwargs) def get_fingerprint(pubkey): diff --git a/aurweb/models/term.py b/aurweb/models/term.py index 0985cd76..59534bbc 100644 --- a/aurweb/models/term.py +++ b/aurweb/models/term.py @@ -1,15 +1,13 @@ -from sqlalchemy import Column, Integer from sqlalchemy.exc import IntegrityError +from aurweb import schema from aurweb.models.declarative import Base class Term(Base): - __tablename__ = "Terms" - - ID = Column(Integer, primary_key=True) - - __mapper_args__ = {"primary_key": [ID]} + __table__ = schema.Terms + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/tu_vote.py b/aurweb/models/tu_vote.py index 634c041e..efb23b19 100644 --- a/aurweb/models/tu_vote.py +++ b/aurweb/models/tu_vote.py @@ -1,28 +1,26 @@ -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.tu_voteinfo import TUVoteInfo as _TUVoteInfo from aurweb.models.user import User as _User class TUVote(Base): - __tablename__ = "TU_Votes" + __table__ = schema.TU_Votes + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.VoteID, __table__.c.UserID] + } - VoteID = Column(Integer, ForeignKey("TU_VoteInfo.ID", ondelete="CASCADE"), - nullable=False) VoteInfo = relationship( _TUVoteInfo, backref=backref("tu_votes", lazy="dynamic"), - foreign_keys=[VoteID]) + foreign_keys=[__table__.c.VoteID]) - UserID = Column(Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) User = relationship( _User, backref=backref("tu_votes", lazy="dynamic"), - foreign_keys=[UserID]) - - __mapper_args__ = {"primary_key": [VoteID, UserID]} + foreign_keys=[__table__.c.UserID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/tu_voteinfo.py b/aurweb/models/tu_voteinfo.py index da43b097..35675ccc 100644 --- a/aurweb/models/tu_voteinfo.py +++ b/aurweb/models/tu_voteinfo.py @@ -2,27 +2,22 @@ import typing from datetime import datetime -from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship +from aurweb import schema from aurweb.models.declarative import Base from aurweb.models.user import User as _User class TUVoteInfo(Base): - __tablename__ = "TU_VoteInfo" + __table__ = schema.TU_VoteInfo + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} - ID = Column(Integer, primary_key=True) - - SubmitterID = Column( - Integer, ForeignKey("Users.ID", ondelete="CASCADE"), - nullable=False) Submitter = relationship( _User, backref=backref("tu_voteinfo_set", lazy="dynamic"), - foreign_keys=[SubmitterID]) - - __mapper_args__ = {"primary_key": [ID]} + foreign_keys=[__table__.c.SubmitterID]) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index e4223144..8db34c38 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -5,14 +5,14 @@ from datetime import datetime import bcrypt from fastapi import Request -from sqlalchemy import Column, ForeignKey, Integer, String, or_, text +from sqlalchemy import or_ from sqlalchemy.orm import backref, relationship import aurweb.config import aurweb.models.account_type import aurweb.schema -from aurweb import db +from aurweb import db, schema from aurweb.models.account_type import AccountType as _AccountType from aurweb.models.ban import is_banned from aurweb.models.declarative import Base @@ -22,23 +22,16 @@ SALT_ROUNDS_DEFAULT = 12 class User(Base): """ An ORM model of a single Users record. """ - __tablename__ = "Users" + __table__ = schema.Users + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} - ID = Column(Integer, primary_key=True) - - AccountTypeID = Column( - Integer, ForeignKey("AccountTypes.ID", ondelete="NO ACTION"), - nullable=False, server_default=text("1")) AccountType = relationship( _AccountType, backref=backref("users", lazy="dynamic"), - foreign_keys=[AccountTypeID], + foreign_keys=[__table__.c.AccountTypeID], uselist=False) - Passwd = Column(String(255), default=str()) - - __mapper_args__ = {"primary_key": [ID]} - # High-level variables used to track authentication (not in DB). authenticated = False nonce = None @@ -49,7 +42,7 @@ class User(Base): SALT_ROUNDS_DEFAULT) def __init__(self, Passwd: str = str(), **kwargs): - super().__init__(**kwargs) + super().__init__(**kwargs, Passwd=str()) # Run this again in the constructor in case we rehashed config. self.salt_rounds = aurweb.config.getint("options", "salt_rounds", From 3517862ecdaf665693bbfb6e06fe547249d34005 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 8 Nov 2021 18:46:21 -0800 Subject: [PATCH 0625/1451] change(poetry): use kevr@upgrade-starlette-0.17.0 as fastapi source Starlette 0.16.0 has a pretty bad bug in terms of logging which has been fixed in the 0.17.0 release. That being said, FastAPI has not yet merged a request at https://github.com/tiangolo/fastapi/pull/4145 which resolves this dependency resolution so we can use the updated starlette package. kevr has forked the pull request in question and we are using it for now in our poetry dependencies to get ahead of the game. When FastAPI upstream is updated to support 0.17.0, we'll need to switch this back to using upstream's source. Signed-off-by: Kevin Morris --- poetry.lock | 317 +++++++++++++++++++++++++++---------------------- pyproject.toml | 2 +- 2 files changed, 177 insertions(+), 142 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9f528d12..37e2f8f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -165,7 +165,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.0.2" +version = "6.1.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -213,12 +213,15 @@ trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] [[package]] name = "dunamai" -version = "1.6.0" +version = "1.7.0" description = "Dynamic version generation" category = "main" optional = false python-versions = ">=3.5,<4.0" +[package.dependencies] +packaging = ">=20.9" + [[package]] name = "email-validator" version = "1.1.3" @@ -256,10 +259,11 @@ description = "FastAPI framework, high performance, easy to learn, fast to code, category = "main" optional = false python-versions = ">=3.6.1" +develop = false [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.16.0" +starlette = "0.17.0" [package.extras] all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] @@ -267,6 +271,12 @@ dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,< doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=7.1.9,<8.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] +[package.source] +type = "git" +url = "https://github.com/kevr/fastapi.git" +reference = "upgrade-starlette-0.17.0" +resolved_reference = "5d2d79e6bafd86564c318b7f99153132cd6ca466" + [[package]] name = "feedgen" version = "0.9.0" @@ -428,7 +438,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.3" +version = "5.10.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -464,7 +474,7 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lxml" -version = "4.6.3" +version = "4.6.4" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -544,14 +554,14 @@ python-versions = ">=3.7" [[package]] name = "packaging" -version = "21.0" +version = "21.2" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3" [[package]] name = "paginate" @@ -619,7 +629,7 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "3.19.0" +version = "3.19.1" description = "Protocol Buffers" category = "main" optional = false @@ -627,11 +637,11 @@ python-versions = ">=3.5" [[package]] name = "py" -version = "1.10.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycodestyle" @@ -643,7 +653,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycparser" -version = "2.20" +version = "2.21" description = "C parser in Python" category = "main" optional = false @@ -743,7 +753,7 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale [[package]] name = "pytest-tap" -version = "3.2" +version = "3.3" description = "Test Anything Protocol (TAP) reporting plugin for pytest" category = "dev" optional = false @@ -876,7 +886,7 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "starlette" -version = "0.16.0" +version = "0.17.0" description = "The little ASGI library that shines." category = "main" optional = false @@ -886,7 +896,7 @@ python-versions = ">=3.6" anyio = ">=3.0.0,<4" [package.extras] -full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "graphene"] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] [[package]] name = "tap.py" @@ -909,7 +919,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.1" +version = "1.2.2" description = "A lil' TOML parser" category = "dev" optional = false @@ -993,7 +1003,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.10" -content-hash = "569b0489389b884d269458f8e4252efcf3ebbbaa5fa77b6d09d7f0cdbda53362" +content-hash = "356b37d545d78b8aa1e1939f42522207bcf79526abe8193308c5a2955897d6fd" [metadata.files] aiofiles = [ @@ -1106,39 +1116,55 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, - {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, - {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, - {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, - {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, - {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, - {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, - {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, - {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, - {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, - {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, - {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, - {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, - {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, - {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, - {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, - {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, - {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, + {file = "coverage-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9"}, + {file = "coverage-6.1.1-cp310-cp310-win32.whl", hash = "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026"}, + {file = "coverage-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9"}, + {file = "coverage-6.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489"}, + {file = "coverage-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003"}, + {file = "coverage-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b"}, + {file = "coverage-6.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b"}, + {file = "coverage-6.1.1-cp37-cp37m-win32.whl", hash = "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685"}, + {file = "coverage-6.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926"}, + {file = "coverage-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad"}, + {file = "coverage-6.1.1-cp38-cp38-win32.whl", hash = "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b"}, + {file = "coverage-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54"}, + {file = "coverage-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a"}, + {file = "coverage-6.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae6de0e41f44794e68d23644636544ed8003ce24845f213b24de097cbf44997f"}, + {file = "coverage-6.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2797ed7a7e883b9ab76e8e778bb4c859fc2037d6fd0644d8675e64d58d1653"}, + {file = "coverage-6.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c40966b683d92869b72ea3c11fd6b99a091fd30e12652727eca117273fc97366"}, + {file = "coverage-6.1.1-cp39-cp39-win32.whl", hash = "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0"}, + {file = "coverage-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b"}, + {file = "coverage-6.1.1-pp36-none-any.whl", hash = "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f"}, + {file = "coverage-6.1.1-pp37-none-any.whl", hash = "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07"}, + {file = "coverage-6.1.1-pp38-none-any.whl", hash = "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5"}, + {file = "coverage-6.1.1.tar.gz", hash = "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0"}, ] cryptography = [ {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, @@ -1167,8 +1193,8 @@ dnspython = [ {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] dunamai = [ - {file = "dunamai-1.6.0-py3-none-any.whl", hash = "sha256:44a94a4edebb145bb6198a2f26de957b12b77d43b7c9c0646be814c60cf5d8df"}, - {file = "dunamai-1.6.0.tar.gz", hash = "sha256:6f1111f47e869ed58d44a7d37f112e3e7c761dce3c71f2c5464526928d7e9896"}, + {file = "dunamai-1.7.0-py3-none-any.whl", hash = "sha256:375e017eb014681e9c8f6e7f2c4c2065ef35832d429f8b70900bed24e8be83f8"}, + {file = "dunamai-1.7.0.tar.gz", hash = "sha256:6abfeb91768caea59d65a4989cec49472fa66ee04dcd6a5c9f92ebc019926a93"}, ] email-validator = [ {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, @@ -1178,10 +1204,7 @@ fakeredis = [ {file = "fakeredis-1.6.1-py3-none-any.whl", hash = "sha256:5eb1516f1fe1813e9da8f6c482178fc067af09f53de587ae03887ef5d9d13024"}, {file = "fakeredis-1.6.1.tar.gz", hash = "sha256:0d06a9384fb79da9f2164ce96e34eb9d4e2ea46215070805ea6fd3c174590b47"}, ] -fastapi = [ - {file = "fastapi-0.70.0-py3-none-any.whl", hash = "sha256:a36d5f2fad931aa3575c07a3472c784e81f3e664e3bb5c8b9c88d0ec1104f59c"}, - {file = "fastapi-0.70.0.tar.gz", hash = "sha256:66da43cfe5185ea1df99552acffd201f1832c6b364e0f4136c0a99f933466ced"}, -] +fastapi = [] feedgen = [ {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, ] @@ -1282,8 +1305,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, - {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, + {file = "isort-5.10.0-py3-none-any.whl", hash = "sha256:1a18ccace2ed8910bd9458b74a3ecbafd7b2f581301b0ab65cfdd4338272d76f"}, + {file = "isort-5.10.0.tar.gz", hash = "sha256:e52ff6d38012b131628cf0f26c51e7bd3a7c81592eefe3ac71411e692f1b9345"}, ] itsdangerous = [ {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, @@ -1294,54 +1317,66 @@ jinja2 = [ {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, ] lxml = [ - {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, - {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, - {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, - {file = "lxml-4.6.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4"}, - {file = "lxml-4.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16"}, - {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, - {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, - {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617"}, - {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, - {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, - {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92"}, - {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, - {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, - {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae"}, - {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, - {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, - {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a"}, - {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, - {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, - {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, + {file = "lxml-4.6.4-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bbf2dc330bd44bfc0254ab37677ec60f7c7ecea55ad8ba1b8b2ea7bf20c265f5"}, + {file = "lxml-4.6.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b667c51682fe9b9788c69465956baa8b6999531876ccedcafc895c74ad716cd8"}, + {file = "lxml-4.6.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:72e730d33fe2e302fd07285f14624fca5e5e2fb2bb4fb2c3941e318c41c443d1"}, + {file = "lxml-4.6.4-cp27-cp27m-win32.whl", hash = "sha256:433df8c7dde0f9e41cbf4f36b0829d50a378116ef5e962ba3881f2f5f025c7be"}, + {file = "lxml-4.6.4-cp27-cp27m-win_amd64.whl", hash = "sha256:35752ee40f7bbf6adc9ff4e1f4b84794a3593736dcce80db32e3c2aa85e294ac"}, + {file = "lxml-4.6.4-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ff5bb2a198ea67403bb6818705e9a4f90e0313f2215428ec51001ce56d939fb"}, + {file = "lxml-4.6.4-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9b87727561c1150c0cc91c5d9d389448b37a7d15f0ba939ed3d1acb2f11bf6c5"}, + {file = "lxml-4.6.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:45fdb2899c755138722797161547a40b3e2a06feda620cc41195ee7e97806d81"}, + {file = "lxml-4.6.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:38b9de0de3aa689fe9fb9877ae1be1e83b8cf9621f7e62049d0436b9ecf4ad64"}, + {file = "lxml-4.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:662523cd2a0246740225c7e32531f2e766544122e58bee70e700a024cfc0cf81"}, + {file = "lxml-4.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:4aa349c5567651f34d4eaae7de6ed5b523f6d70a288f9c6fbac22d13a0784e04"}, + {file = "lxml-4.6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:08eb9200d88b376a8ed5e50f1dc1d1a45b49305169674002a3b5929943390591"}, + {file = "lxml-4.6.4-cp310-cp310-win32.whl", hash = "sha256:bdc224f216ead849e902151112efef6e96c41ee1322e15d4e5f7c8a826929aee"}, + {file = "lxml-4.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ab6db93a2b6b66cbf62b4e4a7135f476e708e8c5c990d186584142c77d7f975a"}, + {file = "lxml-4.6.4-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50790313df028aa05cf22be9a8da033b86c42fa32523e4fd944827b482b17bf0"}, + {file = "lxml-4.6.4-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6764998345552b1dfc9326a932d2bad6367c6b37a176bb73ada6b9486bf602f7"}, + {file = "lxml-4.6.4-cp35-cp35m-win32.whl", hash = "sha256:543b239b191bb3b6d9bef5f09f1fb2be5b7eb09ab4d386aa655e4d53fbe9ff47"}, + {file = "lxml-4.6.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a75c1ad05eedb1a3ff2a34a52a4f0836cfaa892e12796ba39a7732c82701eff4"}, + {file = "lxml-4.6.4-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:47e955112ce64241fdb357acf0216081f9f3255b3ac9c502ca4b3323ec1ca558"}, + {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:20d7c8d90d449c6a353b15ee0459abae8395dbe59ad01e406ccbf30cd81c6f98"}, + {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:240db6f3228d26e3c6f4fad914b9ddaaf8707254e8b3efd564dc680c8ec3c264"}, + {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:351482da8dd028834028537f08724b1de22d40dcf3bb723b469446564f409074"}, + {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e678a643177c0e5ec947b645fa7bc84260dfb9b6bf8fb1fdd83008dfc2ca5928"}, + {file = "lxml-4.6.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:15d0381feb56f08f78c5cc4fc385ddfe0bde1456e37f54a9322833371aec4060"}, + {file = "lxml-4.6.4-cp36-cp36m-win32.whl", hash = "sha256:4ba74afe5ee5cb5e28d83b513a6e8f0875fda1dc1a9aea42cc0065f029160d2a"}, + {file = "lxml-4.6.4-cp36-cp36m-win_amd64.whl", hash = "sha256:9c91a73971a922c13070fd8fa5a114c858251791ba2122a941e6aa781c713e44"}, + {file = "lxml-4.6.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:6020c70ff695106bf80651953a23e37718ef1fee9abd060dcad8e32ab2dc13f3"}, + {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f5dd358536b8a964bf6bd48de038754c1609e72e5f17f5d21efe2dda17594dbf"}, + {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7ae7089d81fc502df4b217ad77f03c54039fe90dac0acbe70448d7e53bfbc57e"}, + {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:80d10d53d3184837445ff8562021bdd37f57c4cadacbf9d8726cc16220a00d54"}, + {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e95da348d57eb448d226a44b868ff2ca5786fbcbe417ac99ff62d0a7d724b9c7"}, + {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ffd65cfa33fed01735c82aca640fde4cc63f0414775cba11e06f84fae2085a6e"}, + {file = "lxml-4.6.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:877666418598f6cb289546c77ff87590cfd212f903b522b0afa0b9fb73b3ccfb"}, + {file = "lxml-4.6.4-cp37-cp37m-win32.whl", hash = "sha256:e91d24623e747eeb2d8121f4a94c6a7ad27dc48e747e2dc95bfe88632bd028a2"}, + {file = "lxml-4.6.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4ec9a80dd5704ecfde54319b6964368daf02848c8954d3bacb9b64d1c7659159"}, + {file = "lxml-4.6.4-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:2901625f4a878a055d275beedc20ba9cb359cefc4386a967222fee29eb236038"}, + {file = "lxml-4.6.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b567178a74a2261345890eac66fbf394692a6e002709d329f28a673ca6042473"}, + {file = "lxml-4.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4717123f7c11c81e0da69989e5a64079c3f402b0efeb4c6241db6c369d657bd8"}, + {file = "lxml-4.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:cf201bf5594d1aab139fe53e3fca457e4f8204a5bbd65d48ab3b82a16f517868"}, + {file = "lxml-4.6.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a77a3470ba37e11872c75ca95baf9b3312133a3d5a5dc720803b23098c653976"}, + {file = "lxml-4.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:619c6d2b552bba00491e96c0518aad94002651c108a0f7364ff2d7798812c00e"}, + {file = "lxml-4.6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:601f0ab75538b280aaf1e720eb9d68d4fa104ac274e1e9e6971df488f4dcdb0f"}, + {file = "lxml-4.6.4-cp38-cp38-win32.whl", hash = "sha256:75d3c5bbc0ddbad03bb68b9be638599f67e4b98ed3dcd0fec9f6f39e41ee96cb"}, + {file = "lxml-4.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:4341d135f5660db10184963d9c3418c3e28d7f868aaf8b11a323ebf85813f7f4"}, + {file = "lxml-4.6.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:9db24803fa71e3305fe4a7812782b708da21a0b774b130dd1860cf40a6d7a3ee"}, + {file = "lxml-4.6.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:afd60230ad9d8bcba005945ec3a343722f09e0b7f8ae804246e5d2cfc6bd71a6"}, + {file = "lxml-4.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0c15e1cd55055956e77b0732270f1c6005850696bc3ef3e03d01e78af84eaa42"}, + {file = "lxml-4.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d422b3c729737d8a39279a25fa156c983a56458f8b2f97661ee6fb22b80b1d6"}, + {file = "lxml-4.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb90f6ec3c236ef2f1bb38aee7c0d23e77d423d395af6326e7cca637519a4cb"}, + {file = "lxml-4.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:51a0e5d243687596f46e24e464121d4b232ad772e2d1785b2a2c0eb413c285d4"}, + {file = "lxml-4.6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d43bd68714049c84e297c005456a15ecdec818f7b5aa5868c8b0a865cfb78a44"}, + {file = "lxml-4.6.4-cp39-cp39-win32.whl", hash = "sha256:ee9e4b07b0eba4b6a521509e9e1877476729c1243246b6959de697ebea739643"}, + {file = "lxml-4.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:48eaac2991b3036175b42ee8d3c23f4cca13f2be8426bf29401a690ab58c88f4"}, + {file = "lxml-4.6.4-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:2b06a91cf7b8acea7793006e4ae50646cef0fe35ce5acd4f5cb1c77eb228e4a1"}, + {file = "lxml-4.6.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:523f195948a1ba4f9f5b7294d83c6cd876547dc741820750a7e5e893a24bbe38"}, + {file = "lxml-4.6.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b0ca0ada9d3bc18bd6f611bd001a28abdd49ab9698bd6d717f7f5394c8e94628"}, + {file = "lxml-4.6.4-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:197b7cb7a753cf553a45115739afd8458464a28913da00f5c525063f94cd3f48"}, + {file = "lxml-4.6.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6298f5b42a26581206ef63fffa97c754245d329414108707c525512a5197f2ba"}, + {file = "lxml-4.6.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0b12c95542f04d10cba46b3ff28ea52ea56995b78cf918f0b11b05e75812bb79"}, + {file = "lxml-4.6.4.tar.gz", hash = "sha256:daf9bd1fee31f1c7a5928b3e1059e09a8d683ea58fb3ffc773b6c88cb8d1399c"}, ] mako = [ {file = "Mako-1.1.5-py2.py3-none-any.whl", hash = "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23"}, @@ -1445,8 +1480,8 @@ orjson = [ {file = "orjson-3.6.4.tar.gz", hash = "sha256:f8dbc428fc6d7420f231a7133d8dff4c882e64acb585dcf2fda74bdcfe1a6d9d"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, + {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, ] paginate = [ {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, @@ -1472,42 +1507,42 @@ prometheus-fastapi-instrumentator = [ {file = "prometheus_fastapi_instrumentator-5.7.1-py3-none-any.whl", hash = "sha256:da40ea0df14b0e95d584769747fba777522a8df6a8c47cec2edf798f1fff49b5"}, ] protobuf = [ - {file = "protobuf-3.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:01a0645ef3acddfbc90237e1cdfae1086130fc7cb480b5874656193afd657083"}, - {file = "protobuf-3.19.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d3861c9721a90ba83ee0936a9cfcc4fa1c4b4144ac9658fb6f6343b38558e9b4"}, - {file = "protobuf-3.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b64be5d7270cf5e76375bac049846e8a9543a2d4368b69afe78ab725380a7487"}, - {file = "protobuf-3.19.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2f6046b9e2feee0dce994493186e8715b4392ed5f50f356280ad9c2f9f93080a"}, - {file = "protobuf-3.19.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac2f8ec942d414609aba0331952ae12bb823e8f424bbb6b8c422f1cef32dc842"}, - {file = "protobuf-3.19.0-cp36-cp36m-win32.whl", hash = "sha256:3fea09aa04ef2f8b01fcc9bb87f19509934f8a35d177c865b8f9ee5c32b60c1b"}, - {file = "protobuf-3.19.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d1f4277d321f60456845ca9b882c4845736f1f5c1c69eb778eba22a97977d8af"}, - {file = "protobuf-3.19.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8488c2276f14f294e890cc1260ab342a13e90cd20dcc03319d2eea258f1fd321"}, - {file = "protobuf-3.19.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:36bf292f44966c67080e535321501717f4f1eba30faef8f2cd4b0c745a027211"}, - {file = "protobuf-3.19.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99af73ae34c93e0e2ace57ea2e70243f34fc015c8c23fd39ee93652e726f7e7"}, - {file = "protobuf-3.19.0-cp37-cp37m-win32.whl", hash = "sha256:f7a031cf8e2fc14acc0ba694f6dff0a01e06b70d817eba6edc72ee6cc20517ac"}, - {file = "protobuf-3.19.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d4ca5f0c7bc8d2e6966ca3bbd85e9ebe7191b6e21f067896d4af6b28ecff29fe"}, - {file = "protobuf-3.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a8a880593015ef2c83f7af797fa4fbf583b2c98b4bd94e46c5b61fee319d84b"}, - {file = "protobuf-3.19.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:6f16925f5c977dd7787973a50c242e60c22b1d1182aba6bec7bd02862579c10f"}, - {file = "protobuf-3.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9097327d277b0aa4a3224e61cd6850aef3269172397715299bcffc9f90293c9"}, - {file = "protobuf-3.19.0-cp38-cp38-win32.whl", hash = "sha256:708d04394a63ee9bdc797938b6e15ed5bf24a1cb37743eb3886fd74a5a67a234"}, - {file = "protobuf-3.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:ee4d07d596357f51316b6ecf1cc1927660e9d5e418385bb1c51fd2496cd9bee7"}, - {file = "protobuf-3.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34a77b8fafdeb8f89fee2b7108ae60d8958d72e33478680cc1e05517892ecc46"}, - {file = "protobuf-3.19.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4f93e0f6af796ddd1502225ff8ea25340ced186ca05b601c44d5c88b45ba80a0"}, - {file = "protobuf-3.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:942dd6bc8bd2a3c6a156d8ab0f80bd45313f22b78e1176283270054dcc8ca4c2"}, - {file = "protobuf-3.19.0-cp39-cp39-win32.whl", hash = "sha256:7b3867795708ac88fde8d6f34f0d9a50af56087e41f624bdb2e9ff808ea5dda7"}, - {file = "protobuf-3.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:a74432e9d28a6072a2359a0f49f81eb14dd718e7dbbfb6c0789b456c49e1f130"}, - {file = "protobuf-3.19.0-py2.py3-none-any.whl", hash = "sha256:c96e94d3e523a82caa3e5f74b35dd1c4884199358d01c950d95c341255ff48bc"}, - {file = "protobuf-3.19.0.tar.gz", hash = "sha256:6a1dc6584d24ef86f5b104bcad64fa0fe06ed36e5687f426e0445d363a041d18"}, + {file = "protobuf-3.19.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8"}, + {file = "protobuf-3.19.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a529e7df52204565bcd33738a7a5f288f3d2d37d86caa5d78c458fa5fabbd54d"}, + {file = "protobuf-3.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28ccea56d4dc38d35cd70c43c2da2f40ac0be0a355ef882242e8586c6d66666f"}, + {file = "protobuf-3.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b30a7de128c46b5ecb343917d9fa737612a6e8280f440874e5cc2ba0d79b8f6"}, + {file = "protobuf-3.19.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5935c8ce02e3d89c7900140a8a42b35bc037ec07a6aeb61cc108be8d3c9438a6"}, + {file = "protobuf-3.19.1-cp36-cp36m-win32.whl", hash = "sha256:74f33edeb4f3b7ed13d567881da8e5a92a72b36495d57d696c2ea1ae0cfee80c"}, + {file = "protobuf-3.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:038daf4fa38a7e818dd61f51f22588d61755160a98db087a046f80d66b855942"}, + {file = "protobuf-3.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e51561d72efd5bd5c91490af1f13e32bcba8dab4643761eb7de3ce18e64a853"}, + {file = "protobuf-3.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6e8ea9173403219239cdfd8d946ed101f2ab6ecc025b0fda0c6c713c35c9981d"}, + {file = "protobuf-3.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db3532d9f7a6ebbe2392041350437953b6d7a792de10e629c1e4f5a6b1fe1ac6"}, + {file = "protobuf-3.19.1-cp37-cp37m-win32.whl", hash = "sha256:615b426a177780ce381ecd212edc1e0f70db8557ed72560b82096bd36b01bc04"}, + {file = "protobuf-3.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d8919368410110633717c406ab5c97e8df5ce93020cfcf3012834f28b1fab1ea"}, + {file = "protobuf-3.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:71b0250b0cfb738442d60cab68abc166de43411f2a4f791d31378590bfb71bd7"}, + {file = "protobuf-3.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3cd0458870ea7d1c58e948ac8078f6ba8a7ecc44a57e03032ed066c5bb318089"}, + {file = "protobuf-3.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:655264ed0d0efe47a523e2255fc1106a22f6faab7cc46cfe99b5bae085c2a13e"}, + {file = "protobuf-3.19.1-cp38-cp38-win32.whl", hash = "sha256:b691d996c6d0984947c4cf8b7ae2fe372d99b32821d0584f0b90277aa36982d3"}, + {file = "protobuf-3.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:e7e8d2c20921f8da0dea277dfefc6abac05903ceac8e72839b2da519db69206b"}, + {file = "protobuf-3.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd390367fc211cc0ffcf3a9e149dfeca78fecc62adb911371db0cec5c8b7472d"}, + {file = "protobuf-3.19.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d83e1ef8cb74009bebee3e61cc84b1c9cd04935b72bca0cbc83217d140424995"}, + {file = "protobuf-3.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d90676d6f426718463fe382ec6274909337ca6319d375eebd2044e6c6ac560"}, + {file = "protobuf-3.19.1-cp39-cp39-win32.whl", hash = "sha256:e7b24c11df36ee8e0c085e5b0dc560289e4b58804746fb487287dda51410f1e2"}, + {file = "protobuf-3.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:77d2fadcf369b3f22859ab25bd12bb8e98fb11e05d9ff9b7cd45b711c719c002"}, + {file = "protobuf-3.19.1-py2.py3-none-any.whl", hash = "sha256:e813b1c9006b6399308e917ac5d298f345d95bb31f46f02b60cd92970a9afa17"}, + {file = "protobuf-3.19.1.tar.gz", hash = "sha256:62a8e4baa9cb9e064eb62d1002eca820857ab2138440cb4b3ea4243830f94ca7"}, ] py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydantic = [ {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, @@ -1575,8 +1610,8 @@ pytest-cov = [ {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] pytest-tap = [ - {file = "pytest-tap-3.2.tar.gz", hash = "sha256:1b585c4a636458dbd958d136381bbabb1752c5877d05fac7d6a6001a8a9ddc29"}, - {file = "pytest_tap-3.2-py3-none-any.whl", hash = "sha256:18f59047f8bc68247d37f807fae7f2f8897d2c7397aea2fd2870f0421dc566cb"}, + {file = "pytest-tap-3.3.tar.gz", hash = "sha256:5f0919a147cf0396b2f10d64d365a0bf8062e06543e93c675c9d37f5605e983c"}, + {file = "pytest_tap-3.3-py3-none-any.whl", hash = "sha256:4fbbc0e090c2e94f6199bee4e4f68ab3c5e176b37a72a589ad84e0f72a2fce55"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -1648,8 +1683,8 @@ sqlalchemy = [ {file = "SQLAlchemy-1.4.26.tar.gz", hash = "sha256:6bc7f9d7d90ef55e8c6db1308a8619cd8f40e24a34f759119b95e7284dca351a"}, ] starlette = [ - {file = "starlette-0.16.0-py3-none-any.whl", hash = "sha256:38eb24bf705a2c317e15868e384c1b8a12ca396e5a3c3a003db7e667c43f939f"}, - {file = "starlette-0.16.0.tar.gz", hash = "sha256:e1904b5d0007aee24bdd3c43994be9b3b729f4f58e740200de1d623f8c3a8870"}, + {file = "starlette-0.17.0-py3-none-any.whl", hash = "sha256:64ffd950183d474df2cf7a4018c8bbb31a481367691c70f5ace4b2d376235f72"}, + {file = "starlette-0.17.0.tar.gz", hash = "sha256:31a889e7d7bf487f70d9d197ed7efadb47fa938c58626ed93e381480833c5b84"}, ] "tap.py" = [ {file = "tap.py-3.0-py2.py3-none-any.whl", hash = "sha256:a598bfaa2e224d71f2e86147c2ef822c18ff2e1b8ef006397e5056b08f92f699"}, @@ -1660,8 +1695,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, - {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, + {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, + {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, ] tomlkit = [ {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, diff --git a/pyproject.toml b/pyproject.toml index 20855fa6..1d4c858c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ bcrypt = "^3.2.0" bleach = "^4.1.0" email-validator = "^1.1.3" fakeredis = "^1.6.1" -fastapi = "^0.70.0" +fastapi = { git = "https://github.com/kevr/fastapi.git", branch = "upgrade-starlette-0.17.0" } feedgen = "^0.9.0" httpx = "^0.20.0" itsdangerous = "^2.0.1" From 85ebc72e8af542d73909b6f58f9bfb3b4f40ccd3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 8 Nov 2021 18:18:41 -0800 Subject: [PATCH 0626/1451] fix(fastapi): only elevated users are allowed to suspend accounts Signed-off-by: Kevin Morris --- aurweb/auth.py | 3 ++ aurweb/routers/accounts.py | 7 +++- po/aurweb.pot | 4 +++ templates/partials/account_form.html | 18 +++++----- test/test_accounts_routes.py | 49 ++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 5e45ee83..38754db0 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -51,6 +51,9 @@ class AnonymousUser: LangPreference = aurweb.config.get("options", "default_lang") Timezone = aurweb.config.get("options", "default_timezone") + Suspended = 0 + InactivityTS = 0 + # A stub ssh_pub_key relationship. ssh_pub_key = None diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 152b0a15..498568ad 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -143,6 +143,10 @@ def process_account_form(request: Request, user: models.User, args: dict): if not email or not username: return (False, ["Missing a required field."]) + inactive = args.get("J", False) + if not request.user.is_elevated() and inactive != bool(user.InactivityTS): + return (False, ["You do not have permission to suspend accounts."]) + username_min_len = aurweb.config.getint("options", "username_min_len") username_max_len = aurweb.config.getint("options", "username_max_len") if not util.valid_username(args.get("U")): @@ -528,7 +532,8 @@ async def account_edit_post(request: Request, user.Homepage = HP or user.Homepage user.IRCNick = I or user.IRCNick user.PGPKey = K or user.PGPKey - user.InactivityTS = datetime.utcnow().timestamp() if J else 0 + user.Suspended = J + user.InactivityTS = int(datetime.utcnow().timestamp()) * int(J) # If we update the language, update the cookie as well. if L and L != user.LangPreference: diff --git a/po/aurweb.pot b/po/aurweb.pot index 721f874e..f4deee70 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -879,6 +879,10 @@ msgstr "" msgid "Account suspended" msgstr "" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index f166c230..2e47a932 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -42,14 +42,16 @@ "account is inactive." | tr }}

    -

    - - -

    + {% if request.user.is_elevated() %} +

    + + +

    + {% endif %} {% if request.user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") %}

    diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 188f7048..5e855daf 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -780,6 +780,55 @@ def test_post_account_edit_error_invalid_password(): assert "Invalid password." in content +def test_post_account_edit_inactivity_unauthorized(): + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_data = { + "U": "test", + "E": "test@example.org", + "J": True, + "passwd": "testPassword" + } + with client as request: + resp = request.post(f"/account/{user.Username}/edit", data=post_data, + cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + errors = get_errors(resp.text) + expected = "You do not have permission to suspend accounts." + assert errors[0].text.strip() == expected + + +def test_post_account_edit_inactivity(): + with db.begin(): + user.AccountTypeID = TRUSTED_USER_ID + assert not user.Suspended + + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_data = { + "U": "test", + "E": "test@example.org", + "J": True, + "passwd": "testPassword" + } + with client as request: + resp = request.post(f"/account/{user.Username}/edit", data=post_data, + cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # Make sure the user record got updated correctly. + assert user.Suspended + assert user.InactivityTS > 0 + + post_data.update({"J": False}) + with client as request: + resp = request.post(f"/account/{user.Username}/edit", data=post_data, + cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + assert not user.Suspended + assert user.InactivityTS == 0 + + def test_post_account_edit_error_unauthorized(): request = Request() sid = user.login(request, "testPassword") From 464540c9a9f41aad52e633698647a9030092dc51 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 00:14:24 -0800 Subject: [PATCH 0627/1451] fix: use https for aurblup's default mirror instead of ftp It seems the ftp mirror from kernel.org cannot be used anymore, but the https mirror can. So, the default config has been updated to reflect this; otherwise, aurblup bugs out. Signed-off-by: Kevin Morris --- conf/config.defaults | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/config.defaults b/conf/config.defaults index b078e57c..babfd482 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -98,7 +98,7 @@ max-blob-size = 256000 [aurblup] db-path = /srv/http/aurweb/aurblup/ sync-dbs = core extra community multilib testing community-testing -server = ftp://mirrors.kernel.org/archlinux/%s/os/x86_64 +server = https://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] packagesfile = /srv/http/aurweb/web/html/packages.gz From b8d7619dbc34c41d2dac59fd717c7553054eb9d9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 00:17:52 -0800 Subject: [PATCH 0628/1451] change: add mkpkglists options to config.dev Here, we default to using root as the storage directory. Primarily because it makes sense in Docker; config.dev can always be fixed up by developers to reflect local system changes. Signed-off-by: Kevin Morris --- conf/config.dev | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conf/config.dev b/conf/config.dev index fb43612e..6cbe97cc 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -65,3 +65,8 @@ session_secret = secret [devel] ;commit_hash = 1234567 + +[mkpkglists] +packagesfile = /packages.gz +pkgbasefile = /pkgbase.gz +userfile = /users.gz From 338a44839f02850890e69f2985827cab9a1aefb4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 00:18:54 -0800 Subject: [PATCH 0629/1451] fix: override aurblup's db-path option in config.dev Signed-off-by: Kevin Morris --- conf/config.dev | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conf/config.dev b/conf/config.dev index 6cbe97cc..dac85477 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -70,3 +70,6 @@ session_secret = secret packagesfile = /packages.gz pkgbasefile = /pkgbase.gz userfile = /users.gz + +[aurblup] +db-path = YOUR_AUR_ROOT/aurblup/ From 4b8963b7bac999e2effb9840b01c9c01b8218fc0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 00:29:19 -0800 Subject: [PATCH 0630/1451] feat(docker): add cron service (aurblup + mkpkglists) Normally, these scripts are used to update official providers in the aurweb database along with archives that can be retrieved. Run both of these scripts in a 5 minute cron job, to both reflect the live instance database and production load. Signed-off-by: Kevin Morris --- docker-compose.yml | 17 +++++++++++++++++ docker/config/aurweb-cron | 2 ++ docker/cron-entrypoint.sh | 16 ++++++++++++++++ docker/scripts/install-deps.sh | 2 +- docker/scripts/run-cron.sh | 7 +++++++ 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 docker/config/aurweb-cron create mode 100755 docker/cron-entrypoint.sh create mode 100755 docker/scripts/run-cron.sh diff --git a/docker-compose.yml b/docker-compose.yml index 038eb65b..c2b14f91 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -144,6 +144,19 @@ services: volumes: - git_data:/aurweb/aur.git + cron: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=/aurweb/conf/config + entrypoint: /docker/cron-entrypoint.sh + command: /docker/scripts/run-cron.sh + depends_on: + mariadb_init: + condition: service_started + volumes: + - mariadb_run:/var/run/mysqld + php-fpm: image: aurweb:latest init: true @@ -163,6 +176,8 @@ services: condition: service_healthy memcached: condition: service_healthy + cron: + condition: service_started volumes: - mariadb_run:/var/run/mysqld ports: @@ -190,6 +205,8 @@ services: condition: service_healthy redis: condition: service_healthy + cron: + condition: service_started volumes: - mariadb_run:/var/run/mysqld ports: diff --git a/docker/config/aurweb-cron b/docker/config/aurweb-cron new file mode 100644 index 00000000..1be7c13c --- /dev/null +++ b/docker/config/aurweb-cron @@ -0,0 +1,2 @@ +*/5 * * * * aurweb-aurblup +*/5 * * * * aurweb-mkpkglists diff --git a/docker/cron-entrypoint.sh b/docker/cron-entrypoint.sh new file mode 100755 index 00000000..d4173eaf --- /dev/null +++ b/docker/cron-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -eou pipefail + +# Prepare AUR_CONFIG. +cp -vf conf/config.dev conf/config +sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config + +# Create directories we need. +mkdir -p /aurweb/aurblup + +# Install the cron configuration. +cp /docker/config/aurweb-cron /etc/cron.d/aurweb-cron +chmod 0644 /etc/cron.d/aurweb-cron +crontab /etc/cron.d/aurweb-cron + +exec "$@" diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 52ad6747..d64340e3 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -8,7 +8,7 @@ pacman -Syu --noconfirm --noprogressbar \ --cachedir .pkg-cache git gpgme nginx redis openssh \ mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ php php-fpm memcached php-memcached python-pip pyalpm \ - python-srcinfo curl libeatmydata + python-srcinfo curl libeatmydata cronie # https://python-poetry.org/docs/ Installation section. curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - diff --git a/docker/scripts/run-cron.sh b/docker/scripts/run-cron.sh new file mode 100755 index 00000000..d927a790 --- /dev/null +++ b/docker/scripts/run-cron.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cd /aurweb +aurweb-aurblup +aurweb-mkpkglists + +exec /usr/bin/crond -n From 10fcf939911cce4481bc1ec825329c8bc57ebd1d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 01:51:23 -0800 Subject: [PATCH 0631/1451] fix(fastapi): use correct official pkg base url Signed-off-by: Kevin Morris --- aurweb/models/official_provider.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aurweb/models/official_provider.py b/aurweb/models/official_provider.py index a8282ff1..ff70fe43 100644 --- a/aurweb/models/official_provider.py +++ b/aurweb/models/official_provider.py @@ -3,8 +3,7 @@ from sqlalchemy.exc import IntegrityError from aurweb import schema from aurweb.models.declarative import Base -# TODO: Fix this! Official packages aren't from aur.archlinux.org... -OFFICIAL_BASE = "https://aur.archlinux.org" +OFFICIAL_BASE = "https://archlinux.org" class OfficialProvider(Base): From f6061400509fdd808d066fceb4572c2728a39fde Mon Sep 17 00:00:00 2001 From: Kristian Klausen Date: Fri, 15 Oct 2021 20:14:31 +0200 Subject: [PATCH 0632/1451] feat(PHP): Add packages dump file with more metadata --- aurweb/scripts/mkpkglists.py | 10 ++++++++++ conf/config.defaults | 1 + test/setup.sh | 1 + web/html/index.php | 1 + 4 files changed, 13 insertions(+) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 6724141a..c73cc3be 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -2,11 +2,13 @@ import datetime import gzip +import json import aurweb.config import aurweb.db packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') +packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') userfile = aurweb.config.get('mkpkglists', 'userfile') @@ -27,6 +29,14 @@ def main(): "WHERE PackageBases.PackagerUID IS NOT NULL") f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + with gzip.open(packagesmetafile, "wt") as f: + cur = conn.execute("SELECT * FROM Packages") + json.dump({ + "warning": "This is a experimental! It can be removed or modified without warning!", + "columns": [d[0] for d in cur.description], + "data": cur.fetchall() + }, f) + with gzip.open(pkgbasefile, "w") as f: f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) cur = conn.execute("SELECT Name FROM PackageBases " + diff --git a/conf/config.defaults b/conf/config.defaults index babfd482..6ccf42d0 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -102,6 +102,7 @@ server = https://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] packagesfile = /srv/http/aurweb/web/html/packages.gz +packagesmetafile = /srv/http/aurweb/web/html/packages-meta-v1.json.gz pkgbasefile = /srv/http/aurweb/web/html/pkgbase.gz userfile = /srv/http/aurweb/web/html/users.gz diff --git a/test/setup.sh b/test/setup.sh index 191a73d8..8ee9eef2 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -67,6 +67,7 @@ server = file://$(pwd)/remote/ [mkpkglists] packagesfile = packages.gz +packagesmetafile = packages-meta-v1.json.gz pkgbasefile = pkgbase.gz userfile = users.gz EOF diff --git a/web/html/index.php b/web/html/index.php index e57e7708..3163c3e8 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -189,6 +189,7 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { readfile("./$path"); break; case "/packages.gz": + case "/packages-teapot.json.gz": case "/pkgbase.gz": case "/users.gz": header("Content-Type: text/plain"); From f3f662c696aaa35b57737c63bc506ded464a7d81 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 31 Oct 2021 16:52:30 -0700 Subject: [PATCH 0633/1451] fix(mkpkglists): improve package meta archive The SQL logic in this file for package metadata now exactly reflects RPC's search logic, without searching for specific packages. Two command line arguments are available: --extended | Include License, Keywords, Groups, relations and dependencies. When --extended is passed, the script will create a packages-meta-ext-v1.json.gz, configured via packagesmetaextfile. Archive JSON is in the following format: line-separated package objects enclosed in a list: [ {...}, {...}, {...} ] Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 273 ++++++++++++++++++++++++++++++++--- conf/config.defaults | 1 + test/setup.sh | 2 + web/html/index.php | 3 +- 4 files changed, 255 insertions(+), 24 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index c73cc3be..f2095a20 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -1,16 +1,192 @@ #!/usr/bin/env python3 +""" +Produces package, package base and user archives for the AUR +database. + +Archives: + + packages.gz | A line-separated list of package names + packages-meta-v1.json | A type=search RPC-formatted JSON dataset + packages-meta-ext-v1.json | An --extended archive + pkgbase.gz | A line-separated list of package base names + users.gz | A line-separated list of user names + +This script takes an optional argument: --extended. Based +on the following, right-hand side fields are added to each item. + + --extended | License, Keywords, Groups, relations and dependencies + +""" import datetime import gzip -import json +import os +import sys + +from collections import defaultdict +from decimal import Decimal +from typing import Tuple + +import orjson import aurweb.config import aurweb.db + +def state_path(archive: str) -> str: + # A hard-coded /tmp state directory. + # TODO: Use Redis cache to store this state after we merge + # FastAPI into master and removed PHP from the tree. + return os.path.join("/tmp", os.path.basename(archive) + ".state") + + packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') +packagesmetaextfile = aurweb.config.get('mkpkglists', 'packagesmetaextfile') +packages_state = state_path(packagesfile) + pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') +pkgbases_state = state_path(pkgbasefile) + userfile = aurweb.config.get('mkpkglists', 'userfile') +users_state = state_path(userfile) + + +def should_update(state: str, tablename: str) -> Tuple[bool, int]: + if aurweb.config.get("database", "backend") != "mysql": + return (False, 0) + + db_name = aurweb.config.get("database", "name") + conn = aurweb.db.Connection() + cur = conn.execute("SELECT auto_increment FROM information_schema.tables " + "WHERE table_schema = ? AND table_name = ?", + (db_name, tablename,)) + update_time = cur.fetchone()[0] + + saved_update_time = 0 + if os.path.exists(state): + with open(state) as f: + saved_update_time = int(f.read().strip()) + + return (saved_update_time == update_time, update_time) + + +def update_state(state: str, update_time: int) -> None: + with open(state, "w") as f: + f.write(str(update_time)) + + +TYPE_MAP = { + "depends": "Depends", + "makedepends": "MakeDepends", + "checkdepends": "CheckDepends", + "optdepends": "OptDepends", + "conflicts": "Conflicts", + "provides": "Provides", + "replaces": "Replaces", +} + + +def get_extended_dict(query: str): + """ + Produce data in the form in a single bulk SQL query: + + { + : { + "Depends": [...], + "Conflicts": [...], + "License": [...] + } + } + + The caller can then use this data to populate a dataset of packages. + + output = produce_base_output_data() + data = get_extended_dict(query) + for i in range(len(output)): + package_id = output[i].get("ID") + output[i].update(data.get(package_id)) + """ + + conn = aurweb.db.Connection() + + cursor = conn.execute(query) + + data = defaultdict(lambda: defaultdict(list)) + + for result in cursor.fetchall(): + + pkgid = result[0] + key = TYPE_MAP.get(result[1]) + output = result[2] + if result[3]: + output += result[3] + + # In all cases, we have at least an empty License list. + if "License" not in data[pkgid]: + data[pkgid]["License"] = [] + + # In all cases, we have at least an empty Keywords list. + if "Keywords" not in data[pkgid]: + data[pkgid]["Keywords"] = [] + + data[pkgid][key].append(output) + + conn.close() + return data + + +def get_extended_fields(): + # Returns: [ID, Type, Name, Cond] + query = """ + SELECT PackageDepends.PackageID AS ID, DependencyTypes.Name AS Type, + PackageDepends.DepName AS Name, PackageDepends.DepCondition AS Cond + FROM PackageDepends + LEFT JOIN DependencyTypes + ON DependencyTypes.ID = PackageDepends.DepTypeID + UNION SELECT PackageRelations.PackageID AS ID, RelationTypes.Name AS Type, + PackageRelations.RelName AS Name, + PackageRelations.RelCondition AS Cond + FROM PackageRelations + LEFT JOIN RelationTypes + ON RelationTypes.ID = PackageRelations.RelTypeID + UNION SELECT PackageGroups.PackageID AS ID, 'Groups' AS Type, + Groups.Name, '' AS Cond + FROM Groups + INNER JOIN PackageGroups ON PackageGroups.GroupID = Groups.ID + UNION SELECT PackageLicenses.PackageID AS ID, 'License' AS Type, + Licenses.Name, '' as Cond + FROM Licenses + INNER JOIN PackageLicenses ON PackageLicenses.LicenseID = Licenses.ID + UNION SELECT Packages.ID AS ID, 'Keywords' AS Type, + PackageKeywords.Keyword AS Name, '' as Cond + FROM PackageKeywords + INNER JOIN Packages ON Packages.PackageBaseID = PackageKeywords.PackageBaseID + """ + return get_extended_dict(query) + + +EXTENDED_FIELD_HANDLERS = { + "--extended": get_extended_fields +} + + +def is_decimal(column): + """ Check if an SQL column is of decimal.Decimal type. """ + if isinstance(column, Decimal): + return float(column) + return column + + +def write_archive(archive: str, output: list): + with gzip.open(archive, "wb") as f: + f.write(b"[\n") + for i, item in enumerate(output): + f.write(orjson.dumps(item)) + if i < len(output) - 1: + f.write(b",") + f.write(b"\n") + f.write(b"]") def main(): @@ -21,32 +197,83 @@ def main(): pkgbaselist_header = "# AUR package base list, generated on " + datestr userlist_header = "# AUR user name list, generated on " + datestr - with gzip.open(packagesfile, "w") as f: - f.write(bytes(pkglist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT Packages.Name FROM Packages " + - "INNER JOIN PackageBases " + - "ON PackageBases.ID = Packages.PackageBaseID " + + updated, update_time = should_update(packages_state, "Packages") + if not updated: + print("Updating Packages...") + + # Query columns; copied from RPC. + columns = ("Packages.ID, Packages.Name, " + "PackageBases.ID AS PackageBaseID, " + "PackageBases.Name AS PackageBase, " + "Version, Description, URL, NumVotes, " + "Popularity, OutOfDateTS AS OutOfDate, " + "Users.UserName AS Maintainer, " + "SubmittedTS AS FirstSubmitted, " + "ModifiedTS AS LastModified") + + # Perform query. + cur = conn.execute(f"SELECT {columns} FROM Packages " + "LEFT JOIN PackageBases " + "ON PackageBases.ID = Packages.PackageBaseID " + "LEFT JOIN Users " + "ON PackageBases.MaintainerUID = Users.ID " "WHERE PackageBases.PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - with gzip.open(packagesmetafile, "wt") as f: - cur = conn.execute("SELECT * FROM Packages") - json.dump({ - "warning": "This is a experimental! It can be removed or modified without warning!", - "columns": [d[0] for d in cur.description], - "data": cur.fetchall() - }, f) + # Produce packages-meta-v1.json.gz + output = list() + snapshot_uri = aurweb.config.get("options", "snapshot_uri") + for result in cur.fetchall(): + item = { + column[0]: is_decimal(result[i]) + for i, column in enumerate(cur.description) + } + item["URLPath"] = snapshot_uri % item.get("Name") + output.append(item) - with gzip.open(pkgbasefile, "w") as f: - f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT Name FROM PackageBases " + - "WHERE PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + write_archive(packagesmetafile, output) - with gzip.open(userfile, "w") as f: - f.write(bytes(userlist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT UserName FROM Users") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + # Produce packages-meta-ext-v1.json.gz + if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: + f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) + data = f() + + default_ = {"Groups": [], "License": [], "Keywords": []} + for i in range(len(output)): + data_ = data.get(output[i].get("ID"), default_) + output[i].update(data_) + + write_archive(packagesmetaextfile, output) + + # Produce packages.gz + with gzip.open(packagesfile, "wb") as f: + f.write(bytes(pkglist_header + "\n", "UTF-8")) + f.writelines([ + bytes(x.get("Name") + "\n", "UTF-8") + for x in output + ]) + + update_state(packages_state, update_time) + + updated, update_time = should_update(pkgbases_state, "PackageBases") + if not updated: + print("Updating PackageBases...") + # Produce pkgbase.gz + with gzip.open(pkgbasefile, "w") as f: + f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT Name FROM PackageBases " + + "WHERE PackagerUID IS NOT NULL") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + update_state(pkgbases_state, update_time) + + updated, update_time = should_update(users_state, "Users") + if not updated: + print("Updating Users...") + # Produce users.gz + with gzip.open(userfile, "w") as f: + f.write(bytes(userlist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT UserName FROM Users") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + update_state(users_state, update_time) conn.close() diff --git a/conf/config.defaults b/conf/config.defaults index 6ccf42d0..25d6dff9 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -103,6 +103,7 @@ server = https://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] packagesfile = /srv/http/aurweb/web/html/packages.gz packagesmetafile = /srv/http/aurweb/web/html/packages-meta-v1.json.gz +packagesmetaextfile = /srv/http/aurweb/web/html/packages-meta-ext-v1.json.gz pkgbasefile = /srv/http/aurweb/web/html/pkgbase.gz userfile = /srv/http/aurweb/web/html/users.gz diff --git a/test/setup.sh b/test/setup.sh index 8ee9eef2..d0c15b82 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -37,6 +37,7 @@ enable-maintenance = 0 maintenance-exceptions = 127.0.0.1 commit_uri = https://aur.archlinux.org/cgit/aur.git/log/?h=%s&id=%s localedir = $TOPLEVEL/web/locale/ +snapshot_uri = /cgit/aur.git/snapshot/%s.tar.gz [notifications] notify-cmd = $NOTIFY @@ -68,6 +69,7 @@ server = file://$(pwd)/remote/ [mkpkglists] packagesfile = packages.gz packagesmetafile = packages-meta-v1.json.gz +packagesmetaextfile = packages-meta-ext-v1.json.gz pkgbasefile = pkgbase.gz userfile = users.gz EOF diff --git a/web/html/index.php b/web/html/index.php index 3163c3e8..dc435162 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -189,7 +189,8 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { readfile("./$path"); break; case "/packages.gz": - case "/packages-teapot.json.gz": + case "/packages-meta-v1.json.gz": + case "/packages-meta-ext-v1.json.gz": case "/pkgbase.gz": case "/users.gz": header("Content-Type: text/plain"); From d62af4ceb582c360a6d7bbb876418170efbd92fc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 6 Nov 2021 16:23:08 -0700 Subject: [PATCH 0634/1451] feat(mkpkglists): added metadata archives Two new archives are available: - packages-meta-v1.json.gz - RPC search formatted data for all packages - ~2.1MB at the time of writing. - packages-meta-ext-v1.json.gz (via --extended) - RPC multiinfo formatted data for all packages. - ~9.8MB at the time of writing. New dependencies are required for this update: - `python-orjson` All archives served out by aur.archlinux.org distribute the Last-Modified header and support the If-Modified-Since header, which should be populated with Last-Modified's value. These should be used by clients to avoid redownloading the archive when unnecessary. Additionally, the new meta archives contain a format suitable for streaming the data as the file is retrieved. It is still in JSON format, however, users can parse package objects line by line after the first '[' found in the file, until the last ']'; both contained on their own lines. Note: This commit is a documentation change and commit body. Signed-off-by: Kevin Morris --- doc/maintenance.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/maintenance.txt b/doc/maintenance.txt index d6094545..2c5c9faf 100644 --- a/doc/maintenance.txt +++ b/doc/maintenance.txt @@ -70,7 +70,8 @@ computations and clean up the database: * aurweb-pkgmaint automatically removes empty repositories that were created within the last 24 hours but never populated. -* aurweb-mkpkglists generates the package list files. +* aurweb-mkpkglists generates the package list files; it takes an optional + --extended flag, which additionally produces multiinfo metadata. * aurweb-usermaint removes the last login IP address of all users that did not login within the past seven days. @@ -79,7 +80,7 @@ These scripts can be installed by running `python3 setup.py install` and are usually scheduled using Cron. The current setup is: ---- -*/5 * * * * aurweb-mkpkglists +*/5 * * * * aurweb-mkpkglists [--extended] 1 */2 * * * aurweb-popupdate 2 */2 * * * aurweb-aurblup 3 */2 * * * aurweb-pkgmaint From 0155f4ea84917ca12c905e9035a1d202ea91a3ff Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 6 Nov 2021 17:13:16 -0700 Subject: [PATCH 0635/1451] fix(mkpkglists): remove caching We really need caching for this; however, our current caching method will cause the script to bypass changes to columns if they have nothing to do with IDs. Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 159 ++++++++++++----------------------- 1 file changed, 54 insertions(+), 105 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index f2095a20..2566a146 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -20,60 +20,23 @@ on the following, right-hand side fields are added to each item. import datetime import gzip -import os import sys from collections import defaultdict from decimal import Decimal -from typing import Tuple import orjson import aurweb.config import aurweb.db - -def state_path(archive: str) -> str: - # A hard-coded /tmp state directory. - # TODO: Use Redis cache to store this state after we merge - # FastAPI into master and removed PHP from the tree. - return os.path.join("/tmp", os.path.basename(archive) + ".state") - - packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') packagesmetaextfile = aurweb.config.get('mkpkglists', 'packagesmetaextfile') -packages_state = state_path(packagesfile) pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') -pkgbases_state = state_path(pkgbasefile) userfile = aurweb.config.get('mkpkglists', 'userfile') -users_state = state_path(userfile) - - -def should_update(state: str, tablename: str) -> Tuple[bool, int]: - if aurweb.config.get("database", "backend") != "mysql": - return (False, 0) - - db_name = aurweb.config.get("database", "name") - conn = aurweb.db.Connection() - cur = conn.execute("SELECT auto_increment FROM information_schema.tables " - "WHERE table_schema = ? AND table_name = ?", - (db_name, tablename,)) - update_time = cur.fetchone()[0] - - saved_update_time = 0 - if os.path.exists(state): - with open(state) as f: - saved_update_time = int(f.read().strip()) - - return (saved_update_time == update_time, update_time) - - -def update_state(state: str, update_time: int) -> None: - with open(state, "w") as f: - f.write(str(update_time)) TYPE_MAP = { @@ -197,83 +160,69 @@ def main(): pkgbaselist_header = "# AUR package base list, generated on " + datestr userlist_header = "# AUR user name list, generated on " + datestr - updated, update_time = should_update(packages_state, "Packages") - if not updated: - print("Updating Packages...") + # Query columns; copied from RPC. + columns = ("Packages.ID, Packages.Name, " + "PackageBases.ID AS PackageBaseID, " + "PackageBases.Name AS PackageBase, " + "Version, Description, URL, NumVotes, " + "Popularity, OutOfDateTS AS OutOfDate, " + "Users.UserName AS Maintainer, " + "SubmittedTS AS FirstSubmitted, " + "ModifiedTS AS LastModified") - # Query columns; copied from RPC. - columns = ("Packages.ID, Packages.Name, " - "PackageBases.ID AS PackageBaseID, " - "PackageBases.Name AS PackageBase, " - "Version, Description, URL, NumVotes, " - "Popularity, OutOfDateTS AS OutOfDate, " - "Users.UserName AS Maintainer, " - "SubmittedTS AS FirstSubmitted, " - "ModifiedTS AS LastModified") + # Perform query. + cur = conn.execute(f"SELECT {columns} FROM Packages " + "LEFT JOIN PackageBases " + "ON PackageBases.ID = Packages.PackageBaseID " + "LEFT JOIN Users " + "ON PackageBases.MaintainerUID = Users.ID " + "WHERE PackageBases.PackagerUID IS NOT NULL") - # Perform query. - cur = conn.execute(f"SELECT {columns} FROM Packages " - "LEFT JOIN PackageBases " - "ON PackageBases.ID = Packages.PackageBaseID " - "LEFT JOIN Users " - "ON PackageBases.MaintainerUID = Users.ID " - "WHERE PackageBases.PackagerUID IS NOT NULL") + # Produce packages-meta-v1.json.gz + output = list() + snapshot_uri = aurweb.config.get("options", "snapshot_uri") + for result in cur.fetchall(): + item = { + column[0]: is_decimal(result[i]) + for i, column in enumerate(cur.description) + } + item["URLPath"] = snapshot_uri % item.get("Name") + output.append(item) - # Produce packages-meta-v1.json.gz - output = list() - snapshot_uri = aurweb.config.get("options", "snapshot_uri") - for result in cur.fetchall(): - item = { - column[0]: is_decimal(result[i]) - for i, column in enumerate(cur.description) - } - item["URLPath"] = snapshot_uri % item.get("Name") - output.append(item) + write_archive(packagesmetafile, output) - write_archive(packagesmetafile, output) + # Produce packages-meta-ext-v1.json.gz + if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: + f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) + data = f() - # Produce packages-meta-ext-v1.json.gz - if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: - f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) - data = f() + default_ = {"Groups": [], "License": [], "Keywords": []} + for i in range(len(output)): + data_ = data.get(output[i].get("ID"), default_) + output[i].update(data_) - default_ = {"Groups": [], "License": [], "Keywords": []} - for i in range(len(output)): - data_ = data.get(output[i].get("ID"), default_) - output[i].update(data_) + write_archive(packagesmetaextfile, output) - write_archive(packagesmetaextfile, output) + # Produce packages.gz + with gzip.open(packagesfile, "wb") as f: + f.write(bytes(pkglist_header + "\n", "UTF-8")) + f.writelines([ + bytes(x.get("Name") + "\n", "UTF-8") + for x in output + ]) - # Produce packages.gz - with gzip.open(packagesfile, "wb") as f: - f.write(bytes(pkglist_header + "\n", "UTF-8")) - f.writelines([ - bytes(x.get("Name") + "\n", "UTF-8") - for x in output - ]) + # Produce pkgbase.gz + with gzip.open(pkgbasefile, "w") as f: + f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT Name FROM PackageBases " + + "WHERE PackagerUID IS NOT NULL") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - update_state(packages_state, update_time) - - updated, update_time = should_update(pkgbases_state, "PackageBases") - if not updated: - print("Updating PackageBases...") - # Produce pkgbase.gz - with gzip.open(pkgbasefile, "w") as f: - f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT Name FROM PackageBases " + - "WHERE PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - update_state(pkgbases_state, update_time) - - updated, update_time = should_update(users_state, "Users") - if not updated: - print("Updating Users...") - # Produce users.gz - with gzip.open(userfile, "w") as f: - f.write(bytes(userlist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT UserName FROM Users") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) - update_state(users_state, update_time) + # Produce users.gz + with gzip.open(userfile, "w") as f: + f.write(bytes(userlist_header + "\n", "UTF-8")) + cur = conn.execute("SELECT UserName FROM Users") + f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) conn.close() From 0403b89f53302f7d58b170081ef7bbd2726d8fc1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 02:08:03 -0800 Subject: [PATCH 0636/1451] feat: add packagesmeta[ext]file option to conf/config.dev Better defaults for Docker here. Signed-off-by: Kevin Morris --- conf/config.dev | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conf/config.dev b/conf/config.dev index dac85477..05566e8b 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -68,6 +68,8 @@ session_secret = secret [mkpkglists] packagesfile = /packages.gz +packagesmetafile = /packages-meta-v1.json.gz +packagesmetaextfile = /packages-meta-ext-v1.json.gz pkgbasefile = /pkgbase.gz userfile = /users.gz From 068b067e148cb4f39fdfb7aa2086568084ac6a0a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 02:28:52 -0800 Subject: [PATCH 0637/1451] feat(docker): log cron executions Signed-off-by: Kevin Morris --- docker/config/aurweb-cron | 4 ++-- docker/scripts/run-cron.sh | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docker/config/aurweb-cron b/docker/config/aurweb-cron index 1be7c13c..87d7ed05 100644 --- a/docker/config/aurweb-cron +++ b/docker/config/aurweb-cron @@ -1,2 +1,2 @@ -*/5 * * * * aurweb-aurblup -*/5 * * * * aurweb-mkpkglists +*/5 * * * * bash -c 'aurweb-aurblup && echo "[$(date -u)] executed aurblup" >> /var/log/mkpkglists.log' +*/5 * * * * bash -c 'aurweb-mkpkglists && echo "[$(date -u)] executed mkpkglists" >> /var/log/mkpkglists.log' diff --git a/docker/scripts/run-cron.sh b/docker/scripts/run-cron.sh index d927a790..d227be94 100755 --- a/docker/scripts/run-cron.sh +++ b/docker/scripts/run-cron.sh @@ -2,6 +2,13 @@ cd /aurweb aurweb-aurblup +if [ $? -eq 0 ]; then + echo "[$(date -u)] executed aurblup" >> /var/log/aurblup.log +fi + aurweb-mkpkglists +if [ $? -eq 0 ]; then + echo "[$(date -u)] executed mkpkglists" >> /var/log/mkpkglists.log +fi exec /usr/bin/crond -n From 107367f958ef320d63952def915b65cef1ed31bd Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Nov 2021 02:29:39 -0800 Subject: [PATCH 0638/1451] feat(docker): use mkpkglists --extended flag Signed-off-by: Kevin Morris --- docker/config/aurweb-cron | 2 +- docker/scripts/run-cron.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/config/aurweb-cron b/docker/config/aurweb-cron index 87d7ed05..149b9d19 100644 --- a/docker/config/aurweb-cron +++ b/docker/config/aurweb-cron @@ -1,2 +1,2 @@ */5 * * * * bash -c 'aurweb-aurblup && echo "[$(date -u)] executed aurblup" >> /var/log/mkpkglists.log' -*/5 * * * * bash -c 'aurweb-mkpkglists && echo "[$(date -u)] executed mkpkglists" >> /var/log/mkpkglists.log' +*/5 * * * * bash -c 'aurweb-mkpkglists --extended && echo "[$(date -u)] executed mkpkglists" >> /var/log/mkpkglists.log' diff --git a/docker/scripts/run-cron.sh b/docker/scripts/run-cron.sh index d227be94..83ad6566 100755 --- a/docker/scripts/run-cron.sh +++ b/docker/scripts/run-cron.sh @@ -6,7 +6,7 @@ if [ $? -eq 0 ]; then echo "[$(date -u)] executed aurblup" >> /var/log/aurblup.log fi -aurweb-mkpkglists +aurweb-mkpkglists --extended if [ $? -eq 0 ]; then echo "[$(date -u)] executed mkpkglists" >> /var/log/mkpkglists.log fi From abbecf5194b69d419e81b547eaa8b3e72d550524 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 06:18:34 -0800 Subject: [PATCH 0639/1451] change(mkpkglists): remove header comments These comments change every time mkpkglists is run; which would invalidate the ETag headers disbursed by the gzip host. This commit removes those changing headers. Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 2566a146..b2d37d85 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -18,7 +18,6 @@ on the following, right-hand side fields are added to each item. """ -import datetime import gzip import sys @@ -155,11 +154,6 @@ def write_archive(archive: str, output: list): def main(): conn = aurweb.db.Connection() - datestr = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT") - pkglist_header = "# AUR package list, generated on " + datestr - pkgbaselist_header = "# AUR package base list, generated on " + datestr - userlist_header = "# AUR user name list, generated on " + datestr - # Query columns; copied from RPC. columns = ("Packages.ID, Packages.Name, " "PackageBases.ID AS PackageBaseID, " @@ -205,7 +199,6 @@ def main(): # Produce packages.gz with gzip.open(packagesfile, "wb") as f: - f.write(bytes(pkglist_header + "\n", "UTF-8")) f.writelines([ bytes(x.get("Name") + "\n", "UTF-8") for x in output @@ -213,14 +206,12 @@ def main(): # Produce pkgbase.gz with gzip.open(pkgbasefile, "w") as f: - f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) cur = conn.execute("SELECT Name FROM PackageBases " + "WHERE PackagerUID IS NOT NULL") f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) # Produce users.gz with gzip.open(userfile, "w") as f: - f.write(bytes(userlist_header + "\n", "UTF-8")) cur = conn.execute("SELECT UserName FROM Users") f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) From 4f7aeafa8d9fc835c604b82bd8f94308220ed0dd Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 06:20:22 -0800 Subject: [PATCH 0640/1451] feat(docker): host gzip archive downloads - added config option [mkpkglists] archivedir - created by mkpkglists Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 4 ++++ conf/config.defaults | 1 + conf/config.dev | 11 ++++++----- docker-compose.yml | 5 +++++ docker/config/nginx.conf | 19 ++++++++++++++++++- docker/php-entrypoint.sh | 4 ++++ 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index b2d37d85..f4b0fbe5 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -19,6 +19,7 @@ on the following, right-hand side fields are added to each item. """ import gzip +import os import sys from collections import defaultdict @@ -29,6 +30,9 @@ import orjson import aurweb.config import aurweb.db +archivedir = aurweb.config.get("mkpkglists", "archivedir") +os.makedirs(archivedir, exist_ok=True) + packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') packagesmetaextfile = aurweb.config.get('mkpkglists', 'packagesmetaextfile') diff --git a/conf/config.defaults b/conf/config.defaults index 25d6dff9..c29d7045 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -101,6 +101,7 @@ sync-dbs = core extra community multilib testing community-testing server = https://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] +archivedir = /srv/http/aurweb/web/html packagesfile = /srv/http/aurweb/web/html/packages.gz packagesmetafile = /srv/http/aurweb/web/html/packages-meta-v1.json.gz packagesmetaextfile = /srv/http/aurweb/web/html/packages-meta-ext-v1.json.gz diff --git a/conf/config.dev b/conf/config.dev index 05566e8b..9467615e 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -67,11 +67,12 @@ session_secret = secret ;commit_hash = 1234567 [mkpkglists] -packagesfile = /packages.gz -packagesmetafile = /packages-meta-v1.json.gz -packagesmetaextfile = /packages-meta-ext-v1.json.gz -pkgbasefile = /pkgbase.gz -userfile = /users.gz +archivedir = /var/lib/aurweb/archives +packagesfile = /var/lib/aurweb/archives/packages.gz +packagesmetafile = /var/lib/aurweb/archives/packages-meta-v1.json.gz +packagesmetaextfile = /var/lib/aurweb/archives/packages-meta-ext-v1.json.gz +pkgbasefile = /var/lib/aurweb/archives/pkgbase.gz +userfile = /var/lib/aurweb/archives/users.gz [aurblup] db-path = YOUR_AUR_ROOT/aurblup/ diff --git a/docker-compose.yml b/docker-compose.yml index c2b14f91..2fba1305 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -156,6 +156,7 @@ services: condition: service_started volumes: - mariadb_run:/var/run/mysqld + - archives:/var/lib/aurweb/archives php-fpm: image: aurweb:latest @@ -180,6 +181,7 @@ services: condition: service_started volumes: - mariadb_run:/var/run/mysqld + - archives:/var/lib/aurweb/archives ports: - "19000:9000" @@ -236,6 +238,8 @@ services: condition: service_healthy php-fpm: condition: service_healthy + volumes: + - archives:/var/lib/aurweb/archives sharness: image: aurweb:latest @@ -343,3 +347,4 @@ volumes: mariadb_data: {} # Share /var/lib/mysql git_data: {} # Share aurweb/aur.git smartgit_run: {} + archives: {} diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index 4288a57d..c3ffd7fa 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -94,7 +94,24 @@ http { ssl_certificate /etc/ssl/certs/web.cert.pem; ssl_certificate_key /etc/ssl/private/web.key.pem; - root /aurweb/web/html; + location ~ ^/.*\.gz$ { + # Override mime type to text/plain. + types { text/plain gz; } + default_type text/plain; + + # Filesystem location of .gz archives. + root /var/lib/aurweb/archives; + + # When we match this block, fix-up trying without a trailing slash. + try_files $uri $uri/ =404; + + # Caching headers. + expires max; + add_header Content-Encoding gzip; + add_header Cache-Control public; + add_header Last-Modified ""; + add_header ETag ""; + } location / { try_files $uri @proxy_to_app; diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index 81f70673..274f8e17 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/bash set -eou pipefail +for archive in packages pkgbase users packages-meta-v1.json packages-meta-ext-v1.json; do + ln -vsf /var/lib/aurweb/archives/${archive}.gz /aurweb/web/html/${archive}.gz +done + # Setup a config for our mysql db. cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config From 0c57c53da18b36c470471a754e5ea892a46db505 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 07:04:03 -0800 Subject: [PATCH 0641/1451] fix(sharness): fix AUR_CONFIG generation for mkpkglists test Signed-off-by: Kevin Morris --- test/setup.sh | 1 + test/t2100-mkpkglists.t | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/setup.sh b/test/setup.sh index d0c15b82..a09e0c6e 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -67,6 +67,7 @@ sync-dbs = test server = file://$(pwd)/remote/ [mkpkglists] +archivedir = $(pwd)/archive packagesfile = packages.gz packagesmetafile = packages-meta-v1.json.gz packagesmetaextfile = packages-meta-ext-v1.json.gz diff --git a/test/t2100-mkpkglists.t b/test/t2100-mkpkglists.t index 6bb6838d..d217c4f6 100755 --- a/test/t2100-mkpkglists.t +++ b/test/t2100-mkpkglists.t @@ -8,8 +8,8 @@ test_expect_success 'Test package list generation with no packages.' ' echo "DELETE FROM Packages;" | sqlite3 aur.db && echo "DELETE FROM PackageBases;" | sqlite3 aur.db && cover "$MKPKGLISTS" && - test $(zcat packages.gz | wc -l) -eq 1 && - test $(zcat pkgbase.gz | wc -l) -eq 1 + test $(zcat packages.gz | wc -l) -eq 0 && + test $(zcat pkgbase.gz | wc -l) -eq 0 ' test_expect_success 'Test package list generation.' ' From 4b2be7fff8f8e5203cd84a9f25f590d4bdf8ef5d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 13:00:01 -0800 Subject: [PATCH 0642/1451] feat(docker): add poetry caching Signed-off-by: Kevin Morris --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index b490d2fa..3c12cbf8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM archlinux:base-devel +VOLUME /root/.cache/pypoetry/cache +VOLUME /root/.cache/pypoetry/artifacts + ENV PATH="/root/.poetry/bin:${PATH}" ENV PYTHONPATH=/aurweb ENV AUR_CONFIG=conf/config From daef98080e7f1f595dde09c20da1b7b57f5478ed Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 13:05:19 -0800 Subject: [PATCH 0643/1451] fix(fastapi): fix broken official package query Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index be351ebe..cdec26f3 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -56,8 +56,8 @@ def dep_extra_desc(dep: models.PackageDependency) -> str: def pkgname_link(pkgname: str) -> str: base = "/".join([OFFICIAL_BASE, "packages"]) official = db.query(models.OfficialProvider).filter( - models.OfficialProvider.Name == pkgname) - if official.scalar(): + models.OfficialProvider.Name == pkgname).exists() + if db.query(official).scalar(): return f"{base}/?q={pkgname}" return f"/packages/{pkgname}" From 52110b7db5e9b6f3a1dc4fefdf60a31dc946d487 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 13:40:19 -0800 Subject: [PATCH 0644/1451] fix(mkpkglists): default keys to result[1] Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 2566a146..72efcd0a 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -21,14 +21,12 @@ on the following, right-hand side fields are added to each item. import datetime import gzip import sys - from collections import defaultdict from decimal import Decimal -import orjson - import aurweb.config import aurweb.db +import orjson packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') @@ -80,7 +78,7 @@ def get_extended_dict(query: str): for result in cursor.fetchall(): pkgid = result[0] - key = TYPE_MAP.get(result[1]) + key = TYPE_MAP.get(result[1], result[1]) output = result[2] if result[3]: output += result[3] From 6e344ce9da894cdb70fb02cc531145907ac28a48 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 13:40:19 -0800 Subject: [PATCH 0645/1451] fix(mkpkglists): default keys to result[1] Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index f4b0fbe5..74d41c7c 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -21,7 +21,6 @@ on the following, right-hand side fields are added to each item. import gzip import os import sys - from collections import defaultdict from decimal import Decimal @@ -83,7 +82,7 @@ def get_extended_dict(query: str): for result in cursor.fetchall(): pkgid = result[0] - key = TYPE_MAP.get(result[1]) + key = TYPE_MAP.get(result[1], result[1]) output = result[2] if result[3]: output += result[3] From 8788f9900576bc7fa79b54566f23cdaeaeafa9eb Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 13:54:18 -0800 Subject: [PATCH 0646/1451] fix(mkpkglists): restore isort order Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 74d41c7c..307b2b12 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -21,6 +21,7 @@ on the following, right-hand side fields are added to each item. import gzip import os import sys + from collections import defaultdict from decimal import Decimal From 66978e40a46cd096a49c0c7cba1ad60010313ba1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 10 Nov 2021 13:57:33 -0800 Subject: [PATCH 0647/1451] fix(mkpkglists): fix isort order (master) Signed-off-by: Kevin Morris --- aurweb/scripts/mkpkglists.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 72efcd0a..2d34a17b 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -21,12 +21,14 @@ on the following, right-hand side fields are added to each item. import datetime import gzip import sys + from collections import defaultdict from decimal import Decimal +import orjson + import aurweb.config import aurweb.db -import orjson packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') From 567090547d5f96b5e4266bb2d88c7344348bccf2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 13:09:05 -0800 Subject: [PATCH 0648/1451] add labels to gitlab issue templates Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Account Request.md | 2 ++ .gitlab/issue_templates/Bug.md | 2 ++ .gitlab/issue_templates/Feature.md | 2 ++ .gitlab/issue_templates/Feedback.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/.gitlab/issue_templates/Account Request.md b/.gitlab/issue_templates/Account Request.md index 244cfbe8..6831d3ad 100644 --- a/.gitlab/issue_templates/Account Request.md +++ b/.gitlab/issue_templates/Account Request.md @@ -10,3 +10,5 @@ - Username: the_username_you_want - Email: valid@email.org - Account Type: (User|Trusted User) + +/label account-request diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index d84a5181..d125e22b 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -32,3 +32,5 @@ aurweb's HTML render output. If you're testing locally, use the commit on which you are experiencing the bug. If you have found a bug which exists on live aur.archlinux.org, include the version located at the bottom of the webpage. + +/label bug unconfirmed diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md index 5b1524b1..c907adcd 100644 --- a/.gitlab/issue_templates/Feature.md +++ b/.gitlab/issue_templates/Feature.md @@ -28,3 +28,5 @@ Example: - [Feature] Do not allow users to be Tyrants - \<(issue|merge_request)_link\> + +/label feature unconsidered diff --git a/.gitlab/issue_templates/Feedback.md b/.gitlab/issue_templates/Feedback.md index e32120aa..950ec0c6 100644 --- a/.gitlab/issue_templates/Feedback.md +++ b/.gitlab/issue_templates/Feedback.md @@ -54,3 +54,5 @@ That being said: please include an overall summary of your experience and how you felt about the current implementation which you're testing in comparison with PHP (current aur.archlinux.org, or https://localhost:8443 through docker). + +/label feedback From 0da11f068bd34bc534baf34027b2a925475f6666 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 16:22:30 -0800 Subject: [PATCH 0649/1451] fix(fastapi): check for prometheus info.response When this is unchecked, exceptions cause the resulting stack trace to be oblivious to the original exception thrown. This commit changes that behavior so that metrics are created only when info.response exists. Signed-off-by: Kevin Morris --- aurweb/prometheus.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index a64f6b27..dae56320 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -79,9 +79,10 @@ def http_requests_total() -> Callable[[Info], None]: method = scope.get("method") path = get_matching_route_path(base_scope, scope.get("router").routes) - status = str(int(info.response.status_code))[:1] + "xx" - metric.labels(method=method, path=path, status=status).inc() + if info.response: + status = str(int(info.response.status_code))[:1] + "xx" + metric.labels(method=method, path=path, status=status).inc() return instrumentation @@ -95,7 +96,8 @@ def http_api_requests_total() -> Callable[[Info], None]: def instrumentation(info: Info) -> None: if info.request.url.path.rstrip("/") == "/rpc": type = info.request.query_params.get("type", "None") - status = str(info.response.status_code)[:1] + "xx" - metric.labels(type=type, status=status).inc() + if info.response: + status = str(info.response.status_code)[:1] + "xx" + metric.labels(type=type, status=status).inc() return instrumentation From cef217388adaa97e66e638ca4baf99f3b8d0f865 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 13:09:05 -0800 Subject: [PATCH 0650/1451] add labels to gitlab issue templates Signed-off-by: Kevin Morris --- .gitlab/issue_templates/Account Request.md | 2 ++ .gitlab/issue_templates/Bug.md | 2 ++ .gitlab/issue_templates/Feature.md | 2 ++ .gitlab/issue_templates/Feedback.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/.gitlab/issue_templates/Account Request.md b/.gitlab/issue_templates/Account Request.md index 244cfbe8..6831d3ad 100644 --- a/.gitlab/issue_templates/Account Request.md +++ b/.gitlab/issue_templates/Account Request.md @@ -10,3 +10,5 @@ - Username: the_username_you_want - Email: valid@email.org - Account Type: (User|Trusted User) + +/label account-request diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index d84a5181..d125e22b 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -32,3 +32,5 @@ aurweb's HTML render output. If you're testing locally, use the commit on which you are experiencing the bug. If you have found a bug which exists on live aur.archlinux.org, include the version located at the bottom of the webpage. + +/label bug unconfirmed diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md index 5b1524b1..c907adcd 100644 --- a/.gitlab/issue_templates/Feature.md +++ b/.gitlab/issue_templates/Feature.md @@ -28,3 +28,5 @@ Example: - [Feature] Do not allow users to be Tyrants - \<(issue|merge_request)_link\> + +/label feature unconsidered diff --git a/.gitlab/issue_templates/Feedback.md b/.gitlab/issue_templates/Feedback.md index e32120aa..950ec0c6 100644 --- a/.gitlab/issue_templates/Feedback.md +++ b/.gitlab/issue_templates/Feedback.md @@ -54,3 +54,5 @@ That being said: please include an overall summary of your experience and how you felt about the current implementation which you're testing in comparison with PHP (current aur.archlinux.org, or https://localhost:8443 through docker). + +/label feedback From 5f5fa44d0d3b1930ce22e64968392c8b595205c7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 17:12:13 -0800 Subject: [PATCH 0651/1451] fix(fastapi): fix licenses check Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index e8414bf4..583149f8 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -61,7 +61,7 @@ {% endif %} - {% if licenses and licenses.scalar() and show_package_details %} + {% if licenses and licenses.count() and show_package_details %} {{ "Licenses" | tr }}: {{ licenses | join(', ', attribute='Name') | default('None' | tr) }} From 363afff33260b2e3ce6236a22c8d6c32f0ffdd70 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 17:36:08 -0800 Subject: [PATCH 0652/1451] feat(fastapi): add /pkgbase/{name}/keywords (post) Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 23 ++++++++++++++++++ templates/partials/packages/details.html | 4 ++-- test/test_packages_routes.py | 30 ++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index bcc0be56..e227fe23 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -816,6 +816,29 @@ async def requests_close_post(request: Request, id: int, return RedirectResponse("/requests", status_code=HTTPStatus.SEE_OTHER) +@router.post("/pkgbase/{name}/keywords") +async def pkgbase_keywords(request: Request, name: str, + keywords: str = Form(default=str())): + pkgbase = get_pkg_or_base(name, models.PackageBase) + keywords = set(keywords.split(" ")) + + # Delete all keywords which are not supplied by the user. + with db.begin(): + db.delete(models.PackageKeyword, + and_(models.PackageKeyword.PackageBaseID == pkgbase.ID, + ~models.PackageKeyword.Keyword.in_(keywords))) + + existing_keywords = set(kwd.Keyword for kwd in pkgbase.keywords.all()) + with db.begin(): + for keyword in keywords.difference(existing_keywords): + db.create(models.PackageKeyword, + PackageBase=pkgbase, + Keyword=keyword) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=HTTPStatus.SEE_OTHER) + + @router.get("/pkgbase/{name}/flag") @auth_required(True, redirect="/pkgbase/{name}/flag") async def pkgbase_flag_get(request: Request, name: str): diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 583149f8..d525f63b 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -37,7 +37,7 @@ {{ "Keywords" | tr }}: {% if request.user.has_credential("CRED_PKGBASE_SET_KEYWORDS", approved=[pkgbase.Maintainer]) %} -

    @@ -51,7 +51,7 @@ {% else %} - {% for keyword in pkgbase.keywords %} + {% for keyword in pkgbase.keywords.all() %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index c4d9ab1c..887945d9 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -2604,3 +2604,33 @@ def test_account_comments(client: TestClient, user: User, package: Package): expected = rendered_comment.RenderedComment.replace( "

    ", "").replace("

    ", "") assert rendered[0].text.strip() == expected + + +def test_pkgbase_keywords(client: TestClient, user: User, package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}" + with client as request: + resp = request.get(endpoint) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + keywords = root.xpath('//a[@class="keyword"]') + assert len(keywords) == 0 + + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_endpoint = f"{endpoint}/keywords" + with client as request: + resp = request.post(post_endpoint, data={ + "keywords": "abc test" + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + with client as request: + resp = request.get(resp.headers.get("location")) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + keywords = root.xpath('//a[@class="keyword"]') + assert len(keywords) == 2 + expected = ["abc", "test"] + for i, keyword in enumerate(keywords): + assert keyword.text.strip() == expected[i] From 20f5519b99a29dc376a6d8a2325f58ca9305380c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 18:13:21 -0800 Subject: [PATCH 0653/1451] fix(fastapi): hide keywords when there are none or they can't be edited Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 56 ++++++++++++------------ 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index d525f63b..00859068 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -33,34 +33,36 @@ {% endif %} - - {{ "Keywords" | tr }}: - {% if request.user.has_credential("CRED_PKGBASE_SET_KEYWORDS", approved=[pkgbase.Maintainer]) %} - - -
    - - -
    - - - {% else %} - - {% for keyword in pkgbase.keywords.all() %} -
    + {{ "Keywords" | tr }}: + {% if request.user.has_credential("CRED_PKGBASE_SET_KEYWORDS", approved=[pkgbase.Maintainer]) %} + +
    - {{ keyword.Keyword }} - - {% endfor %} - - {% endif %} - +
    + + +
    +
    + + {% else %} + + {% for keyword in pkgbase.keywords.all() %} + + {{ keyword.Keyword }} + + {% endfor %} + + {% endif %} + + {% endif %} {% if licenses and licenses.count() and show_package_details %} {{ "Licenses" | tr }}: From 2dc6cfec23dcc8432db52f0676e7aa9f643d521c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 18:14:15 -0800 Subject: [PATCH 0654/1451] fix(fastapi): reorganize licenses display Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 00859068..1fbd47d9 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -63,10 +63,10 @@ {% endif %} {% endif %} - {% if licenses and licenses.count() and show_package_details %} + {% if show_package_details and licenses and licenses.count() %} {{ "Licenses" | tr }}: - {{ licenses | join(', ', attribute='Name') | default('None' | tr) }} + {{ licenses.all() | join(', ', attribute='Name') | default('None' | tr) }} {% endif %} {% if show_package_details %} From 2016b80ea97dc89d3170a91265abfd05763b81ab Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 18:14:50 -0800 Subject: [PATCH 0655/1451] fix(fastapi): hide conflicts when there are none Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 1fbd47d9..da99cf1b 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -69,11 +69,11 @@ {{ licenses.all() | join(', ', attribute='Name') | default('None' | tr) }} {% endif %} - {% if show_package_details %} + {% if show_package_details and conflicts and conflicts.count() %} {{ "Conflicts" | tr }}: - {{ conflicts | join(', ', attribute='RelName') }} + {{ conflicts.all() | join(', ', attribute='RelName') }} {% endif %} From 50a9690c2ddefeb3fe758ed998f2edbe3773d5f1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 19:09:24 -0800 Subject: [PATCH 0656/1451] feat(fastapi): add Provides field in package details Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 6 +++++- templates/partials/packages/details.html | 8 ++++++++ test/test_packages_routes.py | 14 +++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index e227fe23..fb91a8e3 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -12,7 +12,7 @@ import aurweb.packages.util from aurweb import db, defaults, l10n, logging, models, util from aurweb.auth import auth_required from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID -from aurweb.models.relation_type import CONFLICTS_ID +from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted @@ -258,6 +258,10 @@ async def package(request: Request, name: str) -> Response: ) context["conflicts"] = conflicts + provides = pkg.package_relations.filter( + models.PackageRelation.RelTypeID == PROVIDES_ID) + context["provides"] = provides + return render_template(request, "packages/show.html", context) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index da99cf1b..70636926 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -77,6 +77,14 @@ {% endif %} + {% if show_package_details and provides and provides.count() %} + + {{ "Provides" | tr }}: + + {{ provides.all() | join(', ', attribute='RelName') }} + + + {% endif %} {{ "Submitter" | tr }}: diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 887945d9..464a7f60 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -217,8 +217,16 @@ def test_package_official_not_found(client: TestClient, package: Package): def test_package(client: TestClient, package: Package): """ Test a single / packages / {name} route. """ - with client as request: + with db.begin(): + db.create(PackageRelation, PackageID=package.ID, + RelTypeID=PROVIDES_ID, + RelName="test_provider1") + db.create(PackageRelation, PackageID=package.ID, + RelTypeID=PROVIDES_ID, + RelName="test_provider2") + + with client as request: resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) @@ -238,6 +246,10 @@ def test_package(client: TestClient, package: Package): pkgbase = row.find("./td/a") assert pkgbase.text.strip() == package.PackageBase.Name + provides = root.xpath('//tr[@id="provides"]/td') + expected = ["test_provider1", "test_provider2"] + assert provides[0].text.strip() == ", ".join(expected) + def test_package_comments(client: TestClient, user: User, package: Package): now = (datetime.utcnow().timestamp()) From a33e9bd571dfaf1bd2a719df0a80e66f0c33cc2e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 19:14:08 -0800 Subject: [PATCH 0657/1451] feat(fastapi): add Replaces field to package details Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 6 +++++- templates/partials/packages/details.html | 8 ++++++++ test/test_packages_routes.py | 13 ++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index fb91a8e3..f03be217 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -12,7 +12,7 @@ import aurweb.packages.util from aurweb import db, defaults, l10n, logging, models, util from aurweb.auth import auth_required from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID -from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID +from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted @@ -262,6 +262,10 @@ async def package(request: Request, name: str) -> Response: models.PackageRelation.RelTypeID == PROVIDES_ID) context["provides"] = provides + replaces = pkg.package_relations.filter( + models.PackageRelation.RelTypeID == REPLACES_ID) + context["replaces"] = replaces + return render_template(request, "packages/show.html", context) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 70636926..7516b324 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -85,6 +85,14 @@ {% endif %} + {% if show_package_details and replaces and replaces.count() %} + + {{ "Replaces" | tr }}: + + {{ replaces.all() | join(', ', attribute='RelName') }} + + + {% endif %} {{ "Submitter" | tr }}: diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 464a7f60..b00844c2 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -24,7 +24,7 @@ from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation from aurweb.models.package_request import ACCEPTED_ID, REJECTED_ID, PackageRequest from aurweb.models.package_vote import PackageVote -from aurweb.models.relation_type import PROVIDES_ID, RelationType +from aurweb.models.relation_type import PROVIDES_ID, REPLACES_ID, RelationType from aurweb.models.request_type import DELETION_ID, MERGE_ID, RequestType from aurweb.models.user import User from aurweb.testing import setup_test_db @@ -226,6 +226,13 @@ def test_package(client: TestClient, package: Package): RelTypeID=PROVIDES_ID, RelName="test_provider2") + db.create(PackageRelation, PackageID=package.ID, + RelTypeID=REPLACES_ID, + RelName="test_replacer1") + db.create(PackageRelation, PackageID=package.ID, + RelTypeID=REPLACES_ID, + RelName="test_replacer2") + with client as request: resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) @@ -250,6 +257,10 @@ def test_package(client: TestClient, package: Package): expected = ["test_provider1", "test_provider2"] assert provides[0].text.strip() == ", ".join(expected) + replaces = root.xpath('//tr[@id="replaces"]/td') + expected = ["test_replacer1", "test_replacer2"] + assert replaces[0].text.strip() == ", ".join(expected) + def test_package_comments(client: TestClient, user: User, package: Package): now = (datetime.utcnow().timestamp()) From e8e9edbb21cfe5dd33f491acbdcc1d9b98845a69 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 19:30:21 -0800 Subject: [PATCH 0658/1451] change(fastapi): simplify package details database queries Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 16 ++++------------ templates/partials/packages/details.html | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index f03be217..0949909e 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -229,9 +229,7 @@ async def package(request: Request, name: str) -> Response: context["package"] = pkg # Package sources. - context["sources"] = db.query(models.PackageSource).join( - models.Package).join(models.PackageBase).filter( - models.PackageBase.ID == pkgbase.ID) + context["sources"] = pkg.package_sources # Package dependencies. dependencies = db.query(models.PackageDependency).join( @@ -246,16 +244,10 @@ async def package(request: Request, name: str) -> Response: models.Package.Name.asc()) context["required_by"] = required_by - licenses = db.query(models.License).join(models.PackageLicense).join( - models.Package).join(models.PackageBase).filter( - models.PackageBase.ID == pkgbase.ID) - context["licenses"] = licenses + context["licenses"] = pkg.package_licenses - conflicts = db.query(models.PackageRelation).join(models.Package).join( - models.PackageBase).filter( - and_(models.PackageRelation.RelTypeID == CONFLICTS_ID, - models.PackageBase.ID == pkgbase.ID) - ) + conflicts = pkg.package_relations.filter( + models.PackageRelation.RelTypeID == CONFLICTS_ID) context["conflicts"] = conflicts provides = pkg.package_relations.filter( diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 7516b324..7e20b082 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -66,7 +66,7 @@ {% if show_package_details and licenses and licenses.count() %} {{ "Licenses" | tr }}: - {{ licenses.all() | join(', ', attribute='Name') | default('None' | tr) }} + {{ licenses.all() | join(', ', attribute='License.Name') }} {% endif %} {% if show_package_details and conflicts and conflicts.count() %} From 7aa959150ec6614fbf0684917a3efe1c2e3918d4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 19:53:50 -0800 Subject: [PATCH 0659/1451] feat(fastapi): add id="conflicts" to package details conflicts Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 2 +- test/test_packages_routes.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 7e20b082..c9b95a26 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -70,7 +70,7 @@ {% endif %} {% if show_package_details and conflicts and conflicts.count() %} - + {{ "Conflicts" | tr }}: {{ conflicts.all() | join(', ', attribute='RelName') }} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index b00844c2..1dabada8 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -24,7 +24,7 @@ from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation from aurweb.models.package_request import ACCEPTED_ID, REJECTED_ID, PackageRequest from aurweb.models.package_vote import PackageVote -from aurweb.models.relation_type import PROVIDES_ID, REPLACES_ID, RelationType +from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID, RelationType from aurweb.models.request_type import DELETION_ID, MERGE_ID, RequestType from aurweb.models.user import User from aurweb.testing import setup_test_db @@ -233,6 +233,13 @@ def test_package(client: TestClient, package: Package): RelTypeID=REPLACES_ID, RelName="test_replacer2") + db.create(PackageRelation, PackageID=package.ID, + RelTypeID=CONFLICTS_ID, + RelName="test_conflict1") + db.create(PackageRelation, PackageID=package.ID, + RelTypeID=CONFLICTS_ID, + RelName="test_conflict2") + with client as request: resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) @@ -261,6 +268,10 @@ def test_package(client: TestClient, package: Package): expected = ["test_replacer1", "test_replacer2"] assert replaces[0].text.strip() == ", ".join(expected) + conflicts = root.xpath('//tr[@id="conflicts"]/td') + expected = ["test_conflict1", "test_conflict2"] + assert conflicts[0].text.strip() == ", ".join(expected) + def test_package_comments(client: TestClient, user: User, package: Package): now = (datetime.utcnow().timestamp()) From 686c0322907ed3bb8015c42f050f845323549776 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 11 Nov 2021 19:55:04 -0800 Subject: [PATCH 0660/1451] feat(fastapi): add id="licenses" to package details licenses Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 2 +- test/test_packages_routes.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index c9b95a26..67c32170 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -64,7 +64,7 @@ {% endif %} {% if show_package_details and licenses and licenses.count() %} - + {{ "Licenses" | tr }}: {{ licenses.all() | join(', ', attribute='License.Name') }} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 1dabada8..1bdb3ea3 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -11,6 +11,7 @@ from fastapi.testclient import TestClient from sqlalchemy import and_ from aurweb import asgi, db, defaults +from aurweb.models import License, PackageLicense from aurweb.models.account_type import USER_ID, AccountType from aurweb.models.dependency_type import DependencyType from aurweb.models.official_provider import OfficialProvider @@ -240,6 +241,17 @@ def test_package(client: TestClient, package: Package): RelTypeID=CONFLICTS_ID, RelName="test_conflict2") + # Create some licenses. + licenses = [ + db.create(License, Name="test_license1"), + db.create(License, Name="test_license2") + ] + + db.create(PackageLicense, PackageID=package.ID, + License=licenses[0]) + db.create(PackageLicense, PackageID=package.ID, + License=licenses[1]) + with client as request: resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) @@ -260,6 +272,10 @@ def test_package(client: TestClient, package: Package): pkgbase = row.find("./td/a") assert pkgbase.text.strip() == package.PackageBase.Name + licenses = root.xpath('//tr[@id="licenses"]/td') + expected = ["test_license1", "test_license2"] + assert licenses[0].text.strip() == ", ".join(expected) + provides = root.xpath('//tr[@id="provides"]/td') expected = ["test_provider1", "test_provider2"] assert provides[0].text.strip() == ", ".join(expected) From bd59adc886a6ce53fc5fe4c874a81cc9f8d4fcb5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 12 Nov 2021 17:39:26 -0800 Subject: [PATCH 0661/1451] fix(fastapi): use NumVotes for votes field in package details Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 67c32170..dbb81c19 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -132,11 +132,11 @@ {{ "Votes" | tr }}: {% if not is_maintainer %} - {{ pkgbase.package_votes.count() }} + {{ pkgbase.NumVotes }} {% else %} - {{ pkgbase.package_votes.count() }} + {{ pkgbase.NumVotes }} {% endif %} From cee7512e4d843c771a1aee42277781fd949648cc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 12 Nov 2021 20:49:25 -0800 Subject: [PATCH 0662/1451] cleanup(fastapi): simplify PackageDependency.is_package() Signed-off-by: Kevin Morris --- aurweb/models/package_dependency.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index edaa6538..c4c5f6c1 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -1,9 +1,10 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship -from aurweb import schema +from aurweb import db, schema from aurweb.models.declarative import Base from aurweb.models.dependency_type import DependencyType as _DependencyType +from aurweb.models.official_provider import OfficialProvider as _OfficialProvider from aurweb.models.package import Package as _Package @@ -46,11 +47,7 @@ class PackageDependency(Base): params=("NULL")) def is_package(self) -> bool: - # TODO: Improve the speed of this query if possible. - from aurweb import db - from aurweb.models.official_provider import OfficialProvider - from aurweb.models.package import Package - pkg = db.query(Package, Package.Name == self.DepName) - official = db.query(OfficialProvider, - OfficialProvider.Name == self.DepName) - return pkg.scalar() or official.scalar() + pkg = db.query(_Package).filter(_Package.Name == self.DepName).exists() + official = db.query(_OfficialProvider).filter( + _OfficialProvider.Name == self.DepName).exists() + return db.query(pkg).scalar() or db.query(official).scalar() From f8ba2c53421050ab154040a7cbb2c0de554ae8d1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 14 Nov 2021 15:32:11 -0800 Subject: [PATCH 0663/1451] cleanup(fastapi): simplify aurweb.routers.accounts.accounts_post Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 498568ad..83c16ed0 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -662,7 +662,7 @@ async def accounts(request: Request): account_type.TRUSTED_USER_AND_DEV}) async def accounts_post(request: Request, O: int = Form(default=0), # Offset - SB: str = Form(default=str()), # Search By + SB: str = Form(default=str()), # Sort By U: str = Form(default=str()), # Username T: str = Form(default=str()), # Account Type S: bool = Form(default=False), # Suspended @@ -705,23 +705,19 @@ async def accounts_post(request: Request, # Populate this list with any additional statements to # be ANDed together. - statements = [] - if account_type_id is not None: - statements.append(models.AccountType.ID == account_type_id) - if U: - statements.append(models.User.Username.like(f"%{U}%")) - if S: - statements.append(models.User.Suspended == S) - if E: - statements.append(models.User.Email.like(f"%{E}%")) - if R: - statements.append(models.User.RealName.like(f"%{R}%")) - if I: - statements.append(models.User.IRCNick.like(f"%{I}%")) - if K: - statements.append(models.User.PGPKey.like(f"%{K}%")) + statements = [ + v for k, v in [ + (account_type_id is not None, models.AccountType.ID == account_type_id), + (bool(U), models.User.Username.like(f"%{U}%")), + (bool(S), models.User.Suspended == S), + (bool(E), models.User.Email.like(f"%{E}%")), + (bool(R), models.User.RealName.like(f"%{R}%")), + (bool(I), models.User.IRCNick.like(f"%{I}%")), + (bool(K), models.User.PGPKey.like(f"%{K}%")), + ] if k + ] - # Filter the query by combining all statements added above into + # Filter the query by coe-mbining all statements added above into # an AND statement, unless there's just one statement, which # we pass on to filter() as args. if statements: From 4103ab49c9c6922e89f89a25fc3b2b5b461c1bcb Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 14 Nov 2021 15:36:06 -0800 Subject: [PATCH 0664/1451] housekeep(fastapi): rework aurweb.db session API Changes: ------- - Add aurweb.db.get_session() - Returns aurweb.db's global `session` instance - Provides us a way to change the implementation of the session instance without interrupting user code. - Use aurweb.db.get_session() in session API methods - Add docstrings to session API methods - Refactor aurweb.db.delete - Normalize aurweb.db.delete to an alias of session.delete - Refresh instances in places we depend on their non-PK columns being up to date. Signed-off-by: Kevin Morris --- aurweb/auth.py | 8 ++- aurweb/db.py | 89 ++++++++++++++++++++------------- aurweb/models/ban.py | 9 ++-- aurweb/models/user.py | 2 +- aurweb/packages/util.py | 18 +++++-- aurweb/ratelimit.py | 4 +- aurweb/routers/accounts.py | 49 ++++++++---------- aurweb/routers/packages.py | 68 +++++++++++++------------ aurweb/rpc.py | 31 ++++++++++-- aurweb/scripts/popupdate.py | 2 +- aurweb/scripts/rendercomment.py | 12 +++-- aurweb/testing/__init__.py | 10 ++-- aurweb/users/__init__.py | 0 aurweb/users/util.py | 19 +++++++ aurweb/util.py | 1 + test/test_account_type.py | 4 +- test/test_db.py | 6 +-- test/test_dependency_type.py | 4 +- test/test_packages_util.py | 6 +++ test/test_ratelimit.py | 2 +- test/test_relation_type.py | 2 +- test/test_request_type.py | 4 +- 22 files changed, 212 insertions(+), 138 deletions(-) create mode 100644 aurweb/users/__init__.py create mode 100644 aurweb/users/util.py diff --git a/aurweb/auth.py b/aurweb/auth.py index 38754db0..98a43fd5 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -13,7 +13,7 @@ from starlette.requests import HTTPConnection import aurweb.config -from aurweb import l10n, util +from aurweb import db, l10n, util from aurweb.models import Session, User from aurweb.models.account_type import ACCOUNT_TYPE_ID from aurweb.templates import make_variable_context, render_template @@ -98,14 +98,12 @@ class AnonymousUser: class BasicAuthBackend(AuthenticationBackend): async def authenticate(self, conn: HTTPConnection): - from aurweb.db import session - sid = conn.cookies.get("AURSID") if not sid: return (None, AnonymousUser()) now_ts = datetime.utcnow().timestamp() - record = session.query(Session).filter( + record = db.query(Session).filter( and_(Session.SessionID == sid, Session.LastUpdateTS >= now_ts)).first() @@ -116,7 +114,7 @@ class BasicAuthBackend(AuthenticationBackend): # At this point, we cannot have an invalid user if the record # exists, due to ForeignKey constraints in the schema upheld # by mysqlclient. - user = session.query(User).filter(User.ID == record.UsersID).first() + user = db.query(User).filter(User.ID == record.UsersID).first() user.nonce = util.make_nonce() user.authenticated = True diff --git a/aurweb/db.py b/aurweb/db.py index c1e80751..39232d5a 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -2,10 +2,10 @@ import functools import math import re -from typing import Iterable +from typing import Iterable, NewType from sqlalchemy import event -from sqlalchemy.orm import scoped_session +from sqlalchemy.orm import Query, scoped_session import aurweb.config import aurweb.util @@ -22,6 +22,9 @@ session = None # Global introspected object memo. introspected = dict() +# A mocked up type. +Base = NewType("aurweb.models.declarative_base.Base", "Base") + def make_random_value(table: str, column: str): """ Generate a unique, random value for a string column in a table. @@ -58,55 +61,69 @@ def make_random_value(table: str, column: str): return string -def query(model, *args, **kwargs): - return session.query(model).filter(*args, **kwargs) +def get_session(): + """ Return aurweb.db's global session. """ + return session -def create(model, *args, **kwargs): - instance = model(*args, **kwargs) +def refresh(model: Base) -> Base: + """ Refresh the session's knowledge of `model`. """ + get_session().refresh(model) + return model + + +def query(Model: Base, *args, **kwargs) -> Query: + """ + Perform an ORM query against the database session. + + This method also runs Query.filter on the resulting model + query with *args and **kwargs. + + :param Model: Declarative ORM class + """ + return get_session().query(Model).filter(*args, **kwargs) + + +def create(Model: Base, *args, **kwargs) -> Base: + """ + Create a record and add() it to the database session. + + :param Model: Declarative ORM class + :return: Model instance + """ + instance = Model(*args, **kwargs) return add(instance) -def delete(model, *args, **kwargs): - instance = session.query(model).filter(*args, **kwargs) - for record in instance: - session.delete(record) +def delete(model: Base) -> None: + """ + Delete a set of records found by Query.filter(*args, **kwargs). + + :param Model: Declarative ORM class + """ + get_session().delete(model) -def delete_all(iterable: Iterable): - with begin(): - for obj in iterable: - session.delete(obj) +def delete_all(iterable: Iterable) -> None: + """ Delete each instance found in `iterable`. """ + session_ = get_session() + aurweb.util.apply_all(iterable, session_.delete) -def rollback(): - session.rollback() +def rollback() -> None: + """ Rollback the database session. """ + get_session().rollback() -def add(model): - session.add(model) +def add(model: Base) -> Base: + """ Add `model` to the database session. """ + get_session().add(model) return model def begin(): - """ Begin an SQLAlchemy SessionTransaction. - - This context is **required** to perform an modifications to the - database. - - Example: - - with db.begin(): - object = db.create(...) - # On __exit__, db.commit() is run. - - with db.begin(): - object = db.delete(...) - # On __exit__, db.commit() is run. - - :return: A new SessionTransaction based on session - """ - return session.begin() + """ Begin an SQLAlchemy SessionTransaction. """ + return get_session().begin() def get_sqlalchemy_url(): diff --git a/aurweb/models/ban.py b/aurweb/models/ban.py index a70be7b9..0fcb6d2e 100644 --- a/aurweb/models/ban.py +++ b/aurweb/models/ban.py @@ -1,6 +1,6 @@ from fastapi import Request -from aurweb import schema +from aurweb import db, schema from aurweb.models.declarative import Base @@ -10,11 +10,10 @@ class Ban(Base): __mapper_args__ = {"primary_key": [__table__.c.IPAddress]} def __init__(self, **kwargs): - self.IPAddress = kwargs.get("IPAddress") - self.BanTS = kwargs.get("BanTS") + super().__init__(**kwargs) def is_banned(request: Request): - from aurweb.db import session ip = request.client.host - return session.query(Ban).filter(Ban.IPAddress == ip).first() is not None + exists = db.query(Ban).filter(Ban.IPAddress == ip).exists() + return db.query(exists).scalar() diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 8db34c38..43910db9 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -146,7 +146,7 @@ class User(Base): self.authenticated = False if self.session: with db.begin(): - db.session.delete(self.session) + db.delete(self.session) def is_trusted_user(self): return self.AccountType.ID in { diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index cdec26f3..78f5bf18 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -110,18 +110,26 @@ def get_pkg_or_base( raise HTTPException(status_code=HTTPStatus.NOT_FOUND) instance = db.query(cls).filter(cls.Name == name).first() - if cls == models.PackageBase and not instance: + if not instance: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - return instance + return db.refresh(instance) -def get_pkgbase_comment( - pkgbase: models.PackageBase, id: int) -> models.PackageComment: +def get_pkgbase_comment(pkgbase: models.PackageBase, id: int) \ + -> models.PackageComment: comment = pkgbase.comments.filter(models.PackageComment.ID == id).first() if not comment: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - return comment + return db.refresh(comment) + + +def get_pkgreq_by_id(id: int): + pkgreq = db.query(models.PackageRequest).filter( + models.PackageRequest.ID == id).first() + if not pkgreq: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + return db.refresh(pkgreq) @register_filter("out_of_date") diff --git a/aurweb/ratelimit.py b/aurweb/ratelimit.py index e306f7a7..a71cb1cc 100644 --- a/aurweb/ratelimit.py +++ b/aurweb/ratelimit.py @@ -40,8 +40,10 @@ def _update_ratelimit_db(request: Request): now = int(datetime.utcnow().timestamp()) time_to_delete = now - window_length + records = db.query(ApiRateLimit).filter( + ApiRateLimit.WindowStart < time_to_delete) with db.begin(): - db.delete(ApiRateLimit, ApiRateLimit.WindowStart < time_to_delete) + db.delete_all(records) host = request.client.host record = db.query(ApiRateLimit, ApiRateLimit.IP == host).first() diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 83c16ed0..aca322b5 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -4,7 +4,7 @@ import typing from datetime import datetime from http import HTTPStatus -from fastapi import APIRouter, Form, HTTPException, Request +from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, func, or_ @@ -20,6 +20,7 @@ from aurweb.models.account_type import (DEVELOPER, DEVELOPER_ID, TRUSTED_USER, T from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification from aurweb.templates import make_context, make_variable_context, render_template +from aurweb.users.util import get_user_by_name router = APIRouter() logger = logging.get_logger(__name__) @@ -49,6 +50,7 @@ async def passreset_post(request: Request, return render_template(request, "passreset.html", context, status_code=HTTPStatus.NOT_FOUND) + db.refresh(user) if resetkey: context["resetkey"] = resetkey @@ -83,7 +85,7 @@ async def passreset_post(request: Request, with db.begin(): user.ResetKey = str() if user.session: - db.session.delete(user.session) + db.delete(user.session) user.update_password(password) # Render ?step=complete. @@ -458,15 +460,15 @@ def cannot_edit(request, user): @router.get("/account/{username}/edit", response_class=HTMLResponse) @auth_required(True, redirect="/account/{username}") -async def account_edit(request: Request, - username: str): +async def account_edit(request: Request, username: str): user = db.query(models.User, models.User.Username == username).first() + response = cannot_edit(request, user) if response: return response context = await make_variable_context(request, "Accounts") - context["user"] = user + context["user"] = db.refresh(user) context = make_account_form_context(context, request, user, dict()) return render_template(request, "account/edit.html", context) @@ -497,16 +499,14 @@ async def account_edit_post(request: Request, ON: bool = Form(default=False), # Owner Notify T: int = Form(default=None), passwd: str = Form(default=str())): - from aurweb.db import session - - user = session.query(models.User).filter( + user = db.query(models.User).filter( models.User.Username == username).first() response = cannot_edit(request, user) if response: return response context = await make_variable_context(request, "Accounts") - context["user"] = user + context["user"] = db.refresh(user) args = dict(await request.form()) context = make_account_form_context(context, request, user, args) @@ -575,7 +575,7 @@ async def account_edit_post(request: Request, user.ssh_pub_key.Fingerprint = fingerprint elif user.ssh_pub_key: # Else, if the user has a public key already, delete it. - session.delete(user.ssh_pub_key) + db.delete(user.ssh_pub_key) if T and T != user.AccountTypeID: with db.begin(): @@ -617,27 +617,16 @@ account_template = ( status_code=HTTPStatus.UNAUTHORIZED) async def account(request: Request, username: str): _ = l10n.get_translator_for_request(request) - context = await make_variable_context(request, - _("Account") + " " + username) - - user = db.query(models.User, models.User.Username == username).first() - if not user: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - - context["user"] = user - + context = await make_variable_context( + request, _("Account") + " " + username) + context["user"] = get_user_by_name(username) return render_template(request, "account/show.html", context) @router.get("/account/{username}/comments") @auth_required(redirect="/account/{username}/comments") async def account_comments(request: Request, username: str): - user = db.query(models.User).filter( - models.User.Username == username - ).first() - if not user: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - + user = get_user_by_name(username) context = make_context(request, "Accounts") context["username"] = username context["comments"] = user.package_comments.order_by( @@ -725,7 +714,7 @@ async def accounts_post(request: Request, # Finally, order and truncate our users for the current page. users = query.order_by(*order_by).limit(pp).offset(offset) - context["users"] = users + context["users"] = util.apply_all(users, db.refresh) return render_template(request, "account/index.html", context) @@ -751,6 +740,9 @@ async def terms_of_service(request: Request): unaccepted = db.query(models.Term).filter( ~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))).all() + for record in (diffs + unaccepted): + db.refresh(record) + # Translate the 'Terms of Service' part of our page title. _ = l10n.get_translator_for_request(request) title = f"AUR {_('Terms of Service')}" @@ -782,18 +774,21 @@ async def terms_of_service_post(request: Request, # We already did the database filters here, so let's just use # them instead of reiterating the process in terms_of_service. accept_needed = sorted(unaccepted + diffs) - return render_terms_of_service(request, context, accept_needed) + return render_terms_of_service( + request, context, util.apply_all(accept_needed, db.refresh)) with db.begin(): # For each term we found, query for the matching accepted term # and update its Revision to the term's current Revision. for term in diffs: + db.refresh(term) accepted_term = request.user.accepted_terms.filter( models.AcceptedTerm.TermsID == term.ID).first() accepted_term.Revision = term.Revision # For each term that was never accepted, accept it! for term in unaccepted: + db.refresh(term) db.create(models.AcceptedTerm, User=request.user, Term=term, Revision=term.Revision) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 0949909e..07e8af72 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -4,7 +4,7 @@ from typing import Any, Dict, List from fastapi import APIRouter, Form, HTTPException, Query, Request, Response from fastapi.responses import JSONResponse, RedirectResponse -from sqlalchemy import and_, case +from sqlalchemy import case import aurweb.filters import aurweb.packages.util @@ -15,9 +15,9 @@ from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID from aurweb.packages.search import PackageSearch -from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, query_notified, query_voted +from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, get_pkgreq_by_id, query_notified, query_voted from aurweb.scripts import notify, popupdate -from aurweb.scripts.rendercomment import update_comment_render +from aurweb.scripts.rendercomment import update_comment_render_fastapi from aurweb.templates import make_context, make_variable_context, render_raw_template, render_template logger = logging.get_logger(__name__) @@ -92,7 +92,10 @@ async def packages_get(request: Request, context: Dict[str, Any], # Insert search results into the context. results = search.results() - context["packages"] = results.limit(per_page).offset(offset) + + packages = results.limit(per_page).offset(offset) + util.apply_all(packages, db.refresh) + context["packages"] = packages context["packages_voted"] = query_voted( context.get("packages"), request.user) context["packages_notified"] = query_notified( @@ -132,6 +135,7 @@ def create_request_if_missing(requests: List[models.PackageRequest], ClosedTS=now, Closer=user) requests.append(pkgreq) + return pkgreq def delete_package(deleter: models.User, package: models.Package): @@ -147,8 +151,9 @@ def delete_package(deleter: models.User, package: models.Package): ).first() with db.begin(): - create_request_if_missing( + pkgreq = create_request_if_missing( requests, reqtype, deleter, package) + db.refresh(pkgreq) bases_to_delete.append(package.PackageBase) @@ -171,8 +176,9 @@ def delete_package(deleter: models.User, package: models.Package): ) # Perform all the deletions. - db.delete_all([package]) - db.delete_all(bases_to_delete) + with db.begin(): + db.delete(package) + db.delete_all(bases_to_delete) # Send out all the notifications. util.apply_all(notifications, lambda n: n.send()) @@ -221,8 +227,7 @@ async def make_single_context(request: Request, async def package(request: Request, name: str) -> Response: # Get the Package. pkg = get_pkg_or_base(name, models.Package) - pkgbase = (get_pkg_or_base(name, models.PackageBase) - if not pkg else pkg.PackageBase) + pkgbase = pkg.PackageBase # Add our base information. context = await make_single_context(request, pkgbase) @@ -312,7 +317,7 @@ async def pkgbase_comments_post( db.create(models.PackageNotification, User=request.user, PackageBase=pkgbase) - update_comment_render(comment.ID) + update_comment_render_fastapi(comment) # Redirect to the pkgbase page. return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}", @@ -374,7 +379,7 @@ async def pkgbase_comment_post( db.create(models.PackageNotification, User=request.user, PackageBase=pkgbase) - update_comment_render(db_comment.ID) + update_comment_render_fastapi(db_comment) if not next: next = f"/pkgbase/{pkgbase.Name}" @@ -539,7 +544,7 @@ def remove_users(pkgbase, usernames): conn, comaintainer.User.ID, pkgbase.ID ) ) - db.session.delete(comaintainer) + db.delete(comaintainer) # Send out notifications if need be. for notify_ in notifications: @@ -679,14 +684,8 @@ async def requests(request: Request, @router.get("/pkgbase/{name}/request") @auth_required(True, redirect="/pkgbase/{name}/request") async def package_request(request: Request, name: str): + pkgbase = get_pkg_or_base(name, models.PackageBase) context = await make_variable_context(request, "Submit Request") - - pkgbase = db.query(models.PackageBase).filter( - models.PackageBase.Name == name).first() - - if not pkgbase: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - context["pkgbase"] = pkgbase return render_template(request, "pkgbase/request.html", context) @@ -729,6 +728,7 @@ async def pkgbase_request_post(request: Request, name: str, ] return render_template(request, "pkgbase/request.html", context) + db.refresh(target) if target.ID == pkgbase.ID: # TODO: This error needs to be translated. context["errors"] = [ @@ -767,8 +767,7 @@ async def pkgbase_request_post(request: Request, name: str, @router.get("/requests/{id}/close") @auth_required(True, redirect="/requests/{id}/close") async def requests_close(request: Request, id: int): - pkgreq = db.query(models.PackageRequest).filter( - models.PackageRequest.ID == id).first() + pkgreq = get_pkgreq_by_id(id) if not request.user.is_elevated() and request.user != pkgreq.User: # Request user doesn't have permission here: redirect to '/'. return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) @@ -783,8 +782,7 @@ async def requests_close(request: Request, id: int): async def requests_close_post(request: Request, id: int, reason: int = Form(default=0), comments: str = Form(default=str())): - pkgreq = db.query(models.PackageRequest).filter( - models.PackageRequest.ID == id).first() + pkgreq = get_pkgreq_by_id(id) if not request.user.is_elevated() and request.user != pkgreq.User: # Request user doesn't have permission here: redirect to '/'. return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) @@ -823,13 +821,17 @@ async def pkgbase_keywords(request: Request, name: str, keywords = set(keywords.split(" ")) # Delete all keywords which are not supplied by the user. - with db.begin(): - db.delete(models.PackageKeyword, - and_(models.PackageKeyword.PackageBaseID == pkgbase.ID, - ~models.PackageKeyword.Keyword.in_(keywords))) + other_keywords = pkgbase.keywords.filter( + ~models.PackageKeyword.Keyword.in_(keywords)) + other_keyword_strings = [kwd.Keyword for kwd in other_keywords] - existing_keywords = set(kwd.Keyword for kwd in pkgbase.keywords.all()) + existing_keywords = set( + kwd.Keyword for kwd in + pkgbase.keywords.filter( + ~models.PackageKeyword.Keyword.in_(other_keyword_strings)) + ) with db.begin(): + db.delete_all(other_keywords) for keyword in keywords.difference(existing_keywords): db.create(models.PackageKeyword, PackageBase=pkgbase, @@ -940,7 +942,7 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: models.PackageBase): has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") if has_cred and notif: with db.begin(): - db.session.delete(notif) + db.delete(notif) @router.post("/pkgbase/{name}/unnotify") @@ -988,7 +990,7 @@ async def pkgbase_unvote(request: Request, name: str): has_cred = request.user.has_credential("CRED_PKGBASE_VOTE") if has_cred and vote: with db.begin(): - db.session.delete(vote) + db.delete(vote) # Update NumVotes/Popularity. conn = db.ConnectionExecutor(db.get_engine().raw_connection()) @@ -1015,7 +1017,7 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase): if co: with db.begin(): pkgbase.Maintainer = co.User - db.session.delete(co) + db.delete(co) else: pkgbase.Maintainer = None @@ -1463,8 +1465,8 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase, with db.begin(): # Delete pkgbase and its packages now that everything's merged. for pkg in pkgbase.packages: - db.session.delete(pkg) - db.session.delete(pkgbase) + db.delete(pkg) + db.delete(pkgbase) # Accept merge requests related to this pkgbase and target. for pkgreq in requests: diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 4ab005af..03662790 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List, NewType from sqlalchemy import and_ @@ -25,6 +25,10 @@ REL_TYPES = { } +DataGenerator = NewType("DataGenerator", + Callable[[models.Package], Dict[str, Any]]) + + class RPCError(Exception): pass @@ -188,15 +192,32 @@ class RPC: self._update_json_relations(package, data) return data - def _handle_multiinfo_type(self, args: List[str] = [], **kwargs): + def _assemble_json_data(self, packages: List[models.Package], + data_generator: DataGenerator) \ + -> List[Dict[str, Any]]: + """ + Assemble JSON data out of a list of packages. + + :param packages: A list of Package instances or a Package ORM query + :param data_generator: Generator callable of single-Package JSON data + """ + output = [] + for pkg in packages: + db.refresh(pkg) + output.append(data_generator(pkg)) + return output + + def _handle_multiinfo_type(self, args: List[str] = [], **kwargs) \ + -> List[Dict[str, Any]]: self._enforce_args(args) args = set(args) packages = db.query(models.Package).filter( models.Package.Name.in_(args)) - return [self._get_info_json_data(pkg) for pkg in packages] + return self._assemble_json_data(packages, self._get_info_json_data) def _handle_search_type(self, by: str = defaults.RPC_SEARCH_BY, - args: List[str] = []): + args: List[str] = []) \ + -> List[Dict[str, Any]]: # If `by` isn't maintainer and we don't have any args, raise an error. # In maintainer's case, return all orphans if there are no args, # so we need args to pass through to the handler without errors. @@ -212,7 +233,7 @@ class RPC: max_results = config.getint("options", "max_rpc_results") results = search.results().limit(max_results) - return [self._get_json_data(pkg) for pkg in results] + return self._assemble_json_data(results, self._get_json_data) def _handle_msearch_type(self, args: List[str] = [], **kwargs): return self._handle_search_type(by="m", args=args) diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index fa82208d..db4ba170 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -29,7 +29,7 @@ def run_single(conn, pkgbase): conn.commit() conn.close() - aurweb.db.session.refresh(pkgbase) + aurweb.db.refresh(pkgbase) def main(): diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index a00448d8..efa5357f 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -129,9 +129,14 @@ def save_rendered_comment(conn, commentid, html): [html, commentid]) -def update_comment_render(commentid): - conn = aurweb.db.Connection() +def update_comment_render_fastapi(comment): + conn = aurweb.db.ConnectionExecutor( + aurweb.db.get_engine().raw_connection()) + update_comment_render(conn, comment.ID) + aurweb.db.refresh(comment) + +def update_comment_render(conn, commentid): text, pkgbase = get_comment(conn, commentid) html = markdown.markdown(text, extensions=[ 'fenced_code', @@ -152,7 +157,8 @@ def update_comment_render(commentid): def main(): commentid = int(sys.argv[1]) - update_comment_render(commentid) + conn = aurweb.db.Connection() + update_comment_render(conn, commentid) if __name__ == '__main__': diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 65d34253..2dd377e1 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -19,7 +19,7 @@ def references_graph(table): "regexp_1": r'(?i)\s+references\s+("|\')?', "regexp_2": r'("|\')?\s*\(', } - cursor = aurweb.db.session.execute(query, params=params) + cursor = aurweb.db.get_session().execute(query, params=params) return [row[0] for row in cursor.fetchall()] @@ -51,7 +51,7 @@ def setup_test_db(*args): db_backend = aurweb.config.get("database", "backend") if db_backend != "sqlite": # pragma: no cover - aurweb.db.session.execute("SET FOREIGN_KEY_CHECKS = 0") + aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 0") else: # We're using sqlite, setup tables to be deleted without violating # foreign key constraints by graphing references. @@ -59,10 +59,10 @@ def setup_test_db(*args): references_graph(table) for table in tables)) for table in tables: - aurweb.db.session.execute(f"DELETE FROM {table}") + aurweb.db.get_session().execute(f"DELETE FROM {table}") if db_backend != "sqlite": # pragma: no cover - aurweb.db.session.execute("SET FOREIGN_KEY_CHECKS = 1") + aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 1") # Expunge all objects from SQLAlchemy's IdentityMap. - aurweb.db.session.expunge_all() + aurweb.db.get_session().expunge_all() diff --git a/aurweb/users/__init__.py b/aurweb/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aurweb/users/util.py b/aurweb/users/util.py new file mode 100644 index 00000000..e9635f08 --- /dev/null +++ b/aurweb/users/util.py @@ -0,0 +1,19 @@ +from http import HTTPStatus + +from fastapi import HTTPException + +from aurweb import db +from aurweb.models import User + + +def get_user_by_name(username: str) -> User: + """ + Query a user by its username. + + :param username: User.Username + :return: User instance + """ + user = db.query(User).filter(User.Username == username).first() + if not user: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + return db.refresh(user) diff --git a/aurweb/util.py b/aurweb/util.py index 88142cbc..1c2042fa 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -155,6 +155,7 @@ def get_ssh_fingerprints(): def apply_all(iterable: Iterable, fn: Callable): for item in iterable: fn(item) + return iterable def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: diff --git a/test/test_account_type.py b/test/test_account_type.py index 86e68253..12472348 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -20,7 +20,7 @@ def setup(): yield account_type with begin(): - delete(AccountType, AccountType.ID == account_type.ID) + delete(account_type) def test_account_type(): @@ -50,4 +50,4 @@ def test_user_account_type_relationship(): # This must be deleted here to avoid foreign key issues when # deleting the temporary AccountType in the fixture. with begin(): - delete(User, User.ID == user.ID) + delete(user) diff --git a/test/test_db.py b/test/test_db.py index 7798d2f6..8283a957 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -279,13 +279,13 @@ def test_connection_execute_paramstyle_unsupported(): def test_create_delete(): with db.begin(): - db.create(AccountType, AccountType="test") + account_type = db.create(AccountType, AccountType="test") record = db.query(AccountType, AccountType.AccountType == "test").first() assert record is not None with db.begin(): - db.delete(AccountType, AccountType.AccountType == "test") + db.delete(account_type) record = db.query(AccountType, AccountType.AccountType == "test").first() assert record is None @@ -306,7 +306,7 @@ def test_add_commit(): # Remove the record. with db.begin(): - db.delete(AccountType, AccountType.ID == account_type.ID) + db.delete(account_type) def test_connection_executor_mysql_paramstyle(): diff --git a/test/test_dependency_type.py b/test/test_dependency_type.py index 4d555123..cb8dece4 100644 --- a/test/test_dependency_type.py +++ b/test/test_dependency_type.py @@ -24,7 +24,7 @@ def test_dependency_type_creation(): assert bool(dependency_type.ID) assert dependency_type.Name == "Test Type" with begin(): - delete(DependencyType, DependencyType.ID == dependency_type.ID) + delete(dependency_type) def test_dependency_type_null_name_uses_default(): @@ -32,4 +32,4 @@ def test_dependency_type_null_name_uses_default(): dependency_type = create(DependencyType) assert dependency_type.Name == str() with begin(): - delete(DependencyType, DependencyType.ID == dependency_type.ID) + delete(dependency_type) diff --git a/test/test_packages_util.py b/test/test_packages_util.py index 1396734b..622c08c2 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -2,6 +2,7 @@ from datetime import datetime import pytest +from fastapi import HTTPException from fastapi.testclient import TestClient from aurweb import asgi, db @@ -98,3 +99,8 @@ def test_query_notified(maintainer: User, package: Package): query = db.query(Package).filter(Package.ID == package.ID).all() query_notified = util.query_notified(query, maintainer) assert query_notified[package.PackageBase.ID] + + +def test_pkgreq_by_id_not_found(): + with pytest.raises(HTTPException): + util.get_pkgreq_by_id(0) diff --git a/test/test_ratelimit.py b/test/test_ratelimit.py index 2634b714..0a72a7e4 100644 --- a/test/test_ratelimit.py +++ b/test/test_ratelimit.py @@ -103,7 +103,7 @@ def test_ratelimit_db(get: mock.MagicMock, getboolean: mock.MagicMock, # Delete the ApiRateLimit record. with db.begin(): - db.delete(ApiRateLimit) + db.delete(db.query(ApiRateLimit).first()) # Should be good to go again! assert not check_ratelimit(request) diff --git a/test/test_relation_type.py b/test/test_relation_type.py index fbc22c71..d2dabceb 100644 --- a/test/test_relation_type.py +++ b/test/test_relation_type.py @@ -18,7 +18,7 @@ def test_relation_type_creation(): assert relation_type.Name == "test-relation" with db.begin(): - db.delete(RelationType, RelationType.ID == relation_type.ID) + db.delete(relation_type) def test_relation_types(): diff --git a/test/test_request_type.py b/test/test_request_type.py index 8d21c2d9..0db24921 100644 --- a/test/test_request_type.py +++ b/test/test_request_type.py @@ -18,7 +18,7 @@ def test_request_type_creation(): assert request_type.Name == "Test Request" with db.begin(): - db.delete(RequestType, RequestType.ID == request_type.ID) + db.delete(request_type) def test_request_type_null_name_returns_empty_string(): @@ -29,7 +29,7 @@ def test_request_type_null_name_returns_empty_string(): assert request_type.Name == str() with db.begin(): - db.delete(RequestType, RequestType.ID == request_type.ID) + db.delete(request_type) def test_request_type_name_display(): From 12400147fc0e1f3bed2bc54a92c4de76cf8312f2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 14 Nov 2021 16:02:00 -0800 Subject: [PATCH 0665/1451] fix: initialize engine and session in util/adduser.py Signed-off-by: Kevin Morris --- util/adduser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/util/adduser.py b/util/adduser.py index 7e35d13d..1853869a 100644 --- a/util/adduser.py +++ b/util/adduser.py @@ -33,6 +33,7 @@ def parse_args(): def main(): args = parse_args() + db.get_engine() type = db.query(AccountType, AccountType.AccountType == args.type).first() with db.begin(): From 9424341b55bf55b4b064cfc2b8e4d89536901e69 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 14 Nov 2021 23:33:58 -0800 Subject: [PATCH 0666/1451] fix(docker): fix cgit css config Signed-off-by: Kevin Morris --- docker-compose.yml | 2 ++ docker/cgit-entrypoint.sh | 1 + 2 files changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2fba1305..bda4ddfb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -112,6 +112,7 @@ services: environment: - AUR_CONFIG=/aurweb/conf/config - CGIT_CLONE_PREFIX=${AURWEB_PHP_PREFIX} + - CGIT_CSS=/css/cgit.css entrypoint: /docker/cgit-entrypoint.sh command: /docker/scripts/run-cgit.sh 3000 healthcheck: @@ -131,6 +132,7 @@ services: environment: - AUR_CONFIG=/aurweb/conf/config - CGIT_CLONE_PREFIX=${AURWEB_FASTAPI_PREFIX} + - CGIT_CSS=/static/css/cgit.css entrypoint: /docker/cgit-entrypoint.sh command: /docker/scripts/run-cgit.sh 3000 healthcheck: diff --git a/docker/cgit-entrypoint.sh b/docker/cgit-entrypoint.sh index 3615ade5..f9ca86c0 100755 --- a/docker/cgit-entrypoint.sh +++ b/docker/cgit-entrypoint.sh @@ -8,5 +8,6 @@ sed -ri "s|clone-prefix=.*|clone-prefix=${CGIT_CLONE_PREFIX}|" /etc/cgitrc sed -ri 's|header=.*|header=/aurweb/web/template/cgit/header.html|' /etc/cgitrc sed -ri 's|footer=.*|footer=/aurweb/web/template/cgit/footer.html|' /etc/cgitrc sed -ri 's|repo\.path=.*|repo.path=/aurweb/aur.git|' /etc/cgitrc +sed -ri "s|^(css)=.*$|\1=${CGIT_CSS}|" /etc/cgitrc exec "$@" From 7f6d9966e585626da181a6e642e1a73710f2f817 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 14 Nov 2021 16:02:00 -0800 Subject: [PATCH 0667/1451] fix: initialize engine and session in util/adduser.py Signed-off-by: Kevin Morris --- util/adduser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/util/adduser.py b/util/adduser.py index 7e35d13d..1853869a 100644 --- a/util/adduser.py +++ b/util/adduser.py @@ -33,6 +33,7 @@ def parse_args(): def main(): args = parse_args() + db.get_engine() type = db.query(AccountType, AccountType.AccountType == args.type).first() with db.begin(): From b0b05df19341f5c4a75a78b69b3abc08d61fc238 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 16 Nov 2021 21:10:53 -0800 Subject: [PATCH 0668/1451] fix(fastapi): pin markdown to 3.3.4 Signed-off-by: Kevin Morris --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 37e2f8f9..550a91fa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1003,7 +1003,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.10" -content-hash = "356b37d545d78b8aa1e1939f42522207bcf79526abe8193308c5a2955897d6fd" +content-hash = "844618f499e19d6d20f8479d165be3f60495bfa66fcb1f462256b101f9d395f9" [metadata.files] aiofiles = [ diff --git a/pyproject.toml b/pyproject.toml index 1d4c858c..7b2e9ef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ alembic = "^1.7.4" mysqlclient = "^2.0.3" Authlib = "^0.15.5" Jinja2 = "^3.0.2" -Markdown = "^3.3.4" +Markdown = "3.3.4" Werkzeug = "^2.0.2" SQLAlchemy = "^1.4.26" From cea9104efb3c496230bee60f6b76be42a8719c61 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 16 Nov 2021 01:45:32 -0800 Subject: [PATCH 0669/1451] feat(poetry): add pytest-xdist Signed-off-by: Kevin Morris --- poetry.lock | 67 ++++++++++++++++++++++++++++++++++++++++++++------ pyproject.toml | 1 + 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 550a91fa..8d42cc50 100644 --- a/poetry.lock +++ b/poetry.lock @@ -53,7 +53,7 @@ tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -61,7 +61,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -234,6 +234,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" dnspython = ">=1.15.0" idna = ">=2.0.0" +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + [[package]] name = "fakeredis" version = "1.6.1" @@ -432,7 +443,7 @@ python-versions = ">=3.5" name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -575,7 +586,7 @@ python-versions = "*" name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -639,7 +650,7 @@ python-versions = ">=3.5" name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -705,7 +716,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -751,6 +762,18 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-forked" +version = "1.3.0" +description = "run tests in isolated forked subprocesses" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +py = "*" +pytest = ">=3.10" + [[package]] name = "pytest-tap" version = "3.3" @@ -763,6 +786,24 @@ python-versions = "*" pytest = ">=3.0" "tap.py" = ">=3.0,<4.0" +[[package]] +name = "pytest-xdist" +version = "2.4.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.0.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1003,7 +1044,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.10" -content-hash = "844618f499e19d6d20f8479d165be3f60495bfa66fcb1f462256b101f9d395f9" +content-hash = "6ab137fb829b2a6d49552c4864d00be04a2d58d80a872f3cd3b9e5cc67f95b9d" [metadata.files] aiofiles = [ @@ -1200,6 +1241,10 @@ email-validator = [ {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, ] +execnet = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] fakeredis = [ {file = "fakeredis-1.6.1-py3-none-any.whl", hash = "sha256:5eb1516f1fe1813e9da8f6c482178fc067af09f53de587ae03887ef5d9d13024"}, {file = "fakeredis-1.6.1.tar.gz", hash = "sha256:0d06a9384fb79da9f2164ce96e34eb9d4e2ea46215070805ea6fd3c174590b47"}, @@ -1609,10 +1654,18 @@ pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] +pytest-forked = [ + {file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"}, + {file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"}, +] pytest-tap = [ {file = "pytest-tap-3.3.tar.gz", hash = "sha256:5f0919a147cf0396b2f10d64d365a0bf8062e06543e93c675c9d37f5605e983c"}, {file = "pytest_tap-3.3-py3-none-any.whl", hash = "sha256:4fbbc0e090c2e94f6199bee4e4f68ab3c5e176b37a72a589ad84e0f72a2fce55"}, ] +pytest-xdist = [ + {file = "pytest-xdist-2.4.0.tar.gz", hash = "sha256:89b330316f7fc475f999c81b577c2b926c9569f3d397ae432c0c2e2496d61ff9"}, + {file = "pytest_xdist-2.4.0-py3-none-any.whl", hash = "sha256:7b61ebb46997a0820a263553179d6d1e25a8c50d8a8620cd1aa1e20e3be99168"}, +] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, diff --git a/pyproject.toml b/pyproject.toml index 7b2e9ef3..d296fb4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ gunicorn = "^20.1.0" Hypercorn = "^0.11.2" mysql-connector = "^2.2.9" prometheus-fastapi-instrumentator = "^5.7.1" +pytest-xdist = "^2.4.0" [tool.poetry.dev-dependencies] flake8 = "^4.0.1" From 40b21203ed8b1cd833fa0333ebf3d1985567fcbe Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 16 Nov 2021 01:46:07 -0800 Subject: [PATCH 0670/1451] feat(poetry): add filelock Signed-off-by: Kevin Morris --- poetry.lock | 18 +++++++++++++++++- pyproject.toml | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 8d42cc50..ab559b77 100644 --- a/poetry.lock +++ b/poetry.lock @@ -300,6 +300,18 @@ python-versions = "*" lxml = "*" python-dateutil = "*" +[[package]] +name = "filelock" +version = "3.3.2" +description = "A platform independent file lock." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + [[package]] name = "flake8" version = "4.0.1" @@ -1044,7 +1056,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.10" -content-hash = "6ab137fb829b2a6d49552c4864d00be04a2d58d80a872f3cd3b9e5cc67f95b9d" +content-hash = "ca42bd35717062d6784025ed3956423502ac66adba059ccc080bcaaa666651cd" [metadata.files] aiofiles = [ @@ -1253,6 +1265,10 @@ fastapi = [] feedgen = [ {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, ] +filelock = [ + {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, + {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, +] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, diff --git a/pyproject.toml b/pyproject.toml index d296fb4e..8d14735a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ Hypercorn = "^0.11.2" mysql-connector = "^2.2.9" prometheus-fastapi-instrumentator = "^5.7.1" pytest-xdist = "^2.4.0" +filelock = "^3.3.2" [tool.poetry.dev-dependencies] flake8 = "^4.0.1" From 0abdf8d468579c5e98ddac29c5f97ec1e546bd5e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:22:52 -0800 Subject: [PATCH 0671/1451] fix(fastapi): close connection used for initdb Signed-off-by: Kevin Morris --- aurweb/initdb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aurweb/initdb.py b/aurweb/initdb.py index 9a063ba4..a4a9f621 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -46,7 +46,9 @@ def run(args): engine = aurweb.db.get_engine(echo=(args.verbose >= 1)) aurweb.schema.metadata.create_all(engine) - feed_initial_data(engine.connect()) + conn = engine.connect() + feed_initial_data(conn) + conn.close() if args.use_alembic: alembic.command.stamp(alembic_config, 'head') From 07aac768d633288d61abd85d055629cfe65800b2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:27:44 -0800 Subject: [PATCH 0672/1451] change(fastapi): remove sqlite support Signed-off-by: Kevin Morris --- aurweb/asgi.py | 6 ++++++ test/test_asgi.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 16de771e..aafb00b2 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -48,6 +48,12 @@ async def app_startup(): "TEST_RECURSION_LIMIT", sys.getrecursionlimit())) sys.setrecursionlimit(recursion_limit) + backend = aurweb.config.get("database", "backend") + if backend not in aurweb.db.DRIVERS: + raise ValueError( + f"The configured database backend ({backend}) is unsupported. " + f"Supported backends: {str(aurweb.db.DRIVERS.keys())}") + session_secret = aurweb.config.get("fastapi", "session_secret") if not session_secret: raise Exception("[fastapi] session_secret must not be empty") diff --git a/test/test_asgi.py b/test/test_asgi.py index b8856741..fa2df5a1 100644 --- a/test/test_asgi.py +++ b/test/test_asgi.py @@ -45,3 +45,20 @@ async def test_asgi_http_exception_handler(): response = await aurweb.asgi.http_exception_handler(None, exc) assert response.body.decode() == \ f"

    {exc.status_code} {phrase}

    {exc.detail}

    " + + +@pytest.mark.asyncio +async def test_asgi_app_unsupported_backends(): + config_get = aurweb.config.get + + # Test that the previously supported "sqlite" backend is now + # unsupported by FastAPI. + def mock_sqlite_backend(section: str, key: str): + if section == "database" and key == "backend": + return "sqlite" + return config_get(section, key) + + with mock.patch("aurweb.config.get", side_effect=mock_sqlite_backend): + expr = r"^.*\(sqlite\) is unsupported.*$" + with pytest.raises(ValueError, match=expr): + await aurweb.asgi.app_startup() From fa43f6bc3ebebff7d97f3361ec07a61e92bd1ce5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:33:41 -0800 Subject: [PATCH 0673/1451] change(aurweb): add parallel tests and improve aurweb.db This change utilizes pytest-xdist to perform a multiproc test run and reworks aurweb.db's code. We no longer use a global engine, session or Session, but we now use a memo of engines and sessions as they are requested, based on the PYTEST_CURRENT_TEST environment variable, which is available during testing. Additionally, this change strips several SQLite components out of the Python code-base. SQLite is still compatible with PHP and sharness tests, but not with our FastAPI implementation. More changes: ------------ - Remove use of aurweb.db.session global in other code. - Use new aurweb.db.name() dynamic db name function in env.py. - Added 'addopts' to pytest.ini which utilizes multiprocessing. - Highly recommended to leave this be or modify `-n auto` to `-n {cpu_threads}` where cpu_threads is at least 2. Signed-off-by: Kevin Morris --- aurweb/db.py | 251 +++++++++++++++++++++--------- aurweb/routers/auth.py | 6 +- aurweb/routers/packages.py | 8 +- aurweb/schema.py | 4 +- aurweb/testing/__init__.py | 68 ++++---- migrations/env.py | 8 +- pytest.ini | 6 + test/conftest.py | 178 +++++++++++++++++++++ test/test_accepted_term.py | 37 ++--- test/test_account_type.py | 54 ++++--- test/test_accounts_routes.py | 9 +- test/test_api_rate_limit.py | 19 +-- test/test_auth.py | 26 ++-- test/test_auth_routes.py | 5 +- test/test_ban.py | 9 +- test/test_cache.py | 7 +- test/test_captcha.py | 7 + test/test_db.py | 110 +------------ test/test_dependency_type.py | 5 +- test/test_group.py | 9 +- test/test_homepage.py | 11 +- test/test_html.py | 5 +- test/test_initdb.py | 9 ++ test/test_license.py | 9 +- test/test_official_provider.py | 23 +-- test/test_package.py | 5 +- test/test_package_base.py | 5 +- test/test_package_blacklist.py | 16 +- test/test_package_comaintainer.py | 28 ++-- test/test_package_comment.py | 59 +++---- test/test_package_dependency.py | 95 +++-------- test/test_package_group.py | 37 ++--- test/test_package_keyword.py | 37 ++--- test/test_package_license.py | 38 ++--- test/test_package_notification.py | 25 ++- test/test_package_relation.py | 90 +++-------- test/test_package_request.py | 107 +++++-------- test/test_package_source.py | 2 +- test/test_package_vote.py | 29 ++-- test/test_packages_routes.py | 23 +-- test/test_packages_util.py | 17 +- test/test_popupdate.py | 7 + test/test_ratelimit.py | 20 ++- test/test_relation_type.py | 5 +- test/test_request_type.py | 5 +- test/test_routes.py | 12 +- test/test_rpc.py | 8 +- test/test_rss.py | 8 +- test/test_session.py | 5 +- test/test_ssh_pub_key.py | 14 +- test/test_term.py | 18 +-- test/test_trusted_user_routes.py | 5 +- test/test_tu_vote.py | 43 +++-- test/test_tu_voteinfo.py | 5 +- test/test_user.py | 14 +- 55 files changed, 781 insertions(+), 884 deletions(-) create mode 100644 test/conftest.py diff --git a/aurweb/db.py b/aurweb/db.py index 39232d5a..b8b49e40 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,29 +1,34 @@ import functools +import hashlib import math +import os import re from typing import Iterable, NewType -from sqlalchemy import event -from sqlalchemy.orm import Query, scoped_session +import sqlalchemy + +from sqlalchemy import create_engine, event +from sqlalchemy.engine.base import Engine +from sqlalchemy.engine.url import URL +from sqlalchemy.orm import Query, Session, SessionTransaction, scoped_session, sessionmaker import aurweb.config import aurweb.util -# See get_engine. -engine = None +from aurweb import logging -# ORM Session class. -Session = None +logger = logging.get_logger(__name__) -# Global ORM Session object. -session = None +DRIVERS = { + "mysql": "mysql+mysqldb" +} # Global introspected object memo. introspected = dict() -# A mocked up type. -Base = NewType("aurweb.models.declarative_base.Base", "Base") +# Some types we don't get access to in this module. +Base = NewType("Base", "aurweb.models.declarative_base.Base") def make_random_value(table: str, column: str): @@ -56,14 +61,85 @@ def make_random_value(table: str, column: str): length = col.type.length string = aurweb.util.make_random_string(length) - while session.query(table).filter(column == string).first(): + while query(table).filter(column == string).first(): string = aurweb.util.make_random_string(length) return string -def get_session(): +def test_name() -> str: + """ + Return the unhashed database name. + + The unhashed database name is determined (lower = higher priority) by: + ------------------------------------------- + 1. {test_suite} portion of PYTEST_CURRENT_TEST + 2. aurweb.config.get("database", "name") + + During `pytest` runs, the PYTEST_CURRENT_TEST environment variable + is set to the current test in the format `{test_suite}::{test_func}`. + + This allows tests to use a suite-specific database for its runs, + which decouples database state from test suites. + + :return: Unhashed database name + """ + db = os.environ.get("PYTEST_CURRENT_TEST", + aurweb.config.get("database", "name")) + return db.split(":")[0] + + +def name() -> str: + """ + Return sanitized database name that can be used for tests or production. + + If test_name() starts with "test/", the database name is SHA-1 hashed, + prefixed with 'db', and returned. Otherwise, test_name() is passed + through and not hashed at all. + + :return: SHA1-hashed database name prefixed with 'db' + """ + dbname = test_name() + if not dbname.startswith("test/"): + return dbname + sha1 = hashlib.sha1(dbname.encode()).hexdigest() + return "db" + sha1 + + +# Module-private global memo used to store SQLAlchemy sessions. +_sessions = dict() + + +def get_session(engine: Engine = None) -> Session: """ Return aurweb.db's global session. """ - return session + dbname = name() + + global _sessions + if dbname not in _sessions: + + if not engine: # pragma: no cover + engine = get_engine() + + Session = scoped_session( + sessionmaker(autocommit=True, autoflush=False, bind=engine)) + _sessions[dbname] = Session() + + # If this is the first grab of this session, log out the + # database name used. + raw_dbname = test_name() + logger.debug(f"DBName({raw_dbname}): {dbname}") + + return _sessions.get(dbname) + + +def pop_session(dbname: str) -> None: + """ + Pop a Session out of the private _sessions memo. + + :param dbname: Database name + :raises KeyError: When `dbname` does not exist in the memo + """ + global _sessions + _sessions.pop(dbname) def refresh(model: Base) -> Base: @@ -121,41 +197,40 @@ def add(model: Base) -> Base: return model -def begin(): +def begin() -> SessionTransaction: """ Begin an SQLAlchemy SessionTransaction. """ return get_session().begin() -def get_sqlalchemy_url(): +def get_sqlalchemy_url() -> URL: """ - Build an SQLAlchemy for use with create_engine based on the aurweb configuration. - """ - import sqlalchemy + Build an SQLAlchemy URL for use with create_engine. - constructor = sqlalchemy.engine.url.URL + :return: sqlalchemy.engine.url.URL + """ + constructor = URL parts = sqlalchemy.__version__.split('.') major = int(parts[0]) minor = int(parts[1]) if major == 1 and minor >= 4: # pragma: no cover - constructor = sqlalchemy.engine.url.URL.create + constructor = URL.create aur_db_backend = aurweb.config.get('database', 'backend') if aur_db_backend == 'mysql': - if aurweb.config.get_with_fallback('database', 'port', fallback=None): - port = aurweb.config.get('database', 'port') - param_query = None - else: - port = None - param_query = { - 'unix_socket': aurweb.config.get('database', 'socket') - } + param_query = {} + port = aurweb.config.get_with_fallback("database", "port", None) + if not port: + param_query["unix_socket"] = aurweb.config.get( + "database", "socket") + return constructor( - 'mysql+mysqldb', + DRIVERS.get(aur_db_backend), username=aurweb.config.get('database', 'user'), - password=aurweb.config.get('database', 'password'), + password=aurweb.config.get_with_fallback('database', 'password', + fallback=None), host=aurweb.config.get('database', 'host'), - database=aurweb.config.get('database', 'name'), + database=name(), port=port, query=param_query ) @@ -168,58 +243,83 @@ def get_sqlalchemy_url(): raise ValueError('unsupported database backend') -def get_engine(echo: bool = False): +def sqlite_regexp(regex, item) -> bool: # pragma: no cover + """ Method which mimics SQL's REGEXP for SQLite. """ + return bool(re.search(regex, str(item))) + + +def setup_sqlite(engine: Engine) -> None: # pragma: no cover + """ Perform setup for an SQLite engine. """ + @event.listens_for(engine, "connect") + def do_begin(conn, record): + create_deterministic_function = functools.partial( + conn.create_function, + deterministic=True + ) + create_deterministic_function("REGEXP", 2, sqlite_regexp) + + +# Module-private global memo used to store SQLAlchemy engines. +_engines = dict() + + +def get_engine(dbname: str = None, echo: bool = False) -> Engine: """ - Return the global SQLAlchemy engine. + Return the SQLAlchemy engine for `dbname`. The engine is created on the first call to get_engine and then stored in the `engine` global variable for the next calls. + + :param dbname: Database name (default: aurweb.db.name()) + :param echo: Flag passed through to sqlalchemy.create_engine + :return: SQLAlchemy Engine instance """ - from sqlalchemy import create_engine - from sqlalchemy.orm import sessionmaker + if not dbname: + dbname = name() - global engine, session, Session - - if engine is None: + global _engines + if dbname not in _engines: + db_backend = aurweb.config.get("database", "backend") connect_args = dict() - db_backend = aurweb.config.get("database", "backend") - if db_backend == "sqlite": - # check_same_thread is for a SQLite technicality - # https://fastapi.tiangolo.com/tutorial/sql-databases/#note + is_sqlite = bool(db_backend == "sqlite") + if is_sqlite: # pragma: no cover connect_args["check_same_thread"] = False - engine = create_engine(get_sqlalchemy_url(), - connect_args=connect_args, - echo=echo) + kwargs = { + "echo": echo, + "connect_args": connect_args + } + _engines[dbname] = create_engine(get_sqlalchemy_url(), **kwargs) - Session = scoped_session( - sessionmaker(autocommit=True, autoflush=False, bind=engine)) - session = Session() + if is_sqlite: # pragma: no cover + setup_sqlite(_engines.get(dbname)) - if db_backend == "sqlite": - # For SQLite, we need to add some custom functions as - # they are used in the reference graph method. - def regexp(regex, item): - return bool(re.search(regex, str(item))) - - @event.listens_for(engine, "connect") - def do_begin(conn, record): - create_deterministic_function = functools.partial( - conn.create_function, - deterministic=True - ) - create_deterministic_function("REGEXP", 2, regexp) - - return engine + return _engines.get(dbname) -def kill_engine(): - global engine, Session, session - if engine: - session.close() - engine.dispose() - engine = Session = session = None +def pop_engine(dbname: str) -> None: + """ + Pop an Engine out of the private _engines memo. + + :param dbname: Database name + :raises KeyError: When `dbname` does not exist in the memo + """ + global _engines + _engines.pop(dbname) + + +def kill_engine() -> None: + """ Close the current session and dispose of the engine. """ + dbname = name() + + session = get_session() + session.close() + pop_session(dbname) + + engine = get_engine() + engine.dispose() + pop_engine(dbname) def connect(): @@ -248,7 +348,9 @@ class ConnectionExecutor: def paramstyle(self): return self._paramstyle - def execute(self, query, params=()): + def execute(self, query, params=()): # pragma: no cover + # TODO: SQLite support has been removed in FastAPI. It remains + # here to fund its support for PHP until it is removed. if self._paramstyle in ('format', 'pyformat'): query = query.replace('%', '%%').replace('?', '%s') elif self._paramstyle == 'qmark': @@ -278,16 +380,19 @@ class Connection: if aur_db_backend == 'mysql': import MySQLdb aur_db_host = aurweb.config.get('database', 'host') - aur_db_name = aurweb.config.get('database', 'name') + aur_db_name = name() aur_db_user = aurweb.config.get('database', 'user') - aur_db_pass = aurweb.config.get('database', 'password') + aur_db_pass = aurweb.config.get_with_fallback( + 'database', 'password', str()) aur_db_socket = aurweb.config.get('database', 'socket') self._conn = MySQLdb.connect(host=aur_db_host, user=aur_db_user, passwd=aur_db_pass, db=aur_db_name, unix_socket=aur_db_socket) - elif aur_db_backend == 'sqlite': + elif aur_db_backend == 'sqlite': # pragma: no cover + # TODO: SQLite support has been removed in FastAPI. It remains + # here to fund its support for PHP until it is removed. import sqlite3 aur_db_name = aurweb.config.get('database', 'name') self._conn = sqlite3.connect(aur_db_name) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index b8e83c7d..055f0dca 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -6,7 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse import aurweb.config -from aurweb import cookies +from aurweb import cookies, db from aurweb.auth import auth_required from aurweb.l10n import get_translator_for_request from aurweb.models import User @@ -45,9 +45,7 @@ async def login_post(request: Request, raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=_("Bad Referer header.")) - from aurweb.db import session - - user = session.query(User).filter(User.Username == user).first() + user = db.query(User).filter(User.Username == user).first() if not user: return await login_template(request, next, errors=["Bad username or password."]) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 07e8af72..c8ceb275 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -1014,12 +1014,12 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase): models.PackageComaintainer.Priority.asc() ).limit(1).first() - if co: - with db.begin(): + with db.begin(): + if co: pkgbase.Maintainer = co.User db.delete(co) - else: - pkgbase.Maintainer = None + else: + pkgbase.Maintainer = None notif.send() diff --git a/aurweb/schema.py b/aurweb/schema.py index fb8f0dee..43db920d 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -16,13 +16,13 @@ db_backend = aurweb.config.get("database", "backend") @compiles(TINYINT, 'sqlite') -def compile_tinyint_sqlite(type_, compiler, **kw): +def compile_tinyint_sqlite(type_, compiler, **kw): # pragma: no cover """TINYINT is not supported on SQLite. Substitute it with INTEGER.""" return 'INTEGER' @compiles(BIGINT, 'sqlite') -def compile_bigint_sqlite(type_, compiler, **kw): +def compile_bigint_sqlite(type_, compiler, **kw): # pragma: no cover """ For SQLite's AUTOINCREMENT to work on BIGINT columns, we need to map BIGINT to INTEGER. Aside from that, BIGINT is the same as INTEGER for SQLite. diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 2dd377e1..8261051d 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -1,26 +1,6 @@ -from itertools import chain - import aurweb.db - -def references_graph(table): - """ Taken from Django's sqlite3/operations.py. """ - query = """ - WITH tables AS ( - SELECT :table name - UNION - SELECT sqlite_master.name - FROM sqlite_master - JOIN tables ON (sql REGEXP :regexp_1 || tables.name || :regexp_2) - ) SELECT name FROM tables; - """ - params = { - "table": table, - "regexp_1": r'(?i)\s+references\s+("|\')?', - "regexp_2": r'("|\')?\s*\(', - } - cursor = aurweb.db.get_session().execute(query, params=params) - return [row[0] for row in cursor.fetchall()] +from aurweb import models def setup_test_db(*args): @@ -47,22 +27,38 @@ def setup_test_db(*args): aurweb.db.get_engine() tables = list(args) + if not tables: + tables = [ + models.AcceptedTerm.__tablename__, + models.ApiRateLimit.__tablename__, + models.Ban.__tablename__, + models.Group.__tablename__, + models.License.__tablename__, + models.OfficialProvider.__tablename__, + models.Package.__tablename__, + models.PackageBase.__tablename__, + models.PackageBlacklist.__tablename__, + models.PackageComaintainer.__tablename__, + models.PackageComment.__tablename__, + models.PackageDependency.__tablename__, + models.PackageGroup.__tablename__, + models.PackageKeyword.__tablename__, + models.PackageLicense.__tablename__, + models.PackageNotification.__tablename__, + models.PackageRelation.__tablename__, + models.PackageRequest.__tablename__, + models.PackageSource.__tablename__, + models.PackageVote.__tablename__, + models.Session.__tablename__, + models.SSHPubKey.__tablename__, + models.Term.__tablename__, + models.TUVote.__tablename__, + models.TUVoteInfo.__tablename__, + models.User.__tablename__, + ] - db_backend = aurweb.config.get("database", "backend") - - if db_backend != "sqlite": # pragma: no cover - aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 0") - else: - # We're using sqlite, setup tables to be deleted without violating - # foreign key constraints by graphing references. - tables = set(chain.from_iterable( - references_graph(table) for table in tables)) - + aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 0") for table in tables: aurweb.db.get_session().execute(f"DELETE FROM {table}") - - if db_backend != "sqlite": # pragma: no cover - aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 1") - - # Expunge all objects from SQLAlchemy's IdentityMap. + aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 1") aurweb.db.get_session().expunge_all() diff --git a/migrations/env.py b/migrations/env.py index 7130d141..774ecdeb 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -41,8 +41,8 @@ def run_migrations_offline(): script output. """ - db_name = aurweb.config.get("database", "name") - logging.info(f"Performing offline migration on database '{db_name}'.") + dbname = aurweb.db.name() + logging.info(f"Performing offline migration on database '{dbname}'.") context.configure( url=aurweb.db.get_sqlalchemy_url(), target_metadata=target_metadata, @@ -61,8 +61,8 @@ def run_migrations_online(): and associate a connection with the context. """ - db_name = aurweb.config.get("database", "name") - logging.info(f"Performing online migration on database '{db_name}'.") + dbname = aurweb.db.name() + logging.info(f"Performing online migration on database '{dbname}'.") connectable = sqlalchemy.create_engine( aurweb.db.get_sqlalchemy_url(), poolclass=sqlalchemy.pool.NullPool, diff --git a/pytest.ini b/pytest.ini index 510447c9..9f70a2bd 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,3 +8,9 @@ # https://bugs.python.org/issue45097 filterwarnings = ignore::DeprecationWarning:asyncio.base_events + +# Build in coverage and pytest-xdist multiproc testing. +addopts = --cov=aurweb --cov-append --dist load --dist loadfile -n auto + +# Our pytest units are located in the ./test/ directory. +testpaths = test diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..47d9ca4b --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,178 @@ +""" +pytest configuration. + +The conftest.py file is used to define pytest-global fixtures +or actions run before tests. + +Module scoped fixtures: +---------------------- +- setup_database +- db_session (depends: setup_database) + +Function scoped fixtures: +------------------------ +- db_test (depends: db_session) + +Tests in aurweb which access the database **must** use the `db_test` +function fixture. Most database tests simply require this fixture in +an autouse=True setup fixture, or for fixtures used in DB tests example: + + # In scenarios which there are no other database fixtures + # or other database fixtures dependency paths don't always + # hit `db_test`. + @pytest.fixture(autouse=True) + def setup(db_test): + return + + # In scenarios where we can embed the `db_test` fixture in + # specific fixtures that already exist. + @pytest.fixture + def user(db_test): + with db.begin(): + user = db.create(User, ...) + yield user + +The `db_test` fixture triggers our module-level database fixtures, +then clears the database for each test function run in that module. +It is done this way because migration has a large cost; migrating +ahead of each function takes too long when compared to this method. +""" +import pytest + +from filelock import FileLock +from sqlalchemy import create_engine +from sqlalchemy.engine import URL +from sqlalchemy.engine.base import Engine +from sqlalchemy.orm import scoped_session + +import aurweb.config +import aurweb.db + +from aurweb import initdb, logging, testing + +logger = logging.get_logger(__name__) + + +def test_engine() -> Engine: + """ + Return a privileged SQLAlchemy engine with no database. + + This method is particularly useful for providing an engine that + can be used to create and drop databases from an SQL server. + + :return: SQLAlchemy Engine instance (not connected to a database) + """ + unix_socket = aurweb.config.get_with_fallback("database", "socket", None) + kwargs = { + "username": aurweb.config.get("database", "user"), + "password": aurweb.config.get_with_fallback( + "database", "password", None), + "host": aurweb.config.get("database", "host"), + "port": aurweb.config.get_with_fallback("database", "port", None), + "query": { + "unix_socket": unix_socket + } + } + + backend = aurweb.config.get("database", "backend") + driver = aurweb.db.DRIVERS.get(backend) + return create_engine(URL.create(driver, **kwargs)) + + +class AlembicArgs: + """ + Masquerade an ArgumentParser like structure. + + This structure is needed to pass conftest-specific arguments + to initdb.run duration database creation. + """ + verbose = False + use_alembic = True + + +def _create_database(engine: Engine, dbname: str) -> None: + """ + Create a test database. + + :param engine: Engine returned by test_engine() + :param dbname: Database name to create + """ + conn = engine.connect() + conn.execute(f"CREATE DATABASE {dbname}") + conn.close() + initdb.run(AlembicArgs) + + +def _drop_database(engine: Engine, dbname: str) -> None: + """ + Drop a test database. + + :param engine: Engine returned by test_engine() + :param dbname: Database name to drop + """ + aurweb.schema.metadata.drop_all(bind=engine) + conn = engine.connect() + conn.execute(f"DROP DATABASE {dbname}") + conn.close() + + +@pytest.fixture(scope="module") +def setup_database(tmp_path_factory: pytest.fixture, + worker_id: pytest.fixture) -> None: + """ Create and drop a database for the suite this fixture is used in. """ + engine = test_engine() + dbname = aurweb.db.name() + + if worker_id == "master": # pragma: no cover + # If we're not running tests through multiproc pytest-xdist. + yield _create_database(engine, dbname) + _drop_database(engine, dbname) + return + + root_tmp_dir = tmp_path_factory.getbasetemp().parent + fn = root_tmp_dir / dbname + + with FileLock(str(fn) + ".lock"): + if fn.is_file(): + # If the data file exists, skip database creation. + yield + else: + # Otherwise, create the data file and create the database. + fn.write_text("1") + yield _create_database(engine, dbname) + _drop_database(engine, dbname) + + +@pytest.fixture(scope="module") +def db_session(setup_database: pytest.fixture) -> scoped_session: + """ + Yield a database session based on aurweb.db.name(). + + The returned session is popped out of persistence after the test is run. + """ + # After the test runs, aurweb.db.name() ends up returning the + # configured database, because PYTEST_CURRENT_TEST is removed. + dbname = aurweb.db.name() + session = aurweb.db.get_session() + yield session + + # Close the session and pop it. + session.close() + aurweb.db.pop_session(dbname) + + +@pytest.fixture +def db_test(db_session: scoped_session) -> None: + """ + Database test fixture. + + This fixture should be included in any tests which access the + database. It ensures that a test database is created and + alembic migrated, takes care of dropping the database when + the module is complete, and runs setup_test_db() to clear out + tables for each test. + + Tests using this fixture should access the database + session via aurweb.db.get_session(). + """ + testing.setup_test_db() diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py index cd8bd7af..de18c61a 100644 --- a/test/test_accepted_term.py +++ b/test/test_accepted_term.py @@ -2,38 +2,33 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query +from aurweb import db from aurweb.models.accepted_term import AcceptedTerm -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.term import Term from aurweb.models.user import User -from aurweb.testing import setup_test_db user = term = accepted_term = None @pytest.fixture(autouse=True) -def setup(): - global user, term, accepted_term +def setup(db_test): + global user, term - setup_test_db("Users", "AcceptedTerms", "Terms") + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - - term = create(Term, Description="Test term", URL="https://test.term") + term = db.create(Term, Description="Test term", + URL="https://test.term") yield term - # Eradicate any terms we created. - setup_test_db("AcceptedTerms", "Terms") - def test_accepted_term(): - accepted_term = create(AcceptedTerm, User=user, Term=term) + with db.begin(): + accepted_term = db.create(AcceptedTerm, User=user, Term=term) # Make sure our AcceptedTerm relationships got initialized properly. assert accepted_term.User == user @@ -42,14 +37,10 @@ def test_accepted_term(): def test_accepted_term_null_user_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(AcceptedTerm, Term=term) - session.rollback() + AcceptedTerm(Term=term) def test_accepted_term_null_term_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(AcceptedTerm, User=user) - session.rollback() + AcceptedTerm(User=user) diff --git a/test/test_account_type.py b/test/test_account_type.py index 12472348..1d71f878 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -1,31 +1,29 @@ import pytest -from aurweb.db import begin, create, delete, query +from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.user import User -from aurweb.testing import setup_test_db - -account_type = None @pytest.fixture(autouse=True) -def setup(): - setup_test_db("Users") - - global account_type - - with begin(): - account_type = create(AccountType, AccountType="TestUser") - - yield account_type - - with begin(): - delete(account_type) +def setup(db_test): + return -def test_account_type(): +@pytest.fixture +def account_type() -> AccountType: + with db.begin(): + account_type_ = db.create(AccountType, AccountType="TestUser") + + yield account_type_ + + with db.begin(): + db.delete(account_type_) + + +def test_account_type(account_type): """ Test creating an AccountType, and reading its columns. """ - # Make sure it got created and was given an ID. + # Make sure it got db.created and was given an ID. assert bool(account_type.ID) # Next, test our string functions. @@ -34,20 +32,20 @@ def test_account_type(): "" % ( account_type.ID) - record = query(AccountType, - AccountType.AccountType == "TestUser").first() + record = db.query(AccountType, + AccountType.AccountType == "TestUser").first() assert account_type == record -def test_user_account_type_relationship(): - with begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) +def test_user_account_type_relationship(account_type): + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountType=account_type) assert user.AccountType == account_type - # This must be deleted here to avoid foreign key issues when + # This must be db.deleted here to avoid foreign key issues when # deleting the temporary AccountType in the fixture. - with begin(): - delete(user) + with db.begin(): + db.delete(user) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 5e855daf..e828f70f 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -20,7 +20,6 @@ from aurweb.models.session import Session from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.term import Term from aurweb.models.user import User -from aurweb.testing import setup_test_db from aurweb.testing.html import get_errors from aurweb.testing.requests import Request @@ -50,11 +49,9 @@ def make_ssh_pubkey(): @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user - setup_test_db("Users", "Sessions", "Bans", "Terms", "AcceptedTerms") - account_type = query(AccountType, AccountType.AccountType == "User").first() @@ -65,10 +62,6 @@ def setup(): yield user - # Remove term records so other tests don't get them - # and falsely redirect. - setup_test_db("Terms", "AcceptedTerms") - @pytest.fixture def tu_user(): diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py index 25cb3e0f..82805ecf 100644 --- a/test/test_api_rate_limit.py +++ b/test/test_api_rate_limit.py @@ -3,19 +3,18 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.db import create from aurweb.models.api_rate_limit import ApiRateLimit -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db("ApiRateLimit") +def setup(db_test): + return def test_api_rate_key_creation(): with db.begin(): - rate = create(ApiRateLimit, IP="127.0.0.1", Requests=10, WindowStart=1) + rate = db.create(ApiRateLimit, IP="127.0.0.1", Requests=10, + WindowStart=1) assert rate.IP == "127.0.0.1" assert rate.Requests == 10 assert rate.WindowStart == 1 @@ -23,19 +22,15 @@ def test_api_rate_key_creation(): def test_api_rate_key_ip_default(): with db.begin(): - api_rate_limit = create(ApiRateLimit, Requests=10, WindowStart=1) + api_rate_limit = db.create(ApiRateLimit, Requests=10, WindowStart=1) assert api_rate_limit.IP == str() def test_api_rate_key_null_requests_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - create(ApiRateLimit, IP="127.0.0.1", WindowStart=1) - db.rollback() + ApiRateLimit(IP="127.0.0.1", WindowStart=1) def test_api_rate_key_null_window_start_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - create(ApiRateLimit, IP="127.0.0.1", Requests=1) - db.rollback() + ApiRateLimit(IP="127.0.0.1", Requests=1) diff --git a/test/test_auth.py b/test/test_auth.py index 7aea17a0..08eaac0b 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -6,28 +6,22 @@ from sqlalchemy.exc import IntegrityError from aurweb import db from aurweb.auth import AnonymousUser, BasicAuthBackend, account_type_required, has_credential -from aurweb.db import create, query -from aurweb.models.account_type import USER, USER_ID, AccountType +from aurweb.models.account_type import USER, USER_ID from aurweb.models.session import Session from aurweb.models.user import User -from aurweb.testing import setup_test_db from aurweb.testing.requests import Request user = backend = request = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, backend, request - setup_test_db("Users", "Sessions") - - account_type = query(AccountType, - AccountType.AccountType == "User").first() with db.begin(): - user = create(User, Username="test", Email="test@example.com", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = db.create(User, Username="test", Email="test@example.com", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) backend = BasicAuthBackend() request = Request() @@ -56,10 +50,8 @@ async def test_auth_backend_invalid_user_id(): # Create a new session with a fake user id. now_ts = datetime.utcnow().timestamp() with pytest.raises(IntegrityError): - with db.begin(): - create(Session, UsersID=666, SessionID="realSession", - LastUpdateTS=now_ts + 5) - db.rollback() + Session(UsersID=666, SessionID="realSession", + LastUpdateTS=now_ts + 5) @pytest.mark.asyncio @@ -68,8 +60,8 @@ async def test_basic_auth_backend(): # equal the real_user. now_ts = datetime.utcnow().timestamp() with db.begin(): - create(Session, UsersID=user.ID, SessionID="realSession", - LastUpdateTS=now_ts + 5) + db.create(Session, UsersID=user.ID, SessionID="realSession", + LastUpdateTS=now_ts + 5) request.cookies["AURSID"] = "realSession" _, result = await backend.authenticate(request) assert result == user diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 39afc6f9..a0bb8a7c 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -13,7 +13,6 @@ from aurweb.db import begin, create, query from aurweb.models.account_type import AccountType from aurweb.models.session import Session from aurweb.models.user import User -from aurweb.testing import setup_test_db # Some test global constants. TEST_USERNAME = "test" @@ -27,11 +26,9 @@ user = client = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, client - setup_test_db("Users", "Sessions", "Bans") - account_type = query(AccountType, AccountType.AccountType == "User").first() diff --git a/test/test_ban.py b/test/test_ban.py index f96e9d14..2c705410 100644 --- a/test/test_ban.py +++ b/test/test_ban.py @@ -9,18 +9,15 @@ from sqlalchemy import exc as sa_exc from aurweb import db from aurweb.db import create from aurweb.models.ban import Ban, is_banned -from aurweb.testing import setup_test_db from aurweb.testing.requests import Request ban = request = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global ban, request - setup_test_db("Bans") - ts = datetime.utcnow() + timedelta(seconds=30) with db.begin(): ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) @@ -33,8 +30,6 @@ def test_ban(): def test_invalid_ban(): - from aurweb.db import session - with pytest.raises(sa_exc.IntegrityError): bad_ban = Ban(BanTS=datetime.utcnow()) @@ -44,7 +39,7 @@ def test_invalid_ban(): with warnings.catch_warnings(): warnings.simplefilter("ignore", sa_exc.SAWarning) with db.begin(): - session.add(bad_ban) + db.add(bad_ban) # Since we got a transaction failure, we need to rollback. db.rollback() diff --git a/test/test_cache.py b/test/test_cache.py index 35346e52..b49ee386 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -3,14 +3,11 @@ import pytest from aurweb import cache, db from aurweb.models.account_type import USER_ID from aurweb.models.user import User -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db( - User.__tablename__ - ) +def setup(db_test): + return class StubRedis: diff --git a/test/test_captcha.py b/test/test_captcha.py index ec19dee9..e5f8c71a 100644 --- a/test/test_captcha.py +++ b/test/test_captcha.py @@ -1,8 +1,15 @@ import hashlib +import pytest + from aurweb import captcha +@pytest.fixture(autouse=True) +def setup(db_test): + return + + def test_captcha_salts(): """ Make sure we can get some captcha salts. """ salts = captcha.get_captcha_salts() diff --git a/test/test_db.py b/test/test_db.py index 8283a957..f36fff2c 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -12,7 +12,6 @@ import aurweb.initdb from aurweb import db from aurweb.models.account_type import AccountType -from aurweb.testing import setup_test_db class Args: @@ -96,16 +95,10 @@ def make_temp_mysql_config(): @pytest.fixture(autouse=True) -def setup_db(): +def setup(db_test): if os.path.exists("/tmp/aurweb.sqlite3"): os.remove("/tmp/aurweb.sqlite3") - # In various places in this test, we reinitialize the engine. - # Make sure we kill the previous engine before initializing - # it via setup_test_db(). - aurweb.db.kill_engine() - setup_test_db() - def test_sqlalchemy_sqlite_url(): tmpctx, tmp = make_temp_sqlite_config() @@ -159,24 +152,6 @@ def test_sqlalchemy_unknown_backend(): def test_db_connects_without_fail(): """ This only tests the actual config supplied to pytest. """ db.connect() - assert db.engine is not None - - -def test_connection_class_sqlite_without_fail(): - tmpctx, tmp = make_temp_sqlite_config() - with tmpctx: - with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): - aurweb.config.rehash() - - aurweb.db.kill_engine() - aurweb.initdb.run(Args()) - - conn = db.Connection() - cur = conn.execute( - "SELECT AccountType FROM AccountTypes WHERE ID = ?", (1,)) - account_type = cur.fetchone()[0] - assert account_type == "User" - aurweb.config.rehash() def test_connection_class_unsupported_backend(): @@ -200,83 +175,6 @@ def test_connection_mysql(): aurweb.config.rehash() -@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) -@mock.patch.object(sqlite3, "paramstyle", "qmark") -def test_connection_sqlite(): - db.Connection() - - -@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) -@mock.patch.object(sqlite3, "paramstyle", "format") -def test_connection_execute_paramstyle_format(): - tmpctx, tmp = make_temp_sqlite_config() - - with tmpctx: - with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): - aurweb.config.rehash() - - aurweb.db.kill_engine() - aurweb.initdb.run(Args()) - - # Test SQLite route of clearing tables. - setup_test_db("Users", "Bans") - - conn = db.Connection() - - # First, test ? to %s format replacement. - account_types = conn\ - .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", - ["User"]).fetchall() - assert account_types == \ - ["SELECT * FROM AccountTypes WHERE AccountType = %s", ["User"]] - - # Test other format replacement. - account_types = conn\ - .execute("SELECT * FROM AccountTypes WHERE AccountType = %", - ["User"]).fetchall() - assert account_types == \ - ["SELECT * FROM AccountTypes WHERE AccountType = %%", ["User"]] - aurweb.config.rehash() - - -@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) -@mock.patch.object(sqlite3, "paramstyle", "qmark") -def test_connection_execute_paramstyle_qmark(): - tmpctx, tmp = make_temp_sqlite_config() - - with tmpctx: - with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): - aurweb.config.rehash() - - aurweb.db.kill_engine() - aurweb.initdb.run(Args()) - - conn = db.Connection() - # We don't modify anything when using qmark, so test equality. - account_types = conn\ - .execute("SELECT * FROM AccountTypes WHERE AccountType = ?", - ["User"]).fetchall() - assert account_types == \ - ["SELECT * FROM AccountTypes WHERE AccountType = ?", ["User"]] - aurweb.config.rehash() - - -@mock.patch("sqlite3.connect", mock.MagicMock(return_value=DBConnection())) -@mock.patch.object(sqlite3, "paramstyle", "unsupported") -def test_connection_execute_paramstyle_unsupported(): - tmpctx, tmp = make_temp_sqlite_config() - with tmpctx: - with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): - aurweb.config.rehash() - conn = db.Connection() - with pytest.raises(ValueError, match="unsupported paramstyle"): - conn.execute( - "SELECT * FROM AccountTypes WHERE AccountType = ?", - ["User"] - ).fetchall() - aurweb.config.rehash() - - def test_create_delete(): with db.begin(): account_type = db.create(AccountType, AccountType="test") @@ -318,3 +216,9 @@ def test_connection_executor_mysql_paramstyle(): def test_connection_executor_sqlite_paramstyle(): executor = db.ConnectionExecutor(None, backend="sqlite") assert executor.paramstyle() == sqlite3.paramstyle + + +def test_name_without_pytest_current_test(): + with mock.patch.dict("os.environ", {}, clear=True): + dbname = aurweb.db.name() + assert dbname == aurweb.config.get("database", "name") diff --git a/test/test_dependency_type.py b/test/test_dependency_type.py index cb8dece4..c5afd38d 100644 --- a/test/test_dependency_type.py +++ b/test/test_dependency_type.py @@ -2,12 +2,11 @@ import pytest from aurweb.db import begin, create, delete, query from aurweb.models.dependency_type import DependencyType -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db() +def setup(db_test): + return def test_dependency_types(): diff --git a/test/test_group.py b/test/test_group.py index cea69b68..82b82464 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -4,12 +4,11 @@ from sqlalchemy.exc import IntegrityError from aurweb import db from aurweb.models.group import Group -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db("Groups") +def setup(db_test): + return def test_group_creation(): @@ -21,6 +20,4 @@ def test_group_creation(): def test_group_null_name_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(Group) - db.rollback() + Group() diff --git a/test/test_homepage.py b/test/test_homepage.py index 5c678b71..2e6d53c9 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -18,7 +18,6 @@ from aurweb.models.package_request import PackageRequest from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User from aurweb.redis import redis_connection -from aurweb.testing import setup_test_db from aurweb.testing.html import parse_root from aurweb.testing.requests import Request @@ -26,14 +25,8 @@ client = TestClient(app) @pytest.fixture(autouse=True) -def setup(): - yield setup_test_db( - User.__tablename__, - Package.__tablename__, - PackageBase.__tablename__, - PackageComaintainer.__tablename__, - PackageRequest.__tablename__ - ) +def setup(db_test): + return @pytest.fixture diff --git a/test/test_html.py b/test/test_html.py index 8e7cb2d1..db47c5e5 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -8,14 +8,13 @@ from fastapi.testclient import TestClient from aurweb import asgi, db from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID, AccountType from aurweb.models.user import User -from aurweb.testing import setup_test_db from aurweb.testing.html import get_errors, get_successes, parse_root from aurweb.testing.requests import Request @pytest.fixture(autouse=True) -def setup(): - setup_test_db(User.__tablename__) +def setup(db_test): + return @pytest.fixture diff --git a/test/test_initdb.py b/test/test_initdb.py index c7d29ee2..44681d8e 100644 --- a/test/test_initdb.py +++ b/test/test_initdb.py @@ -1,3 +1,5 @@ +import pytest + import aurweb.config import aurweb.db import aurweb.initdb @@ -5,6 +7,11 @@ import aurweb.initdb from aurweb.models.account_type import AccountType +@pytest.fixture(autouse=True) +def setup(db_test): + return + + class Args: use_alembic = True verbose = True @@ -15,6 +22,8 @@ def test_run(): aurweb.db.kill_engine() metadata.drop_all(aurweb.db.get_engine()) aurweb.initdb.run(Args()) + + # Check that constant table rows got added via initdb. record = aurweb.db.query(AccountType, AccountType.AccountType == "User").first() assert record is not None diff --git a/test/test_license.py b/test/test_license.py index 2c52f058..b34bd260 100644 --- a/test/test_license.py +++ b/test/test_license.py @@ -4,12 +4,11 @@ from sqlalchemy.exc import IntegrityError from aurweb import db from aurweb.models.license import License -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db("Licenses") +def setup(db_test): + return def test_license_creation(): @@ -21,6 +20,4 @@ def test_license_creation(): def test_license_null_name_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(License) - db.rollback() + License() diff --git a/test/test_official_provider.py b/test/test_official_provider.py index 0aa4f1d1..9287ea2d 100644 --- a/test/test_official_provider.py +++ b/test/test_official_provider.py @@ -4,12 +4,11 @@ from sqlalchemy.exc import IntegrityError from aurweb import db from aurweb.models.official_provider import OfficialProvider -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db("OfficialProviders") +def setup(db_test): + return def test_official_provider_creation(): @@ -53,26 +52,14 @@ def test_official_provider_cs(): def test_official_provider_null_name_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(OfficialProvider, - Repo="some-repo", - Provides="some-provides") - db.rollback() + OfficialProvider(Repo="some-repo", Provides="some-provides") def test_official_provider_null_repo_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(OfficialProvider, - Name="some-name", - Provides="some-provides") - db.rollback() + OfficialProvider(Name="some-name", Provides="some-provides") def test_official_provider_null_provides_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(OfficialProvider, - Name="some-name", - Repo="some-repo") - db.rollback() + OfficialProvider(Name="some-name", Repo="some-repo") diff --git a/test/test_package.py b/test/test_package.py index 112ca9b4..c2afa660 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -8,17 +8,14 @@ from aurweb.models.account_type import AccountType from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = package = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase, package - setup_test_db("Packages", "PackageBases", "Users") - account_type = db.query(AccountType, AccountType.AccountType == "User").first() diff --git a/test/test_package_base.py b/test/test_package_base.py index 2bc6278f..8e4b2edf 100644 --- a/test/test_package_base.py +++ b/test/test_package_base.py @@ -8,17 +8,14 @@ from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.package_base import PackageBase from aurweb.models.user import User -from aurweb.testing import setup_test_db user = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user - setup_test_db("Users", "PackageBases") - account_type = db.query(AccountType, AccountType.AccountType == "User").first() with db.begin(): diff --git a/test/test_package_blacklist.py b/test/test_package_blacklist.py index 93f15de7..6f4c36d7 100644 --- a/test/test_package_blacklist.py +++ b/test/test_package_blacklist.py @@ -6,20 +6,18 @@ from aurweb import db from aurweb.models.package_base import PackageBase from aurweb.models.package_blacklist import PackageBlacklist from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase - setup_test_db("PackageBlacklist", "PackageBases", "Users") - - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) def test_package_blacklist_creation(): @@ -31,6 +29,4 @@ def test_package_blacklist_creation(): def test_package_blacklist_null_name_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(PackageBlacklist) - db.rollback() + PackageBlacklist() diff --git a/test/test_package_comaintainer.py b/test/test_package_comaintainer.py index cba99ba0..ff74cddf 100644 --- a/test/test_package_comaintainer.py +++ b/test/test_package_comaintainer.py @@ -2,29 +2,28 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, rollback +from aurweb import db from aurweb.models.package_base import PackageBase from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase - setup_test_db("Users", "PackageBases", "PackageComaintainers") - - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) def test_package_comaintainer_creation(): - package_comaintainer = create(PackageComaintainer, User=user, - PackageBase=pkgbase, Priority=5) + with db.begin(): + package_comaintainer = db.create(PackageComaintainer, User=user, + PackageBase=pkgbase, Priority=5) assert bool(package_comaintainer) assert package_comaintainer.User == user assert package_comaintainer.PackageBase == pkgbase @@ -33,17 +32,14 @@ def test_package_comaintainer_creation(): def test_package_comaintainer_null_user_raises_exception(): with pytest.raises(IntegrityError): - create(PackageComaintainer, PackageBase=pkgbase, Priority=1) - rollback() + PackageComaintainer(PackageBase=pkgbase, Priority=1) def test_package_comaintainer_null_pkgbase_raises_exception(): with pytest.raises(IntegrityError): - create(PackageComaintainer, User=user, Priority=1) - rollback() + PackageComaintainer(User=user, Priority=1) def test_package_comaintainer_null_priority_raises_exception(): with pytest.raises(IntegrityError): - create(PackageComaintainer, User=user, PackageBase=pkgbase) - rollback() + PackageComaintainer(User=user, PackageBase=pkgbase) diff --git a/test/test_package_comment.py b/test/test_package_comment.py index 60f0333d..b00e08c3 100644 --- a/test/test_package_comment.py +++ b/test/test_package_comment.py @@ -2,70 +2,55 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import begin, create, query, rollback -from aurweb.models.account_type import AccountType +from aurweb import db +from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase from aurweb.models.package_comment import PackageComment from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = None @pytest.fixture(autouse=True) -def setup(): - setup_test_db("PackageBases", "PackageComments", "Users") - +def setup(db_test): global user, pkgbase - account_type = query(AccountType, - AccountType.AccountType == "User").first() - with begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) def test_package_comment_creation(): - with begin(): - package_comment = create(PackageComment, - PackageBase=pkgbase, - User=user, - Comments="Test comment.", - RenderedComment="Test rendered comment.") + with db.begin(): + package_comment = db.create(PackageComment, PackageBase=pkgbase, + User=user, Comments="Test comment.", + RenderedComment="Test rendered comment.") assert bool(package_comment.ID) def test_package_comment_null_package_base_raises_exception(): with pytest.raises(IntegrityError): - with begin(): - create(PackageComment, User=user, Comments="Test comment.", - RenderedComment="Test rendered comment.") - rollback() + PackageComment(User=user, Comments="Test comment.", + RenderedComment="Test rendered comment.") def test_package_comment_null_user_raises_exception(): with pytest.raises(IntegrityError): - with begin(): - create(PackageComment, PackageBase=pkgbase, - Comments="Test comment.", - RenderedComment="Test rendered comment.") - rollback() + PackageComment(PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment="Test rendered comment.") def test_package_comment_null_comments_raises_exception(): with pytest.raises(IntegrityError): - with begin(): - create(PackageComment, PackageBase=pkgbase, User=user, - RenderedComment="Test rendered comment.") - rollback() + PackageComment(PackageBase=pkgbase, User=user, + RenderedComment="Test rendered comment.") def test_package_comment_null_renderedcomment_defaults(): - with begin(): - record = create(PackageComment, - PackageBase=pkgbase, - User=user, - Comments="Test comment.") + with db.begin(): + record = db.create(PackageComment, PackageBase=pkgbase, + User=user, Comments="Test comment.") assert record.RenderedComment == str() diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py index 2ddef68e..e6125669 100644 --- a/test/test_package_dependency.py +++ b/test/test_package_dependency.py @@ -3,117 +3,70 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.db import create, query -from aurweb.models.account_type import AccountType -from aurweb.models.dependency_type import DependencyType +from aurweb.models.account_type import USER_ID +from aurweb.models.dependency_type import CHECKDEPENDS_ID, DEPENDS_ID, MAKEDEPENDS_ID, OPTDEPENDS_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_dependency import PackageDependency from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = package = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase, package - setup_test_db("Users", "PackageBases", "Packages", "PackageDepends") - - account_type = query(AccountType, - AccountType.AccountType == "User").first() - with db.begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, - Name="test-package", - Maintainer=user) - package = create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + pkgbase = db.create(PackageBase, + Name="test-package", + Maintainer=user) + package = db.create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") def test_package_dependencies(): - depends = query(DependencyType, DependencyType.Name == "depends").first() - with db.begin(): - pkgdep = create(PackageDependency, Package=package, - DependencyType=depends, - DepName="test-dep") + pkgdep = db.create(PackageDependency, Package=package, + DepTypeID=DEPENDS_ID, DepName="test-dep") assert pkgdep.DepName == "test-dep" assert pkgdep.Package == package - assert pkgdep.DependencyType == depends - assert pkgdep in depends.package_dependencies assert pkgdep in package.package_dependencies - makedepends = query(DependencyType, - DependencyType.Name == "makedepends").first() with db.begin(): - pkgdep.DependencyType = makedepends - assert pkgdep.DepName == "test-dep" - assert pkgdep.Package == package - assert pkgdep.DependencyType == makedepends - assert pkgdep in makedepends.package_dependencies - assert pkgdep in package.package_dependencies + pkgdep.DepTypeID = MAKEDEPENDS_ID - checkdepends = query(DependencyType, - DependencyType.Name == "checkdepends").first() with db.begin(): - pkgdep.DependencyType = checkdepends - assert pkgdep.DepName == "test-dep" - assert pkgdep.Package == package - assert pkgdep.DependencyType == checkdepends - assert pkgdep in checkdepends.package_dependencies - assert pkgdep in package.package_dependencies + pkgdep.DepTypeID = CHECKDEPENDS_ID - optdepends = query(DependencyType, - DependencyType.Name == "optdepends").first() with db.begin(): - pkgdep.DependencyType = optdepends - assert pkgdep.DepName == "test-dep" - assert pkgdep.Package == package - assert pkgdep.DependencyType == optdepends - assert pkgdep in optdepends.package_dependencies - assert pkgdep in package.package_dependencies + pkgdep.DepTypeID = OPTDEPENDS_ID assert not pkgdep.is_package() with db.begin(): - base = create(PackageBase, Name=pkgdep.DepName, Maintainer=user) - create(Package, PackageBase=base, Name=pkgdep.DepName) + base = db.create(PackageBase, Name=pkgdep.DepName, Maintainer=user) + db.create(Package, PackageBase=base, Name=pkgdep.DepName) assert pkgdep.is_package() def test_package_dependencies_null_package_raises_exception(): - depends = query(DependencyType, DependencyType.Name == "depends").first() with pytest.raises(IntegrityError): - with db.begin(): - create(PackageDependency, - DependencyType=depends, - DepName="test-dep") - db.rollback() + PackageDependency(DepTypeID=DEPENDS_ID, DepName="test-dep") def test_package_dependencies_null_dependency_type_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - create(PackageDependency, - Package=package, - DepName="test-dep") - db.rollback() + PackageDependency(Package=package, DepName="test-dep") def test_package_dependencies_null_depname_raises_exception(): - depends = query(DependencyType, DependencyType.Name == "depends").first() with pytest.raises(IntegrityError): - with db.begin(): - create(PackageDependency, - Package=package, - DependencyType=depends) - db.rollback() + PackageDependency(DepTypeID=DEPENDS_ID, Package=package) diff --git a/test/test_package_group.py b/test/test_package_group.py index 0e6e41e3..2c91e0b1 100644 --- a/test/test_package_group.py +++ b/test/test_package_group.py @@ -2,51 +2,44 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query -from aurweb.models.account_type import AccountType +from aurweb import db +from aurweb.models.account_type import USER_ID from aurweb.models.group import Group from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_group import PackageGroup from aurweb.models.user import User -from aurweb.testing import setup_test_db user = group = pkgbase = package = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, group, pkgbase, package - setup_test_db("Users", "PackageBases", "Packages", - "Groups", "PackageGroups") + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + group = db.create(Group, Name="Test Group") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - - group = create(Group, Name="Test Group") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) - package = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) def test_package_group(): - package_group = create(PackageGroup, Package=package, Group=group) + with db.begin(): + package_group = db.create(PackageGroup, Package=package, Group=group) assert package_group.Group == group assert package_group.Package == package def test_package_group_null_package_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(PackageGroup, Group=group) - session.rollback() + PackageGroup(Group=group) def test_package_group_null_group_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(PackageGroup, Package=package) - session.rollback() + PackageGroup(Package=package) diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py index 316e7ca8..88ccb734 100644 --- a/test/test_package_keyword.py +++ b/test/test_package_keyword.py @@ -2,44 +2,37 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query -from aurweb.models.account_type import AccountType +from aurweb import db +from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase from aurweb.models.package_keyword import PackageKeyword from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase - setup_test_db("Users", "PackageBases", "PackageKeywords") - - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, - Name="beautiful-package", - Maintainer=user) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + pkgbase = db.create(PackageBase, + Name="beautiful-package", + Maintainer=user) def test_package_keyword(): - pkg_keyword = create(PackageKeyword, - PackageBase=pkgbase, - Keyword="test") + with db.begin(): + pkg_keyword = db.create(PackageKeyword, + PackageBase=pkgbase, + Keyword="test") assert pkg_keyword in pkgbase.keywords assert pkgbase == pkg_keyword.PackageBase def test_package_keyword_null_pkgbase_raises_exception(): - from aurweb.db import session - with pytest.raises(IntegrityError): - create(PackageKeyword, - Keyword="test") - session.rollback() + PackageKeyword(Keyword="test") diff --git a/test/test_package_license.py b/test/test_package_license.py index f7654dee..965d0c6f 100644 --- a/test/test_package_license.py +++ b/test/test_package_license.py @@ -2,51 +2,45 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query -from aurweb.models.account_type import AccountType +from aurweb import db +from aurweb.models.account_type import USER_ID from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_license import PackageLicense from aurweb.models.user import User -from aurweb.testing import setup_test_db user = license = pkgbase = package = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, license, pkgbase, package - setup_test_db("Users", "PackageBases", "Packages", - "Licenses", "PackageLicenses") + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + license = db.create(License, Name="Test License") - account_type = query(AccountType, - AccountType.AccountType == "User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - - license = create(License, Name="Test License") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) - package = create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) def test_package_license(): - package_license = create(PackageLicense, Package=package, License=license) + with db.begin(): + package_license = db.create(PackageLicense, Package=package, + License=license) assert package_license.License == license assert package_license.Package == package def test_package_license_null_package_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(PackageLicense, License=license) - session.rollback() + PackageLicense(License=license) def test_package_license_null_license_raises_exception(): - from aurweb.db import session with pytest.raises(IntegrityError): - create(PackageLicense, Package=package) - session.rollback() + PackageLicense(Package=package) diff --git a/test/test_package_notification.py b/test/test_package_notification.py index 2898a904..2e505dd8 100644 --- a/test/test_package_notification.py +++ b/test/test_package_notification.py @@ -2,29 +2,28 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, rollback +from aurweb import db from aurweb.models.package_base import PackageBase from aurweb.models.package_notification import PackageNotification from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase - setup_test_db("Users", "PackageBases", "PackageNotifications") - - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) def test_package_notification_creation(): - package_notification = create(PackageNotification, User=user, - PackageBase=pkgbase) + with db.begin(): + package_notification = db.create( + PackageNotification, User=user, PackageBase=pkgbase) assert bool(package_notification) assert package_notification.User == user assert package_notification.PackageBase == pkgbase @@ -32,11 +31,9 @@ def test_package_notification_creation(): def test_package_notification_null_user_raises_exception(): with pytest.raises(IntegrityError): - create(PackageNotification, PackageBase=pkgbase) - rollback() + PackageNotification(PackageBase=pkgbase) def test_package_notification_null_pkgbase_raises_exception(): with pytest.raises(IntegrityError): - create(PackageNotification, User=user) - rollback() + PackageNotification(User=user) diff --git a/test/test_package_relation.py b/test/test_package_relation.py index edb67078..e5f7f453 100644 --- a/test/test_package_relation.py +++ b/test/test_package_relation.py @@ -1,103 +1,63 @@ import pytest -from sqlalchemy.exc import IntegrityError, OperationalError +from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.db import create, query -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_relation import PackageRelation -from aurweb.models.relation_type import RelationType +from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = package = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase, package - setup_test_db("Users", "PackageBases", "Packages", "PackageRelations") - - account_type = query(AccountType, - AccountType.AccountType == "User").first() - with db.begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, - Name="test-package", - Maintainer=user) - package = create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + pkgbase = db.create(PackageBase, + Name="test-package", + Maintainer=user) + package = db.create(Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package") def test_package_relation(): - conflicts = query(RelationType, RelationType.Name == "conflicts").first() - with db.begin(): - pkgrel = create(PackageRelation, Package=package, - RelationType=conflicts, - RelName="test-relation") + pkgrel = db.create(PackageRelation, Package=package, + RelTypeID=CONFLICTS_ID, + RelName="test-relation") + assert pkgrel.RelName == "test-relation" assert pkgrel.Package == package - assert pkgrel.RelationType == conflicts - assert pkgrel in conflicts.package_relations assert pkgrel in package.package_relations - provides = query(RelationType, RelationType.Name == "provides").first() with db.begin(): - pkgrel.RelationType = provides - assert pkgrel.RelName == "test-relation" - assert pkgrel.Package == package - assert pkgrel.RelationType == provides - assert pkgrel in provides.package_relations - assert pkgrel in package.package_relations + pkgrel.RelTypeID = PROVIDES_ID - replaces = query(RelationType, RelationType.Name == "replaces").first() with db.begin(): - pkgrel.RelationType = replaces - assert pkgrel.RelName == "test-relation" - assert pkgrel.Package == package - assert pkgrel.RelationType == replaces - assert pkgrel in replaces.package_relations - assert pkgrel in package.package_relations + pkgrel.RelTypeID = REPLACES_ID def test_package_relation_null_package_raises_exception(): - conflicts = query(RelationType, RelationType.Name == "conflicts").first() - assert conflicts is not None - with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRelation, - RelationType=conflicts, - RelName="test-relation") - db.rollback() + PackageRelation(RelTypeID=CONFLICTS_ID, RelName="test-relation") def test_package_relation_null_relation_type_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRelation, - Package=package, - RelName="test-relation") - db.rollback() + PackageRelation(Package=package, RelName="test-relation") def test_package_relation_null_relname_raises_exception(): - depends = query(RelationType, RelationType.Name == "conflicts").first() - assert depends is not None - - with pytest.raises((OperationalError, IntegrityError)): - with db.begin(): - create(PackageRelation, - Package=package, - RelationType=depends) - db.rollback() + with pytest.raises(IntegrityError): + PackageRelation(Package=package, RelTypeID=CONFLICTS_ID) diff --git a/test/test_package_request.py b/test/test_package_request.py index 1589ffc2..4b5dfb2b 100644 --- a/test/test_package_request.py +++ b/test/test_package_request.py @@ -5,41 +5,35 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.db import create, query, rollback +from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase from aurweb.models.package_request import (ACCEPTED, ACCEPTED_ID, CLOSED, CLOSED_ID, PENDING, PENDING_ID, REJECTED, REJECTED_ID, PackageRequest) -from aurweb.models.request_type import RequestType +from aurweb.models.request_type import MERGE_ID from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase - setup_test_db("PackageRequests", "PackageBases", "Users") - with db.begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) def test_package_request_creation(): - request_type = query(RequestType, RequestType.Name == "merge").first() - assert request_type.Name == "merge" - with db.begin(): - package_request = create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) assert bool(package_request.ID) - assert package_request.RequestType == request_type assert package_request.User == user assert package_request.PackageBase == pkgbase assert package_request.PackageBaseName == pkgbase.Name @@ -47,22 +41,18 @@ def test_package_request_creation(): assert package_request.ClosureComment == str() # Make sure that everything is cross-referenced with relationships. - assert package_request in request_type.package_requests assert package_request in user.package_requests assert package_request in pkgbase.requests def test_package_request_closed(): - request_type = query(RequestType, RequestType.Name == "merge").first() - assert request_type.Name == "merge" - ts = int(datetime.utcnow().timestamp()) with db.begin(): - package_request = create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Closer=user, ClosedTS=ts, - Comments=str(), ClosureComment=str()) + package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Closer=user, ClosedTS=ts, + Comments=str(), ClosureComment=str()) assert package_request.Closer == user assert package_request.ClosedTS == ts @@ -73,73 +63,54 @@ def test_package_request_closed(): def test_package_request_null_request_type_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRequest, User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) - rollback() + PackageRequest(User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) def test_package_request_null_user_raises_exception(): - request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRequest, RequestType=request_type, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) - rollback() + PackageRequest(ReqTypeID=MERGE_ID, + PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) def test_package_request_null_package_base_raises_exception(): - request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRequest, RequestType=request_type, - User=user, PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) - rollback() + PackageRequest(ReqTypeID=MERGE_ID, + User=user, PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str()) def test_package_request_null_package_base_name_raises_exception(): - request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - Comments=str(), ClosureComment=str()) - rollback() + PackageRequest(ReqTypeID=MERGE_ID, + User=user, PackageBase=pkgbase, + Comments=str(), ClosureComment=str()) def test_package_request_null_comments_raises_exception(): - request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRequest, RequestType=request_type, User=user, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - ClosureComment=str()) - rollback() + PackageRequest(ReqTypeID=MERGE_ID, User=user, + PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + ClosureComment=str()) def test_package_request_null_closure_comment_raises_exception(): - request_type = query(RequestType, RequestType.Name == "merge").first() with pytest.raises(IntegrityError): - with db.begin(): - create(PackageRequest, RequestType=request_type, User=user, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - Comments=str()) - rollback() + PackageRequest(ReqTypeID=MERGE_ID, User=user, + PackageBase=pkgbase, PackageBaseName=pkgbase.Name, + Comments=str()) def test_package_request_status_display(): """ Test status_display() based on the Status column value. """ - request_type = query(RequestType, RequestType.Name == "merge").first() - with db.begin(): - pkgreq = create(PackageRequest, RequestType=request_type, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str(), - Status=PENDING_ID) + pkgreq = db.create(PackageRequest, ReqTypeID=MERGE_ID, + User=user, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), ClosureComment=str(), + Status=PENDING_ID) assert pkgreq.status_display() == PENDING with db.begin(): diff --git a/test/test_package_source.py b/test/test_package_source.py index d1adcf9c..b83c9d48 100644 --- a/test/test_package_source.py +++ b/test/test_package_source.py @@ -14,7 +14,7 @@ user = pkgbase = package = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase, package setup_test_db("PackageSources", "Packages", "PackageBases", "Users") diff --git a/test/test_package_vote.py b/test/test_package_vote.py index cb15e217..d1ec203b 100644 --- a/test/test_package_vote.py +++ b/test/test_package_vote.py @@ -4,30 +4,30 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, rollback +from aurweb import db from aurweb.models.package_base import PackageBase from aurweb.models.package_vote import PackageVote from aurweb.models.user import User -from aurweb.testing import setup_test_db user = pkgbase = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, pkgbase - setup_test_db("Users", "PackageBases", "PackageVotes") - - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = create(PackageBase, Name="test-package", Maintainer=user) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword") + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) def test_package_vote_creation(): ts = int(datetime.utcnow().timestamp()) - package_vote = create(PackageVote, User=user, PackageBase=pkgbase, - VoteTS=ts) + + with db.begin(): + package_vote = db.create(PackageVote, User=user, + PackageBase=pkgbase, VoteTS=ts) assert bool(package_vote) assert package_vote.User == user assert package_vote.PackageBase == pkgbase @@ -36,17 +36,14 @@ def test_package_vote_creation(): def test_package_vote_null_user_raises_exception(): with pytest.raises(IntegrityError): - create(PackageVote, PackageBase=pkgbase, VoteTS=1) - rollback() + PackageVote(PackageBase=pkgbase, VoteTS=1) def test_package_vote_null_pkgbase_raises_exception(): with pytest.raises(IntegrityError): - create(PackageVote, User=user, VoteTS=1) - rollback() + PackageVote(User=user, VoteTS=1) def test_package_vote_null_votets_raises_exception(): with pytest.raises(IntegrityError): - create(PackageVote, User=user, PackageBase=pkgbase) - rollback() + PackageVote(User=user, PackageBase=pkgbase) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 1bdb3ea3..02c22d9d 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -28,7 +28,6 @@ from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID, RelationType from aurweb.models.request_type import DELETION_ID, MERGE_ID, RequestType from aurweb.models.user import User -from aurweb.testing import setup_test_db from aurweb.testing.html import get_errors, get_successes, parse_root from aurweb.testing.requests import Request @@ -65,21 +64,8 @@ def create_package_rel(package: Package, @pytest.fixture(autouse=True) -def setup(): - setup_test_db( - User.__tablename__, - Package.__tablename__, - PackageBase.__tablename__, - PackageDependency.__tablename__, - PackageRelation.__tablename__, - PackageKeyword.__tablename__, - PackageVote.__tablename__, - PackageNotification.__tablename__, - PackageComaintainer.__tablename__, - PackageComment.__tablename__, - PackageRequest.__tablename__, - OfficialProvider.__tablename__ - ) +def setup(db_test): + return @pytest.fixture @@ -91,12 +77,11 @@ def client() -> TestClient: @pytest.fixture def user() -> User: """ Yield a user. """ - account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): user = db.create(User, Username="test", Email="test@example.org", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) yield user @@ -1173,7 +1158,7 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, PackageNotification.UserID == maintainer.ID ).first() with db.begin(): - db.session.delete(db_notif) + db.delete(db_notif) # Now, let's edit the comment we just created. comment_id = int(headers[0].attrib["id"].split("-")[-1]) diff --git a/test/test_packages_util.py b/test/test_packages_util.py index 622c08c2..cd0982b2 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -6,7 +6,7 @@ from fastapi import HTTPException from fastapi.testclient import TestClient from aurweb import asgi, db -from aurweb.models.account_type import USER_ID, AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -15,29 +15,20 @@ from aurweb.models.package_vote import PackageVote from aurweb.models.user import User from aurweb.packages import util from aurweb.redis import kill_redis -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db( - User.__tablename__, - Package.__tablename__, - PackageBase.__tablename__, - PackageVote.__tablename__, - PackageNotification.__tablename__, - OfficialProvider.__tablename__ - ) +def setup(db_test): + return @pytest.fixture def maintainer() -> User: - account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): maintainer = db.create(User, Username="test_maintainer", Email="test_maintainer@examepl.org", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) yield maintainer diff --git a/test/test_popupdate.py b/test/test_popupdate.py index 93f86f10..ce3f9f11 100644 --- a/test/test_popupdate.py +++ b/test/test_popupdate.py @@ -1,5 +1,12 @@ +import pytest + from aurweb.scripts import popupdate +@pytest.fixture(autouse=True) +def setup(db_test): + return + + def test_popupdate(): popupdate.main() diff --git a/test/test_ratelimit.py b/test/test_ratelimit.py index 0a72a7e4..859adea9 100644 --- a/test/test_ratelimit.py +++ b/test/test_ratelimit.py @@ -8,15 +8,14 @@ from aurweb import config, db, logging from aurweb.models import ApiRateLimit from aurweb.ratelimit import check_ratelimit from aurweb.redis import redis_connection -from aurweb.testing import setup_test_db from aurweb.testing.requests import Request logger = logging.get_logger(__name__) @pytest.fixture(autouse=True) -def setup(): - setup_test_db(ApiRateLimit.__tablename__) +def setup(db_test): + return @pytest.fixture @@ -31,27 +30,36 @@ def pipeline(): yield pipeline +config_getint = config.getint + + def mock_config_getint(section: str, key: str): if key == "request_limit": return 4 elif key == "window_length": return 100 - return config.getint(section, key) + return config_getint(section, key) + + +config_getboolean = config.getboolean def mock_config_getboolean(return_value: int = 0): def fn(section: str, key: str): if section == "ratelimit" and key == "cache": return return_value - return config.getboolean(section, key) + return config_getboolean(section, key) return fn +config_get = config.get + + def mock_config_get(return_value: str = "none"): def fn(section: str, key: str): if section == "options" and key == "cache": return return_value - return config.get(section, key) + return config_get(section, key) return fn diff --git a/test/test_relation_type.py b/test/test_relation_type.py index d2dabceb..263ae1ec 100644 --- a/test/test_relation_type.py +++ b/test/test_relation_type.py @@ -2,12 +2,11 @@ import pytest from aurweb import db from aurweb.models.relation_type import RelationType -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db() +def setup(db_test): + return def test_relation_type_creation(): diff --git a/test/test_request_type.py b/test/test_request_type.py index 0db24921..0bc86319 100644 --- a/test/test_request_type.py +++ b/test/test_request_type.py @@ -2,12 +2,11 @@ import pytest from aurweb import db from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID, RequestType -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db() +def setup(db_test): + return def test_request_type_creation(): diff --git a/test/test_routes.py b/test/test_routes.py index e3f69d7a..32f507f3 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -10,27 +10,21 @@ from fastapi.testclient import TestClient from aurweb import db from aurweb.asgi import app -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.user import User -from aurweb.testing import setup_test_db from aurweb.testing.requests import Request user = client = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, client - setup_test_db("Users", "Sessions") - - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() - with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) client = TestClient(app) diff --git a/test/test_rpc.py b/test/test_rpc.py index 055baa33..f20c9b02 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -26,7 +26,6 @@ from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import RelationType from aurweb.models.user import User from aurweb.redis import redis_connection -from aurweb.testing import setup_test_db def make_request(path, headers: Dict[str, str] = {}): @@ -35,11 +34,8 @@ def make_request(path, headers: Dict[str, str] = {}): @pytest.fixture(autouse=True) -def setup(): - # Set up tables. - setup_test_db("Users", "PackageBases", "Packages", "Licenses", - "PackageDepends", "PackageRelations", "PackageLicenses", - "PackageKeywords", "PackageVotes", "ApiRateLimit") +def setup(db_test): + # TODO: Rework this into organized fixtures. # Create test package details. with begin(): diff --git a/test/test_rss.py b/test/test_rss.py index 40607ade..7123fbf1 100644 --- a/test/test_rss.py +++ b/test/test_rss.py @@ -12,17 +12,13 @@ from aurweb.models.account_type import AccountType from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.user import User -from aurweb.testing import setup_test_db logger = logging.get_logger(__name__) @pytest.fixture(autouse=True) -def setup(): - setup_test_db( - Package.__tablename__, - PackageBase.__tablename__, - User.__tablename__) +def setup(db_test): + return @pytest.fixture diff --git a/test/test_session.py b/test/test_session.py index 4e6f4db4..7d3037a1 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -8,17 +8,14 @@ from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.session import Session, generate_unique_sid from aurweb.models.user import User -from aurweb.testing import setup_test_db account_type = user = session = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global account_type, user, session - setup_test_db("Users", "Sessions") - account_type = db.query(AccountType, AccountType.AccountType == "User").first() with db.begin(): diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index 12a3e1ce..bb787759 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -1,10 +1,9 @@ import pytest from aurweb import db -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.user import User -from aurweb.testing import setup_test_db TEST_SSH_PUBKEY = """ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCycoCi5yGCvSclH2wmNBUuwsYEzRZZBJaQquRc4ysl+Tg+/jiDkR3Zn9fIznC4KnFoyrIHzkKuePZ3bNDYwkZxkJKoWBCh4hXKDXSm87FMN0+VDC+1QxF/z0XaAGr/P6f4XukabyddypBdnHcZiplbw+YOSqcAE2TCqOlSXwNMOcF9U89UsR/Q9i9I52hlvU0q8+fZVGhou1KCowFSnHYtrr5KYJ04CXkJ13DkVf3+pjQWyrByvBcf1hGEaczlgfobrrv/y96jDhgfXucxliNKLdufDPPkii3LhhsNcDmmI1VZ3v0irKvd9WZuauqloobY84zEFcDTyjn0hxGjVeYFejm4fBnvjga0yZXORuWksdNfXWLDxFk6MDDd1jF0ExRbP+OxDuU4IVyIuDL7S3cnbf2YjGhkms/8voYT2OBE7FwNlfv98Kr0NUp51zpf55Arxn9j0Rz9xTA7FiODQgCn6iQ0SDtzUNL0IKTCw26xJY5gzMxbfpvzPQGeulx/ioM= kevr@volcano @@ -14,21 +13,16 @@ user = ssh_pub_key = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, ssh_pub_key - setup_test_db("Users", "SSHPubKeys") - - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) with db.begin(): - ssh_pub_key = db.create(SSHPubKey, - UserID=user.ID, + ssh_pub_key = db.create(SSHPubKey, UserID=user.ID, Fingerprint="testFingerprint", PubKey="testPubKey") diff --git a/test/test_term.py b/test/test_term.py index 3f28311f..bfa73a76 100644 --- a/test/test_term.py +++ b/test/test_term.py @@ -4,17 +4,11 @@ from sqlalchemy.exc import IntegrityError from aurweb import db from aurweb.models.term import Term -from aurweb.testing import setup_test_db @pytest.fixture(autouse=True) -def setup(): - setup_test_db("Terms") - - yield None - - # Wipe em out just in case records are leftover. - setup_test_db("Terms") +def setup(db_test): + return def test_term_creation(): @@ -29,13 +23,9 @@ def test_term_creation(): def test_term_null_description_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(Term, URL="https://fake_url.io") - db.rollback() + Term(URL="https://fake_url.io") def test_term_null_url_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(Term, Description="Term description") - db.rollback() + Term(Description="Term description") diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index 0579247e..43a3443b 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -14,7 +14,6 @@ from aurweb.models.account_type import AccountType from aurweb.models.tu_vote import TUVote from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User -from aurweb.testing import setup_test_db from aurweb.testing.requests import Request DATETIME_REGEX = r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$' @@ -76,8 +75,8 @@ def assert_past_vote_html(row, expected): @pytest.fixture(autouse=True) -def setup(): - setup_test_db("TU_Votes", "TU_VoteInfo", "Users") +def setup(db_test): + return @pytest.fixture diff --git a/test/test_tu_vote.py b/test/test_tu_vote.py index 9ff4a8d9..1dd33387 100644 --- a/test/test_tu_vote.py +++ b/test/test_tu_vote.py @@ -4,53 +4,48 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import create, query, rollback -from aurweb.models.account_type import AccountType +from aurweb import db +from aurweb.models.account_type import TRUSTED_USER_ID from aurweb.models.tu_vote import TUVote from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User -from aurweb.testing import setup_test_db user = tu_voteinfo = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user, tu_voteinfo - setup_test_db("Users", "TU_VoteInfo", "TU_Votes") - - tu_type = query(AccountType, - AccountType.AccountType == "Trusted User").first() - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=tu_type) - ts = int(datetime.utcnow().timestamp()) - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 5, - Quorum=0.5, - Submitter=user) + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=TRUSTED_USER_ID) + + tu_voteinfo = db.create(TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, End=ts + 5, + Quorum=0.5, + Submitter=user) def test_tu_vote_creation(): - tu_vote = create(TUVote, User=user, VoteInfo=tu_voteinfo) + with db.begin(): + tu_vote = db.create(TUVote, User=user, VoteInfo=tu_voteinfo) + assert tu_vote.VoteInfo == tu_voteinfo assert tu_vote.User == user - assert tu_vote in user.tu_votes assert tu_vote in tu_voteinfo.tu_votes def test_tu_vote_null_user_raises_exception(): with pytest.raises(IntegrityError): - create(TUVote, VoteInfo=tu_voteinfo) - rollback() + TUVote(VoteInfo=tu_voteinfo) def test_tu_vote_null_voteinfo_raises_exception(): with pytest.raises(IntegrityError): - create(TUVote, User=user) - rollback() + TUVote(User=user) diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py index b60e2e6a..5926fbf9 100644 --- a/test/test_tu_voteinfo.py +++ b/test/test_tu_voteinfo.py @@ -9,17 +9,14 @@ from aurweb.db import create, query, rollback from aurweb.models.account_type import AccountType from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User -from aurweb.testing import setup_test_db user = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global user - setup_test_db("Users", "PackageBases", "TU_VoteInfo") - tu_type = query(AccountType, AccountType.AccountType == "Trusted User").first() with db.begin(): diff --git a/test/test_user.py b/test/test_user.py index 771611d8..07f10487 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -19,27 +19,15 @@ from aurweb.models.package_vote import PackageVote from aurweb.models.session import Session from aurweb.models.ssh_pub_key import SSHPubKey from aurweb.models.user import User -from aurweb.testing import setup_test_db from aurweb.testing.requests import Request account_type = user = None @pytest.fixture(autouse=True) -def setup(): +def setup(db_test): global account_type, user - setup_test_db( - User.__tablename__, - Session.__tablename__, - Ban.__tablename__, - SSHPubKey.__tablename__, - Package.__tablename__, - PackageBase.__tablename__, - PackageVote.__tablename__, - PackageNotification.__tablename__ - ) - account_type = db.query(AccountType, AccountType.AccountType == "User").first() From fa26c8078b5dd305fcc62e349a2856ddd29d0b68 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:44:35 -0800 Subject: [PATCH 0674/1451] fix(docker): modify db configuration for new tests A user that can create databases is now required for tests, we use the 'root' user in Docker. Added docker services: --------------------- - mariadb_test - host localhost:13307 Signed-off-by: Kevin Morris --- conf/config.defaults | 2 +- conf/config.dev | 7 ++-- docker-compose.yml | 65 ++++++++++++++----------------- docker/fastapi-entrypoint.sh | 4 ++ docker/mariadb-entrypoint.sh | 15 +++---- docker/mariadb-init-entrypoint.sh | 2 + docker/php-entrypoint.sh | 7 +++- docker/scripts/run-php.sh | 3 -- docker/scripts/run-pytests.sh | 11 +----- docker/scripts/run-tests.sh | 6 --- docker/test-mysql-entrypoint.sh | 9 ----- docker/tests-entrypoint.sh | 1 - 12 files changed, 53 insertions(+), 79 deletions(-) diff --git a/conf/config.defaults b/conf/config.defaults index c29d7045..68e235be 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -5,7 +5,7 @@ socket = /var/run/mysqld/mysqld.sock ;port = 3306 name = AUR user = aur -password = aur +;password = aur [options] username_min_len = 3 diff --git a/conf/config.dev b/conf/config.dev index 9467615e..e97f6f12 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -6,7 +6,8 @@ ; development-specific options too. [database] -; Options: mysql, sqlite. +; PHP options: mysql, sqlite. +; FastAPI options: mysql. backend = mysql ; If using sqlite, set name to the database file path. @@ -14,8 +15,8 @@ name = aurweb ; MySQL database information. User defaults to root for containerized ; testing with mysqldb. This should be set to a non-root user. -user = aur -password = aur +user = root +;password = aur host = localhost ;port = 3306 socket = /var/run/mysqld/mysqld.sock diff --git a/docker-compose.yml b/docker-compose.yml index bda4ddfb..c39d38bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -77,6 +77,24 @@ services: mariadb: condition: service_healthy + mariadb_test: + # Test database. + image: aurweb:latest + init: true + environment: + - MARIADB_PRIVILEGED=1 + entrypoint: /docker/mariadb-entrypoint.sh + command: /usr/bin/mysqld_safe --datadir=/var/lib/mysql + ports: + # This will expose mariadbd on 127.0.0.1:13307 in the host. + # Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb` + - "13307:3306" + volumes: + - mariadb_test_run:/var/run/mysqld # Bind socket in this volume. + healthcheck: + test: "bash /docker/health/mariadb.sh" + interval: 3s + git: image: aurweb:latest init: true @@ -254,10 +272,9 @@ services: stdin_open: true tty: true depends_on: - git: + mariadb_test: condition: service_healthy volumes: - - git_data:/aurweb/aur.git - ./cache:/cache - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations @@ -280,34 +297,12 @@ services: stdin_open: true tty: true depends_on: - mariadb_init: - condition: service_started + mariadb_test: + condition: service_healthy + tmpfs: + - /tmp volumes: - - mariadb_run:/var/run/mysqld - - git_data:/aurweb/aur.git - - ./cache:/cache - - ./aurweb:/aurweb/aurweb - - ./migrations:/aurweb/migrations - - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - - ./templates:/aurweb/templates - - pytest-sqlite: - image: aurweb:latest - profiles: ["dev"] - init: true - environment: - - AUR_CONFIG=conf/config.sqlite - - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} - - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus - entrypoint: /docker/test-sqlite-entrypoint.sh - command: setup-sqlite.sh run-pytests.sh clean - stdin_open: true - tty: true - volumes: - - git_data:/aurweb/aur.git + - mariadb_test_run:/var/run/mysqld - ./cache:/cache - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations @@ -325,16 +320,15 @@ services: - AUR_CONFIG=conf/config - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus - entrypoint: /docker/tests-entrypoint.sh - command: setup-sqlite.sh run-tests.sh + entrypoint: /docker/test-mysql-entrypoint.sh + command: /docker/scripts/run-tests.sh stdin_open: true tty: true depends_on: - mariadb_init: - condition: service_started + mariadb_test: + condition: service_healthy volumes: - - mariadb_run:/var/run/mysqld - - git_data:/aurweb/aur.git + - mariadb_test_run:/var/run/mysqld - ./cache:/cache - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations @@ -345,6 +339,7 @@ services: - ./templates:/aurweb/templates volumes: + mariadb_test_run: {} mariadb_run: {} # Share /var/run/mysqld/mysqld.sock mariadb_data: {} # Share /var/lib/mysql git_data: {} # Share aurweb/aur.git diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index f4ceaafa..9df6382d 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -5,6 +5,10 @@ set -eou pipefail cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config +# Change database user/password. +sed -ri "s/^;?(user) = .*$/\1 = aur/" conf/config +sed -ri "s/^;?(password) = .*$/\1 = aur/" conf/config + sed -ri "s;^(aur_location) = .+;\1 = ${AURWEB_FASTAPI_PREFIX};" conf/config # Setup Redis for FastAPI. diff --git a/docker/mariadb-entrypoint.sh b/docker/mariadb-entrypoint.sh index e1ebfa6a..a00f6106 100755 --- a/docker/mariadb-entrypoint.sh +++ b/docker/mariadb-entrypoint.sh @@ -13,23 +13,18 @@ done # Configure databases. DATABASE="aurweb" # Persistent database for fastapi/php-fpm. -TEST_DB="aurweb_test" # Test database (ephemereal). echo "Taking care of primary database '${DATABASE}'..." mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'localhost' IDENTIFIED BY 'aur';" mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" mysql -u root -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" -mysql -u root -e "GRANT ALL ON ${DATABASE}.* TO 'aur'@'localhost';" -mysql -u root -e "GRANT ALL ON ${DATABASE}.* TO 'aur'@'%';" -# Drop and create our test database. -echo "Dropping test database '$TEST_DB'..." -mysql -u root -e "DROP DATABASE IF EXISTS $TEST_DB;" -mysql -u root -e "CREATE DATABASE $TEST_DB;" -mysql -u root -e "GRANT ALL ON ${TEST_DB}.* TO 'aur'@'localhost';" -mysql -u root -e "GRANT ALL ON ${TEST_DB}.* TO 'aur'@'%';" +mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" +mysql -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'localhost';" +mysql -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'%';" -echo "Created new '$TEST_DB'!" +mysql -u root -e "CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'aur';" +mysql -u root -e "GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION;" mysqladmin -uroot shutdown diff --git a/docker/mariadb-init-entrypoint.sh b/docker/mariadb-init-entrypoint.sh index 413227b9..6df98e4f 100755 --- a/docker/mariadb-init-entrypoint.sh +++ b/docker/mariadb-init-entrypoint.sh @@ -4,6 +4,8 @@ set -eou pipefail # Setup a config for our mysql db. cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config +sed -ri "s/^;?(user) = .*$/\1 = aur/g" conf/config +sed -ri "s/^;?(password) = .*$/\1 = aur/g" conf/config python -m aurweb.initdb 2>/dev/null || /bin/true diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index 274f8e17..05b76408 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -9,14 +9,19 @@ done cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config -sed -ri "s;^(aur_location) = .+;\1 = ${AURWEB_PHP_PREFIX};" conf/config +# Change database user/password. +sed -ri "s/^;?(user) = .*$/\1 = aur/" conf/config +sed -ri "s/^;?(password) = .*$/\1 = aur/" conf/config # Enable memcached. sed -ri 's/^(cache) = .+$/\1 = memcache/' conf/config +# Setup various location configurations. +sed -ri "s;^(aur_location) = .+;\1 = ${AURWEB_PHP_PREFIX};" conf/config sed -ri "s|^(git_clone_uri_anon) = .+|\1 = ${AURWEB_PHP_PREFIX}/%s.git|" conf/config.defaults sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ${AURWEB_SSHD_PREFIX}/%s.git|" conf/config.defaults +# Listen on :9000. sed -ri 's/^(listen).*/\1 = 0.0.0.0:9000/' /etc/php/php-fpm.d/www.conf sed -ri 's/^;?(clear_env).*/\1 = no/' /etc/php/php-fpm.d/www.conf diff --git a/docker/scripts/run-php.sh b/docker/scripts/run-php.sh index 22346b47..b86f8ce5 100755 --- a/docker/scripts/run-php.sh +++ b/docker/scripts/run-php.sh @@ -1,7 +1,4 @@ #!/bin/bash set -eou pipefail -# Initialize the new database; ignore errors. -python -m aurweb.initdb 2>/dev/null || /bin/true - exec php-fpm --fpm-config /etc/php/php-fpm.conf --nodaemonize diff --git a/docker/scripts/run-pytests.sh b/docker/scripts/run-pytests.sh index ee546fb7..b8f695df 100755 --- a/docker/scripts/run-pytests.sh +++ b/docker/scripts/run-pytests.sh @@ -25,17 +25,8 @@ done rm -rf $PROMETHEUS_MULTIPROC_DIR mkdir -p $PROMETHEUS_MULTIPROC_DIR -# Initialize the new database; ignore errors. -python -m aurweb.initdb 2>/dev/null || \ - (echo "Error: aurweb.initdb failed; already initialized?" && /bin/true) - -# Run test_initdb ahead of time, which clears out the database, -# in case of previous failures which stopped the test suite before -# finishing the ends of some test fixtures. -eatmydata -- pytest test/test_initdb.py - # Run pytest with optional targets in front of it. -eatmydata -- make -C test "${PARAMS[@]}" pytest +pytest # By default, report coverage and move it into cache. if [ $COVERAGE -eq 1 ]; then diff --git a/docker/scripts/run-tests.sh b/docker/scripts/run-tests.sh index 3181a623..45c7835f 100755 --- a/docker/scripts/run-tests.sh +++ b/docker/scripts/run-tests.sh @@ -12,12 +12,6 @@ bash $dir/run-sharness.sh # Pass --silence to avoid reporting coverage. We will do that below. bash $dir/run-pytests.sh --no-coverage -# Export SQLite aurweb configuration. -export AUR_CONFIG=conf/config.sqlite - -# Run Python tests. -bash $dir/run-pytests.sh --no-coverage - make -C test coverage # /cache is mounted as a volume. Copy coverage into it. diff --git a/docker/test-mysql-entrypoint.sh b/docker/test-mysql-entrypoint.sh index 7be3626b..a46b2572 100755 --- a/docker/test-mysql-entrypoint.sh +++ b/docker/test-mysql-entrypoint.sh @@ -1,17 +1,8 @@ #!/bin/bash set -eou pipefail -DB_NAME="aurweb_test" - # Setup a config for our mysql db. cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config -sed -ri "s/^(name) = .+/\1 = ${DB_NAME}/" conf/config -# The port can be excluded from use if properly using -# volumes to share the mysql socket from the mariadb service. -# Example port sed: -# sed -i "s/^;?(port = .+)$/\1/" conf/config - -# Continue onto the main command. exec "$@" diff --git a/docker/tests-entrypoint.sh b/docker/tests-entrypoint.sh index ca3d3d9a..145bee6e 100755 --- a/docker/tests-entrypoint.sh +++ b/docker/tests-entrypoint.sh @@ -3,6 +3,5 @@ set -eou pipefail dir="$(dirname $0)" bash $dir/test-mysql-entrypoint.sh -bash $dir/test-sqlite-entrypoint.sh exec "$@" From a025118344ff857f5eab7a869b56b952cb46e6d8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:47:33 -0800 Subject: [PATCH 0675/1451] change(docker): get python-poetry from arch instead of poetry Signed-off-by: Kevin Morris --- docker/scripts/install-deps.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index d64340e3..ad0157f8 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -8,9 +8,7 @@ pacman -Syu --noconfirm --noprogressbar \ --cachedir .pkg-cache git gpgme nginx redis openssh \ mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ php php-fpm memcached php-memcached python-pip pyalpm \ - python-srcinfo curl libeatmydata cronie - -# https://python-poetry.org/docs/ Installation section. -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - + python-srcinfo curl libeatmydata cronie python-poetry \ + python-poetry-core exec "$@" From 60f63876c42b5900cfa7af3442d4feaced3c88c4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:48:31 -0800 Subject: [PATCH 0676/1451] change(.gitignore): ignore archives Signed-off-by: Kevin Morris --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e3201e94..8388694c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ schema/aur-schema-sqlite.sql test/test-results/ test/trash directory* web/locale/*/ +web/html/*.gz # Do not stage compiled asciidoc: make -C doc doc/rpc.html From fb92fb509b4a5769154096969cbfb90f8f69e798 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:49:00 -0800 Subject: [PATCH 0677/1451] change(fastapi): use sys.getrecursionlimit() + 1000 as default Without the increment, we've seen tests failed due to recursion errors caused by starlette's base middleware. Just make it safe in case nobody supplies TEST_RECURSION_LIMIT. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index aafb00b2..b399cfb1 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -45,7 +45,7 @@ async def app_startup(): # when running test suites. # TODO: Find a proper fix to this issue. recursion_limit = int(os.environ.get( - "TEST_RECURSION_LIMIT", sys.getrecursionlimit())) + "TEST_RECURSION_LIMIT", sys.getrecursionlimit() + 1000)) sys.setrecursionlimit(recursion_limit) backend = aurweb.config.get("database", "backend") From a5c0c47e5b82334b6416b13ffcc0f2948377b582 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 00:59:10 -0800 Subject: [PATCH 0678/1451] change(.gitlab-ci): adapt for new conftest No longer do we need to create any database in .gitlab-ci. Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1590bf34..739c9408 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,14 +22,11 @@ test: - (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & - 'until : > /dev/tcp/127.0.0.1/3306; do sleep 1s; done' - ./docker/test-mysql-entrypoint.sh # Create mysql AUR_CONFIG. - - ./docker/test-sqlite-entrypoint.sh # Create sqlite AUR_CONFIG. - make -C po all install - - python -m aurweb.initdb # Initialize MySQL tables. - - AUR_CONFIG=conf/config.sqlite python -m aurweb.initdb - make -C test clean script: - - make -C test sh pytest # sharness tests use sqlite & pytest w/ mysql. - - AUR_CONFIG=conf/config.sqlite make -C test pytest + - make -C test sh # sharness tests use sqlite. + - pytest # Run pytest suites. - make -C test coverage # Produce coverage reports. - flake8 --count aurweb # Assert no flake8 violations in aurweb. - flake8 --count test # Assert no flake8 violations in test. @@ -65,8 +62,11 @@ deploy: # Set secure login config for aurweb. - sed -ri "s/^(disable_http_login).*$/\1 = 1/" conf/config.dev - docker-compose build - - docker system prune -f - docker-compose -f docker-compose.yml -f docker-compose.aur-dev.yml up -d + - docker image prune -f + - docker container prune -f + - docker volume prune -f + environment: name: development url: https://aur-dev.archlinux.org From 912b7e0c118c5717d96fc2770770be8b91b2f81e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 02:19:43 -0800 Subject: [PATCH 0679/1451] fix(docker): fix database user/password for git-entrypoint Signed-off-by: Kevin Morris --- docker/git-entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index 3fee426a..296c1e47 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -42,6 +42,9 @@ EOF cp -vf conf/config.dev $AUR_CONFIG sed -i "s;YOUR_AUR_ROOT;$(pwd);g" $AUR_CONFIG +sed -ri "s/^;?(user) = .*$/\1 = aur/" $AUR_CONFIG +sed -ri "s/^;?(password) = .*$/\1 = aur/" $AUR_CONFIG + AUR_CONFIG_DEFAULTS="${AUR_CONFIG}.defaults" if [[ "$AUR_CONFIG_DEFAULTS" != "/aurweb/conf/config.defaults" ]]; then From abe8c0630c52bc5694f9284347045ae353c67507 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 3 Nov 2021 19:34:20 -0700 Subject: [PATCH 0680/1451] fix(rpc): improve type=info performance Now, we use an equivalent query to PHP's query, yet we grab every piece of data we need for all packages asked for in one database query. At this time, local benchmarks have shown a slight performance improvement when compared to PHP. fastapi 262 requests/sec php 250 requests/sec Extras: - Moved RPCError to the aurweb.exceptions module Signed-off-by: Kevin Morris --- aurweb/exceptions.py | 4 + aurweb/rpc.py | 220 +++++++++++++++++++++++++++---------------- 2 files changed, 141 insertions(+), 83 deletions(-) diff --git a/aurweb/exceptions.py b/aurweb/exceptions.py index 62015284..82628b0a 100644 --- a/aurweb/exceptions.py +++ b/aurweb/exceptions.py @@ -73,3 +73,7 @@ class NotVotedException(AurwebException): class InvalidArgumentsException(AurwebException): def __init__(self, msg): super(InvalidArgumentsException, self).__init__(msg) + + +class RPCError(AurwebException): + pass diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 03662790..c70ddf1a 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -1,38 +1,28 @@ from collections import defaultdict -from typing import Any, Callable, Dict, List, NewType +from typing import Any, Callable, Dict, List, NewType, Union -from sqlalchemy import and_ +from sqlalchemy import and_, literal import aurweb.config as config from aurweb import db, defaults, models, util -from aurweb.models import dependency_type, relation_type +from aurweb.exceptions import RPCError from aurweb.packages.search import RPCSearch -# Define dependency type mappings from ID to RPC-compatible keys. -DEP_TYPES = { - dependency_type.DEPENDS_ID: "Depends", - dependency_type.MAKEDEPENDS_ID: "MakeDepends", - dependency_type.CHECKDEPENDS_ID: "CheckDepends", - dependency_type.OPTDEPENDS_ID: "OptDepends" +TYPE_MAPPING = { + "depends": "Depends", + "makedepends": "MakeDepends", + "checkdepends": "CheckDepends", + "optdepends": "OptDepends", + "conflicts": "Conflicts", + "provides": "Provides", + "replaces": "Replaces", } -# Define relationship type mappings from ID to RPC-compatible keys. -REL_TYPES = { - relation_type.CONFLICTS_ID: "Conflicts", - relation_type.PROVIDES_ID: "Provides", - relation_type.REPLACES_ID: "Replaces" -} - - DataGenerator = NewType("DataGenerator", Callable[[models.Package], Dict[str, Any]]) -class RPCError(Exception): - pass - - class RPC: """ RPC API handler class. @@ -76,11 +66,11 @@ class RPC: # A mapping of by aliases. BY_ALIASES = {"name-desc": "nd", "name": "n", "maintainer": "m"} - def __init__(self, version: int = 0, type: str = None): + def __init__(self, version: int = 0, type: str = None) -> "RPC": self.version = version - self.type = type + self.type = RPC.TYPE_ALIASES.get(type, type) - def error(self, message: str) -> dict: + def error(self, message: str) -> Dict[str, Any]: return { "version": self.version, "results": [], @@ -89,7 +79,7 @@ class RPC: "error": message } - def _verify_inputs(self, by: str = [], args: List[str] = []): + def _verify_inputs(self, by: str = [], args: List[str] = []) -> None: if self.version is None: raise RPCError("Please specify an API version.") @@ -105,39 +95,11 @@ class RPC: if self.type not in RPC.EXPOSED_TYPES: raise RPCError("Incorrect request type specified.") - def _enforce_args(self, args: List[str]): + def _enforce_args(self, args: List[str]) -> None: if not args: raise RPCError("No request type/data specified.") - def _update_json_depends(self, package: models.Package, - data: Dict[str, Any]): - # Walk through all related PackageDependencies and produce - # the appropriate dict entries. - for dep in package.package_dependencies: - if dep.DepTypeID in DEP_TYPES: - key = DEP_TYPES.get(dep.DepTypeID) - - display = dep.DepName - if dep.DepCondition: - display += dep.DepCondition - - data[key].append(display) - - def _update_json_relations(self, package: models.Package, - data: Dict[str, Any]): - # Walk through all related PackageRelations and produce - # the appropriate dict entries. - for rel in package.package_relations: - if rel.RelTypeID in REL_TYPES: - key = REL_TYPES.get(rel.RelTypeID) - - display = rel.RelName - if rel.RelCondition: - display += rel.RelCondition - - data[key].append(display) - - def _get_json_data(self, package: models.Package): + def _get_json_data(self, package: models.Package) -> Dict[str, Any]: """ Produce dictionary data of one Package that can be JSON-serialized. :param package: Package instance @@ -175,21 +137,21 @@ class RPC: return data - def _get_info_json_data(self, package: models.Package): + def _get_info_json_data(self, package: models.Package) -> Dict[str, Any]: data = self._get_json_data(package) - # Add licenses and keywords to info output. + # All info results have _at least_ an empty list of + # License and Keywords. data.update({ - "License": [ - lic.License.Name for lic in package.package_licenses - ], - "Keywords": [ - keyword.Keyword for keyword in package.PackageBase.keywords - ] + "License": [], + "Keywords": [] }) - self._update_json_depends(package, data) - self._update_json_relations(package, data) + # If we actually got extra_info records, update data with + # them for this particular package. + if self.extra_info: + data.update(self.extra_info.get(package.ID, {})) + return data def _assemble_json_data(self, packages: List[models.Package], @@ -211,13 +173,97 @@ class RPC: -> List[Dict[str, Any]]: self._enforce_args(args) args = set(args) - packages = db.query(models.Package).filter( + + packages = db.query(models.Package).join(models.PackageBase).filter( models.Package.Name.in_(args)) + ids = {pkg.ID for pkg in packages} + + # Aliases for 80-width. + Package = models.Package + PackageKeyword = models.PackageKeyword + + subqueries = [ + # PackageDependency + db.query( + models.PackageDependency + ).join(models.DependencyType).filter( + models.PackageDependency.PackageID.in_(ids) + ).with_entities( + models.PackageDependency.PackageID.label("ID"), + models.DependencyType.Name.label("Type"), + models.PackageDependency.DepName.label("Name"), + models.PackageDependency.DepCondition.label("Cond") + ).distinct().order_by("ID"), + + # PackageRelation + db.query( + models.PackageRelation + ).join(models.RelationType).filter( + models.PackageRelation.PackageID.in_(ids) + ).with_entities( + models.PackageRelation.PackageID.label("ID"), + models.RelationType.Name.label("Type"), + models.PackageRelation.RelName.label("Name"), + models.PackageRelation.RelCondition.label("Cond") + ).distinct().order_by("ID"), + + # Groups + db.query(models.PackageGroup).join( + models.Group, + and_(models.PackageGroup.GroupID == models.Group.ID, + models.PackageGroup.PackageID.in_(ids)) + ).with_entities( + models.PackageGroup.PackageID.label("ID"), + literal("Groups").label("Type"), + models.Group.Name.label("Name"), + literal(str()).label("Cond") + ).distinct().order_by("ID"), + + # Licenses + db.query(models.PackageLicense).join( + models.License, + models.PackageLicense.LicenseID == models.License.ID + ).filter( + models.PackageLicense.PackageID.in_(ids) + ).with_entities( + models.PackageLicense.PackageID.label("ID"), + literal("License").label("Type"), + models.License.Name.label("Name"), + literal(str()).label("Cond") + ).distinct().order_by("ID"), + + # Keywords + db.query(models.PackageKeyword).join( + models.Package, + and_(Package.PackageBaseID == PackageKeyword.PackageBaseID, + Package.ID.in_(ids)) + ).with_entities( + models.Package.ID.label("ID"), + literal("Keywords").label("Type"), + models.PackageKeyword.Keyword.label("Name"), + literal(str()).label("Cond") + ).distinct().order_by("ID") + ] + + # Union all subqueries together. + query = subqueries[0].union_all(*subqueries[1:]) + + # Store our extra information in a class-wise dictionary, + # which contains package id -> extra info dict mappings. + self.extra_info = defaultdict(lambda: defaultdict(list)) + for record in query: + type_ = TYPE_MAPPING.get(record.Type, record.Type) + + name = record.Name + if record.Cond: + name += record.Cond + + self.extra_info[record.ID][type_].append(name) + return self._assemble_json_data(packages, self._get_info_json_data) def _handle_search_type(self, by: str = defaults.RPC_SEARCH_BY, - args: List[str] = []) \ - -> List[Dict[str, Any]]: + args: List[str] = []) -> List[Dict[str, Any]]: # If `by` isn't maintainer and we don't have any args, raise an error. # In maintainer's case, return all orphans if there are no args, # so we need args to pass through to the handler without errors. @@ -235,10 +281,12 @@ class RPC: results = search.results().limit(max_results) return self._assemble_json_data(results, self._get_json_data) - def _handle_msearch_type(self, args: List[str] = [], **kwargs): + def _handle_msearch_type(self, args: List[str] = [], **kwargs)\ + -> List[Dict[str, Any]]: return self._handle_search_type(by="m", args=args) - def _handle_suggest_type(self, args: List[str] = [], **kwargs): + def _handle_suggest_type(self, args: List[str] = [], **kwargs)\ + -> List[str]: if not args: return [] @@ -251,7 +299,8 @@ class RPC: ).order_by(models.Package.Name.asc()).limit(20) return [pkg.Name for pkg in packages] - def _handle_suggest_pkgbase_type(self, args: List[str] = [], **kwargs): + def _handle_suggest_pkgbase_type(self, args: List[str] = [], **kwargs)\ + -> List[str]: if not args: return [] @@ -261,7 +310,19 @@ class RPC: ).order_by(models.PackageBase.Name.asc()).limit(20) return [pkg.Name for pkg in packages] - def handle(self, by: str = defaults.RPC_SEARCH_BY, args: List[str] = []): + def _is_suggestion(self) -> bool: + return self.type.startswith("suggest") + + def _handle_callback(self, by: str, args: List[str])\ + -> Union[List[Dict[str, Any]], List[str]]: + # Get a handle to our callback and trap an RPCError with + # an empty list of results based on callback's execution. + callback = getattr(self, f"_handle_{self.type.replace('-', '_')}_type") + results = callback(by=by, args=args) + return results + + def handle(self, by: str = defaults.RPC_SEARCH_BY, args: List[str] = [])\ + -> Union[List[Dict[str, Any]], Dict[str, Any]]: """ Request entrypoint. A router should pass v, type and args to this function and expect an output dictionary to be returned. @@ -269,10 +330,6 @@ class RPC: :param type: RPC type argument :param args: Deciphered list of arguments based on arg/arg[] inputs """ - # Convert type aliased types. - if self.type in RPC.TYPE_ALIASES: - self.type = RPC.TYPE_ALIASES.get(self.type) - # Prepare our output data dictionary with some basic keys. data = {"version": self.version, "type": self.type} @@ -283,20 +340,17 @@ class RPC: return self.error(str(exc)) # Convert by to its aliased value if it has one. - if by in RPC.BY_ALIASES: - by = RPC.BY_ALIASES.get(by) + by = RPC.BY_ALIASES.get(by, by) - # Get a handle to our callback and trap an RPCError with - # an empty list of results based on callback's execution. - callback = getattr(self, f"_handle_{self.type.replace('-', '_')}_type") + # Process the requested handler. try: - results = callback(by=by, args=args) + results = self._handle_callback(by, args) except RPCError as exc: return self.error(str(exc)) # These types are special: we produce a different kind of # successful JSON output: a list of results. - if self.type in ("suggest", "suggest-pkgbase"): + if self._is_suggestion(): return results # Return JSON output. From ccf50cbdf5135978dbb86e2c37e04b72911d4732 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 3 Nov 2021 19:36:08 -0700 Subject: [PATCH 0681/1451] change: rework test_rpc's TestClient usage into a fixture This is the first step on our path to reworking the test suite in general. Signed-off-by: Kevin Morris --- test/test_rpc.py | 186 ++++++++++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 73 deletions(-) diff --git a/test/test_rpc.py b/test/test_rpc.py index f20c9b02..a4cdb5da 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1,7 +1,6 @@ import re from http import HTTPStatus -from typing import Dict from unittest import mock import orjson @@ -10,8 +9,7 @@ import pytest from fastapi.testclient import TestClient from redis.client import Pipeline -from aurweb import config, db, scripts -from aurweb.asgi import app +from aurweb import asgi, config, db, scripts from aurweb.db import begin, create, query from aurweb.models.account_type import AccountType from aurweb.models.dependency_type import DependencyType @@ -28,9 +26,9 @@ from aurweb.models.user import User from aurweb.redis import redis_connection -def make_request(path, headers: Dict[str, str] = {}): - with TestClient(app) as request: - return request.get(path, headers=headers) +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) @pytest.fixture(autouse=True) @@ -205,7 +203,7 @@ def pipeline(): yield pipeline -def test_rpc_singular_info(): +def test_rpc_singular_info(client: TestClient): # Define expected response. expected_data = { "version": 5, @@ -239,7 +237,9 @@ def test_rpc_singular_info(): } # Make dummy request. - response_arg = make_request("/rpc/?v=5&type=info&arg=chungy-chungus&arg=big-chungus") + with client as request: + response_arg = request.get( + "/rpc/?v=5&type=info&arg=chungy-chungus&arg=big-chungus") # Load request response into Python dictionary. response_info_arg = orjson.loads(response_arg.content.decode()) @@ -254,9 +254,10 @@ def test_rpc_singular_info(): assert response_info_arg == expected_data -def test_rpc_nonexistent_package(): +def test_rpc_nonexistent_package(client: TestClient): # Make dummy request. - response = make_request("/rpc/?v=5&type=info&arg=nonexistent-package") + with client as request: + response = request.get("/rpc/?v=5&type=info&arg=nonexistent-package") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -265,10 +266,12 @@ def test_rpc_nonexistent_package(): assert response_data["resultcount"] == 0 -def test_rpc_multiinfo(): +def test_rpc_multiinfo(client: TestClient): # Make dummy request. request_packages = ["big-chungus", "chungy-chungus"] - response = make_request("/rpc/?v=5&type=info&arg[]=big-chungus&arg[]=chungy-chungus") + with client as request: + response = request.get( + "/rpc/?v=5&type=info&arg[]=big-chungus&arg[]=chungy-chungus") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -280,13 +283,20 @@ def test_rpc_multiinfo(): assert request_packages == [] -def test_rpc_mixedargs(): +def test_rpc_mixedargs(client: TestClient): # Make dummy request. response1_packages = ["gluggly-chungus"] response2_packages = ["gluggly-chungus", "chungy-chungus"] - response1 = make_request("/rpc/?v=5&arg[]=big-chungus&arg=gluggly-chungus&type=info") - response2 = make_request("/rpc/?v=5&arg=big-chungus&arg[]=gluggly-chungus&type=info&arg[]=chungy-chungus") + with client as request: + response1 = request.get( + "/rpc?v=5&arg[]=big-chungus&arg=gluggly-chungus&type=info") + assert response1.status_code == int(HTTPStatus.OK) + + with client as request: + response2 = request.get( + "/rpc?v=5&arg=big-chungus&arg[]=gluggly-chungus&type=info&arg[]=chungy-chungus") + assert response1.status_code == int(HTTPStatus.OK) # Load request response into Python dictionary. response1_data = orjson.loads(response1.content.decode()) @@ -303,7 +313,7 @@ def test_rpc_mixedargs(): assert i == [] -def test_rpc_no_dependencies(): +def test_rpc_no_dependencies(client: TestClient): """This makes sure things like 'MakeDepends' get removed from JSON strings when they don't have set values.""" @@ -330,7 +340,8 @@ def test_rpc_no_dependencies(): } # Make dummy request. - response = make_request("/rpc/?v=5&type=info&arg=chungy-chungus") + with client as request: + response = request.get("/rpc/?v=5&type=info&arg=chungy-chungus") response_data = orjson.loads(response.content.decode()) # Remove inconsistent keys. @@ -340,7 +351,7 @@ def test_rpc_no_dependencies(): assert response_data == expected_response -def test_rpc_bad_type(): +def test_rpc_bad_type(client: TestClient): # Define expected response. expected_data = { 'version': 5, @@ -351,7 +362,8 @@ def test_rpc_bad_type(): } # Make dummy request. - response = make_request("/rpc/?v=5&type=invalid-type&arg=big-chungus") + with client as request: + response = request.get("/rpc/?v=5&type=invalid-type&arg=big-chungus") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -360,7 +372,7 @@ def test_rpc_bad_type(): assert expected_data == response_data -def test_rpc_bad_version(): +def test_rpc_bad_version(client: TestClient): # Define expected response. expected_data = { 'version': 0, @@ -371,7 +383,8 @@ def test_rpc_bad_version(): } # Make dummy request. - response = make_request("/rpc/?v=0&type=info&arg=big-chungus") + with client as request: + response = request.get("/rpc/?v=0&type=info&arg=big-chungus") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -380,7 +393,7 @@ def test_rpc_bad_version(): assert expected_data == response_data -def test_rpc_no_version(): +def test_rpc_no_version(client: TestClient): # Define expected response. expected_data = { 'version': None, @@ -391,7 +404,8 @@ def test_rpc_no_version(): } # Make dummy request. - response = make_request("/rpc/?type=info&arg=big-chungus") + with client as request: + response = request.get("/rpc/?type=info&arg=big-chungus") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -400,7 +414,7 @@ def test_rpc_no_version(): assert expected_data == response_data -def test_rpc_no_type(): +def test_rpc_no_type(client: TestClient): # Define expected response. expected_data = { 'version': 5, @@ -411,7 +425,8 @@ def test_rpc_no_type(): } # Make dummy request. - response = make_request("/rpc/?v=5&arg=big-chungus") + with client as request: + response = request.get("/rpc/?v=5&arg=big-chungus") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -420,7 +435,7 @@ def test_rpc_no_type(): assert expected_data == response_data -def test_rpc_no_args(): +def test_rpc_no_args(client: TestClient): # Define expected response. expected_data = { 'version': 5, @@ -431,7 +446,8 @@ def test_rpc_no_args(): } # Make dummy request. - response = make_request("/rpc/?v=5&type=info") + with client as request: + response = request.get("/rpc/?v=5&type=info") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -440,9 +456,10 @@ def test_rpc_no_args(): assert expected_data == response_data -def test_rpc_no_maintainer(): +def test_rpc_no_maintainer(client: TestClient): # Make dummy request. - response = make_request("/rpc/?v=5&type=info&arg=woogly-chungus") + with client as request: + response = request.get("/rpc/?v=5&type=info&arg=woogly-chungus") # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -451,33 +468,39 @@ def test_rpc_no_maintainer(): assert response_data["results"][0]["Maintainer"] is None -def test_rpc_suggest_pkgbase(): - response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") +def test_rpc_suggest_pkgbase(client: TestClient): + with client as request: + response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") data = response.json() assert data == ["big-chungus"] - response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=chungy") + with client as request: + response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=chungy") data = response.json() assert data == ["chungy-chungus"] # Test no arg supplied. - response = make_request("/rpc?v=5&type=suggest-pkgbase") + with client as request: + response = request.get("/rpc?v=5&type=suggest-pkgbase") data = response.json() assert data == [] -def test_rpc_suggest(): - response = make_request("/rpc?v=5&type=suggest&arg=other") +def test_rpc_suggest(client: TestClient): + with client as request: + response = request.get("/rpc?v=5&type=suggest&arg=other") data = response.json() assert data == ["other-pkg"] # Test non-existent Package. - response = make_request("/rpc?v=5&type=suggest&arg=nonexistent") + with client as request: + response = request.get("/rpc?v=5&type=suggest&arg=nonexistent") data = response.json() assert data == [] # Test no arg supplied. - response = make_request("/rpc?v=5&type=suggest") + with client as request: + response = request.get("/rpc?v=5&type=suggest") data = response.json() assert data == [] @@ -491,14 +514,17 @@ def mock_config_getint(section: str, key: str): @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) -def test_rpc_ratelimit(getint: mock.MagicMock, pipeline: Pipeline): +def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, + pipeline: Pipeline): for i in range(4): # The first 4 requests should be good. - response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") + with client as request: + response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") assert response.status_code == int(HTTPStatus.OK) # The fifth request should be banned. - response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") + with client as request: + response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") assert response.status_code == int(HTTPStatus.TOO_MANY_REQUESTS) # Delete the cached records. @@ -508,26 +534,32 @@ def test_rpc_ratelimit(getint: mock.MagicMock, pipeline: Pipeline): assert one and two # The new first request should be good. - response = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") + with client as request: + response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") assert response.status_code == int(HTTPStatus.OK) -def test_rpc_etag(): - response1 = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") - response2 = make_request("/rpc?v=5&type=suggest-pkgbase&arg=big") +def test_rpc_etag(client: TestClient): + with client as request: + response1 = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") + + with client as request: + response2 = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") assert response1.headers.get("ETag") is not None assert response1.headers.get("ETag") != str() assert response1.headers.get("ETag") == response2.headers.get("ETag") -def test_rpc_search_arg_too_small(): - response = make_request("/rpc?v=5&type=search&arg=b") +def test_rpc_search_arg_too_small(client: TestClient): + with client as request: + response = request.get("/rpc?v=5&type=search&arg=b") assert response.status_code == int(HTTPStatus.OK) assert response.json().get("error") == "Query arg too small." -def test_rpc_search(): - response = make_request("/rpc?v=5&type=search&arg=big") +def test_rpc_search(client: TestClient): + with client as request: + response = request.get("/rpc?v=5&type=search&arg=big") assert response.status_code == int(HTTPStatus.OK) data = response.json() @@ -539,17 +571,18 @@ def test_rpc_search(): # Test the If-None-Match headers. etag = response.headers.get("ETag").strip('"') headers = {"If-None-Match": etag} - response = make_request("/rpc?v=5&type=search&arg=big", headers=headers) + response = request.get("/rpc?v=5&type=search&arg=big", headers=headers) assert response.status_code == int(HTTPStatus.NOT_MODIFIED) assert response.content == b'' # No args on non-m by types return an error. - response = make_request("/rpc?v=5&type=search") + response = request.get("/rpc?v=5&type=search") assert response.json().get("error") == "No request type/data specified." -def test_rpc_msearch(): - response = make_request("/rpc?v=5&type=msearch&arg=user1") +def test_rpc_msearch(client: TestClient): + with client as request: + response = request.get("/rpc?v=5&type=msearch&arg=user1") data = response.json() # user1 maintains 4 packages; assert that we got them all. @@ -564,73 +597,80 @@ def test_rpc_msearch(): assert names == expected_results # Search for a non-existent maintainer, giving us zero packages. - response = make_request("/rpc?v=5&type=msearch&arg=blah-blah") + response = request.get("/rpc?v=5&type=msearch&arg=blah-blah") data = response.json() assert data.get("resultcount") == 0 # A missing arg still succeeds, but it returns all orphans. # Just verify that we receive no error and the orphaned result. - response = make_request("/rpc?v=5&type=msearch") + response = request.get("/rpc?v=5&type=msearch") data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] assert result.get("Name") == "woogly-chungus" -def test_rpc_search_depends(): - response = make_request( - "/rpc?v=5&type=search&by=depends&arg=chungus-depends") +def test_rpc_search_depends(client: TestClient): + with client as request: + response = request.get( + "/rpc?v=5&type=search&by=depends&arg=chungus-depends") data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] assert result.get("Name") == "big-chungus" -def test_rpc_search_makedepends(): - response = make_request( - "/rpc?v=5&type=search&by=makedepends&arg=chungus-makedepends") +def test_rpc_search_makedepends(client: TestClient): + with client as request: + response = request.get( + "/rpc?v=5&type=search&by=makedepends&arg=chungus-makedepends") data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] assert result.get("Name") == "big-chungus" -def test_rpc_search_optdepends(): - response = make_request( - "/rpc?v=5&type=search&by=optdepends&arg=chungus-optdepends") +def test_rpc_search_optdepends(client: TestClient): + with client as request: + response = request.get( + "/rpc?v=5&type=search&by=optdepends&arg=chungus-optdepends") data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] assert result.get("Name") == "big-chungus" -def test_rpc_search_checkdepends(): - response = make_request( - "/rpc?v=5&type=search&by=checkdepends&arg=chungus-checkdepends") +def test_rpc_search_checkdepends(client: TestClient): + with client as request: + response = request.get( + "/rpc?v=5&type=search&by=checkdepends&arg=chungus-checkdepends") data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] assert result.get("Name") == "big-chungus" -def test_rpc_incorrect_by(): - response = make_request("/rpc?v=5&type=search&by=fake&arg=big") +def test_rpc_incorrect_by(client: TestClient): + with client as request: + response = request.get("/rpc?v=5&type=search&by=fake&arg=big") assert response.json().get("error") == "Incorrect by field specified." -def test_rpc_jsonp_callback(): +def test_rpc_jsonp_callback(client: TestClient): """ Test the callback parameter. For end-to-end verification, the `examples/jsonp.html` file can be used to submit jsonp callback requests to the RPC. """ - response = make_request( - "/rpc?v=5&type=search&arg=big&callback=jsonCallback") + with client as request: + response = request.get( + "/rpc?v=5&type=search&arg=big&callback=jsonCallback") assert response.headers.get("content-type") == "text/javascript" assert re.search(r'^/\*\*/jsonCallback\(.*\)$', response.text) is not None # Test an invalid callback name; we get an application/json error. - response = make_request( - "/rpc?v=5&type=search&arg=big&callback=jsonCallback!") + with client as request: + response = request.get( + "/rpc?v=5&type=search&arg=big&callback=jsonCallback!") assert response.headers.get("content-type") == "application/json" assert response.json().get("error") == "Invalid callback name." From 94972841d6c6330503cdf33d53336c1bd47f9469 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 04:36:39 -0800 Subject: [PATCH 0682/1451] change(fastapi): decouple error logic from process_account_form Signed-off-by: Kevin Morris --- aurweb/exceptions.py | 9 ++ aurweb/routers/accounts.py | 209 ++++++++--------------------------- aurweb/users/validate.py | 204 ++++++++++++++++++++++++++++++++++ test/test_accounts_routes.py | 25 ++++- 4 files changed, 278 insertions(+), 169 deletions(-) create mode 100644 aurweb/users/validate.py diff --git a/aurweb/exceptions.py b/aurweb/exceptions.py index 82628b0a..31212676 100644 --- a/aurweb/exceptions.py +++ b/aurweb/exceptions.py @@ -1,3 +1,6 @@ +from typing import Any + + class AurwebException(Exception): pass @@ -77,3 +80,9 @@ class InvalidArgumentsException(AurwebException): class RPCError(AurwebException): pass + + +class ValidationError(AurwebException): + def __init__(self, data: Any, *args, **kwargs): + super().__init__(*args, **kwargs) + self.data = data diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index aca322b5..47483acc 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -6,20 +6,20 @@ from http import HTTPStatus from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse -from sqlalchemy import and_, func, or_ +from sqlalchemy import and_, or_ import aurweb.config -from aurweb import cookies, db, l10n, logging, models, time, util +from aurweb import cookies, db, l10n, logging, models, util from aurweb.auth import account_type_required, auth_required -from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token +from aurweb.captcha import get_captcha_salts +from aurweb.exceptions import ValidationError from aurweb.l10n import get_translator_for_request -from aurweb.models import account_type -from aurweb.models.account_type import (DEVELOPER, DEVELOPER_ID, TRUSTED_USER, TRUSTED_USER_AND_DEV, TRUSTED_USER_AND_DEV_ID, - TRUSTED_USER_ID, USER_ID) +from aurweb.models import account_type as at from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification from aurweb.templates import make_context, make_variable_context, render_template +from aurweb.users import validate from aurweb.users.util import get_user_by_name router = APIRouter() @@ -126,146 +126,31 @@ def process_account_form(request: Request, user: models.User, args: dict): # Get a local translator. _ = get_translator_for_request(request) - host = request.client.host - ban = db.query(models.Ban, models.Ban.IPAddress == host).first() - if ban: - return (False, [ - "Account registration has been disabled for your " - "IP address, probably due to sustained spam attacks. " - "Sorry for the inconvenience." - ]) + checks = [ + validate.is_banned, + validate.invalid_user_password, + validate.invalid_fields, + validate.invalid_suspend_permission, + validate.invalid_username, + validate.invalid_password, + validate.invalid_email, + validate.invalid_backup_email, + validate.invalid_homepage, + validate.invalid_pgp_key, + validate.invalid_ssh_pubkey, + validate.invalid_language, + validate.invalid_timezone, + validate.username_in_use, + validate.email_in_use, + validate.invalid_account_type, + validate.invalid_captcha + ] - if request.user.is_authenticated(): - if not request.user.valid_password(args.get("passwd", None)): - return (False, ["Invalid password."]) - - email = args.get("E", None) - username = args.get("U", None) - - if not email or not username: - return (False, ["Missing a required field."]) - - inactive = args.get("J", False) - if not request.user.is_elevated() and inactive != bool(user.InactivityTS): - return (False, ["You do not have permission to suspend accounts."]) - - username_min_len = aurweb.config.getint("options", "username_min_len") - username_max_len = aurweb.config.getint("options", "username_max_len") - if not util.valid_username(args.get("U")): - return (False, [ - "The username is invalid.", - [ - _("It must be between %s and %s characters long") % ( - username_min_len, username_max_len), - "Start and end with a letter or number", - "Can contain only one period, underscore or hyphen.", - ] - ]) - - password = args.get("P", None) - if password: - confirmation = args.get("C", None) - if not util.valid_password(password): - return (False, [ - _("Your password must be at least %s characters.") % ( - username_min_len) - ]) - elif not confirmation: - return (False, ["Please confirm your new password."]) - elif password != confirmation: - return (False, ["Password fields do not match."]) - - backup_email = args.get("BE", None) - homepage = args.get("HP", None) - pgp_key = args.get("K", None) - ssh_pubkey = args.get("PK", None) - language = args.get("L", None) - timezone = args.get("TZ", None) - - def username_exists(username): - return and_(models.User.ID != user.ID, - func.lower(models.User.Username) == username.lower()) - - def email_exists(email): - return and_(models.User.ID != user.ID, - func.lower(models.User.Email) == email.lower()) - - if not util.valid_email(email): - return (False, ["The email address is invalid."]) - elif backup_email and not util.valid_email(backup_email): - return (False, ["The backup email address is invalid."]) - elif homepage and not util.valid_homepage(homepage): - return (False, [ - "The home page is invalid, please specify the full HTTP(s) URL."]) - elif pgp_key and not util.valid_pgp_fingerprint(pgp_key): - return (False, ["The PGP key fingerprint is invalid."]) - elif ssh_pubkey and not util.valid_ssh_pubkey(ssh_pubkey): - return (False, ["The SSH public key is invalid."]) - elif language and language not in l10n.SUPPORTED_LANGUAGES: - return (False, ["Language is not currently supported."]) - elif timezone and timezone not in time.SUPPORTED_TIMEZONES: - return (False, ["Timezone is not currently supported."]) - elif db.query(models.User, username_exists(username)).first(): - # If the username already exists... - return (False, [ - _("The username, %s%s%s, is already in use.") % ( - "", username, "") - ]) - elif db.query(models.User, email_exists(email)).first(): - # If the email already exists... - return (False, [ - _("The address, %s%s%s, is already in use.") % ( - "", email, "") - ]) - - def ssh_fingerprint_exists(fingerprint): - return and_(models.SSHPubKey.UserID != user.ID, - models.SSHPubKey.Fingerprint == fingerprint) - - if ssh_pubkey: - fingerprint = get_fingerprint(ssh_pubkey.strip().rstrip()) - if fingerprint is None: - return (False, ["The SSH public key is invalid."]) - - if db.query(models.SSHPubKey, - ssh_fingerprint_exists(fingerprint)).first(): - return (False, [ - _("The SSH public key, %s%s%s, is already in use.") % ( - "", fingerprint, "") - ]) - - T = int(args.get("T", user.AccountTypeID)) - if T != user.AccountTypeID: - if T not in account_type.ACCOUNT_TYPE_NAME: - return (False, - ["Invalid account type provided."]) - elif not request.user.is_elevated(): - return (False, - ["You do not have permission to change account types."]) - - credential_checks = { - DEVELOPER_ID: request.user.is_developer, - TRUSTED_USER_AND_DEV_ID: request.user.is_developer, - TRUSTED_USER_ID: request.user.is_elevated, - USER_ID: request.user.is_elevated - } - credential_check = credential_checks.get(T) - - if not credential_check(): - name = account_type.ACCOUNT_TYPE_NAME.get(T) - error = _("You do not have permission to change " - "this user's account type to %s.") % name - return (False, [error]) - - captcha_salt = args.get("captcha_salt", None) - if captcha_salt and captcha_salt not in get_captcha_salts(): - return (False, ["This CAPTCHA has expired. Please try again."]) - - captcha = args.get("captcha", None) - if captcha: - answer = get_captcha_answer(get_captcha_token(captcha_salt)) - if captcha != answer: - return (False, ["The entered CAPTCHA answer is invalid."]) + try: + for check in checks: + check(**args, request=request, user=user, _=_) + except ValidationError as exc: + return (False, exc.data) return (True, []) @@ -286,16 +171,16 @@ def make_account_form_context(context: dict, context = copy.copy(context) context["account_types"] = [ - (USER_ID, "Normal User"), - (TRUSTED_USER_ID, TRUSTED_USER) + (at.USER_ID, "Normal User"), + (at.TRUSTED_USER_ID, at.TRUSTED_USER) ] user_account_type_id = context.get("account_types")[0][0] if request.user.has_credential("CRED_ACCOUNT_EDIT_DEV"): - context["account_types"].append((DEVELOPER_ID, DEVELOPER)) - context["account_types"].append((TRUSTED_USER_AND_DEV_ID, - TRUSTED_USER_AND_DEV)) + context["account_types"].append((at.DEVELOPER_ID, at.DEVELOPER)) + context["account_types"].append((at.TRUSTED_USER_AND_DEV_ID, + at.TRUSTED_USER_AND_DEV)) if request.user.is_authenticated(): context["username"] = args.get("U", user.Username) @@ -389,12 +274,10 @@ async def account_register_post(request: Request, captcha: str = Form(default=None), captcha_salt: str = Form(...)): context = await make_variable_context(request, "Register") - args = dict(await request.form()) + context = make_account_form_context(context, request, None, args) - ok, errors = process_account_form(request, request.user, args) - if not ok: # If the field values given do not meet the requirements, # return HTTP 400 with an error. @@ -636,9 +519,9 @@ async def account_comments(request: Request, username: str): @router.get("/accounts") @auth_required(True, redirect="/accounts") -@account_type_required({account_type.TRUSTED_USER, - account_type.DEVELOPER, - account_type.TRUSTED_USER_AND_DEV}) +@account_type_required({at.TRUSTED_USER, + at.DEVELOPER, + at.TRUSTED_USER_AND_DEV}) async def accounts(request: Request): context = make_context(request, "Accounts") return render_template(request, "account/search.html", context) @@ -646,9 +529,9 @@ async def accounts(request: Request): @router.post("/accounts") @auth_required(True, redirect="/accounts") -@account_type_required({account_type.TRUSTED_USER, - account_type.DEVELOPER, - account_type.TRUSTED_USER_AND_DEV}) +@account_type_required({at.TRUSTED_USER, + at.DEVELOPER, + at.TRUSTED_USER_AND_DEV}) async def accounts_post(request: Request, O: int = Form(default=0), # Offset SB: str = Form(default=str()), # Sort By @@ -680,10 +563,10 @@ async def accounts_post(request: Request, # Convert parameter T to an AccountType ID. account_types = { - "u": account_type.USER_ID, - "t": account_type.TRUSTED_USER_ID, - "d": account_type.DEVELOPER_ID, - "td": account_type.TRUSTED_USER_AND_DEV_ID + "u": at.USER_ID, + "t": at.TRUSTED_USER_ID, + "d": at.DEVELOPER_ID, + "td": at.TRUSTED_USER_AND_DEV_ID } account_type_id = account_types.get(T, None) diff --git a/aurweb/users/validate.py b/aurweb/users/validate.py new file mode 100644 index 00000000..4959e316 --- /dev/null +++ b/aurweb/users/validate.py @@ -0,0 +1,204 @@ +""" +Validation functions for account registration and edit fields. +Each of these functions extracts a subset of keyword arguments +out of form data from /account/register or /account/{username}/edit. + +All functions in this module raise aurweb.exceptions.ValidationError +when encountering invalid criteria and return silently otherwise. +""" +from typing import List, Optional, Tuple + +from fastapi import Request +from sqlalchemy import and_ + +from aurweb import config, db, l10n, models, time, util +from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token +from aurweb.exceptions import ValidationError +from aurweb.models import account_type as at +from aurweb.models.account_type import ACCOUNT_TYPE_NAME +from aurweb.models.ssh_pub_key import get_fingerprint + + +def invalid_fields(E: str = str(), U: str = str(), **kwargs) \ + -> Optional[Tuple[bool, List[str]]]: + if not E or not U: + raise ValidationError(["Missing a required field."]) + + +def invalid_suspend_permission(request: Request = None, + user: models.User = None, + J: bool = False, + **kwargs) \ + -> Optional[Tuple[bool, List[str]]]: + if not request.user.is_elevated() and J != bool(user.InactivityTS): + raise ValidationError([ + "You do not have permission to suspend accounts."]) + + +def invalid_username(request: Request = None, U: str = str(), _=None, + **kwargs): + if not util.valid_username(U): + username_min_len = config.getint("options", "username_min_len") + username_max_len = config.getint("options", "username_max_len") + raise ValidationError([ + "The username is invalid.", + [ + _("It must be between %s and %s characters long") % ( + username_min_len, username_max_len), + "Start and end with a letter or number", + "Can contain only one period, underscore or hyphen.", + ] + ]) + + +def invalid_password(P: str = str(), C: str = str(), + _: l10n.Translator = None, **kwargs) -> None: + if P: + if not util.valid_password(P): + username_min_len = config.getint( + "options", "username_min_len") + raise ValidationError([ + _("Your password must be at least %s characters.") % ( + username_min_len) + ]) + elif not C: + raise ValidationError(["Please confirm your new password."]) + elif P != C: + raise ValidationError(["Password fields do not match."]) + + +def is_banned(request: Request = None, **kwargs) -> None: + host = request.client.host + exists = db.query(models.Ban, models.Ban.IPAddress == host).exists() + if db.query(exists).scalar(): + raise ValidationError([ + "Account registration has been disabled for your " + "IP address, probably due to sustained spam attacks. " + "Sorry for the inconvenience." + ]) + + +def invalid_user_password(request: Request = None, passwd: str = str(), + **kwargs) -> None: + if request.user.is_authenticated(): + if not request.user.valid_password(passwd): + raise ValidationError(["Invalid password."]) + + +def invalid_email(E: str = str(), **kwargs) -> None: + if not util.valid_email(E): + raise ValidationError(["The email address is invalid."]) + + +def invalid_backup_email(BE: str = str(), **kwargs) -> None: + if BE and not util.valid_email(BE): + raise ValidationError(["The backup email address is invalid."]) + + +def invalid_homepage(HP: str = str(), **kwargs) -> None: + if HP and not util.valid_homepage(HP): + raise ValidationError([ + "The home page is invalid, please specify the full HTTP(s) URL."]) + + +def invalid_pgp_key(K: str = str(), **kwargs) -> None: + if K and not util.valid_pgp_fingerprint(K): + raise ValidationError(["The PGP key fingerprint is invalid."]) + + +def invalid_ssh_pubkey(PK: str = str(), user: models.User = None, + _: l10n.Translator = None, **kwargs) -> None: + if PK: + invalid_exc = ValidationError(["The SSH public key is invalid."]) + if not util.valid_ssh_pubkey(PK): + raise invalid_exc + + fingerprint = get_fingerprint(PK.strip().rstrip()) + if not fingerprint: + raise invalid_exc + + exists = db.query(models.SSHPubKey).filter( + and_(models.SSHPubKey.UserID != user.ID, + models.SSHPubKey.Fingerprint == fingerprint) + ).exists() + if db.query(exists).scalar(): + raise ValidationError([ + _("The SSH public key, %s%s%s, is already in use.") % ( + "", fingerprint, "") + ]) + + +def invalid_language(L: str = str(), **kwargs) -> None: + if L and L not in l10n.SUPPORTED_LANGUAGES: + raise ValidationError(["Language is not currently supported."]) + + +def invalid_timezone(TZ: str = str(), **kwargs) -> None: + if TZ and TZ not in time.SUPPORTED_TIMEZONES: + raise ValidationError(["Timezone is not currently supported."]) + + +def username_in_use(U: str = str(), user: models.User = None, + _: l10n.Translator = None, **kwargs) -> None: + exists = db.query(models.User).filter( + and_(models.User.ID != user.ID, + models.User.Username == U) + ).exists() + if db.query(exists).scalar(): + # If the username already exists... + raise ValidationError([ + _("The username, %s%s%s, is already in use.") % ( + "", U, "") + ]) + + +def email_in_use(E: str = str(), user: models.User = None, + _: l10n.Translator = None, **kwargs) -> None: + exists = db.query(models.User).filter( + and_(models.User.ID != user.ID, + models.User.Email == E) + ).exists() + if db.query(exists).scalar(): + # If the email already exists... + raise ValidationError([ + _("The address, %s%s%s, is already in use.") % ( + "", E, "") + ]) + + +def invalid_account_type(T: int = None, request: Request = None, + user: models.User = None, + _: l10n.Translator = None, + **kwargs) -> None: + if T is not None and (T := int(T)) != user.AccountTypeID: + if T not in ACCOUNT_TYPE_NAME: + raise ValidationError(["Invalid account type provided."]) + elif not request.user.is_elevated(): + raise ValidationError([ + "You do not have permission to change account types."]) + + credential_checks = { + at.USER_ID: request.user.is_trusted_user, + at.TRUSTED_USER_ID: request.user.is_trusted_user, + at.DEVELOPER_ID: lambda: request.user.is_developer(), + at.TRUSTED_USER_AND_DEV_ID: (lambda: request.user.is_trusted_user() + and request.user.is_developer()) + } + credential_check = credential_checks.get(T) + + if not credential_check(): + name = ACCOUNT_TYPE_NAME.get(T) + error = _("You do not have permission to change " + "this user's account type to %s.") % name + raise ValidationError([error]) + + +def invalid_captcha(captcha_salt: str = None, captcha: str = None, **kwargs) \ + -> None: + if captcha_salt and captcha_salt not in get_captcha_salts(): + raise ValidationError(["This CAPTCHA has expired. Please try again."]) + + if captcha: + answer = get_captcha_answer(get_captcha_token(captcha_salt)) + if captcha != answer: + raise ValidationError(["The entered CAPTCHA answer is invalid."]) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index e828f70f..be929e97 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -1035,12 +1035,25 @@ def test_post_account_edit_account_types(): # Make sure it got changed to USER_ID as we intended. assert user.AccountTypeID == USER_ID - # Change user to a Developer. + # Change user to a TU & Dev, which can change themselves to a Developer. with db.begin(): - user.AccountTypeID = DEVELOPER_ID + user.AccountTypeID = TRUSTED_USER_AND_DEV_ID - # As a developer, we can absolutely change all account types. - # For example, from DEVELOPER_ID to TRUSTED_USER_AND_DEV_ID: + # As a TU & Dev, we can absolutely change all account types. + # For example, from TRUSTED_USER_AND_DEV_ID to DEVELOPER_ID: + post_data = { + "U": user.Username, + "E": user.Email, + "T": DEVELOPER_ID, + "passwd": "testPassword" + } + with client as request: + resp = request.post(endpoint, data=post_data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + assert user.AccountTypeID == DEVELOPER_ID + + # But we can't change a user to a Trusted User & Developer when + # we're just a Developer. post_data = { "U": user.Username, "E": user.Email, @@ -1049,8 +1062,8 @@ def test_post_account_edit_account_types(): } with client as request: resp = request.post(endpoint, data=post_data, cookies=cookies) - assert resp.status_code == int(HTTPStatus.OK) - assert user.AccountTypeID == TRUSTED_USER_AND_DEV_ID + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + assert user.AccountTypeID == DEVELOPER_ID def test_get_account(): From 303585cdbf50ffa70c7bc6f579c17d5e6bc08a42 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 05:40:11 -0800 Subject: [PATCH 0683/1451] change(fastapi): decouple update logic from account edit Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 85 ++++------------------------ aurweb/users/update.py | 110 +++++++++++++++++++++++++++++++++++++ aurweb/util.py | 7 +++ 3 files changed, 128 insertions(+), 74 deletions(-) create mode 100644 aurweb/users/update.py diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 47483acc..02a7f4c6 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -1,7 +1,6 @@ import copy import typing -from datetime import datetime from http import HTTPStatus from fastapi import APIRouter, Form, Request @@ -19,7 +18,7 @@ from aurweb.models import account_type as at from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification from aurweb.templates import make_context, make_variable_context, render_template -from aurweb.users import validate +from aurweb.users import update, validate from aurweb.users.util import get_user_by_name router = APIRouter() @@ -405,79 +404,17 @@ async def account_edit_post(request: Request, return render_template(request, "account/edit.html", context, status_code=HTTPStatus.BAD_REQUEST) - # Set all updated fields as needed. - with db.begin(): - user.Username = U or user.Username - user.Email = E or user.Email - user.HideEmail = bool(H) - user.BackupEmail = BE or user.BackupEmail - user.RealName = R or user.RealName - user.Homepage = HP or user.Homepage - user.IRCNick = I or user.IRCNick - user.PGPKey = K or user.PGPKey - user.Suspended = J - user.InactivityTS = int(datetime.utcnow().timestamp()) * int(J) + updates = [ + update.simple, + update.language, + update.timezone, + update.ssh_pubkey, + update.account_type, + update.password + ] - # If we update the language, update the cookie as well. - if L and L != user.LangPreference: - request.cookies["AURLANG"] = L - with db.begin(): - user.LangPreference = L - context["language"] = L - - # If we update the timezone, also update the cookie. - if TZ and TZ != user.Timezone: - with db.begin(): - user.Timezone = TZ - request.cookies["AURTZ"] = TZ - context["timezone"] = TZ - - with db.begin(): - user.CommentNotify = bool(CN) - user.UpdateNotify = bool(UN) - user.OwnershipNotify = bool(ON) - - # If a PK is given, compare it against the target user's PK. - with db.begin(): - if PK: - # Get the second token in the public key, which is the actual key. - pubkey = PK.strip().rstrip() - parts = pubkey.split(" ") - if len(parts) == 3: - # Remove the host part. - pubkey = parts[0] + " " + parts[1] - fingerprint = get_fingerprint(pubkey) - if not user.ssh_pub_key: - # No public key exists, create one. - user.ssh_pub_key = models.SSHPubKey(UserID=user.ID, - PubKey=pubkey, - Fingerprint=fingerprint) - elif user.ssh_pub_key.PubKey != pubkey: - # A public key already exists, update it. - user.ssh_pub_key.PubKey = pubkey - user.ssh_pub_key.Fingerprint = fingerprint - elif user.ssh_pub_key: - # Else, if the user has a public key already, delete it. - db.delete(user.ssh_pub_key) - - if T and T != user.AccountTypeID: - with db.begin(): - user.AccountTypeID = T - - if P and not user.valid_password(P): - # Remove the fields we consumed for passwords. - context["P"] = context["C"] = str() - - # If a password was given and it doesn't match the user's, update it. - with db.begin(): - user.update_password(P) - - if user == request.user: - remember_me = request.cookies.get("AURREMEMBER", False) - - # If the target user is the request user, login with - # the updated password to update the Session record. - user.login(request, P, cookies.timeout(remember_me)) + for f in updates: + f(**args, request=request, user=user, context=context) if not errors: context["complete"] = True diff --git a/aurweb/users/update.py b/aurweb/users/update.py new file mode 100644 index 00000000..60a6184e --- /dev/null +++ b/aurweb/users/update.py @@ -0,0 +1,110 @@ +from datetime import datetime +from typing import Any, Dict + +from fastapi import Request + +from aurweb import cookies, db, models +from aurweb.models.ssh_pub_key import get_fingerprint +from aurweb.util import strtobool + + +def simple(U: str = str(), E: str = str(), H: bool = False, + BE: str = str(), R: str = str(), HP: str = str(), + I: str = str(), K: str = str(), J: bool = False, + CN: bool = False, UN: bool = False, ON: bool = False, + user: models.User = None, + **kwargs) -> None: + now = int(datetime.utcnow().timestamp()) + with db.begin(): + user.Username = U or user.Username + user.Email = E or user.Email + user.HideEmail = strtobool(H) + user.BackupEmail = BE or user.BackupEmail + user.RealName = R or user.RealName + user.Homepage = HP or user.Homepage + user.IRCNick = I or user.IRCNick + user.PGPKey = K or user.PGPKey + user.Suspended = strtobool(J) + user.InactivityTS = now * int(strtobool(J)) + user.CommentNotify = strtobool(CN) + user.UpdateNotify = strtobool(UN) + user.OwnershipNotify = strtobool(ON) + + +def language(L: str = str(), + request: Request = None, + user: models.User = None, + context: Dict[str, Any] = {}, + **kwargs) -> None: + if L and L != user.LangPreference: + with db.begin(): + user.LangPreference = L + context["language"] = L + + +def timezone(TZ: str = str(), + request: Request = None, + user: models.User = None, + context: Dict[str, Any] = {}, + **kwargs) -> None: + if TZ and TZ != user.Timezone: + with db.begin(): + user.Timezone = TZ + context["language"] = TZ + + +def ssh_pubkey(PK: str = str(), + user: models.User = None, + **kwargs) -> None: + # If a PK is given, compare it against the target user's PK. + if PK: + # Get the second token in the public key, which is the actual key. + pubkey = PK.strip().rstrip() + parts = pubkey.split(" ") + if len(parts) == 3: + # Remove the host part. + pubkey = parts[0] + " " + parts[1] + fingerprint = get_fingerprint(pubkey) + if not user.ssh_pub_key: + # No public key exists, create one. + with db.begin(): + db.create(models.SSHPubKey, UserID=user.ID, + PubKey=pubkey, Fingerprint=fingerprint) + elif user.ssh_pub_key.PubKey != pubkey: + # A public key already exists, update it. + with db.begin(): + user.ssh_pub_key.PubKey = pubkey + user.ssh_pub_key.Fingerprint = fingerprint + elif user.ssh_pub_key: + # Else, if the user has a public key already, delete it. + with db.begin(): + db.delete(user.ssh_pub_key) + + +def account_type(T: int = None, + user: models.User = None, + **kwargs) -> None: + if T is not None and (T := int(T)) != user.AccountTypeID: + with db.begin(): + user.AccountTypeID = T + + +def password(P: str = str(), + request: Request = None, + user: models.User = None, + context: Dict[str, Any] = {}, + **kwargs) -> None: + if P and not user.valid_password(P): + # Remove the fields we consumed for passwords. + context["P"] = context["C"] = str() + + # If a password was given and it doesn't match the user's, update it. + with db.begin(): + user.update_password(P) + + if user == request.user: + remember_me = request.cookies.get("AURREMEMBER", False) + + # If the target user is the request user, login with + # the updated password to update the Session record. + user.login(request, P, cookies.timeout(remember_me)) diff --git a/aurweb/util.py b/aurweb/util.py index 1c2042fa..b95fc6a3 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -7,6 +7,7 @@ import secrets import string from datetime import datetime +from distutils.util import strtobool as _strtobool from typing import Any, Callable, Dict, Iterable, Tuple from urllib.parse import urlencode, urlparse from zoneinfo import ZoneInfo @@ -170,3 +171,9 @@ def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: per_page = defaults.PP return (offset, per_page) + + +def strtobool(value: str) -> bool: + if isinstance(value, str): + return _strtobool(value) + return value From 2892d21ff173af8f484274b1b175447be2ae4bab Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 05:47:22 -0800 Subject: [PATCH 0684/1451] remove global aurweb.models flake8 F401 ignore Signed-off-by: Kevin Morris --- aurweb/models/__init__.py | 60 +++++++++++++++++++-------------------- setup.cfg | 1 - 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/aurweb/models/__init__.py b/aurweb/models/__init__.py index b430acd2..a06077ad 100644 --- a/aurweb/models/__init__.py +++ b/aurweb/models/__init__.py @@ -1,31 +1,31 @@ """ Collection of all aurweb SQLAlchemy declarative models. """ -from .accepted_term import AcceptedTerm -from .account_type import AccountType -from .api_rate_limit import ApiRateLimit -from .ban import Ban -from .dependency_type import DependencyType -from .group import Group -from .license import License -from .official_provider import OfficialProvider -from .package import Package -from .package_base import PackageBase -from .package_blacklist import PackageBlacklist -from .package_comaintainer import PackageComaintainer -from .package_comment import PackageComment -from .package_dependency import PackageDependency -from .package_group import PackageGroup -from .package_keyword import PackageKeyword -from .package_license import PackageLicense -from .package_notification import PackageNotification -from .package_relation import PackageRelation -from .package_request import PackageRequest -from .package_source import PackageSource -from .package_vote import PackageVote -from .relation_type import RelationType -from .request_type import RequestType -from .session import Session -from .ssh_pub_key import SSHPubKey -from .term import Term -from .tu_vote import TUVote -from .tu_voteinfo import TUVoteInfo -from .user import User +from .accepted_term import AcceptedTerm # noqa: F401 +from .account_type import AccountType # noqa: F401 +from .api_rate_limit import ApiRateLimit # noqa: F401 +from .ban import Ban # noqa: F401 +from .dependency_type import DependencyType # noqa: F401 +from .group import Group # noqa: F401 +from .license import License # noqa: F401 +from .official_provider import OfficialProvider # noqa: F401 +from .package import Package # noqa: F401 +from .package_base import PackageBase # noqa: F401 +from .package_blacklist import PackageBlacklist # noqa: F401 +from .package_comaintainer import PackageComaintainer # noqa: F401 +from .package_comment import PackageComment # noqa: F401 +from .package_dependency import PackageDependency # noqa: F401 +from .package_group import PackageGroup # noqa: F401 +from .package_keyword import PackageKeyword # noqa: F401 +from .package_license import PackageLicense # noqa: F401 +from .package_notification import PackageNotification # noqa: F401 +from .package_relation import PackageRelation # noqa: F401 +from .package_request import PackageRequest # noqa: F401 +from .package_source import PackageSource # noqa: F401 +from .package_vote import PackageVote # noqa: F401 +from .relation_type import RelationType # noqa: F401 +from .request_type import RequestType # noqa: F401 +from .session import Session # noqa: F401 +from .ssh_pub_key import SSHPubKey # noqa: F401 +from .term import Term # noqa: F401 +from .tu_vote import TUVote # noqa: F401 +from .tu_voteinfo import TUVoteInfo # noqa: F401 +from .user import User # noqa: F401 diff --git a/setup.cfg b/setup.cfg index 7c64a01f..cec1bcf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,6 @@ per-file-ignores = aurweb/routers/accounts.py:C901 test/test_ssh_pub_key.py:E501 aurweb/routers/packages.py:E741 - aurweb/models/__init__.py:F401 [isort] line_length = 127 From 2df7187514a64d0e0ff88130562a9fc95c0f6611 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 05:48:43 -0800 Subject: [PATCH 0685/1451] fix global test_ssh_pub_key E501 flake8 violation Signed-off-by: Kevin Morris --- setup.cfg | 1 - test/test_ssh_pub_key.py | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index cec1bcf5..997bf4b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,6 @@ ignore = E741, W503, W504 # per-file-ignores = aurweb/routers/accounts.py:C901 - test/test_ssh_pub_key.py:E501 aurweb/routers/packages.py:E741 [isort] diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index bb787759..e17af5a7 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -6,7 +6,14 @@ from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.user import User TEST_SSH_PUBKEY = """ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCycoCi5yGCvSclH2wmNBUuwsYEzRZZBJaQquRc4ysl+Tg+/jiDkR3Zn9fIznC4KnFoyrIHzkKuePZ3bNDYwkZxkJKoWBCh4hXKDXSm87FMN0+VDC+1QxF/z0XaAGr/P6f4XukabyddypBdnHcZiplbw+YOSqcAE2TCqOlSXwNMOcF9U89UsR/Q9i9I52hlvU0q8+fZVGhou1KCowFSnHYtrr5KYJ04CXkJ13DkVf3+pjQWyrByvBcf1hGEaczlgfobrrv/y96jDhgfXucxliNKLdufDPPkii3LhhsNcDmmI1VZ3v0irKvd9WZuauqloobY84zEFcDTyjn0hxGjVeYFejm4fBnvjga0yZXORuWksdNfXWLDxFk6MDDd1jF0ExRbP+OxDuU4IVyIuDL7S3cnbf2YjGhkms/8voYT2OBE7FwNlfv98Kr0NUp51zpf55Arxn9j0Rz9xTA7FiODQgCn6iQ0SDtzUNL0IKTCw26xJY5gzMxbfpvzPQGeulx/ioM= kevr@volcano +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCycoCi5yGCvSclH2wmNBUuwsYEzRZZBJaQquRc4y\ +sl+Tg+/jiDkR3Zn9fIznC4KnFoyrIHzkKuePZ3bNDYwkZxkJKoWBCh4hXKDXSm87FMN0+VDC+1QxF/\ +z0XaAGr/P6f4XukabyddypBdnHcZiplbw+YOSqcAE2TCqOlSXwNMOcF9U89UsR/Q9i9I52hlvU0q8+\ +fZVGhou1KCowFSnHYtrr5KYJ04CXkJ13DkVf3+pjQWyrByvBcf1hGEaczlgfobrrv/y96jDhgfXucx\ +liNKLdufDPPkii3LhhsNcDmmI1VZ3v0irKvd9WZuauqloobY84zEFcDTyjn0hxGjVeYFejm4fBnvjg\ +a0yZXORuWksdNfXWLDxFk6MDDd1jF0ExRbP+OxDuU4IVyIuDL7S3cnbf2YjGhkms/8voYT2OBE7FwN\ +lfv98Kr0NUp51zpf55Arxn9j0Rz9xTA7FiODQgCn6iQ0SDtzUNL0IKTCw26xJY5gzMxbfpvzPQGeul\ +x/ioM= kevr@volcano """ user = ssh_pub_key = None From 672af707ad34a014b1119a9104db4050b706678b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 05:49:08 -0800 Subject: [PATCH 0686/1451] remove C901 and E741 per-file-ignores exclusion We no longer have C901 violations and we're already ignoring E741 (short variable names) in the overall `ignore` option. Signed-off-by: Kevin Morris --- setup.cfg | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/setup.cfg b/setup.cfg index 997bf4b7..08be9186 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,19 +19,6 @@ max-complexity = 10 # do this, so we're ignoring it here. ignore = E741, W503, W504 -# aurweb/routers/accounts.py -# Ignore over-reaching complexity. -# TODO: This should actually be addressed so we do not ignore C901. -# -# test/test_ssh_pub_key.py -# E501 is detected due to our >127 width test constant. Ignore it. -# Due to this, line width should _always_ be looked at in code reviews. -# Anything like this should be questioned. -# -per-file-ignores = - aurweb/routers/accounts.py:C901 - aurweb/routers/packages.py:E741 - [isort] line_length = 127 lines_between_types = 1 From dbe5cb4a33066c90333c32aab077081fec3ba426 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Nov 2021 16:42:26 -0800 Subject: [PATCH 0687/1451] fix(fastapi): only include comment-edit.js where needed Closes: #178 Signed-off-by: Kevin Morris --- templates/account/comments.html | 3 +++ templates/packages/show.html | 3 +++ templates/partials/head.html | 3 --- templates/pkgbase.html | 3 +++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/templates/account/comments.html b/templates/account/comments.html index 95585180..8dff53e4 100644 --- a/templates/account/comments.html +++ b/templates/account/comments.html @@ -17,6 +17,9 @@
    + + + {% for comment in comments %} {% include "partials/account/comment.html" %} {% endfor %} diff --git a/templates/packages/show.html b/templates/packages/show.html index fbc9c0ea..25083020 100644 --- a/templates/packages/show.html +++ b/templates/packages/show.html @@ -15,6 +15,9 @@
    + + + {% set pkgname = package.Name %} {% set pkgbase_id = pkgbase.ID %} {% include "partials/packages/comments.html" %} diff --git a/templates/partials/head.html b/templates/partials/head.html index 21c79887..8bfde020 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -15,8 +15,5 @@ - - - AUR ({{ language }}) - {{ title | tr }} diff --git a/templates/pkgbase.html b/templates/pkgbase.html index 315cdf67..cdf23c35 100644 --- a/templates/pkgbase.html +++ b/templates/pkgbase.html @@ -14,6 +14,9 @@
    + + + {% set pkgname = result.Name %} {% set pkgbase_id = result.ID %} {% set comments = comments %} From 7739b2178ec01828666daf271e8451852af22d2e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Nov 2021 16:43:10 -0800 Subject: [PATCH 0688/1451] fix(fastapi): fix comment edit image sources These were using the old comment image sources. Slipped in due to cache and not checking without cache. Fixed them to use src="/static/images/...". Signed-off-by: Kevin Morris --- templates/partials/comment_actions.html | 10 +++++----- web/html/js/comment-edit.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/partials/comment_actions.html b/templates/partials/comment_actions.html index b21b90bd..b8ccf945 100644 --- a/templates/partials/comment_actions.html +++ b/templates/partials/comment_actions.html @@ -12,7 +12,7 @@ value="{{ request.url.path }}" /> - {{ 'Edit comment' | tr }} @@ -57,7 +57,7 @@ diff --git a/web/html/js/comment-edit.js b/web/html/js/comment-edit.js index 4898c8d4..23ffdd34 100644 --- a/web/html/js/comment-edit.js +++ b/web/html/js/comment-edit.js @@ -1,6 +1,6 @@ function add_busy_indicator(sibling) { const img = document.createElement('img'); - img.src = "/images/ajax-loader.gif"; + img.src = "/static/images/ajax-loader.gif"; img.classList.add('ajax-loader'); img.style.height = 11; img.style.width = 16; From a348cdaac3a36c53ed4e9355f977cf63c4052801 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Nov 2021 16:44:13 -0800 Subject: [PATCH 0689/1451] housekeep(fastapi): cleanup unneeded jinja set statement Signed-off-by: Kevin Morris --- templates/pkgbase.html | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/pkgbase.html b/templates/pkgbase.html index cdf23c35..05583494 100644 --- a/templates/pkgbase.html +++ b/templates/pkgbase.html @@ -19,6 +19,5 @@ {% set pkgname = result.Name %} {% set pkgbase_id = result.ID %} - {% set comments = comments %} {% include "partials/packages/comments.html" %} {% endblock %} From 7f981b9ed7d0ce90d23d4051d743accd0228b515 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Nov 2021 21:15:57 -0800 Subject: [PATCH 0690/1451] fix(fastapi): utilize auto_{orphan,deletion}_age Didn't get this in when the initial request port went down; here it is. Auto-accept orphan requests when the package has been out of date for longer than auto_orphan_age. Auto-accept deletion requests by the package's maintainer if the package has been uploaded within auto_deletion_age seconds ago. Signed-off-by: Kevin Morris --- aurweb/packages/validate.py | 34 +++++++++++++++++++ aurweb/routers/packages.py | 65 ++++++++++++++++++------------------ test/test_packages_routes.py | 43 +++++++++++++++++++++++- 3 files changed, 109 insertions(+), 33 deletions(-) create mode 100644 aurweb/packages/validate.py diff --git a/aurweb/packages/validate.py b/aurweb/packages/validate.py new file mode 100644 index 00000000..e730e98b --- /dev/null +++ b/aurweb/packages/validate.py @@ -0,0 +1,34 @@ +from typing import Any, Dict + +from aurweb import db, models +from aurweb.exceptions import ValidationError + + +def request(pkgbase: models.PackageBase, + type: str, comments: str, merge_into: str, + context: Dict[str, Any]) -> None: + if not comments: + raise ValidationError(["The comment field must not be empty."]) + + if type == "merge": + # Perform merge-related checks. + if not merge_into: + # TODO: This error needs to be translated. + raise ValidationError( + ['The "Merge into" field must not be empty.']) + + target = db.query(models.PackageBase).filter( + models.PackageBase.Name == merge_into + ).first() + if not target: + # TODO: This error needs to be translated. + raise ValidationError([ + "The package base you want to merge into does not exist." + ]) + + db.refresh(target) + if target.ID == pkgbase.ID: + # TODO: This error needs to be translated. + raise ValidationError([ + "You cannot merge a package base into itself." + ]) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index c8ceb275..dfb8e108 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -11,9 +11,11 @@ import aurweb.packages.util from aurweb import db, defaults, l10n, logging, models, util from aurweb.auth import auth_required +from aurweb.exceptions import ValidationError from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID +from aurweb.packages import validate from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, get_pkgreq_by_id, query_notified, query_voted from aurweb.scripts import notify, popupdate @@ -153,7 +155,7 @@ def delete_package(deleter: models.User, package: models.Package): with db.begin(): pkgreq = create_request_if_missing( requests, reqtype, deleter, package) - db.refresh(pkgreq) + pkgreq.Status = ACCEPTED_ID bases_to_delete.append(package.PackageBase) @@ -707,35 +709,13 @@ async def pkgbase_request_post(request: Request, name: str, return render_template(request, "pkgbase/request.html", context, status_code=HTTPStatus.BAD_REQUEST) - if not comments: - context["errors"] = ["The comment field must not be empty."] + try: + validate.request(pkgbase, type, comments, merge_into, context) + except ValidationError as exc: + logger.error(f"Request Validation Error: {str(exc.data)}") + context["errors"] = exc.data return render_template(request, "pkgbase/request.html", context) - if type == "merge": - # Perform merge-related checks. - if not merge_into: - # TODO: This error needs to be translated. - context["errors"] = ['The "Merge into" field must not be empty.'] - return render_template(request, "pkgbase/request.html", context) - - target = db.query(models.PackageBase).filter( - models.PackageBase.Name == merge_into - ).first() - if not target: - # TODO: This error needs to be translated. - context["errors"] = [ - "The package base you want to merge into does not exist." - ] - return render_template(request, "pkgbase/request.html", context) - - db.refresh(target) - if target.ID == pkgbase.ID: - # TODO: This error needs to be translated. - context["errors"] = [ - "You cannot merge a package base into itself." - ] - return render_template(request, "pkgbase/request.html", context) - # All good. Create a new PackageRequest based on the given type. now = int(datetime.utcnow().timestamp()) reqtype = db.query(models.RequestType).filter( @@ -748,16 +728,37 @@ async def pkgbase_request_post(request: Request, name: str, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, MergeBaseName=merge_into, - Comments=comments, ClosureComment=str()) + Comments=comments, + ClosureComment=str()) - # Prepare notification object. conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - notify_ = notify.RequestOpenNotification( + # Prepare notification object. + notif = notify.RequestOpenNotification( conn, request.user.ID, pkgreq.ID, reqtype.Name, pkgreq.PackageBase.ID, merge_into=merge_into or None) # Send the notification now that we're out of the DB scope. - notify_.send() + notif.send() + + auto_orphan_age = aurweb.config.getint("options", "auto_orphan_age") + auto_delete_age = aurweb.config.getint("options", "auto_delete_age") + + flagged = pkgbase.OutOfDateTS and pkgbase.OutOfDateTS >= auto_orphan_age + is_maintainer = pkgbase.Maintainer == request.user + outdated = now - pkgbase.SubmittedTS <= auto_delete_age + + if type == "orphan" and flagged: + with db.begin(): + pkgbase.Maintainer = None + pkgreq.Status = ACCEPTED_ID + db.refresh(pkgreq) + notif = notify.RequestCloseNotification( + conn, request.user.ID, pkgreq.ID, pkgreq.status_display()) + notif.send() + elif type == "deletion" and is_maintainer and outdated: + packages = pkgbase.packages.all() + for package in packages: + delete_package(request.user, package) # Redirect the submitting user to /packages. return RedirectResponse("/packages", diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 02c22d9d..64ee38d0 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -10,7 +10,7 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy import and_ -from aurweb import asgi, db, defaults +from aurweb import asgi, config, db, defaults from aurweb.models import License, PackageLicense from aurweb.models.account_type import USER_ID, AccountType from aurweb.models.dependency_type import DependencyType @@ -1536,6 +1536,24 @@ def test_pkgbase_request_post_deletion(client: TestClient, user: User, assert pkgreq.Comments == "We want to delete this." +def test_pkgbase_request_post_maintainer_deletion( + client: TestClient, maintainer: User, package: Package): + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "deletion", + "comments": "We want to delete this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = db.query(PackageRequest).filter( + PackageRequest.PackageBaseName == pkgbasename + ).first() + assert pkgreq.Status == ACCEPTED_ID + + def test_pkgbase_request_post_orphan(client: TestClient, user: User, package: Package): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" @@ -1556,6 +1574,29 @@ def test_pkgbase_request_post_orphan(client: TestClient, user: User, assert pkgreq.Comments == "We want to disown this." +def test_pkgbase_request_post_auto_orphan(client: TestClient, user: User, + package: Package): + now = int(datetime.utcnow().timestamp()) + auto_orphan_age = config.getint("options", "auto_orphan_age") + with db.begin(): + package.PackageBase.OutOfDateTS = now - auto_orphan_age - 1 + + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, data={ + "type": "orphan", + "comments": "We want to disown this." + }, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = db.query(PackageRequest).filter( + PackageRequest.PackageBaseID == package.PackageBase.ID + ).first() + assert pkgreq is not None + assert pkgreq.Status == ACCEPTED_ID + + def test_pkgbase_request_post_merge(client: TestClient, user: User, package: Package): with db.begin(): From f897411ddf7849123a3df72ae841b27a81bd12bf Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Nov 2021 21:17:40 -0800 Subject: [PATCH 0691/1451] change(fastapi): let conftest bypass create database errors Signed-off-by: Kevin Morris --- test/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/conftest.py b/test/conftest.py index 47d9ca4b..aa44831a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -43,6 +43,7 @@ from filelock import FileLock from sqlalchemy import create_engine from sqlalchemy.engine import URL from sqlalchemy.engine.base import Engine +from sqlalchemy.exc import OperationalError from sqlalchemy.orm import scoped_session import aurweb.config @@ -98,7 +99,10 @@ def _create_database(engine: Engine, dbname: str) -> None: :param dbname: Database name to create """ conn = engine.connect() - conn.execute(f"CREATE DATABASE {dbname}") + try: + conn.execute(f"CREATE DATABASE {dbname}") + except OperationalError: # pragma: no cover + pass conn.close() initdb.run(AlembicArgs) From 008a8824ceb7f243f58c7d228f7cd9c1152d7386 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 13:19:34 -0800 Subject: [PATCH 0692/1451] housekeep(fastapi): simplify package_base_comaintainers_post Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 88 ++++++++++++++++++++++++++++++++++++-- aurweb/routers/packages.py | 73 ++----------------------------- 2 files changed, 88 insertions(+), 73 deletions(-) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 78f5bf18..7c48f4e4 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -4,13 +4,14 @@ from typing import Dict, List, Union import orjson -from fastapi import HTTPException +from fastapi import HTTPException, Request from sqlalchemy import and_, orm -from aurweb import db, models +from aurweb import db, l10n, models, util from aurweb.models.official_provider import OFFICIAL_BASE from aurweb.models.relation_type import PROVIDES_ID from aurweb.redis import redis_connection +from aurweb.scripts import notify from aurweb.templates import register_filter @@ -223,6 +224,85 @@ def query_notified(query: List[models.Package], ).filter( models.PackageNotification.UserID == user.ID ) - for notify in notified: - output[notify.PackageBase.ID] = True + for notif in notified: + output[notif.PackageBase.ID] = True return output + + +def remove_comaintainers(pkgbase: models.PackageBase, + usernames: List[str]) -> None: + """ + Remove comaintainers from `pkgbase`. + + :param pkgbase: PackageBase instance + :param usernames: Iterable of username strings + :return: None + """ + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + notifications = [] + with db.begin(): + for username in usernames: + # We know that the users we passed here are in the DB. + # No need to check for their existence. + comaintainer = pkgbase.comaintainers.join(models.User).filter( + models.User.Username == username + ).first() + notifications.append( + notify.ComaintainerRemoveNotification( + conn, comaintainer.User.ID, pkgbase.ID + ) + ) + db.delete(comaintainer) + + # Send out notifications if need be. + util.apply_all(notifications, lambda n: n.send()) + + +def add_comaintainers(request: Request, pkgbase: models.PackageBase, + priority: int, usernames: List[str]) -> None: + """ + Add comaintainers to `pkgbase`. + + :param request: FastAPI request + :param pkgbase: PackageBase instance + :param priority: Initial priority value + :param usernames: Iterable of username strings + :return: None on success, an error string on failure + """ + + # First, perform a check against all usernames given; for each + # username, add its related User object to memo. + _ = l10n.get_translator_for_request(request) + memo = {} + for username in usernames: + user = db.query(models.User).filter( + models.User.Username == username).first() + if not user: + return _("Invalid user name: %s") % username + memo[username] = user + + # Alright, now that we got past the check, add them all to the DB. + conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + notifications = [] + with db.begin(): + for username in usernames: + user = memo.get(username) + if pkgbase.Maintainer == user: + # Already a maintainer. Move along. + continue + + # If we get here, our user model object is in the memo. + comaintainer = db.create( + models.PackageComaintainer, + PackageBase=pkgbase, + User=user, + Priority=priority) + priority += 1 + + notifications.append( + notify.ComaintainerAddNotification( + conn, comaintainer.User.ID, pkgbase.ID) + ) + + # Send out notifications. + util.apply_all(notifications, lambda n: n.send()) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index dfb8e108..23f44ee3 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -15,6 +15,7 @@ from aurweb.exceptions import ValidationError from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID +from aurweb.packages import util as pkgutil from aurweb.packages import validate from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, get_pkgreq_by_id, query_notified, query_voted @@ -531,28 +532,6 @@ async def package_base_comaintainers(request: Request, name: str) -> Response: return render_template(request, "pkgbase/comaintainers.html", context) -def remove_users(pkgbase, usernames): - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - notifications = [] - with db.begin(): - for username in usernames: - # We know that the users we passed here are in the DB. - # No need to check for their existence. - comaintainer = pkgbase.comaintainers.join(models.User).filter( - models.User.Username == username - ).first() - notifications.append( - notify.ComaintainerRemoveNotification( - conn, comaintainer.User.ID, pkgbase.ID - ) - ) - db.delete(comaintainer) - - # Send out notifications if need be. - for notify_ in notifications: - notify_.send() - - @router.post("/pkgbase/{name}/comaintainers") @auth_required(True, redirect="/pkgbase/{name}/comaintainers") async def package_base_comaintainers_post( @@ -573,7 +552,7 @@ async def package_base_comaintainers_post( users.remove(str()) # Remove any empty strings from the set. records = {c.User.Username for c in pkgbase.comaintainers} - remove_users(pkgbase, records.difference(users)) + pkgutil.remove_comaintainers(pkgbase, records.difference(users)) # Default priority (lowest value; most preferred). priority = 1 @@ -590,52 +569,8 @@ async def package_base_comaintainers_post( if last_priority: priority = last_priority.Priority + 1 - def add_users(usernames): - """ Add users as comaintainers to pkgbase. - - :param usernames: An iterable of username strings - :return: None on success, an error string on failure. """ - nonlocal request, pkgbase, priority - - # First, perform a check against all usernames given; for each - # username, add its related User object to memo. - _ = l10n.get_translator_for_request(request) - memo = {} - for username in usernames: - user = db.query(models.User).filter( - models.User.Username == username).first() - if not user: - return _("Invalid user name: %s") % username - memo[username] = user - - # Alright, now that we got past the check, add them all to the DB. - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - notifications = [] - with db.begin(): - for username in usernames: - user = memo.get(username) - if pkgbase.Maintainer == user: - # Already a maintainer. Move along. - continue - - # If we get here, our user model object is in the memo. - comaintainer = db.create( - models.PackageComaintainer, - PackageBase=pkgbase, - User=user, - Priority=priority) - priority += 1 - - notifications.append( - notify.ComaintainerAddNotification( - conn, comaintainer.User.ID, pkgbase.ID) - ) - - # Send out notifications. - for notify_ in notifications: - notify_.send() - - error = add_users(users.difference(records)) + error = pkgutil.add_comaintainers(request, pkgbase, priority, + users.difference(records)) if error: context = make_context(request, "Manage Co-maintainers") context["pkgbase"] = pkgbase From 0b5d08801615c3afa604d15d0f5db4f03d600b3d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 13:18:48 -0800 Subject: [PATCH 0693/1451] fix(fastapi): catch ProgrammingError instead of OperationalError in conftest Signed-off-by: Kevin Morris --- test/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index aa44831a..db2e5997 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -43,7 +43,7 @@ from filelock import FileLock from sqlalchemy import create_engine from sqlalchemy.engine import URL from sqlalchemy.engine.base import Engine -from sqlalchemy.exc import OperationalError +from sqlalchemy.exc import ProgrammingError from sqlalchemy.orm import scoped_session import aurweb.config @@ -101,7 +101,7 @@ def _create_database(engine: Engine, dbname: str) -> None: conn = engine.connect() try: conn.execute(f"CREATE DATABASE {dbname}") - except OperationalError: # pragma: no cover + except ProgrammingError: # pragma: no cover pass conn.close() initdb.run(AlembicArgs) From 191198ca41a10358804a5d1cdc37a35ca9be7bd6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 13:31:09 -0800 Subject: [PATCH 0694/1451] housekeep(fastapi): simplify aurweb.spawn.stop() Signed-off-by: Kevin Morris --- aurweb/spawn.py | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 6d553dde..568a8a1d 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -18,6 +18,8 @@ import tempfile import time import urllib +from typing import Iterable, List + import aurweb.config import aurweb.schema @@ -127,17 +129,15 @@ def start(): spawn_child(["nginx", "-p", temporary_dir, "-c", generate_nginx_config()]) -def stop(): +def _kill_children(children: Iterable, exceptions: List[Exception] = []) \ + -> List[Exception]: """ - Stop all the child processes. + Kill each process found in `children`. - If an exception occurs during the process, the process continues anyway - because we don’t want to leave runaway processes around, and all the - exceptions are finally raised as a single ProcessExceptions. + :param children: Iterable of child processes + :param exceptions: Exception memo + :return: `exceptions` """ - global children - atexit.unregister(stop) - exceptions = [] for p in children: try: p.terminate() @@ -145,6 +145,18 @@ def stop(): print(f":: Sent SIGTERM to {p.args}", file=sys.stderr) except Exception as e: exceptions.append(e) + return exceptions + + +def _wait_for_children(children: Iterable, exceptions: List[Exception] = []) \ + -> List[Exception]: + """ + Wait for each process to end found in `children`. + + :param children: Iterable of child processes + :param exceptions: Exception memo + :return: `exceptions` + """ for p in children: try: rc = p.wait() @@ -154,6 +166,24 @@ def stop(): raise Exception(f"Process {p.args} exited with {rc}") except Exception as e: exceptions.append(e) + return exceptions + + +def stop() -> None: + """ + Stop all the child processes. + + If an exception occurs during the process, the process continues anyway + because we don’t want to leave runaway processes around, and all the + exceptions are finally raised as a single ProcessExceptions. + + :raises: ProcessException + :return: None + """ + global children + atexit.unregister(stop) + exceptions = _kill_children(children) + exceptions = _wait_for_children(children, exceptions) children = [] if exceptions: raise ProcessExceptions("Errors terminating the child processes:", From 82ca4ad9a0c399e25a2369509df005ba5a5c6860 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 15:33:19 -0800 Subject: [PATCH 0695/1451] feat: check php configuration in aurweb.spawn Signed-off-by: Kevin Morris --- aurweb/spawn.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 568a8a1d..ecb759a5 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -23,11 +23,16 @@ from typing import Iterable, List import aurweb.config import aurweb.schema +from aurweb.exceptions import AurwebException + children = [] temporary_dir = None verbosity = 0 asgi_backend = '' +PHP_BINARY = os.environ.get("PHP_BINARY", "php") +PHP_MODULES = ["pdo_mysql", "pdo_sqlite"] + class ProcessExceptions(Exception): """ @@ -42,6 +47,35 @@ class ProcessExceptions(Exception): super().__init__("\n- ".join(messages)) +def validate_php_config() -> None: + """ + Perform a validation check against PHP_BINARY's configuration. + + AurwebException is raised here if checks fail to pass. We require + the 'pdo_mysql' and 'pdo_sqlite' modules to be enabled. + + :raises: AurwebException + :return: None + """ + try: + proc = subprocess.Popen([PHP_BINARY, "-m"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, _ = proc.communicate() + except FileNotFoundError: + raise AurwebException(f"Unable to locate the '{PHP_BINARY}' " + "executable.") + + assert proc.returncode == 0, ("Received non-zero error code " + f"{proc.returncode} from '{PHP_BINARY}'.") + + modules = out.decode().splitlines() + for module in PHP_MODULES: + if module not in modules: + raise AurwebException( + f"PHP does not have the '{module}' module enabled.") + + def generate_nginx_config(): """ Generate an nginx configuration based on aurweb's configuration. @@ -199,6 +233,13 @@ if __name__ == '__main__': parser.add_argument('-b', '--backend', choices=['hypercorn', 'uvicorn'], default='hypercorn', help='asgi backend used to launch the python server') args = parser.parse_args() + + try: + validate_php_config() + except AurwebException as exc: + print(f"error: {str(exc)}") + sys.exit(1) + verbosity = args.verbose asgi_backend = args.backend with tempfile.TemporaryDirectory(prefix="aurweb-") as tmpdirname: From 47d0df76e6f377a360903f48a96bb32e54884dfc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 15:37:47 -0800 Subject: [PATCH 0696/1451] feat: support gunicorn in aurweb.spawn This also comes with a -w|--workers argument that allows the caller to set the number of gunicorn workers. Signed-off-by: Kevin Morris --- aurweb/spawn.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index ecb759a5..5b4dbe94 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -29,6 +29,7 @@ children = [] temporary_dir = None verbosity = 0 asgi_backend = '' +workers = 1 PHP_BINARY = os.environ.get("PHP_BINARY", "php") PHP_MODULES = ["pdo_mysql", "pdo_sqlite"] @@ -152,12 +153,25 @@ def start(): spawn_child(["php", "-S", php_address, "-t", htmldir]) # FastAPI - host, port = aurweb.config.get("fastapi", "bind_address").rsplit(":", 1) - if asgi_backend == "hypercorn": - portargs = ["-b", f"{host}:{port}"] - elif asgi_backend == "uvicorn": - portargs = ["--host", host, "--port", port] - spawn_child(["python", "-m", asgi_backend] + portargs + ["aurweb.asgi:app"]) + fastapi_host, fastapi_port = aurweb.config.get( + "fastapi", "bind_address").rsplit(":", 1) + + # Logging config. + aurwebdir = aurweb.config.get("options", "aurwebdir") + fastapi_log_config = os.path.join(aurwebdir, "logging.conf") + + backend_args = { + "hypercorn": ["-b", f"{fastapi_host}:{fastapi_port}"], + "uvicorn": ["--host", fastapi_host, "--port", fastapi_port], + "gunicorn": ["--bind", f"{fastapi_host}:{fastapi_port}", + "-k", "uvicorn.workers.UvicornWorker", + "-w", str(workers)] + } + backend_args = backend_args.get(asgi_backend) + spawn_child([ + "python", "-m", asgi_backend, + "--log-config", fastapi_log_config, + ] + backend_args + ["aurweb.asgi:app"]) # nginx spawn_child(["nginx", "-p", temporary_dir, "-c", generate_nginx_config()]) @@ -230,8 +244,11 @@ if __name__ == '__main__': description='Start aurweb\'s test server.') parser.add_argument('-v', '--verbose', action='count', default=0, help='increase verbosity') - parser.add_argument('-b', '--backend', choices=['hypercorn', 'uvicorn'], default='hypercorn', + choices = ['hypercorn', 'gunicorn', 'uvicorn'] + parser.add_argument('-b', '--backend', choices=choices, default='uvicorn', help='asgi backend used to launch the python server') + parser.add_argument("-w", "--workers", default=1, type=int, + help="number of workers to use in gunicorn") args = parser.parse_args() try: @@ -242,6 +259,7 @@ if __name__ == '__main__': verbosity = args.verbose asgi_backend = args.backend + workers = args.workers with tempfile.TemporaryDirectory(prefix="aurweb-") as tmpdirname: temporary_dir = tmpdirname start() From 19191fa8b56c47d256b2c8992853d96f82cabc5a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 15:38:20 -0800 Subject: [PATCH 0697/1451] fix: update nginx config in aurweb.spawn Host a specific FastAPI nginx frontend as well as a PHP nginx frontend, configurable by the (PHP|FASTAPI)_NGINX_PORT environment variables. Signed-off-by: Kevin Morris --- aurweb/spawn.py | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 5b4dbe94..46f2f021 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -16,7 +16,6 @@ import subprocess import sys import tempfile import time -import urllib from typing import Iterable, List @@ -33,6 +32,8 @@ workers = 1 PHP_BINARY = os.environ.get("PHP_BINARY", "php") PHP_MODULES = ["pdo_mysql", "pdo_sqlite"] +PHP_NGINX_PORT = int(os.environ.get("PHP_NGINX_PORT", 8001)) +FASTAPI_NGINX_PORT = int(os.environ.get("FASTAPI_NGINX_PORT", 8002)) class ProcessExceptions(Exception): @@ -83,8 +84,10 @@ def generate_nginx_config(): The file is generated under `temporary_dir`. Returns the path to the created configuration file. """ - aur_location = aurweb.config.get("options", "aur_location") - aur_location_parts = urllib.parse.urlsplit(aur_location) + php_bind = aurweb.config.get("php", "bind_address") + php_host = php_bind.split(":")[0] + fastapi_bind = aurweb.config.get("fastapi", "bind_address") + fastapi_host = fastapi_bind.split(":")[0] config_path = os.path.join(temporary_dir, "nginx.conf") config = open(config_path, "w") # We double nginx's braces because they conflict with Python's f-strings. @@ -101,12 +104,23 @@ def generate_nginx_config(): uwsgi_temp_path {os.path.join(temporary_dir, "uwsgi")}; scgi_temp_path {os.path.join(temporary_dir, "scgi")}; server {{ - listen {aur_location_parts.netloc}; + listen {php_host}:{PHP_NGINX_PORT}; location / {{ - proxy_pass http://{aurweb.config.get("php", "bind_address")}; + proxy_pass http://{php_bind}; }} - location /sso {{ - proxy_pass http://{aurweb.config.get("fastapi", "bind_address")}; + }} + server {{ + listen {fastapi_host}:{FASTAPI_NGINX_PORT}; + location / {{ + try_files $uri @proxy_to_app; + }} + location @proxy_to_app {{ + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_buffering off; + proxy_pass http://{fastapi_bind}; }} }} }} @@ -149,6 +163,7 @@ def start(): # PHP php_address = aurweb.config.get("php", "bind_address") + php_host = php_address.split(":")[0] htmldir = aurweb.config.get("php", "htmldir") spawn_child(["php", "-S", php_address, "-t", htmldir]) @@ -176,6 +191,18 @@ def start(): # nginx spawn_child(["nginx", "-p", temporary_dir, "-c", generate_nginx_config()]) + print(f""" + > Started nginx. + > + > PHP backend: http://{php_address} + > FastAPI backend: http://{fastapi_host}:{fastapi_port} + > + > PHP frontend: http://{php_host}:{PHP_NGINX_PORT} + > FastAPI frontend: http://{fastapi_host}:{FASTAPI_NGINX_PORT} + > + > Frontends are hosted via nginx and should be preferred. +""") + def _kill_children(children: Iterable, exceptions: List[Exception] = []) \ -> List[Exception]: From 233d25b1c3434221871a7ccb04a7897c3213c33d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 15:39:15 -0800 Subject: [PATCH 0698/1451] feat: add test_spawn, an aurweb.spawn test Signed-off-by: Kevin Morris --- test/test_spawn.py | 149 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 test/test_spawn.py diff --git a/test/test_spawn.py b/test/test_spawn.py new file mode 100644 index 00000000..195eb897 --- /dev/null +++ b/test/test_spawn.py @@ -0,0 +1,149 @@ +import os +import tempfile + +from typing import Tuple +from unittest import mock + +import pytest + +import aurweb.config +import aurweb.spawn + +from aurweb.exceptions import AurwebException + +# Some os.environ overrides we use in this suite. +TEST_ENVIRONMENT = { + "PHP_NGINX_PORT": "8001", + "FASTAPI_NGINX_PORT": "8002" +} + + +class FakeProcess: + """ Fake a subprocess.Popen return object. """ + + returncode = 0 + stdout = b'' + stderr = b'' + + def __init__(self, *args, **kwargs): + """ We need this constructor to remain compatible with Popen. """ + pass + + def communicate(self) -> Tuple[bytes, bytes]: + return (self.stdout, self.stderr) + + def terminate(self) -> None: + raise Exception("Fake termination.") + + def wait(self) -> int: + return self.returncode + + +class MockFakeProcess: + """ FakeProcess construction helper to be used in mocks. """ + + def __init__(self, return_code: int = 0, stdout: bytes = b'', + stderr: bytes = b''): + self.returncode = return_code + self.stdout = stdout + self.stderr = stderr + + def process(self, *args, **kwargs) -> FakeProcess: + proc = FakeProcess() + proc.returncode = self.returncode + proc.stdout = self.stdout + proc.stderr = self.stderr + return proc + + +@mock.patch("aurweb.spawn.PHP_BINARY", "does-not-exist") +def test_spawn(): + match = r"^Unable to locate the '.*' executable\.$" + with pytest.raises(AurwebException, match=match): + aurweb.spawn.validate_php_config() + + +@mock.patch("subprocess.Popen", side_effect=MockFakeProcess(1).process) +def test_spawn_non_zero_php_binary(fake_process: FakeProcess): + match = r"^Received non-zero error code.*$" + with pytest.raises(AssertionError, match=match): + aurweb.spawn.validate_php_config() + + +def test_spawn_missing_modules(): + side_effect = MockFakeProcess(stdout=b"pdo_sqlite").process + with mock.patch("subprocess.Popen", side_effect=side_effect): + match = r"PHP does not have the 'pdo_mysql' module enabled\.$" + with pytest.raises(AurwebException, match=match): + aurweb.spawn.validate_php_config() + + side_effect = MockFakeProcess(stdout=b"pdo_mysql").process + with mock.patch("subprocess.Popen", side_effect=side_effect): + match = r"PHP does not have the 'pdo_sqlite' module enabled\.$" + with pytest.raises(AurwebException, match=match): + aurweb.spawn.validate_php_config() + + +@mock.patch.dict("os.environ", TEST_ENVIRONMENT) +def test_spawn_generate_nginx_config(): + ctx = tempfile.TemporaryDirectory() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + aurweb.spawn.generate_nginx_config() + nginx_config_path = os.path.join(ctx.name, "nginx.conf") + with open(nginx_config_path) as f: + nginx_config = f.read().rstrip() + + php_address = aurweb.config.get("php", "bind_address") + php_host = php_address.split(":")[0] + fastapi_address = aurweb.config.get("fastapi", "bind_address") + fastapi_host = fastapi_address.split(":")[0] + expected_content = [ + f'listen {php_host}:{TEST_ENVIRONMENT.get("PHP_NGINX_PORT")}', + f"proxy_pass http://{php_address}", + f'listen {fastapi_host}:{TEST_ENVIRONMENT.get("FASTAPI_NGINX_PORT")}', + f"proxy_pass http://{fastapi_address}" + ] + for expected in expected_content: + assert expected in nginx_config + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +def test_spawn_start_stop(): + ctx = tempfile.TemporaryDirectory() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + aurweb.spawn.start() + aurweb.spawn.stop() + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +@mock.patch("aurweb.spawn.children", [MockFakeProcess().process()]) +def test_spawn_start_noop_with_children(): + aurweb.spawn.start() + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +@mock.patch("aurweb.spawn.children", [MockFakeProcess().process()]) +def test_spawn_stop_terminate_failure(): + ctx = tempfile.TemporaryDirectory() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + match = r"^Errors terminating the child processes" + with pytest.raises(aurweb.spawn.ProcessExceptions, match=match): + aurweb.spawn.stop() + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +@mock.patch("aurweb.spawn.children", [MockFakeProcess(1).process()]) +def test_spawn_stop_wait_failure(): + ctx = tempfile.TemporaryDirectory() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + match = r"^Errors terminating the child processes" + with pytest.raises(aurweb.spawn.ProcessExceptions, match=match): + aurweb.spawn.stop() From ba3ef742ceec27a2667d579ef78eb0fc36f1a364 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 18:40:32 -0800 Subject: [PATCH 0699/1451] feat(docker): allow user-customizable ssh host keys There is a new ./data bind mount used here. If ssh_host_* keys are in ./data when the git service starts, they'll override the container-generated host keys. Signed-off-by: Kevin Morris --- docker-compose.aur-dev.yml | 1 + docker/git-entrypoint.sh | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index 1db306cc..484f353a 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -18,6 +18,7 @@ services: restart: always volumes: - ${GIT_DATA_DIR}:/aurweb/aur.git + - ./data:/aurweb/data - cache:/cache smartgit: diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index 296c1e47..4d15bcb9 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -60,6 +60,13 @@ sed -ri "s|^(ssh-cmdline) = .+|\1 = $ssh_cmdline|" $AUR_CONFIG_DEFAULTS # Setup SSH Keys. ssh-keygen -A +# In docker-compose.aur-dev.yml, we bind ./data to /aurweb/data. +# Production users wishing to include their own SSH keys should +# supply them in ./data. +if [ -d /aurweb/data ]; then + find /aurweb/data -type f -name 'ssh_host_*' -exec cp -vf "{}" /etc/ssh/ \; +fi + # Taken from INSTALL. mkdir -pv $GIT_REPO From a1e547c057da8a2391c94a6d51c7c04fe37ad71b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 19:03:35 -0800 Subject: [PATCH 0700/1451] feat(docker): allow configurable SSH_CMDLINE in git service Signed-off-by: Kevin Morris --- docker-compose.aur-dev.yml | 4 ++++ docker-compose.yml | 1 + docker/git-entrypoint.sh | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index 484f353a..62109deb 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -16,6 +16,10 @@ services: git: restart: always + environment: + - AUR_CONFIG=/aurweb/conf/config + # SSH_CMDLINE should be updated to production's ssh cmdline. + - SSH_CMDLINE=${SSH_CMDLINE:-ssh ssh://aur@localhost:2222} volumes: - ${GIT_DATA_DIR}:/aurweb/aur.git - ./data:/aurweb/data diff --git a/docker-compose.yml b/docker-compose.yml index c39d38bf..26b7d62c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,6 +100,7 @@ services: init: true environment: - AUR_CONFIG=/aurweb/conf/config + - SSH_CMDLINE=${SSH_CMDLINE:-ssh ssh://aur@localhost:2222} entrypoint: /docker/git-entrypoint.sh command: /docker/scripts/run-sshd.sh ports: diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index 4d15bcb9..cfa1879b 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -54,8 +54,8 @@ fi # Set some defaults needed for pathing and ssh uris. sed -ri "s|^(repo-path) = .+|\1 = /aurweb/aur.git/|" $AUR_CONFIG_DEFAULTS -ssh_cmdline='ssh ssh://aur@localhost:2222' -sed -ri "s|^(ssh-cmdline) = .+|\1 = $ssh_cmdline|" $AUR_CONFIG_DEFAULTS +# SSH_CMDLINE can be provided via override in docker-compose.aur-dev.yml. +sed -ri "s|^(ssh-cmdline) = .+$|\1 = ${SSH_CMDLINE}|" $AUR_CONFIG_DEFAULTS # Setup SSH Keys. ssh-keygen -A From c7feecd4b83fde3aa0e1a1e392035be8fb32385e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 19:34:33 -0800 Subject: [PATCH 0701/1451] housekeep(docker): remove configuration regexes in the nginx service Signed-off-by: Kevin Morris --- docker/nginx-entrypoint.sh | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docker/nginx-entrypoint.sh b/docker/nginx-entrypoint.sh index 63307948..a58e67b7 100755 --- a/docker/nginx-entrypoint.sh +++ b/docker/nginx-entrypoint.sh @@ -11,17 +11,6 @@ KEY=/cache/production.key.pem DEST_CERT=/etc/ssl/certs/web.cert.pem DEST_KEY=/etc/ssl/private/web.key.pem -# Setup a config for our mysql db. -cp -vf conf/config.dev conf/config -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config -sed -ri 's/^(host) = .+/\1 = mariadb/' conf/config -sed -ri 's/^(user) = .+/\1 = aur/' conf/config -sed -ri 's/^;?(password) = .+/\1 = aur/' conf/config - -# Setup http(s) stuff. -sed -ri "s|^(aur_location) = .+|\1 = https://localhost:8444|" conf/config -sed -ri 's/^(disable_http_login) = .+/\1 = 1/' conf/config - if [ -f "$CERT" ]; then cp -vf "$CERT" "$DEST_CERT" cp -vf "$KEY" "$DEST_KEY" From 604901fe7475912705e040ab40a509c80d109289 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 20:00:53 -0800 Subject: [PATCH 0702/1451] fix(docker): fix nginx .gz match against cgit snapshots This only deals with .gz files in the root of the request_uri and now more. That is: /packages.gz goes through the nginx regex, but now /cgit/.../snapshot/package.tar.gz is served by the cgit block. Signed-off-by: Kevin Morris --- docker/config/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index c3ffd7fa..e51bd64f 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -94,7 +94,7 @@ http { ssl_certificate /etc/ssl/certs/web.cert.pem; ssl_certificate_key /etc/ssl/private/web.key.pem; - location ~ ^/.*\.gz$ { + location ~ ^/[^\/]+\.gz$ { # Override mime type to text/plain. types { text/plain gz; } default_type text/plain; From d4d9f50b8f540062a3191e254a16f2e4497b7b8f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 20 Nov 2021 20:05:04 -0800 Subject: [PATCH 0703/1451] change(docker): use ./data instead of ./cache For the `git` service, ./data is always used to provide an optional overriding of ssh host keys. In aur-dev production containers, most services which use the data mount use an internal Docker `data` volume instead. Signed-off-by: Kevin Morris --- docker-compose.aur-dev.yml | 13 ++++++------ docker-compose.override.yml | 12 +++++------ docker-compose.yml | 6 +++--- docker/ca-entrypoint.sh | 38 +++++++++++++++++------------------ docker/cgit-entrypoint.sh | 2 +- docker/nginx-entrypoint.sh | 8 ++++---- docker/scripts/run-fastapi.sh | 12 +++++------ docker/scripts/run-nginx.sh | 2 +- docker/scripts/run-pytests.sh | 10 ++++----- docker/scripts/run-tests.sh | 10 ++++----- 10 files changed, 56 insertions(+), 57 deletions(-) diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index 62109deb..ab4ff124 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -3,7 +3,7 @@ version: "3.8" services: ca: volumes: - - cache:/cache + - data:/data memcached: restart: always @@ -23,13 +23,12 @@ services: volumes: - ${GIT_DATA_DIR}:/aurweb/aur.git - ./data:/aurweb/data - - cache:/cache smartgit: restart: always volumes: - ${GIT_DATA_DIR}:/aurweb/aur.git - - cache:/cache + - data:/data - smartgit_run:/var/run/smartgit cgit-php: @@ -48,7 +47,7 @@ services: - AURWEB_PHP_PREFIX=${AURWEB_PHP_PREFIX} - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} volumes: - - cache:/cache + - data:/data fastapi: restart: always @@ -60,13 +59,13 @@ services: - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus volumes: - - cache:/cache + - data:/data nginx: restart: always volumes: - ${GIT_DATA_DIR}:/aurweb/aur.git - - cache:/cache + - data:/data - logs:/var/log/nginx - smartgit_run:/var/run/smartgit @@ -75,5 +74,5 @@ volumes: mariadb_data: {} # Share /var/lib/mysql git_data: {} # Share aurweb/aur.git smartgit_run: {} - cache: {} + data: {} logs: {} diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 7349ac66..eae12a92 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -8,17 +8,17 @@ services: ca: volumes: - - ./cache:/cache + - ./data:/data git: volumes: - git_data:/aurweb/aur.git - - ./cache:/cache + - ./data:/aurweb/data smartgit: volumes: - git_data:/aurweb/aur.git - - ./cache:/cache + - ./data:/data - smartgit_run:/var/run/smartgit depends_on: mariadb: @@ -26,7 +26,7 @@ services: php-fpm: volumes: - - ./cache:/cache + - ./data:/data - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test @@ -37,7 +37,7 @@ services: fastapi: volumes: - - ./cache:/cache + - ./data:/data - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test @@ -49,7 +49,7 @@ services: nginx: volumes: - git_data:/aurweb/aur.git - - ./cache:/cache + - ./data:/data - ./logs:/var/log/nginx - ./web/html:/aurweb/web/html - ./web/template:/aurweb/web/template diff --git a/docker-compose.yml b/docker-compose.yml index 26b7d62c..e3bfacdc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -276,7 +276,7 @@ services: mariadb_test: condition: service_healthy volumes: - - ./cache:/cache + - ./data:/data - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test @@ -304,7 +304,7 @@ services: - /tmp volumes: - mariadb_test_run:/var/run/mysqld - - ./cache:/cache + - ./data:/data - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test @@ -330,7 +330,7 @@ services: condition: service_healthy volumes: - mariadb_test_run:/var/run/mysqld - - ./cache:/cache + - ./data:/data - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test diff --git a/docker/ca-entrypoint.sh b/docker/ca-entrypoint.sh index e95d267c..42d8bd14 100755 --- a/docker/ca-entrypoint.sh +++ b/docker/ca-entrypoint.sh @@ -1,58 +1,58 @@ #!/bin/bash set -eou pipefail -if [ -f /cache/ca.root.pem ]; then +if [ -f /data/ca.root.pem ]; then echo "Already have certs, skipping." exit 0 fi # Generate a new 2048-bit RSA key for the Root CA. -openssl genrsa -des3 -out /cache/ca.key -passout pass:devca 2048 +openssl genrsa -des3 -out /data/ca.key -passout pass:devca 2048 # Request and self-sign a new Root CA certificate, using # the RSA key. Output Root CA PEM-format certificate and key: -# /cache/ca.root.pem and /cache/ca.key.pem +# /data/ca.root.pem and /data/ca.key.pem openssl req -x509 -new -nodes -sha256 -days 1825 \ -passin pass:devca \ -subj "/C=US/ST=California/L=Authority/O=aurweb/CN=localhost" \ - -in /cache/ca.key -out /cache/ca.root.pem -keyout /cache/ca.key.pem + -in /data/ca.key -out /data/ca.root.pem -keyout /data/ca.key.pem # Generate a new 2048-bit RSA key for a localhost server. -openssl genrsa -out /cache/localhost.key 2048 +openssl genrsa -out /data/localhost.key 2048 # Generate a Certificate Signing Request (CSR) for the localhost server # using the RSA key we generated above. -openssl req -new -key /cache/localhost.key -passout pass:devca \ +openssl req -new -key /data/localhost.key -passout pass:devca \ -subj "/C=US/ST=California/L=Server/O=aurweb/CN=localhost" \ - -out /cache/localhost.csr + -out /data/localhost.csr # Get our CSR signed by our Root CA PEM-formatted certificate and key -# to produce a fresh /cache/localhost.cert.pem PEM-formatted certificate. -openssl x509 -req -in /cache/localhost.csr \ - -CA /cache/ca.root.pem -CAkey /cache/ca.key.pem \ +# to produce a fresh /data/localhost.cert.pem PEM-formatted certificate. +openssl x509 -req -in /data/localhost.csr \ + -CA /data/ca.root.pem -CAkey /data/ca.key.pem \ -CAcreateserial \ - -out /cache/localhost.cert.pem \ + -out /data/localhost.cert.pem \ -days 825 -sha256 \ -passin pass:devca \ -extfile /docker/localhost.ext -# Convert RSA key to a PEM-formatted key: /cache/localhost.key.pem -openssl rsa -in /cache/localhost.key -text > /cache/localhost.key.pem +# Convert RSA key to a PEM-formatted key: /data/localhost.key.pem +openssl rsa -in /data/localhost.key -text > /data/localhost.key.pem # At the end here, our notable certificates and keys are: -# - /cache/ca.root.pem -# - /cache/ca.key.pem -# - /cache/localhost.key.pem -# - /cache/localhost.cert.pem +# - /data/ca.root.pem +# - /data/ca.key.pem +# - /data/localhost.key.pem +# - /data/localhost.cert.pem # # When running a server which uses the localhost certificate, a chain # should be used, starting with localhost.cert.pem: -# - cat /cache/localhost.cert.pem /cache/ca.root.pem > localhost.chain.pem +# - cat /data/localhost.cert.pem /data/ca.root.pem > localhost.chain.pem # # The Root CA (ca.root.pem) should be imported into browsers or # ca-certificates on machines wishing to verify localhost. # -chmod 666 /cache/* +chmod 666 /data/* exec "$@" diff --git a/docker/cgit-entrypoint.sh b/docker/cgit-entrypoint.sh index f9ca86c0..a44675e2 100755 --- a/docker/cgit-entrypoint.sh +++ b/docker/cgit-entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eou pipefail -mkdir -p /var/cache/cgit +mkdir -p /var/data/cgit cp -vf conf/cgitrc.proto /etc/cgitrc sed -ri "s|clone-prefix=.*|clone-prefix=${CGIT_CLONE_PREFIX}|" /etc/cgitrc diff --git a/docker/nginx-entrypoint.sh b/docker/nginx-entrypoint.sh index a58e67b7..6b9a6954 100755 --- a/docker/nginx-entrypoint.sh +++ b/docker/nginx-entrypoint.sh @@ -5,8 +5,8 @@ set -eou pipefail # user customization of the certificates that FastAPI uses. # Otherwise, fallback to localhost.{cert,key}.pem, generated by `ca`. -CERT=/cache/production.cert.pem -KEY=/cache/production.key.pem +CERT=/data/production.cert.pem +KEY=/data/production.key.pem DEST_CERT=/etc/ssl/certs/web.cert.pem DEST_KEY=/etc/ssl/private/web.key.pem @@ -15,8 +15,8 @@ if [ -f "$CERT" ]; then cp -vf "$CERT" "$DEST_CERT" cp -vf "$KEY" "$DEST_KEY" else - cat /cache/localhost.cert.pem /cache/ca.root.pem > "$DEST_CERT" - cp -vf /cache/localhost.key.pem "$DEST_KEY" + cat /data/localhost.cert.pem /data/ca.root.pem > "$DEST_CERT" + cp -vf /data/localhost.key.pem "$DEST_KEY" fi cp -vf /docker/config/nginx.conf /etc/nginx/nginx.conf diff --git a/docker/scripts/run-fastapi.sh b/docker/scripts/run-fastapi.sh index effc7fe4..ac54aedc 100755 --- a/docker/scripts/run-fastapi.sh +++ b/docker/scripts/run-fastapi.sh @@ -1,15 +1,15 @@ #!/bin/bash -CERT=/cache/localhost.cert.pem -KEY=/cache/localhost.key.pem +CERT=/data/localhost.cert.pem +KEY=/data/localhost.key.pem # If production.{cert,key}.pem exists, prefer them. This allows # user customization of the certificates that FastAPI uses. -if [ -f /cache/production.cert.pem ]; then - CERT=/cache/production.cert.pem +if [ -f /data/production.cert.pem ]; then + CERT=/data/production.cert.pem fi -if [ -f /cache/production.key.pem ]; then - KEY=/cache/production.key.pem +if [ -f /data/production.key.pem ]; then + KEY=/data/production.key.pem fi # By default, set FASTAPI_WORKERS to 2. In production, this should diff --git a/docker/scripts/run-nginx.sh b/docker/scripts/run-nginx.sh index 7780dae8..6ece3303 100755 --- a/docker/scripts/run-nginx.sh +++ b/docker/scripts/run-nginx.sh @@ -8,7 +8,7 @@ echo " (cgit) : https://localhost:8444/cgit/" echo " - PHP : https://localhost:8443/" echo " (cgit) : https://localhost:8443/cgit/" echo -echo " Note: Copy root CA (./cache/ca.root.pem) to ca-certificates or browser." +echo " Note: Copy root CA (./data/ca.root.pem) to ca-certificates or browser." echo echo " Thanks for using aurweb!" echo diff --git a/docker/scripts/run-pytests.sh b/docker/scripts/run-pytests.sh index b8f695df..2eadee42 100755 --- a/docker/scripts/run-pytests.sh +++ b/docker/scripts/run-pytests.sh @@ -32,10 +32,10 @@ pytest if [ $COVERAGE -eq 1 ]; then make -C test coverage - # /cache is mounted as a volume. Copy coverage into it. + # /data is mounted as a volume. Copy coverage into it. # Users can then sanitize the coverage locally in their - # aurweb root directory: ./util/fix-coverage ./cache/.coverage - rm -f /cache/.coverage - cp -v .coverage /cache/.coverage - chmod 666 /cache/.coverage + # aurweb root directory: ./util/fix-coverage ./data/.coverage + rm -f /data/.coverage + cp -v .coverage /data/.coverage + chmod 666 /data/.coverage fi diff --git a/docker/scripts/run-tests.sh b/docker/scripts/run-tests.sh index 45c7835f..a726c957 100755 --- a/docker/scripts/run-tests.sh +++ b/docker/scripts/run-tests.sh @@ -14,12 +14,12 @@ bash $dir/run-pytests.sh --no-coverage make -C test coverage -# /cache is mounted as a volume. Copy coverage into it. +# /data is mounted as a volume. Copy coverage into it. # Users can then sanitize the coverage locally in their -# aurweb root directory: ./util/fix-coverage ./cache/.coverage -rm -f /cache/.coverage -cp -v .coverage /cache/.coverage -chmod 666 /cache/.coverage +# aurweb root directory: ./util/fix-coverage ./data/.coverage +rm -f /data/.coverage +cp -v .coverage /data/.coverage +chmod 666 /data/.coverage # Run flake8 and isort checks. for dir in aurweb test migrations; do From e8f4c9cf69161076a2cc71fcab060f664b327045 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 21 Nov 2021 00:51:05 -0800 Subject: [PATCH 0704/1451] fix(fastapi): remove aurweb logger definition Both the root and aurweb loggers are included in output, causing repeated log messages. Now, just rely on the root logger for aurweb logging. Signed-off-by: Kevin Morris --- logging.conf | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/logging.conf b/logging.conf index ba41fb7b..310ac76e 100644 --- a/logging.conf +++ b/logging.conf @@ -1,5 +1,5 @@ [loggers] -keys=root,aurweb,test,uvicorn,hypercorn,alembic +keys=root,test,uvicorn,hypercorn,alembic [handlers] keys=simpleHandler,detailedHandler @@ -9,13 +9,7 @@ keys=simpleFormatter,detailedFormatter [logger_root] level=INFO -handlers=simpleHandler - -[logger_aurweb] -level=DEBUG handlers=detailedHandler -qualname=aurweb -propagate=1 [logger_test] level=DEBUG @@ -43,13 +37,13 @@ propagate=0 [handler_simpleHandler] class=StreamHandler -level=DEBUG +level=INFO formatter=simpleFormatter args=(sys.stdout,) [handler_detailedHandler] class=StreamHandler -level=DEBUG +level=INFO formatter=detailedFormatter args=(sys.stdout,) From bc7bf9866ad1f7b3e0ccc9c7b00ffac5d6f72524 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 21 Nov 2021 00:48:53 -0800 Subject: [PATCH 0705/1451] docker: bind ./aurweb in cron service by default Signed-off-by: Kevin Morris --- docker-compose.aur-dev.yml | 6 ++++++ docker-compose.yml | 1 + 2 files changed, 7 insertions(+) diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index ab4ff124..4b522e56 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -41,6 +41,12 @@ services: volumes: - ${GIT_DATA_DIR}:/aurweb/aur.git + cron: + volumes: + # Exclude ./aurweb:/aurweb in production. + - mariadb_run:/var/run/mysqld + - archives:/var/lib/aurweb/archives + php-fpm: restart: always environment: diff --git a/docker-compose.yml b/docker-compose.yml index e3bfacdc..ea0e8d1b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -176,6 +176,7 @@ services: mariadb_init: condition: service_started volumes: + - ./aurweb:/aurweb/aurweb - mariadb_run:/var/run/mysqld - archives:/var/lib/aurweb/archives From 41e0eaaece5df78b4f9abbb17c4ff702e854ab8a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 21 Nov 2021 21:43:14 -0800 Subject: [PATCH 0706/1451] fix(docker): force bind ports to localhost only Signed-off-by: Kevin Morris --- docker-compose.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ea0e8d1b..5ff031e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,7 +48,7 @@ services: test: "bash /docker/health/redis.sh" interval: 3s ports: - - "16379:6379" + - "127.0.0.1:16379:6379" mariadb: image: aurweb:latest @@ -58,7 +58,7 @@ services: ports: # This will expose mariadbd on 127.0.0.1:13306 in the host. # Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb` - - "13306:3306" + - "127.0.0.1:13306:3306" volumes: - mariadb_run:/var/run/mysqld # Bind socket in this volume. - mariadb_data:/var/lib/mysql @@ -88,7 +88,7 @@ services: ports: # This will expose mariadbd on 127.0.0.1:13307 in the host. # Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb` - - "13307:3306" + - "127.0.0.1:13307:3306" volumes: - mariadb_test_run:/var/run/mysqld # Bind socket in this volume. healthcheck: @@ -104,7 +104,7 @@ services: entrypoint: /docker/git-entrypoint.sh command: /docker/scripts/run-sshd.sh ports: - - "2222:2222" + - "127.0.0.1:2222:2222" healthcheck: test: "bash /docker/health/sshd.sh" interval: 3s @@ -141,7 +141,7 @@ services: git: condition: service_healthy ports: - - "13000:3000" + - "127.0.0.1:13000:3000" volumes: - git_data:/aurweb/aur.git @@ -161,7 +161,7 @@ services: git: condition: service_healthy ports: - - "13001:3000" + - "127.0.0.1:13001:3000" volumes: - git_data:/aurweb/aur.git @@ -205,7 +205,7 @@ services: - mariadb_run:/var/run/mysqld - archives:/var/lib/aurweb/archives ports: - - "19000:9000" + - "127.0.0.1:19000:9000" fastapi: image: aurweb:latest @@ -234,7 +234,7 @@ services: volumes: - mariadb_run:/var/run/mysqld ports: - - "18000:8000" + - "127.0.0.1:18000:8000" nginx: image: aurweb:latest @@ -244,8 +244,8 @@ services: entrypoint: /docker/nginx-entrypoint.sh command: /docker/scripts/run-nginx.sh ports: - - "8443:8443" # PHP - - "8444:8444" # FastAPI + - "127.0.0.1:8443:8443" # PHP + - "127.0.0.1:8444:8444" # FastAPI healthcheck: test: "bash /docker/health/nginx.sh" interval: 3s From 34747359ba599d2dda02a404d47a720b7363d367 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 21 Nov 2021 23:11:02 -0800 Subject: [PATCH 0707/1451] fix(docker): expose git service's 2222 through 0.0.0.0 Other ports we use are locked to 127.0.0.1. The `git` service, however, already promotes security in its sshd service and can't really be abused from an external source. This simplifies the need to forward to localhost if deploy targets want the sshd to be available. Signed-off-by: Kevin Morris --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5ff031e4..acb5dd65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -104,7 +104,7 @@ services: entrypoint: /docker/git-entrypoint.sh command: /docker/scripts/run-sshd.sh ports: - - "127.0.0.1:2222:2222" + - "2222:2222" healthcheck: test: "bash /docker/health/sshd.sh" interval: 3s From e891d7c8e86b344af705580c9049d59570bed6f3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 10:18:02 -0800 Subject: [PATCH 0708/1451] change(docker): allow run-pytests to collect coverage Additionally fix up the argument parsing to be a bit less flexible. Signed-off-by: Kevin Morris --- docker/scripts/run-pytests.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docker/scripts/run-pytests.sh b/docker/scripts/run-pytests.sh index 2eadee42..d8c093d5 100755 --- a/docker/scripts/run-pytests.sh +++ b/docker/scripts/run-pytests.sh @@ -1,5 +1,4 @@ #!/bin/bash -set -eou pipefail COVERAGE=1 PARAMS=() @@ -11,13 +10,13 @@ while [ $# -ne 0 ]; do COVERAGE=0 shift ;; - -*) - echo "usage: $0 [--no-coverage] targets ..." - exit 1 + clean) + rm -f .coverage + shift ;; *) - PARAMS+=("$key") - shift + echo "usage: $0 [--no-coverage] targets ..." + exit 1 ;; esac done @@ -30,7 +29,7 @@ pytest # By default, report coverage and move it into cache. if [ $COVERAGE -eq 1 ]; then - make -C test coverage + make -C test coverage || /bin/true # /data is mounted as a volume. Copy coverage into it. # Users can then sanitize the coverage locally in their From 39fd3b891e4c3f86dad74ea8b66b516abdcf45e7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 21 Nov 2021 01:41:10 -0800 Subject: [PATCH 0709/1451] change: set -v for sh tests Signed-off-by: Kevin Morris --- test/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Makefile b/test/Makefile index 4a8207f8..a6abc9de 100644 --- a/test/Makefile +++ b/test/Makefile @@ -26,6 +26,6 @@ clean: rm -f ../.coverage $(T): - @echo "*** $@ ***"; $(SHELL) $@ + @echo "*** $@ ***"; $(SHELL) $@ -v .PHONY: check coverage $(FOREIGN_TARGETS) clean $(T) From 3b686c475d605309abc94d1655369ec4cf44deed Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 07:34:35 -0800 Subject: [PATCH 0710/1451] fix: default detailed loglevel to DEBUG Signed-off-by: Kevin Morris --- logging.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logging.conf b/logging.conf index 310ac76e..3b96e827 100644 --- a/logging.conf +++ b/logging.conf @@ -43,7 +43,7 @@ args=(sys.stdout,) [handler_detailedHandler] class=StreamHandler -level=INFO +level=DEBUG formatter=detailedFormatter args=(sys.stdout,) From 47d83244bbd415b769b151dc64de18c9b62568b9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 22:21:45 -0800 Subject: [PATCH 0711/1451] change(gitlab-ci): add 'fast-single-thread' tag to the test stage Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 739c9408..8980fa78 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,4 @@ image: archlinux:base-devel - cache: key: system-v1 paths: @@ -13,6 +12,8 @@ variables: test: stage: test + tags: + - fast-single-thread before_script: - export PATH="$HOME/.poetry/bin:${PATH}" - ./docker/scripts/install-deps.sh From 6bb002e70889777024384529f37907f595894bf2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 24 Nov 2021 21:23:01 -0800 Subject: [PATCH 0712/1451] fix: use correct u2f ssh key prefixes Signed-off-by: Kevin Morris --- conf/config.defaults | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conf/config.defaults b/conf/config.defaults index a04f21bc..dd9bfd2f 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -62,7 +62,9 @@ ECDSA = SHA256:L71Q91yHwmHPYYkJMDgj0xmUuw16qFOhJbBr1mzsiOI RSA = SHA256:Ju+yWiMb/2O+gKQ9RJCDqvRg7l+Q95KFAeqM5sr6l2s [auth] -valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ssh-ecdsa@openssh.com sk-ssh-ed25519@openssh.com +; For U2F key prefixes, see the following documentation from openssh: +; https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f +valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com username-regex = [a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$ git-serve-cmd = /usr/local/bin/aurweb-git-serve ssh-options = restrict From 1aab9604010e9b58c7bed8586931841751bbab68 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 24 Nov 2021 21:28:29 -0800 Subject: [PATCH 0713/1451] fix: use corrent u2f ssh key prefixes Signed-off-by: Kevin Morris --- conf/config.defaults | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conf/config.defaults b/conf/config.defaults index 68e235be..a589997b 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -72,7 +72,9 @@ ECDSA = SHA256:L71Q91yHwmHPYYkJMDgj0xmUuw16qFOhJbBr1mzsiOI RSA = SHA256:Ju+yWiMb/2O+gKQ9RJCDqvRg7l+Q95KFAeqM5sr6l2s [auth] -valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ssh-ecdsa@openssh.com sk-ssh-ed25519@openssh.com +; For U2F key prefixes, see the following documentation from openssh: +; https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f +valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com username-regex = [a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$ git-serve-cmd = /usr/bin/aurweb-git-serve ssh-options = restrict From e558e979ff481148bb903ca21c7659b7ca43208d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 24 Nov 2021 21:28:49 -0800 Subject: [PATCH 0714/1451] fix(fastapi): check ssh key prefixes against configured valid-keytypes Signed-off-by: Kevin Morris --- aurweb/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aurweb/util.py b/aurweb/util.py index b95fc6a3..62575c71 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -84,9 +84,8 @@ def valid_pgp_fingerprint(fp): def valid_ssh_pubkey(pk): - valid_prefixes = ("ssh-rsa", "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", - "ssh-ed25519") + valid_prefixes = aurweb.config.get("auth", "valid-keytypes") + valid_prefixes = set(valid_prefixes.split(" ")) has_valid_prefix = False for prefix in valid_prefixes: From b98159d5b90fe0fd609a694257bb25a4fa579b0e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 16:43:29 -0800 Subject: [PATCH 0715/1451] change(docker): use step-ca for CA + cert generation Signed-off-by: Kevin Morris --- Dockerfile | 3 +- docker-compose.aur-dev.yml | 3 +- docker-compose.override.yml | 14 --- docker-compose.yml | 14 ++- docker/ca-entrypoint.sh | 163 +++++++++++++++++++++--------- docker/health/ca.sh | 2 + docker/nginx-entrypoint.sh | 2 +- docker/scripts/install-deps.sh | 2 +- docker/scripts/run-ca.sh | 7 ++ docker/scripts/update-step-config | 19 ++++ 10 files changed, 160 insertions(+), 69 deletions(-) create mode 100755 docker/health/ca.sh create mode 100755 docker/scripts/run-ca.sh create mode 100755 docker/scripts/update-step-config diff --git a/Dockerfile b/Dockerfile index 3c12cbf8..9af78c3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,8 @@ RUN /install-deps.sh # Copy Docker scripts COPY ./docker /docker -COPY ./docker/scripts/*.sh /usr/local/bin/ +COPY ./docker/scripts/* /usr/local/bin/ + # Copy over all aurweb files. COPY . /aurweb diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index 4b522e56..f27b2b19 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -70,9 +70,8 @@ services: nginx: restart: always volumes: - - ${GIT_DATA_DIR}:/aurweb/aur.git - data:/data - - logs:/var/log/nginx + - archives:/var/lib/aurweb/archives - smartgit_run:/var/run/smartgit volumes: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index eae12a92..8c74f947 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -6,10 +6,6 @@ services: mariadb: condition: service_healthy - ca: - volumes: - - ./data:/data - git: volumes: - git_data:/aurweb/aur.git @@ -45,13 +41,3 @@ services: - ./web/template:/aurweb/web/template - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates - - nginx: - volumes: - - git_data:/aurweb/aur.git - - ./data:/data - - ./logs:/var/log/nginx - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - - smartgit_run:/var/run/smartgit diff --git a/docker-compose.yml b/docker-compose.yml index acb5dd65..c1f93319 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,16 @@ services: image: aurweb:latest init: true entrypoint: /docker/ca-entrypoint.sh - command: echo + command: /docker/scripts/run-ca.sh + healthcheck: + test: "bash /docker/health/run-ca.sh" + interval: 3s + tmpfs: + - /tmp + volumes: + - ./docker:/docker + - ./data:/data + - step:/root/.step memcached: image: aurweb:latest @@ -261,7 +270,9 @@ services: php-fpm: condition: service_healthy volumes: + - ./data:/data - archives:/var/lib/aurweb/archives + - smartgit_run:/var/run/smartgit sharness: image: aurweb:latest @@ -347,3 +358,4 @@ volumes: git_data: {} # Share aurweb/aur.git smartgit_run: {} archives: {} + step: {} diff --git a/docker/ca-entrypoint.sh b/docker/ca-entrypoint.sh index 42d8bd14..d03efbbc 100755 --- a/docker/ca-entrypoint.sh +++ b/docker/ca-entrypoint.sh @@ -1,58 +1,123 @@ #!/bin/bash +# Initialize step-ca and request certificates from it. +# +# Certificates created by this service are meant to be used in +# aurweb Docker's nginx service. +# +# If ./data/root_ca.crt is present, CA generation is skipped. +# If ./data/${host}.{cert,key}.pem is available, host certificate +# generation is skipped. +# set -eou pipefail -if [ -f /data/ca.root.pem ]; then - echo "Already have certs, skipping." - exit 0 +# /data-based variables. +DATA_DIR="/data" +DATA_ROOT_CA="$DATA_DIR/root_ca.crt" +DATA_CERT="$DATA_DIR/localhost.cert.pem" +DATA_CERT_KEY="$DATA_DIR/localhost.key.pem" + +# Host certificates requested from the CA (separated by spaces). +DATA_CERT_HOSTS='localhost' + +# Local step paths and CA configuration values. +STEP_DIR="$(step-cli path)" +STEP_CA_CONFIG="$STEP_DIR/config/ca.json" +STEP_CA_ADDR='127.0.0.1:8443' +STEP_CA_URL='https://localhost:8443' +STEP_CA_PROVISIONER='admin@localhost' + +# Password file used for both --password-file and --provisioner-password-file. +STEP_PASSWD_FILE="$STEP_DIR/password.txt" + +# Hostnames supported by the CA. +STEP_CA_NAME='aurweb' +STEP_CA_DNS='localhost' + +make_password() { + # Create a random 20-length password and write it to $1. + openssl rand -hex 20 > $1 +} + +setup_step_ca() { + # Cleanup and setup step ca configuration. + rm -rf $STEP_DIR/* + + # Initialize `step` + make_password "$STEP_PASSWD_FILE" + step-cli ca init \ + --name="$STEP_CA_NAME" \ + --dns="$STEP_CA_DNS" \ + --address="$STEP_CA_ADDR" \ + --password-file="$STEP_PASSWD_FILE" \ + --provisioner="$STEP_CA_PROVISIONER" \ + --provisioner-password-file="$STEP_PASSWD_FILE" \ + --with-ca-url="$STEP_CA_URL" + + # Update ca.json max TLS certificate duration to a year. + update-step-config "$STEP_CA_CONFIG" + + # Install root_ca.crt as read/writable to /data/root_ca.crt. + install -m666 "$STEP_DIR/certs/root_ca.crt" "$DATA_ROOT_CA" +} + +start_step_ca() { + # Start the step-ca web server. + step-ca "$STEP_CA_CONFIG" \ + --password-file="$STEP_PASSWD_FILE" & + until printf "" 2>>/dev/null >>/dev/tcp/127.0.0.1/8443; do + sleep 1 + done +} + +kill_step_ca() { + # Stop the step-ca web server. + killall step-ca >/dev/null 2>&1 || /bin/true +} + +install_step_ca() { + # Install step-ca certificate authority to the system. + step-cli certificate install "$STEP_DIR/certs/root_ca.crt" +} + +step_cert_request() { + # Request a certificate from the step ca. + step-cli ca certificate \ + --not-after=8800h \ + --provisioner="$STEP_CA_PROVISIONER" \ + --provisioner-password-file="$STEP_PASSWD_FILE" \ + $1 $2 $3 + chmod 666 /data/${1}.*.pem +} + +if [ ! -f $DATA_ROOT_CA ]; then + setup_step_ca + install_step_ca fi -# Generate a new 2048-bit RSA key for the Root CA. -openssl genrsa -des3 -out /data/ca.key -passout pass:devca 2048 +# For all hosts separated by spaces in $DATA_CERT_HOSTS, perform a check +# for their existence in /data and react accordingly. +for host in $DATA_CERT_HOSTS; do + if [ -f /data/${host}.cert.pem ] && [ -f /data/${host}.key.pem ]; then + # Found an override. Move on to running the service after + # printing a notification to the user. + echo "Found '${host}.{cert,key}.pem' override, skipping..." + echo -n "Note: If you need to regenerate certificates, run " + echo '`rm -f data/*.{cert,key}.pem` before starting this service.' + exec "$@" + else + # Otherwise, we had a missing cert or key, so remove both. + rm -f /data/${host}.cert.pem + rm -f /data/${host}.key.pem + fi +done -# Request and self-sign a new Root CA certificate, using -# the RSA key. Output Root CA PEM-format certificate and key: -# /data/ca.root.pem and /data/ca.key.pem -openssl req -x509 -new -nodes -sha256 -days 1825 \ - -passin pass:devca \ - -subj "/C=US/ST=California/L=Authority/O=aurweb/CN=localhost" \ - -in /data/ca.key -out /data/ca.root.pem -keyout /data/ca.key.pem +start_step_ca +for host in $DATA_CERT_HOSTS; do + step_cert_request $host /data/${host}.cert.pem /data/${host}.key.pem +done +kill_step_ca -# Generate a new 2048-bit RSA key for a localhost server. -openssl genrsa -out /data/localhost.key 2048 - -# Generate a Certificate Signing Request (CSR) for the localhost server -# using the RSA key we generated above. -openssl req -new -key /data/localhost.key -passout pass:devca \ - -subj "/C=US/ST=California/L=Server/O=aurweb/CN=localhost" \ - -out /data/localhost.csr - -# Get our CSR signed by our Root CA PEM-formatted certificate and key -# to produce a fresh /data/localhost.cert.pem PEM-formatted certificate. -openssl x509 -req -in /data/localhost.csr \ - -CA /data/ca.root.pem -CAkey /data/ca.key.pem \ - -CAcreateserial \ - -out /data/localhost.cert.pem \ - -days 825 -sha256 \ - -passin pass:devca \ - -extfile /docker/localhost.ext - -# Convert RSA key to a PEM-formatted key: /data/localhost.key.pem -openssl rsa -in /data/localhost.key -text > /data/localhost.key.pem - -# At the end here, our notable certificates and keys are: -# - /data/ca.root.pem -# - /data/ca.key.pem -# - /data/localhost.key.pem -# - /data/localhost.cert.pem -# -# When running a server which uses the localhost certificate, a chain -# should be used, starting with localhost.cert.pem: -# - cat /data/localhost.cert.pem /data/ca.root.pem > localhost.chain.pem -# -# The Root CA (ca.root.pem) should be imported into browsers or -# ca-certificates on machines wishing to verify localhost. -# - -chmod 666 /data/* +# Set permissions to /data to rwx for everybody. +chmod 777 /data exec "$@" diff --git a/docker/health/ca.sh b/docker/health/ca.sh new file mode 100755 index 00000000..3e4bbe8e --- /dev/null +++ b/docker/health/ca.sh @@ -0,0 +1,2 @@ + +exec printf "" 2>>/dev/null >>/dev/tcp/127.0.0.1/8443 diff --git a/docker/nginx-entrypoint.sh b/docker/nginx-entrypoint.sh index 6b9a6954..1527cda7 100755 --- a/docker/nginx-entrypoint.sh +++ b/docker/nginx-entrypoint.sh @@ -15,7 +15,7 @@ if [ -f "$CERT" ]; then cp -vf "$CERT" "$DEST_CERT" cp -vf "$KEY" "$DEST_KEY" else - cat /data/localhost.cert.pem /data/ca.root.pem > "$DEST_CERT" + cat /data/localhost.cert.pem /data/root_ca.crt > "$DEST_CERT" cp -vf /data/localhost.key.pem "$DEST_KEY" fi diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index ad0157f8..372b6e0c 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -9,6 +9,6 @@ pacman -Syu --noconfirm --noprogressbar \ mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ php php-fpm memcached php-memcached python-pip pyalpm \ python-srcinfo curl libeatmydata cronie python-poetry \ - python-poetry-core + python-poetry-core step-cli step-ca exec "$@" diff --git a/docker/scripts/run-ca.sh b/docker/scripts/run-ca.sh new file mode 100755 index 00000000..1ef45ef7 --- /dev/null +++ b/docker/scripts/run-ca.sh @@ -0,0 +1,7 @@ +#!/bin/bash +STEP_DIR="$(step-cli path)" +STEP_PASSWD_FILE="$STEP_DIR/password.txt" +STEP_CA_CONFIG="$STEP_DIR/config/ca.json" + +# Start the step-ca https server. +exec step-ca "$STEP_CA_CONFIG" --password-file="$STEP_PASSWD_FILE" diff --git a/docker/scripts/update-step-config b/docker/scripts/update-step-config new file mode 100755 index 00000000..bbdb2680 --- /dev/null +++ b/docker/scripts/update-step-config @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import json +import sys + +CA_CONFIG = sys.argv[1] + +with open(CA_CONFIG) as f: + data = json.load(f) + +if "authority" not in data: + data["authority"] = dict() +if "claims" not in data["authority"]: + data["authority"]["claims"] = dict() + +# One year of certificate duration. +data["authority"]["claims"] = {"maxTLSCertDuration": "8800h"} + +with open(CA_CONFIG, "w") as f: + json.dump(data, f) From 759f18ea75a5581cefa5ca6fe323bdc56944f47a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 16:44:56 -0800 Subject: [PATCH 0716/1451] feat: add aurweb-config console script This can be used to update config values for the entirety of a config. When config values are set through this tool, $AUR_CONFIG is overridden with a copy of the config file with all sections and options found in $AUR_CONFIG + $AUR_CONFIG_DEFAULTS. Signed-off-by: Kevin Morris --- aurweb/config.py | 12 ++++ aurweb/scripts/config.py | 61 +++++++++++++++++++ pyproject.toml | 1 + test/test_config.py | 125 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 aurweb/scripts/config.py diff --git a/aurweb/config.py b/aurweb/config.py index aa111f15..0d0cf676 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -1,6 +1,8 @@ import configparser import os +from typing import Any + # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. @@ -52,3 +54,13 @@ def getint(section, option, fallback=None): def get_section(section): if section in _get_parser().sections(): return _get_parser()[section] + + +def replace_key(section: str, option: str, value: Any) -> Any: + _get_parser().set(section, option, value) + + +def save() -> None: + aur_config = os.environ.get("AUR_CONFIG", "/etc/aurweb/config") + with open(aur_config, "w") as fp: + _get_parser().write(fp) diff --git a/aurweb/scripts/config.py b/aurweb/scripts/config.py new file mode 100644 index 00000000..dd7bcf5f --- /dev/null +++ b/aurweb/scripts/config.py @@ -0,0 +1,61 @@ +""" +Perform an action on the aurweb config. + +When AUR_CONFIG_IMMUTABLE is set, the `set` action is noop. +""" +import argparse +import configparser +import os +import sys + +import aurweb.config + + +def action_set(args): + # If AUR_CONFIG_IMMUTABLE is defined, skip out on config setting. + if os.environ.get("AUR_CONFIG_IMMUTABLE", 0): + return + + if not args.value: + print("error: no value provided", file=sys.stderr) + return + + try: + aurweb.config.replace_key(args.section, args.option, args.value) + aurweb.config.save() + except configparser.NoSectionError: + print("error: no section found", file=sys.stderr) + + +def action_get(args): + try: + value = aurweb.config.get(args.section, args.option) + print(value) + except (configparser.NoSectionError): + print("error: no section found", file=sys.stderr) + except (configparser.NoOptionError): + print("error: no option found", file=sys.stderr) + + +def parse_args(): + fmt_cls = argparse.RawDescriptionHelpFormatter + actions = ["get", "set"] + parser = argparse.ArgumentParser( + description="aurweb configuration tool", + formatter_class=lambda prog: fmt_cls(prog=prog, max_help_position=80)) + parser.add_argument("action", choices=actions, help="script action") + parser.add_argument("section", help="config section") + parser.add_argument("option", help="config option") + parser.add_argument("value", nargs="?", default=0, + help="config option value") + return parser.parse_args() + + +def main(): + args = parse_args() + action = getattr(sys.modules[__name__], f"action_{args.action}") + return action(args) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 8d14735a..82c439bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,3 +108,4 @@ aurweb-popupdate = "aurweb.scripts.popupdate:main" aurweb-rendercomment = "aurweb.scripts.rendercomment:main" aurweb-tuvotereminder = "aurweb.scripts.tuvotereminder:main" aurweb-usermaint = "aurweb.scripts.usermaint:main" +aurweb-config = "aurweb.scripts.config:main" diff --git a/test/test_config.py b/test/test_config.py index 4f10b60d..7e9d24b5 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,4 +1,16 @@ +import configparser +import io +import os +import re + +from unittest import mock + from aurweb import config +from aurweb.scripts.config import main + + +def noop(*args, **kwargs) -> None: + return def test_get(): @@ -11,3 +23,116 @@ def test_getboolean(): def test_getint(): assert config.getint("options", "disable_http_login") == 0 + + +def mock_config_get(): + config_get = config.get + + def _mock_config_get(section: str, option: str): + if section == "options": + if option == "salt_rounds": + return "666" + return config_get(section, option) + return _mock_config_get + + +@mock.patch("aurweb.config.get", side_effect=mock_config_get()) +def test_config_main_get(get: str): + stdout = io.StringIO() + args = ["aurweb-config", "get", "options", "salt_rounds"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stdout", stdout): + main() + + expected = "666" + assert stdout.getvalue().strip() == expected + + +@mock.patch("aurweb.config.get", side_effect=mock_config_get()) +def test_config_main_get_unknown_section(get: str): + stderr = io.StringIO() + args = ["aurweb-config", "get", "fakeblahblah", "salt_rounds"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stderr", stderr): + main() + + # With an invalid section, we should get a usage error. + expected = r'^error: no section found$' + assert re.match(expected, stderr.getvalue().strip()) + + +@mock.patch("aurweb.config.get", side_effect=mock_config_get()) +def test_config_main_get_unknown_option(get: str): + stderr = io.StringIO() + args = ["aurweb-config", "get", "options", "fakeblahblah"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stderr", stderr): + main() + + expected = "error: no option found" + assert stderr.getvalue().strip() == expected + + +@mock.patch("aurweb.config.save", side_effect=noop) +def test_config_main_set(save: None): + data = None + + def mock_replace_key(section: str, option: str, value: str) -> None: + nonlocal data + data = value + + args = ["aurweb-config", "set", "options", "salt_rounds", "666"] + with mock.patch("sys.argv", args): + with mock.patch("aurweb.config.replace_key", + side_effect=mock_replace_key): + main() + + expected = "666" + assert data == expected + + +def test_config_main_set_immutable(): + data = None + + def mock_replace_key(section: str, option: str, value: str) -> None: + nonlocal data + data = value + + args = ["aurweb-config", "set", "options", "salt_rounds", "666"] + with mock.patch.dict(os.environ, {"AUR_CONFIG_IMMUTABLE": "1"}): + with mock.patch("sys.argv", args): + with mock.patch("aurweb.config.replace_key", + side_effect=mock_replace_key): + main() + + expected = None + assert data == expected + + +def test_config_main_set_invalid_value(): + stderr = io.StringIO() + + args = ["aurweb-config", "set", "options", "salt_rounds"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stderr", stderr): + main() + + expected = "error: no value provided" + assert stderr.getvalue().strip() == expected + + +@mock.patch("aurweb.config.save", side_effect=noop) +def test_config_main_set_unknown_section(save: None): + stderr = io.StringIO() + + def mock_replace_key(section: str, option: str, value: str) -> None: + raise configparser.NoSectionError(section=section) + + args = ["aurweb-config", "set", "options", "salt_rounds", "666"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stderr", stderr): + with mock.patch("aurweb.config.replace_key", + side_effect=mock_replace_key): + main() + + assert stderr.getvalue().strip() == "error: no section found" From d658627e992dd0fc16e5e2aa76d52dda76de4380 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 19:10:59 -0800 Subject: [PATCH 0717/1451] fix(fastapi): don't redirect to login on authed /login Closes #184 Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 055f0dca..c5a99419 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -24,13 +24,13 @@ async def login_template(request: Request, next: str, errors: list = None): @router.get("/login", response_class=HTMLResponse) -@auth_required(False) +@auth_required(False, login=False) async def login_get(request: Request, next: str = "/"): return await login_template(request, next) @router.post("/login", response_class=HTMLResponse) -@auth_required(False) +@auth_required(False, login=False) async def login_post(request: Request, next: str = Form(...), user: str = Form(default=str()), From 47feb72f48cb0d1c36fffff39160e48b8e870488 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 20:04:26 -0800 Subject: [PATCH 0718/1451] fix(fastapi): fix SessionID (and ResetKey) generation Signed-off-by: Kevin Morris --- aurweb/db.py | 29 +---------------------------- aurweb/models/session.py | 9 ++++----- aurweb/models/user.py | 4 ++++ aurweb/routers/accounts.py | 5 +++-- 4 files changed, 12 insertions(+), 35 deletions(-) diff --git a/aurweb/db.py b/aurweb/db.py index b8b49e40..70ad58d1 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -24,42 +24,15 @@ DRIVERS = { "mysql": "mysql+mysqldb" } -# Global introspected object memo. -introspected = dict() - # Some types we don't get access to in this module. Base = NewType("Base", "aurweb.models.declarative_base.Base") -def make_random_value(table: str, column: str): +def make_random_value(table: str, column: str, length: int): """ Generate a unique, random value for a string column in a table. - This can be used to generate for example, session IDs that - align with the properties of the database column with regards - to size. - - Internally, we use SQLAlchemy introspection to look at column - to decide which length to use for random string generation. - :return: A unique string that is not in the database """ - global introspected - - # Make sure column is converted to a string for memo interaction. - scolumn = str(column) - - # If the target column is not yet introspected, store its introspection - # object into our global `introspected` memo. - if scolumn not in introspected: - from sqlalchemy import inspect - target_column = scolumn.split('.')[-1] - col = list(filter(lambda c: c.name == target_column, - inspect(table).columns))[0] - introspected[scolumn] = col - - col = introspected.get(scolumn) - length = col.type.length - string = aurweb.util.make_random_string(length) while query(table).filter(column == string).first(): string = aurweb.util.make_random_string(length) diff --git a/aurweb/models/session.py b/aurweb/models/session.py index 96f88d85..7a06eddc 100644 --- a/aurweb/models/session.py +++ b/aurweb/models/session.py @@ -1,8 +1,7 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship -from aurweb import schema -from aurweb.db import make_random_value, query +from aurweb import db, schema from aurweb.models.declarative import Base from aurweb.models.user import User as _User @@ -19,8 +18,8 @@ class Session(Base): def __init__(self, **kwargs): super().__init__(**kwargs) - user_exists = query( - query(_User).filter(_User.ID == self.UsersID).exists() + user_exists = db.query( + db.query(_User).filter(_User.ID == self.UsersID).exists() ).scalar() if not user_exists: raise IntegrityError( @@ -31,4 +30,4 @@ class Session(Base): def generate_unique_sid(): - return make_random_value(Session, Session.SessionID) + return db.make_random_value(Session, Session.SessionID, 32) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 43910db9..03634a36 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -230,3 +230,7 @@ class User(Base): def __repr__(self): return "" % ( self.ID, str(self.AccountType), self.Username) + + +def generate_unique_resetkey(): + return db.make_random_value(User, User.ResetKey, 32) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 02a7f4c6..ddee1764 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -16,6 +16,7 @@ from aurweb.exceptions import ValidationError from aurweb.l10n import get_translator_for_request from aurweb.models import account_type as at from aurweb.models.ssh_pub_key import get_fingerprint +from aurweb.models.user import generate_unique_resetkey from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification from aurweb.templates import make_context, make_variable_context, render_template from aurweb.users import update, validate @@ -92,7 +93,7 @@ async def passreset_post(request: Request, status_code=HTTPStatus.SEE_OTHER) # If we got here, we continue with issuing a resetkey for the user. - resetkey = db.make_random_value(models.User, models.User.ResetKey) + resetkey = generate_unique_resetkey() with db.begin(): user.ResetKey = resetkey @@ -291,7 +292,7 @@ async def account_register_post(request: Request, # Create a user with no password with a resetkey, then send # an email off about it. - resetkey = db.make_random_value(models.User, models.User.ResetKey) + resetkey = generate_unique_resetkey() # By default, we grab the User account type to associate with. atype = db.query(models.AccountType, From 7b0d664bc0c4d9f75abc2ed659e14ca5f66dba1c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 21:03:24 -0800 Subject: [PATCH 0719/1451] fix(docker): reorg ./data mounts Signed-off-by: Kevin Morris --- docker-compose.aur-dev.yml | 3 ++- docker-compose.override.yml | 11 +++++++++++ docker-compose.yml | 10 ---------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index f27b2b19..0b91dd93 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -4,6 +4,7 @@ services: ca: volumes: - data:/data + - step:/root/.step memcached: restart: always @@ -22,7 +23,7 @@ services: - SSH_CMDLINE=${SSH_CMDLINE:-ssh ssh://aur@localhost:2222} volumes: - ${GIT_DATA_DIR}:/aurweb/aur.git - - ./data:/aurweb/data + - data:/aurweb/data smartgit: restart: always diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 8c74f947..1e466730 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,6 +1,11 @@ version: "3.8" services: + ca: + volumes: + - ./data:/data + - step:/root/.step + mariadb_init: depends_on: mariadb: @@ -41,3 +46,9 @@ services: - ./web/template:/aurweb/web/template - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates + + nginx: + volumes: + - ./data:/data + - archives:/var/lib/aurweb/archives + - smartgit_run:/var/run/smartgit diff --git a/docker-compose.yml b/docker-compose.yml index c1f93319..5d8f7d78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,12 +33,6 @@ services: healthcheck: test: "bash /docker/health/run-ca.sh" interval: 3s - tmpfs: - - /tmp - volumes: - - ./docker:/docker - - ./data:/data - - step:/root/.step memcached: image: aurweb:latest @@ -269,10 +263,6 @@ services: condition: service_healthy php-fpm: condition: service_healthy - volumes: - - ./data:/data - - archives:/var/lib/aurweb/archives - - smartgit_run:/var/run/smartgit sharness: image: aurweb:latest From 199622c53f65260670868ca3712d2f9e30de4461 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 21:35:35 -0800 Subject: [PATCH 0720/1451] fix(fastapi): refresh records when fetching updated packages Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 7c48f4e4..55af3a34 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -167,6 +167,7 @@ def updated_packages(limit: int = 0, for pkg in query: # For each Package returned by the query, append a dict # containing Package columns we're interested in. + db.refresh(pkg) packages.append({ "Name": pkg.Name, "Version": pkg.Version, From 0e938209afbf25748cf95bd27b32ad2814f8d77b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 22:34:15 -0800 Subject: [PATCH 0721/1451] feat(aurweb-config): add unset action and simplify Signed-off-by: Kevin Morris --- aurweb/config.py | 7 ++++- aurweb/scripts/config.py | 38 ++++++++++++++++---------- test/test_config.py | 59 +++++++++++++++++++++++++++++++++------- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 0d0cf676..86f8ddf7 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -56,8 +56,13 @@ def get_section(section): return _get_parser()[section] -def replace_key(section: str, option: str, value: Any) -> Any: +def unset_option(section: str, option: str) -> None: + _get_parser().remove_option(section, option) + + +def set_option(section: str, option: str, value: Any) -> None: _get_parser().set(section, option, value) + return value def save() -> None: diff --git a/aurweb/scripts/config.py b/aurweb/scripts/config.py index dd7bcf5f..e7c91dd1 100644 --- a/aurweb/scripts/config.py +++ b/aurweb/scripts/config.py @@ -11,35 +11,43 @@ import sys import aurweb.config -def action_set(args): +def do_action(func, *args, save: bool = True): # If AUR_CONFIG_IMMUTABLE is defined, skip out on config setting. - if os.environ.get("AUR_CONFIG_IMMUTABLE", 0): + if int(os.environ.get("AUR_CONFIG_IMMUTABLE", 0)): return + value = None + try: + value = func(*args) + if save: + aurweb.config.save() + except configparser.NoSectionError: + print("error: no section found", file=sys.stderr) + except configparser.NoOptionError: + print("error: no option found", file=sys.stderr) + + return value + + +def action_set(args): if not args.value: print("error: no value provided", file=sys.stderr) return + do_action(aurweb.config.set_option, args.section, args.option, args.value) - try: - aurweb.config.replace_key(args.section, args.option, args.value) - aurweb.config.save() - except configparser.NoSectionError: - print("error: no section found", file=sys.stderr) + +def action_unset(args): + do_action(aurweb.config.unset_option, args.section, args.option) def action_get(args): - try: - value = aurweb.config.get(args.section, args.option) - print(value) - except (configparser.NoSectionError): - print("error: no section found", file=sys.stderr) - except (configparser.NoOptionError): - print("error: no option found", file=sys.stderr) + val = do_action(aurweb.config.get, args.section, args.option, save=False) + print(val) def parse_args(): fmt_cls = argparse.RawDescriptionHelpFormatter - actions = ["get", "set"] + actions = ["get", "set", "unset"] parser = argparse.ArgumentParser( description="aurweb configuration tool", formatter_class=lambda prog: fmt_cls(prog=prog, max_help_position=80)) diff --git a/test/test_config.py b/test/test_config.py index 7e9d24b5..b78f477c 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -5,6 +5,8 @@ import re from unittest import mock +import py + from aurweb import config from aurweb.scripts.config import main @@ -77,32 +79,69 @@ def test_config_main_get_unknown_option(get: str): def test_config_main_set(save: None): data = None - def mock_replace_key(section: str, option: str, value: str) -> None: + def set_option(section: str, option: str, value: str) -> None: nonlocal data data = value args = ["aurweb-config", "set", "options", "salt_rounds", "666"] with mock.patch("sys.argv", args): - with mock.patch("aurweb.config.replace_key", - side_effect=mock_replace_key): + with mock.patch("aurweb.config.set_option", side_effect=set_option): main() expected = "666" assert data == expected +def test_config_main_set_real(tmpdir: py.path.local): + """ + Test a real set_option path. + """ + + # Copy AUR_CONFIG to {tmpdir}/aur.config. + aur_config = os.environ.get("AUR_CONFIG") + tmp_aur_config = os.path.join(str(tmpdir), "aur.config") + with open(aur_config) as f: + with open(tmp_aur_config, "w") as o: + o.write(f.read()) + + # Force reset the parser. This should NOT be done publicly. + config._parser = None + + value = 666 + args = ["aurweb-config", "set", "options", "fake-key", str(value)] + with mock.patch.dict("os.environ", {"AUR_CONFIG": tmp_aur_config}): + with mock.patch("sys.argv", args): + # Run aurweb.config.main(). + main() + + # Update the config; fake-key should be set. + config.rehash() + assert config.getint("options", "fake-key") == 666 + + # Restore config back to normal. + args = ["aurweb-config", "unset", "options", "fake-key"] + with mock.patch("sys.argv", args): + main() + + # Return the config back to normal. + config.rehash() + + # fake-key should no longer exist. + assert config.getint("options", "fake-key") is None + + def test_config_main_set_immutable(): data = None - def mock_replace_key(section: str, option: str, value: str) -> None: + def mock_set_option(section: str, option: str, value: str) -> None: nonlocal data data = value args = ["aurweb-config", "set", "options", "salt_rounds", "666"] with mock.patch.dict(os.environ, {"AUR_CONFIG_IMMUTABLE": "1"}): with mock.patch("sys.argv", args): - with mock.patch("aurweb.config.replace_key", - side_effect=mock_replace_key): + with mock.patch("aurweb.config.set_option", + side_effect=mock_set_option): main() expected = None @@ -121,18 +160,18 @@ def test_config_main_set_invalid_value(): assert stderr.getvalue().strip() == expected -@mock.patch("aurweb.config.save", side_effect=noop) +@ mock.patch("aurweb.config.save", side_effect=noop) def test_config_main_set_unknown_section(save: None): stderr = io.StringIO() - def mock_replace_key(section: str, option: str, value: str) -> None: + def mock_set_option(section: str, option: str, value: str) -> None: raise configparser.NoSectionError(section=section) args = ["aurweb-config", "set", "options", "salt_rounds", "666"] with mock.patch("sys.argv", args): with mock.patch("sys.stderr", stderr): - with mock.patch("aurweb.config.replace_key", - side_effect=mock_replace_key): + with mock.patch("aurweb.config.set_option", + side_effect=mock_set_option): main() assert stderr.getvalue().strip() == "error: no section found" From f3efc18b508d505f242426911ce22231dc182e05 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 22:42:12 -0800 Subject: [PATCH 0722/1451] feat(docker): force test db configuration Signed-off-by: Kevin Morris --- docker/test-mysql-entrypoint.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docker/test-mysql-entrypoint.sh b/docker/test-mysql-entrypoint.sh index a46b2572..262577a6 100755 --- a/docker/test-mysql-entrypoint.sh +++ b/docker/test-mysql-entrypoint.sh @@ -5,4 +5,16 @@ set -eou pipefail cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config +# We use the root user for testing in Docker. +# The test user must be able to create databases and drop them. +aurweb-config set database user 'root' +aurweb-config set database host 'localhost' +aurweb-config set database socket '/var/run/mysqld/mysqld.sock' + +# Remove possibly problematic configuration options. +# We depend on the database socket within Docker and +# being run as the root user. +aurweb-config unset database password +aurweb-config unset database port + exec "$@" From 0726a08677b589136dbfce1d59990c9c744e56b0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 17:42:04 -0800 Subject: [PATCH 0723/1451] fix(docker): remove sqlite scripts Signed-off-by: Kevin Morris --- docker/scripts/setup-sqlite.sh | 7 ------- docker/test-sqlite-entrypoint.sh | 16 ---------------- 2 files changed, 23 deletions(-) delete mode 100755 docker/scripts/setup-sqlite.sh delete mode 100755 docker/test-sqlite-entrypoint.sh diff --git a/docker/scripts/setup-sqlite.sh b/docker/scripts/setup-sqlite.sh deleted file mode 100755 index e0b8de50..00000000 --- a/docker/scripts/setup-sqlite.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Run an sqlite test. This script really just prepares sqlite -# tests by deleting any existing databases so the test can -# initialize cleanly. -DB_NAME="$(grep 'name =' conf/config.sqlite | sed -r 's/^name = (.+)$/\1/')" -rm -vf $DB_NAME -exec "$@" diff --git a/docker/test-sqlite-entrypoint.sh b/docker/test-sqlite-entrypoint.sh deleted file mode 100755 index c26f6735..00000000 --- a/docker/test-sqlite-entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -eou pipefail - -DB_BACKEND="sqlite" -DB_NAME="aurweb.sqlite3" - -# Create an SQLite config from the default dev config. -cp -vf conf/config.dev conf/config.sqlite -cp -vf conf/config.defaults conf/config.sqlite.defaults - -# Modify it for SQLite. -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.sqlite -sed -ri "s/^(backend) = .+/\1 = ${DB_BACKEND}/" conf/config.sqlite -sed -ri "s/^(name) = .+/\1 = ${DB_NAME}/" conf/config.sqlite - -exec "$@" From 5b350bc3614f29794bdfb710893b76b3d40ba96d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 18:46:42 -0800 Subject: [PATCH 0724/1451] change(docker): use aurweb-config to update AUR_CONFIG Signed-off-by: Kevin Morris --- docker/fastapi-entrypoint.sh | 26 ++++++++++++++------------ docker/git-entrypoint.sh | 22 +++++++++------------- docker/mariadb-init-entrypoint.sh | 5 +++-- docker/php-entrypoint.sh | 21 +++++++++++---------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index 9df6382d..d1519bf8 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -5,23 +5,25 @@ set -eou pipefail cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config -# Change database user/password. -sed -ri "s/^;?(user) = .*$/\1 = aur/" conf/config -sed -ri "s/^;?(password) = .*$/\1 = aur/" conf/config +# Setup database. +aurweb-config set database user 'aur' +aurweb-config set database password 'aur' +aurweb-config set database host 'localhost' +aurweb-config set database socket '/var/lib/mysqld/mysqld.sock' +aurweb-config unset database port -sed -ri "s;^(aur_location) = .+;\1 = ${AURWEB_FASTAPI_PREFIX};" conf/config - -# Setup Redis for FastAPI. -sed -ri 's/^(cache) = .+/\1 = redis/' conf/config -sed -ri 's|^(redis_address) = .+|\1 = redis://redis|' conf/config +# Setup some other options. +aurweb-config set options cache 'redis' +aurweb-config set options redis_address 'redis://redis' +aurweb-config set options aur_location "$AURWEB_FASTAPI_PREFIX" +aurweb-config set options git_clone_uri_anon "${AURWEB_FASTAPI_PREFIX}/%s.git" +aurweb-config set options git_clone_uri_priv "${AURWEB_SSHD_PREFIX}/%s.git" if [ ! -z ${COMMIT_HASH+x} ]; then - sed -ri "s/^;?(commit_hash) =.*$/\1 = $COMMIT_HASH/" conf/config + aurweb-config set devel commit_hash "$COMMIT_HASH" fi -sed -ri "s|^(git_clone_uri_anon) = .+|\1 = ${AURWEB_FASTAPI_PREFIX}/%s.git|" conf/config.defaults -sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ${AURWEB_SSHD_PREFIX}/%s.git|" conf/config.defaults - +# Setup prometheus directory. rm -rf $PROMETHEUS_MULTIPROC_DIR mkdir -p $PROMETHEUS_MULTIPROC_DIR diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index cfa1879b..96f4d112 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -42,20 +42,16 @@ EOF cp -vf conf/config.dev $AUR_CONFIG sed -i "s;YOUR_AUR_ROOT;$(pwd);g" $AUR_CONFIG -sed -ri "s/^;?(user) = .*$/\1 = aur/" $AUR_CONFIG -sed -ri "s/^;?(password) = .*$/\1 = aur/" $AUR_CONFIG +# Setup database. +aurweb-config set database user 'aur' +aurweb-config set database password 'aur' +aurweb-config set database host 'localhost' +aurweb-config set database socket '/var/lib/mysqld/mysqld.sock' +aurweb-config unset database port -AUR_CONFIG_DEFAULTS="${AUR_CONFIG}.defaults" - -if [[ "$AUR_CONFIG_DEFAULTS" != "/aurweb/conf/config.defaults" ]]; then - cp -vf conf/config.defaults $AUR_CONFIG_DEFAULTS -fi - -# Set some defaults needed for pathing and ssh uris. -sed -ri "s|^(repo-path) = .+|\1 = /aurweb/aur.git/|" $AUR_CONFIG_DEFAULTS - -# SSH_CMDLINE can be provided via override in docker-compose.aur-dev.yml. -sed -ri "s|^(ssh-cmdline) = .+$|\1 = ${SSH_CMDLINE}|" $AUR_CONFIG_DEFAULTS +# Setup some other options. +aurweb-config set serve repo-path '/aurweb/aur.git/' +aurweb-config set serve ssh-cmdline "$SSH_CMDLINE" # Setup SSH Keys. ssh-keygen -A diff --git a/docker/mariadb-init-entrypoint.sh b/docker/mariadb-init-entrypoint.sh index 6df98e4f..64e66a0f 100755 --- a/docker/mariadb-init-entrypoint.sh +++ b/docker/mariadb-init-entrypoint.sh @@ -4,8 +4,9 @@ set -eou pipefail # Setup a config for our mysql db. cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config -sed -ri "s/^;?(user) = .*$/\1 = aur/g" conf/config -sed -ri "s/^;?(password) = .*$/\1 = aur/g" conf/config + +aurweb-config set database user 'aur' +aurweb-config set database password 'aur' python -m aurweb.initdb 2>/dev/null || /bin/true diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index 05b76408..1756718d 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -9,17 +9,18 @@ done cp -vf conf/config.dev conf/config sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config -# Change database user/password. -sed -ri "s/^;?(user) = .*$/\1 = aur/" conf/config -sed -ri "s/^;?(password) = .*$/\1 = aur/" conf/config +# Setup database. +aurweb-config set database user 'aur' +aurweb-config set database password 'aur' +aurweb-config set database host 'localhost' +aurweb-config set database socket '/var/lib/mysqld/mysqld.sock' +aurweb-config unset database port -# Enable memcached. -sed -ri 's/^(cache) = .+$/\1 = memcache/' conf/config - -# Setup various location configurations. -sed -ri "s;^(aur_location) = .+;\1 = ${AURWEB_PHP_PREFIX};" conf/config -sed -ri "s|^(git_clone_uri_anon) = .+|\1 = ${AURWEB_PHP_PREFIX}/%s.git|" conf/config.defaults -sed -ri "s|^(git_clone_uri_priv) = .+|\1 = ${AURWEB_SSHD_PREFIX}/%s.git|" conf/config.defaults +# Setup some other options. +aurweb-config set options cache 'memcache' +aurweb-config set options aur_location "$AURWEB_PHP_PREFIX" +aurweb-config set options git_clone_uri_anon "${AURWEB_PHP_PREFIX}/%s.git" +aurweb-config set options git_clone_uri_priv "${AURWEB_SSHD_PREFIX}/%s.git" # Listen on :9000. sed -ri 's/^(listen).*/\1 = 0.0.0.0:9000/' /etc/php/php-fpm.d/www.conf From 84beacd4274d27bf039b691a25cae7758f7d9ac2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 18:57:08 -0800 Subject: [PATCH 0725/1451] fix(docker): supply AUR_CONFIG_IMMUTABLE for docker-compose Signed-off-by: Kevin Morris --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 5d8f7d78..401193d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,6 +72,8 @@ services: mariadb_init: image: aurweb:latest init: true + environment: + - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} entrypoint: /docker/mariadb-init-entrypoint.sh command: echo "MariaDB tables initialized." volumes: @@ -104,6 +106,7 @@ services: environment: - AUR_CONFIG=/aurweb/conf/config - SSH_CMDLINE=${SSH_CMDLINE:-ssh ssh://aur@localhost:2222} + - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} entrypoint: /docker/git-entrypoint.sh command: /docker/scripts/run-sshd.sh ports: @@ -190,6 +193,7 @@ services: - AUR_CONFIG=/aurweb/conf/config - AURWEB_PHP_PREFIX=${AURWEB_PHP_PREFIX} - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} + - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} entrypoint: /docker/php-entrypoint.sh command: /docker/scripts/run-php.sh healthcheck: @@ -220,6 +224,7 @@ services: - AURWEB_FASTAPI_PREFIX=${AURWEB_FASTAPI_PREFIX} - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus + - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} entrypoint: /docker/fastapi-entrypoint.sh command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" healthcheck: From 343a306bb8dff96e3d7ab3227646f09f4125255d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 23:14:39 -0800 Subject: [PATCH 0726/1451] change(docker): setup AUR_CONFIG in Dockerfile Signed-off-by: Kevin Morris --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 9af78c3e..38d3ca0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,10 @@ COPY . /aurweb # Working directory is aurweb root @ /aurweb. WORKDIR /aurweb +# Copy initial config to conf/config. +RUN cp -vf conf/config.dev conf/config +RUN sed -i "s;YOUR_AUR_ROOT;/aurweb;g" conf/config + # Install Python dependencies. RUN /docker/scripts/install-python-deps.sh From dbeebd3b01044d508531476dc99571890e150065 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 23:15:19 -0800 Subject: [PATCH 0727/1451] change(fastapi): setup live database in mariadb-init-entrypoint.sh Centralize database setup there and remove all copying of config.dev from the entrypoint scripts (the Dockerfile now does it). Signed-off-by: Kevin Morris --- docker/cron-entrypoint.sh | 28 +++++++++++++++++++++++----- docker/fastapi-entrypoint.sh | 10 +--------- docker/git-entrypoint.sh | 10 +--------- docker/mariadb-init-entrypoint.sh | 12 ++++++++---- docker/php-entrypoint.sh | 10 +--------- docker/test-mysql-entrypoint.sh | 4 ---- 6 files changed, 34 insertions(+), 40 deletions(-) diff --git a/docker/cron-entrypoint.sh b/docker/cron-entrypoint.sh index d4173eaf..5b69ab19 100755 --- a/docker/cron-entrypoint.sh +++ b/docker/cron-entrypoint.sh @@ -1,12 +1,30 @@ #!/bin/bash set -eou pipefail -# Prepare AUR_CONFIG. -cp -vf conf/config.dev conf/config -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config +# Setup the DB. +NO_INITDB=1 /docker/mariadb-init-entrypoint.sh -# Create directories we need. -mkdir -p /aurweb/aurblup +# Create aurblup's directory. +AURBLUP_DIR="/aurweb/aurblup/" +mkdir -p $AURBLUP_DIR + +# Setup aurblup config for Docker. +AURBLUP_DBS='core extra community multilib testing community-testing' +AURBLUP_SERVER='https://mirrors.kernel.org/archlinux/%s/os/x86_64' +aurweb-config set aurblup db-path "$AURBLUP_DIR" +aurweb-config set aurblup sync-dbs "$AURBLUP_DBS" +aurweb-config set aurblup server "$AURBLUP_SERVER" + +# Setup mkpkglists config for Docker. +ARCHIVE_DIR='/var/lib/aurweb/archives' +aurweb-config set mkpkglists archivedir "$ARCHIVE_DIR" +aurweb-config set mkpkglists packagesfile "$ARCHIVE_DIR/packages.gz" +aurweb-config set mkpkglists packagesmetafile \ + "$ARCHIVE_DIR/packages-meta-v1.json.gz" +aurweb-config set mkpkglists packagesmetaextfile \ + "$ARCHIVE_DIR/packages-meta-ext-v1.json.gz" +aurweb-config set mkpkglists pkgbasefile "$ARCHIVE_DIR/pkgbase.gz" +aurweb-config set mkpkglists userfile "$ARCHIVE_DIR/users.gz" # Install the cron configuration. cp /docker/config/aurweb-cron /etc/cron.d/aurweb-cron diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh index d1519bf8..c6597313 100755 --- a/docker/fastapi-entrypoint.sh +++ b/docker/fastapi-entrypoint.sh @@ -1,16 +1,8 @@ #!/bin/bash set -eou pipefail -# Setup a config for our mysql db. -cp -vf conf/config.dev conf/config -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config - # Setup database. -aurweb-config set database user 'aur' -aurweb-config set database password 'aur' -aurweb-config set database host 'localhost' -aurweb-config set database socket '/var/lib/mysqld/mysqld.sock' -aurweb-config unset database port +NO_INITDB=1 /docker/mariadb-init-entrypoint.sh # Setup some other options. aurweb-config set options cache 'redis' diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh index 96f4d112..c9f1ec30 100755 --- a/docker/git-entrypoint.sh +++ b/docker/git-entrypoint.sh @@ -38,16 +38,8 @@ Match User aur AcceptEnv AUR_OVERWRITE EOF -# Setup a config for our mysql db. -cp -vf conf/config.dev $AUR_CONFIG -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" $AUR_CONFIG - # Setup database. -aurweb-config set database user 'aur' -aurweb-config set database password 'aur' -aurweb-config set database host 'localhost' -aurweb-config set database socket '/var/lib/mysqld/mysqld.sock' -aurweb-config unset database port +NO_INITDB=1 /docker/mariadb-init-entrypoint.sh # Setup some other options. aurweb-config set serve repo-path '/aurweb/aur.git/' diff --git a/docker/mariadb-init-entrypoint.sh b/docker/mariadb-init-entrypoint.sh index 64e66a0f..74980031 100755 --- a/docker/mariadb-init-entrypoint.sh +++ b/docker/mariadb-init-entrypoint.sh @@ -2,12 +2,16 @@ set -eou pipefail # Setup a config for our mysql db. -cp -vf conf/config.dev conf/config -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config - +aurweb-config set database name 'aurweb' aurweb-config set database user 'aur' aurweb-config set database password 'aur' +aurweb-config set database host 'localhost' +aurweb-config set database socket '/var/run/mysqld/mysqld.sock' +aurweb-config unset database port + +if [ ! -z ${NO_INITDB+x} ]; then + exec "$@" +fi python -m aurweb.initdb 2>/dev/null || /bin/true - exec "$@" diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh index 1756718d..dc1a91de 100755 --- a/docker/php-entrypoint.sh +++ b/docker/php-entrypoint.sh @@ -5,16 +5,8 @@ for archive in packages pkgbase users packages-meta-v1.json packages-meta-ext-v1 ln -vsf /var/lib/aurweb/archives/${archive}.gz /aurweb/web/html/${archive}.gz done -# Setup a config for our mysql db. -cp -vf conf/config.dev conf/config -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config - # Setup database. -aurweb-config set database user 'aur' -aurweb-config set database password 'aur' -aurweb-config set database host 'localhost' -aurweb-config set database socket '/var/lib/mysqld/mysqld.sock' -aurweb-config unset database port +NO_INITDB=1 /docker/mariadb-init-entrypoint.sh # Setup some other options. aurweb-config set options cache 'memcache' diff --git a/docker/test-mysql-entrypoint.sh b/docker/test-mysql-entrypoint.sh index 262577a6..1bf85b54 100755 --- a/docker/test-mysql-entrypoint.sh +++ b/docker/test-mysql-entrypoint.sh @@ -1,10 +1,6 @@ #!/bin/bash set -eou pipefail -# Setup a config for our mysql db. -cp -vf conf/config.dev conf/config -sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config - # We use the root user for testing in Docker. # The test user must be able to create databases and drop them. aurweb-config set database user 'root' From 3a65e33abe01e6ef6ae1a246062b0fe5ed2c8f09 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 27 Nov 2021 23:34:05 -0800 Subject: [PATCH 0728/1451] fix(gitlab-ci): prepare conf/config for setup Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8980fa78..d6d49a55 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,6 +9,7 @@ variables: AUR_CONFIG: conf/config # Default MySQL config setup in before_script. DB_HOST: localhost TEST_RECURSION_LIMIT: 10000 + CURRENT_DIR: "$(pwd)" test: stage: test @@ -22,6 +23,8 @@ test: - ./docker/mariadb-entrypoint.sh - (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & - 'until : > /dev/tcp/127.0.0.1/3306; do sleep 1s; done' + - cp -v conf/config.dev conf/config + - sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config - ./docker/test-mysql-entrypoint.sh # Create mysql AUR_CONFIG. - make -C po all install - make -C test clean From 3efb9a57b59297fb844c75306351c2817ca2f4b0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 07:54:52 -0800 Subject: [PATCH 0729/1451] change(popupdate): converted to use aurweb.db ORM Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 9 ++-- aurweb/scripts/popupdate.py | 84 +++++++++++++++++++++++-------------- test/test_rpc.py | 5 +-- 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 23f44ee3..eab75e5a 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -908,8 +908,7 @@ async def pkgbase_vote(request: Request, name: str): VoteTS=now) # Update NumVotes/Popularity. - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - popupdate.run_single(conn, pkgbase) + popupdate.run_single(pkgbase) return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -929,8 +928,7 @@ async def pkgbase_unvote(request: Request, name: str): db.delete(vote) # Update NumVotes/Popularity. - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - popupdate.run_single(conn, pkgbase) + popupdate.run_single(pkgbase) return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -1473,8 +1471,7 @@ async def pkgbase_merge_post(request: Request, name: str, pkgbase_merge_instance(request, pkgbase, target) # Run popupdate on the target. - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - popupdate.run_single(conn, target) + popupdate.run_single(target) if not next: next = f"/pkgbase/{target.Name}" diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index db4ba170..e2d008f2 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -1,51 +1,71 @@ #!/usr/bin/env python3 from datetime import datetime +from typing import List -import aurweb.db +from sqlalchemy import and_, func +from sqlalchemy.sql.functions import coalesce +from sqlalchemy.sql.functions import sum as _sum + +from aurweb import db +from aurweb.models import PackageBase, PackageVote -def run_single(conn, pkgbase): +def run_variable(pkgbases: List[PackageBase] = []) -> None: + """ + Update popularity on a list of PackageBases. + + If no PackageBase is included, we update the popularity + of every PackageBase in the database. + + :param pkgbases: List of PackageBase instances + """ + now = int(datetime.utcnow().timestamp()) + + # NumVotes subquery. + votes_subq = db.get_session().query( + func.count("*") + ).select_from(PackageVote).filter( + PackageVote.PackageBaseID == PackageBase.ID + ) + + # Popularity subquery. + pop_subq = db.get_session().query( + coalesce(_sum(func.pow(0.98, (now - PackageVote.VoteTS) / 86400)), 0.0), + ).select_from(PackageVote).filter( + and_(PackageVote.PackageBaseID == PackageBase.ID, + PackageVote.VoteTS.isnot(None)) + ) + + with db.begin(): + query = db.query(PackageBase) + + ids = set() + if pkgbases: + ids = {pkgbase.ID for pkgbase in pkgbases} + query = query.filter(PackageBase.ID.in_(ids)) + + query.update({ + "NumVotes": votes_subq.scalar_subquery(), + "Popularity": pop_subq.scalar_subquery() + }) + + +def run_single(pkgbase: PackageBase) -> None: """ A single popupdate. The given pkgbase instance will be refreshed after the database update is done. NOTE: This function is compatible only with aurweb FastAPI. - :param conn: db.Connection[Executor] :param pkgbase: Instance of db.PackageBase """ - - conn.execute("UPDATE PackageBases SET NumVotes = (" - "SELECT COUNT(*) FROM PackageVotes " - "WHERE PackageVotes.PackageBaseID = PackageBases.ID) " - "WHERE PackageBases.ID = ?", [pkgbase.ID]) - - now = int(datetime.utcnow().timestamp()) - conn.execute("UPDATE PackageBases SET Popularity = (" - "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " - "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " - "PackageBases.ID AND NOT VoteTS IS NULL) WHERE " - "PackageBases.ID = ?", [now, pkgbase.ID]) - - conn.commit() - conn.close() - aurweb.db.refresh(pkgbase) + run_variable([pkgbase]) + db.refresh(pkgbase) def main(): - conn = aurweb.db.Connection() - conn.execute("UPDATE PackageBases SET NumVotes = (" - "SELECT COUNT(*) FROM PackageVotes " - "WHERE PackageVotes.PackageBaseID = PackageBases.ID)") - - now = int(datetime.utcnow().timestamp()) - conn.execute("UPDATE PackageBases SET Popularity = (" - "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " - "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " - "PackageBases.ID AND NOT VoteTS IS NULL)", [now]) - - conn.commit() - conn.close() + db.get_engine() + run_variable() if __name__ == '__main__': diff --git a/test/test_rpc.py b/test/test_rpc.py index a4cdb5da..b61a7e4e 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -9,7 +9,7 @@ import pytest from fastapi.testclient import TestClient from redis.client import Pipeline -from aurweb import asgi, config, db, scripts +from aurweb import asgi, config, scripts from aurweb.db import begin, create, query from aurweb.models.account_type import AccountType from aurweb.models.dependency_type import DependencyType @@ -187,8 +187,7 @@ def setup(db_test): PackageBase=pkgbase1, VoteTS=5000) - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - scripts.popupdate.run_single(conn, pkgbase1) + scripts.popupdate.run_single(pkgbase1) @pytest.fixture From 29989b7fdbb6f8a5bacfd6edef48cb66b483b722 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 08:04:33 -0800 Subject: [PATCH 0730/1451] change(aurblup): converted to use aurweb.db ORM Introduces: - aurweb.testing.alpm.AlpmDatabase - Used to mock up and manage a remote repository. - templates/testing/alpm_package.j2 - Used to generate a single ALPM package desc. - Removed aurblup sharness test Signed-off-by: Kevin Morris --- aurweb/scripts/aurblup.py | 50 ++++++++++------- aurweb/templates.py | 5 ++ aurweb/testing/alpm.py | 87 ++++++++++++++++++++++++++++++ aurweb/util.py | 17 ++++++ templates/testing/alpm_package.j2 | 16 ++++++ test/t2400-aurblup.t | 53 ------------------ test/test_aurblup.py | 90 +++++++++++++++++++++++++++++++ 7 files changed, 246 insertions(+), 72 deletions(-) create mode 100644 aurweb/testing/alpm.py create mode 100644 templates/testing/alpm_package.j2 delete mode 100755 test/t2400-aurblup.t create mode 100644 test/test_aurblup.py diff --git a/aurweb/scripts/aurblup.py b/aurweb/scripts/aurblup.py index e32937ce..9c9059ec 100755 --- a/aurweb/scripts/aurblup.py +++ b/aurweb/scripts/aurblup.py @@ -4,30 +4,34 @@ import re import pyalpm +from sqlalchemy import and_ + import aurweb.config -import aurweb.db -db_path = aurweb.config.get('aurblup', 'db-path') -sync_dbs = aurweb.config.get('aurblup', 'sync-dbs').split(' ') -server = aurweb.config.get('aurblup', 'server') +from aurweb import db, util +from aurweb.models import OfficialProvider -def main(): +def _main(force: bool = False): blacklist = set() providers = set() repomap = dict() + db_path = aurweb.config.get("aurblup", "db-path") + sync_dbs = aurweb.config.get('aurblup', 'sync-dbs').split(' ') + server = aurweb.config.get('aurblup', 'server') + h = pyalpm.Handle("/", db_path) for sync_db in sync_dbs: repo = h.register_syncdb(sync_db, pyalpm.SIG_DATABASE_OPTIONAL) repo.servers = [server.replace("%s", sync_db)] t = h.init_transaction() - repo.update(False) + repo.update(force) t.release() for pkg in repo.pkgcache: blacklist.add(pkg.name) - [blacklist.add(x) for x in pkg.replaces] + util.apply_all(pkg.replaces, blacklist.add) providers.add((pkg.name, pkg.name)) repomap[(pkg.name, pkg.name)] = repo.name for provision in pkg.provides: @@ -35,21 +39,29 @@ def main(): providers.add((pkg.name, provisionname)) repomap[(pkg.name, provisionname)] = repo.name - conn = aurweb.db.Connection() + with db.begin(): + old_providers = set( + db.query(OfficialProvider).with_entities( + OfficialProvider.Name.label("Name"), + OfficialProvider.Provides.label("Provides") + ).distinct().order_by("Name").all() + ) - cur = conn.execute("SELECT Name, Provides FROM OfficialProviders") - oldproviders = set(cur.fetchall()) + for name, provides in old_providers.difference(providers): + db.delete_all(db.query(OfficialProvider).filter( + and_(OfficialProvider.Name == name, + OfficialProvider.Provides == provides) + )) - for pkg, provides in oldproviders.difference(providers): - conn.execute("DELETE FROM OfficialProviders " - "WHERE Name = ? AND Provides = ?", [pkg, provides]) - for pkg, provides in providers.difference(oldproviders): - repo = repomap[(pkg, provides)] - conn.execute("INSERT INTO OfficialProviders (Name, Repo, Provides) " - "VALUES (?, ?, ?)", [pkg, repo, provides]) + for name, provides in providers.difference(old_providers): + repo = repomap.get((name, provides)) + db.create(OfficialProvider, Name=name, + Repo=repo, Provides=provides) - conn.commit() - conn.close() + +def main(force: bool = False): + db.get_engine() + _main(force) if __name__ == '__main__': diff --git a/aurweb/templates.py b/aurweb/templates.py index 0039535d..a7102ae1 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -125,6 +125,11 @@ async def make_variable_context(request: Request, title: str, next: str = None): return context +def base_template(path: str): + templates = copy.copy(_env) + return templates.get_template(path) + + def render_raw_template(request: Request, path: str, context: dict): """ Render a Jinja2 multi-lingual template with some context. """ # Create a deep copy of our jinja2 _environment. The _environment in diff --git a/aurweb/testing/alpm.py b/aurweb/testing/alpm.py new file mode 100644 index 00000000..6015d859 --- /dev/null +++ b/aurweb/testing/alpm.py @@ -0,0 +1,87 @@ +import hashlib +import os +import re +import shutil +import subprocess + +from typing import List + +from aurweb import logging, util +from aurweb.templates import base_template + +logger = logging.get_logger(__name__) + + +class AlpmDatabase: + """ + Fake libalpm database management class. + + This class can be used to add or remove packages from a + test repository. + """ + repo = "test" + + def __init__(self, database_root: str): + self.root = database_root + self.local = os.path.join(self.root, "local") + self.remote = os.path.join(self.root, "remote") + self.repopath = os.path.join(self.remote, self.repo) + + # Make directories. + os.makedirs(self.local) + os.makedirs(self.remote) + + def _get_pkgdir(self, pkgname: str, pkgver: str, repo: str) -> str: + pkgfile = f"{pkgname}-{pkgver}-1" + pkgdir = os.path.join(self.remote, repo, pkgfile) + os.makedirs(pkgdir) + return pkgdir + + def add(self, pkgname: str, pkgver: str, arch: str, + provides: List[str] = []) -> None: + context = { + "pkgname": pkgname, + "pkgver": pkgver, + "arch": arch, + "provides": provides + } + template = base_template("testing/alpm_package.j2") + pkgdir = self._get_pkgdir(pkgname, pkgver, self.repo) + desc = os.path.join(pkgdir, "desc") + with open(desc, "w") as f: + f.write(template.render(context)) + + self.compile() + + def remove(self, pkgname: str): + files = os.listdir(self.repopath) + logger.info(f"Files: {files}") + expr = "^" + pkgname + r"-[0-9.]+-1$" + logger.info(f"Expression: {expr}") + to_delete = filter(lambda e: re.match(expr, e), files) + + for target in to_delete: + logger.info(f"Deleting {target}") + path = os.path.join(self.repopath, target) + shutil.rmtree(path) + + self.compile() + + def clean(self) -> None: + db_file = os.path.join(self.remote, "test.db") + try: + os.remove(db_file) + except Exception: + pass + + def compile(self) -> None: + self.clean() + cmdline = ["bash", "-c", "bsdtar -czvf ../test.db *"] + proc = subprocess.run(cmdline, cwd=self.repopath) + assert proc.returncode == 0, \ + f"Bad return code while creating alpm database: {proc.returncode}" + + # Print out the md5 hash value of the new test.db. + test_db = os.path.join(self.remote, "test.db") + db_hash = util.file_hash(test_db, hashlib.md5) + logger.debug(f"{test_db}: {db_hash}") diff --git a/aurweb/util.py b/aurweb/util.py index 62575c71..bf2d6e4b 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -176,3 +176,20 @@ def strtobool(value: str) -> bool: if isinstance(value, str): return _strtobool(value) return value + + +def file_hash(filepath: str, hash_function: Callable) -> str: + """ + Return a hash of filepath contents using `hash_function`. + + `hash_function` can be any one of the hashlib module's hash + functions which implement the `hexdigest()` method -- e.g. + hashlib.sha1, hashlib.md5, etc. + + :param filepath: Path to file you want to hash + :param hash_function: hashlib hash function + :return: hash_function(filepath_content).hexdigest() + """ + with open(filepath, "rb") as f: + hash_ = hash_function(f.read()) + return hash_.hexdigest() diff --git a/templates/testing/alpm_package.j2 b/templates/testing/alpm_package.j2 new file mode 100644 index 00000000..0e741729 --- /dev/null +++ b/templates/testing/alpm_package.j2 @@ -0,0 +1,16 @@ +%FILENAME% +{{ pkgname }}-{{ pkgver }}-{{ arch }}.pkg.tar.xz + +%NAME% +{{ pkgname }} + +%VERSION% +{{ pkgver }}-1 + +%ARCH% +{{ arch }} + +{% if provides %} +%PROVIDES% +{{ provides | join("\n") }} +{% endif %} diff --git a/test/t2400-aurblup.t b/test/t2400-aurblup.t deleted file mode 100755 index 42da6791..00000000 --- a/test/t2400-aurblup.t +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -test_description='aurblup tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test official provider update script.' ' - mkdir -p remote/test/foobar-1.0-1 && - cat <<-EOD >remote/test/foobar-1.0-1/desc && - %FILENAME% - foobar-1.0-any.pkg.tar.xz - - %NAME% - foobar - - %VERSION% - 1.0-1 - - %ARCH% - any - EOD - mkdir -p remote/test/foobar2-1.0-1 && - cat <<-EOD >remote/test/foobar2-1.0-1/desc && - %FILENAME% - foobar2-1.0-any.pkg.tar.xz - - %NAME% - foobar2 - - %VERSION% - 1.0-1 - - %ARCH% - any - - %PROVIDES% - foobar3 - foobar4 - EOD - ( cd remote/test && bsdtar -czf ../test.db * ) && - mkdir sync && - cover "$AURBLUP" && - cat <<-EOD >expected && - foobar|test|foobar - foobar2|test|foobar2 - foobar2|test|foobar3 - foobar2|test|foobar4 - EOD - echo "SELECT Name, Repo, Provides FROM OfficialProviders ORDER BY Provides;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_done diff --git a/test/test_aurblup.py b/test/test_aurblup.py new file mode 100644 index 00000000..7eaae556 --- /dev/null +++ b/test/test_aurblup.py @@ -0,0 +1,90 @@ +import tempfile + +from unittest import mock + +import pytest + +from aurweb import config, db +from aurweb.models import OfficialProvider +from aurweb.scripts import aurblup +from aurweb.testing.alpm import AlpmDatabase + + +@pytest.fixture +def tempdir() -> str: + with tempfile.TemporaryDirectory() as name: + yield name + + +@pytest.fixture +def alpm_db(tempdir: str) -> AlpmDatabase: + yield AlpmDatabase(tempdir) + + +@pytest.fixture(autouse=True) +def setup(db_test, alpm_db: AlpmDatabase, tempdir: str) -> None: + config_get = config.get + + def mock_config_get(section: str, key: str) -> str: + value = config_get(section, key) + if section == "aurblup": + if key == "db-path": + return alpm_db.local + elif key == "server": + return f'file://{alpm_db.remote}' + elif key == "sync-dbs": + return alpm_db.repo + return value + + with mock.patch("aurweb.config.get", side_effect=mock_config_get): + config.rehash() + yield + config.rehash() + + +def test_aurblup(alpm_db: AlpmDatabase): + # Test that we can add a package. + alpm_db.add("pkg", "1.0", "x86_64", provides=["pkg2", "pkg3"]) + alpm_db.add("pkg2", "2.0", "x86_64") + aurblup.main() + + # Test that the package got added to the database. + for name in ("pkg", "pkg2"): + pkg = db.query(OfficialProvider).filter( + OfficialProvider.Name == name).first() + assert pkg is not None + + # Test that we can remove the package. + alpm_db.remove("pkg") + + # Run aurblup again with forced repository update. + aurblup.main(True) + + # Expect that the database got updated accordingly. + pkg = db.query(OfficialProvider).filter( + OfficialProvider.Name == "pkg").first() + assert pkg is None + pkg2 = db.query(OfficialProvider).filter( + OfficialProvider.Name == "pkg2").first() + assert pkg2 is not None + + +def test_aurblup_cleanup(alpm_db: AlpmDatabase): + # Add a package and sync up the database. + alpm_db.add("pkg", "1.0", "x86_64", provides=["pkg2", "pkg3"]) + aurblup.main() + + # Now, let's insert an OfficialPackage that doesn't exist, + # then exercise the old provider deletion path. + with db.begin(): + db.create(OfficialProvider, Name="fake package", + Repo="test", Provides="package") + + # Run aurblup again. + aurblup.main() + + # Expect that the fake package got deleted because it's + # not in alpm_db anymore. + providers = db.query(OfficialProvider).filter( + OfficialProvider.Name == "fake package").all() + assert len(providers) == 0 From c59acbf6d6594971b3953807256e102dd2e740e9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 21 Nov 2021 22:37:31 -0800 Subject: [PATCH 0731/1451] add noop testing utility Signed-off-by: Kevin Morris --- aurweb/testing/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 8261051d..99671d69 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -62,3 +62,7 @@ def setup_test_db(*args): aurweb.db.get_session().execute(f"DELETE FROM {table}") aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 1") aurweb.db.get_session().expunge_all() + + +def noop(*args, **kwargs) -> None: + return From 29c2d0de6b83a2287ffdb885d9b55aa63b1d4792 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 21 Nov 2021 00:47:48 -0800 Subject: [PATCH 0732/1451] change(mkpkglists): converted to use aurweb.db ORM - Improved speed dramatically - Removed mkpkglists sharness Signed-off-by: Kevin Morris --- aurweb/benchmark.py | 21 +++ aurweb/scripts/mkpkglists.py | 282 +++++++++++++++++++++-------------- test/t2100-mkpkglists.t | 65 -------- test/test_mkpkglists.py | 215 ++++++++++++++++++++++++++ 4 files changed, 403 insertions(+), 180 deletions(-) create mode 100644 aurweb/benchmark.py delete mode 100755 test/t2100-mkpkglists.t create mode 100644 test/test_mkpkglists.py diff --git a/aurweb/benchmark.py b/aurweb/benchmark.py new file mode 100644 index 00000000..7086fb08 --- /dev/null +++ b/aurweb/benchmark.py @@ -0,0 +1,21 @@ +from datetime import datetime + + +class Benchmark: + def __init__(self): + self.start() + + def _timestamp(self) -> float: + """ Generate a timestamp. """ + return float(datetime.utcnow().timestamp()) + + def start(self) -> int: + """ Start a benchmark. """ + self.current = self._timestamp() + return self.current + + def end(self): + """ Return the diff between now - start(). """ + n = self._timestamp() - self.current + self.current = float(0) + return n diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 307b2b12..92de7931 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -23,23 +23,28 @@ import os import sys from collections import defaultdict -from decimal import Decimal +from typing import Any, Dict import orjson +from sqlalchemy import literal, orm + import aurweb.config -import aurweb.db + +from aurweb import db, logging, models, util +from aurweb.benchmark import Benchmark +from aurweb.models import Package, PackageBase, User + +logger = logging.get_logger("aurweb.scripts.mkpkglists") archivedir = aurweb.config.get("mkpkglists", "archivedir") os.makedirs(archivedir, exist_ok=True) -packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') -packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') -packagesmetaextfile = aurweb.config.get('mkpkglists', 'packagesmetaextfile') - -pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') - -userfile = aurweb.config.get('mkpkglists', 'userfile') +PACKAGES = aurweb.config.get('mkpkglists', 'packagesfile') +META = aurweb.config.get('mkpkglists', 'packagesmetafile') +META_EXT = aurweb.config.get('mkpkglists', 'packagesmetaextfile') +PKGBASE = aurweb.config.get('mkpkglists', 'pkgbasefile') +USERS = aurweb.config.get('mkpkglists', 'userfile') TYPE_MAP = { @@ -53,7 +58,7 @@ TYPE_MAP = { } -def get_extended_dict(query: str): +def get_extended_dict(query: orm.Query): """ Produce data in the form in a single bulk SQL query: @@ -74,61 +79,75 @@ def get_extended_dict(query: str): output[i].update(data.get(package_id)) """ - conn = aurweb.db.Connection() - - cursor = conn.execute(query) - data = defaultdict(lambda: defaultdict(list)) - for result in cursor.fetchall(): - + for result in query: pkgid = result[0] key = TYPE_MAP.get(result[1], result[1]) output = result[2] if result[3]: output += result[3] - - # In all cases, we have at least an empty License list. - if "License" not in data[pkgid]: - data[pkgid]["License"] = [] - - # In all cases, we have at least an empty Keywords list. - if "Keywords" not in data[pkgid]: - data[pkgid]["Keywords"] = [] - data[pkgid][key].append(output) - conn.close() return data def get_extended_fields(): - # Returns: [ID, Type, Name, Cond] - query = """ - SELECT PackageDepends.PackageID AS ID, DependencyTypes.Name AS Type, - PackageDepends.DepName AS Name, PackageDepends.DepCondition AS Cond - FROM PackageDepends - LEFT JOIN DependencyTypes - ON DependencyTypes.ID = PackageDepends.DepTypeID - UNION SELECT PackageRelations.PackageID AS ID, RelationTypes.Name AS Type, - PackageRelations.RelName AS Name, - PackageRelations.RelCondition AS Cond - FROM PackageRelations - LEFT JOIN RelationTypes - ON RelationTypes.ID = PackageRelations.RelTypeID - UNION SELECT PackageGroups.PackageID AS ID, 'Groups' AS Type, - Groups.Name, '' AS Cond - FROM Groups - INNER JOIN PackageGroups ON PackageGroups.GroupID = Groups.ID - UNION SELECT PackageLicenses.PackageID AS ID, 'License' AS Type, - Licenses.Name, '' as Cond - FROM Licenses - INNER JOIN PackageLicenses ON PackageLicenses.LicenseID = Licenses.ID - UNION SELECT Packages.ID AS ID, 'Keywords' AS Type, - PackageKeywords.Keyword AS Name, '' as Cond - FROM PackageKeywords - INNER JOIN Packages ON Packages.PackageBaseID = PackageKeywords.PackageBaseID - """ + subqueries = [ + # PackageDependency + db.query( + models.PackageDependency + ).join(models.DependencyType).with_entities( + models.PackageDependency.PackageID.label("ID"), + models.DependencyType.Name.label("Type"), + models.PackageDependency.DepName.label("Name"), + models.PackageDependency.DepCondition.label("Cond") + ).distinct().order_by("Name"), + + # PackageRelation + db.query( + models.PackageRelation + ).join(models.RelationType).with_entities( + models.PackageRelation.PackageID.label("ID"), + models.RelationType.Name.label("Type"), + models.PackageRelation.RelName.label("Name"), + models.PackageRelation.RelCondition.label("Cond") + ).distinct().order_by("Name"), + + # Groups + db.query(models.PackageGroup).join( + models.Group, + models.PackageGroup.GroupID == models.Group.ID + ).with_entities( + models.PackageGroup.PackageID.label("ID"), + literal("Groups").label("Type"), + models.Group.Name.label("Name"), + literal(str()).label("Cond") + ).distinct().order_by("Name"), + + # Licenses + db.query(models.PackageLicense).join( + models.License, + models.PackageLicense.LicenseID == models.License.ID + ).with_entities( + models.PackageLicense.PackageID.label("ID"), + literal("License").label("Type"), + models.License.Name.label("Name"), + literal(str()).label("Cond") + ).distinct().order_by("Name"), + + # Keywords + db.query(models.PackageKeyword).join( + models.Package, + Package.PackageBaseID == models.PackageKeyword.PackageBaseID + ).with_entities( + models.Package.ID.label("ID"), + literal("Keywords").label("Type"), + models.PackageKeyword.Keyword.label("Name"), + literal(str()).label("Cond") + ).distinct().order_by("Name") + ] + query = subqueries[0].union_all(*subqueries[1:]) return get_extended_dict(query) @@ -137,89 +156,122 @@ EXTENDED_FIELD_HANDLERS = { } -def is_decimal(column): - """ Check if an SQL column is of decimal.Decimal type. """ - if isinstance(column, Decimal): - return float(column) - return column +def as_dict(package: Package) -> Dict[str, Any]: + return { + "ID": package.ID, + "Name": package.Name, + "PackageBaseID": package.PackageBaseID, + "PackageBase": package.PackageBase, + "Version": package.Version, + "Description": package.Description, + "NumVotes": package.NumVotes, + "Popularity": float(package.Popularity), + "OutOfDate": package.OutOfDate, + "Maintainer": package.Maintainer, + "FirstSubmitted": package.FirstSubmitted, + "LastModified": package.LastModified, + } -def write_archive(archive: str, output: list): - with gzip.open(archive, "wb") as f: - f.write(b"[\n") - for i, item in enumerate(output): - f.write(orjson.dumps(item)) - if i < len(output) - 1: - f.write(b",") - f.write(b"\n") - f.write(b"]") +def _main(): + bench = Benchmark() + logger.info("Started re-creating archives, wait a while...") - -def main(): - conn = aurweb.db.Connection() - - # Query columns; copied from RPC. - columns = ("Packages.ID, Packages.Name, " - "PackageBases.ID AS PackageBaseID, " - "PackageBases.Name AS PackageBase, " - "Version, Description, URL, NumVotes, " - "Popularity, OutOfDateTS AS OutOfDate, " - "Users.UserName AS Maintainer, " - "SubmittedTS AS FirstSubmitted, " - "ModifiedTS AS LastModified") - - # Perform query. - cur = conn.execute(f"SELECT {columns} FROM Packages " - "LEFT JOIN PackageBases " - "ON PackageBases.ID = Packages.PackageBaseID " - "LEFT JOIN Users " - "ON PackageBases.MaintainerUID = Users.ID " - "WHERE PackageBases.PackagerUID IS NOT NULL") + query = db.query(Package).join( + PackageBase, + PackageBase.ID == Package.PackageBaseID + ).join( + User, + PackageBase.MaintainerUID == User.ID, + isouter=True + ).filter(PackageBase.PackagerUID.isnot(None)).with_entities( + Package.ID, + Package.Name, + PackageBase.ID.label("PackageBaseID"), + PackageBase.Name.label("PackageBase"), + Package.Version, + Package.Description, + PackageBase.NumVotes, + PackageBase.Popularity, + PackageBase.OutOfDateTS.label("OutOfDate"), + User.Username.label("Maintainer"), + PackageBase.SubmittedTS.label("FirstSubmitted"), + PackageBase.ModifiedTS.label("LastModified") + ).distinct().order_by("Name") # Produce packages-meta-v1.json.gz output = list() snapshot_uri = aurweb.config.get("options", "snapshot_uri") - for result in cur.fetchall(): - item = { - column[0]: is_decimal(result[i]) - for i, column in enumerate(cur.description) - } - item["URLPath"] = snapshot_uri % item.get("Name") - output.append(item) + gzips = { + "packages": gzip.open(PACKAGES, "wt"), + "meta": gzip.open(META, "wb"), + } - write_archive(packagesmetafile, output) + # Append list opening to the metafile. + gzips["meta"].write(b"[\n") - # Produce packages-meta-ext-v1.json.gz + # Produce packages.gz + packages-meta-ext-v1.json.gz + extended = False if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: + gzips["meta_ext"] = gzip.open(META_EXT, "wb") + # Append list opening to the meta_ext file. + gzips.get("meta_ext").write(b"[\n") f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) data = f() + extended = True - default_ = {"Groups": [], "License": [], "Keywords": []} - for i in range(len(output)): - data_ = data.get(output[i].get("ID"), default_) - output[i].update(data_) + results = query.all() + n = len(results) - 1 + for i, result in enumerate(results): + # Append to packages.gz. + gzips.get("packages").write(f"{result.Name}\n") - write_archive(packagesmetaextfile, output) + # Construct our result JSON dictionary. + item = as_dict(result) + item["URLPath"] = snapshot_uri % result.Name - # Produce packages.gz - with gzip.open(packagesfile, "wb") as f: - f.writelines([ - bytes(x.get("Name") + "\n", "UTF-8") - for x in output - ]) + # We stream out package json objects line per line, so + # we also need to include the ',' character at the end + # of package lines (excluding the last package). + suffix = b",\n" if i < n else b'\n' + + # Write out to packagesmetafile + output.append(item) + gzips.get("meta").write(orjson.dumps(output[-1]) + suffix) + + if extended: + # Write out to packagesmetaextfile. + data_ = data.get(result.ID, {}) + output[-1].update(data_) + gzips.get("meta_ext").write(orjson.dumps(output[-1]) + suffix) + + # Append the list closing to meta/meta_ext. + gzips.get("meta").write(b"]") + if extended: + gzips.get("meta_ext").write(b"]") + + # Close gzip files. + util.apply_all(gzips.values(), lambda gz: gz.close()) # Produce pkgbase.gz - with gzip.open(pkgbasefile, "w") as f: - cur = conn.execute("SELECT Name FROM PackageBases " + - "WHERE PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + query = db.query(PackageBase.Name).filter( + PackageBase.PackagerUID.isnot(None)).all() + with gzip.open(PKGBASE, "wt") as f: + f.writelines([f"{base.Name}\n" for i, base in enumerate(query)]) # Produce users.gz - with gzip.open(userfile, "w") as f: - cur = conn.execute("SELECT UserName FROM Users") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + query = db.query(User.Username).all() + with gzip.open(USERS, "wt") as f: + f.writelines([f"{user.Username}\n" for i, user in enumerate(query)]) - conn.close() + seconds = util.number_format(bench.end(), 4) + logger.info(f"Completed in {seconds} seconds.") + + +def main(): + db.get_engine() + with db.begin(): + _main() if __name__ == '__main__': diff --git a/test/t2100-mkpkglists.t b/test/t2100-mkpkglists.t deleted file mode 100755 index d217c4f6..00000000 --- a/test/t2100-mkpkglists.t +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/sh - -test_description='mkpkglists tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test package list generation with no packages.' ' - echo "DELETE FROM Packages;" | sqlite3 aur.db && - echo "DELETE FROM PackageBases;" | sqlite3 aur.db && - cover "$MKPKGLISTS" && - test $(zcat packages.gz | wc -l) -eq 0 && - test $(zcat pkgbase.gz | wc -l) -eq 0 -' - -test_expect_success 'Test package list generation.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1, "foobar", 1, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (2, "foobar2", 2, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (3, "foobar3", NULL, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (4, "foobar4", 1, 0, 0, ""); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (1, 1, "pkg1"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (2, 1, "pkg2"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (3, 1, "pkg3"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (4, 2, "pkg4"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (5, 3, "pkg5"); - EOD - cover "$MKPKGLISTS" && - cat <<-EOD >expected && - foobar - foobar2 - foobar4 - EOD - gunzip pkgbase.gz && - sed "/^#/d" pkgbase >actual && - test_cmp actual expected && - cat <<-EOD >expected && - pkg1 - pkg2 - pkg3 - pkg4 - EOD - gunzip packages.gz && - sed "/^#/d" packages >actual && - test_cmp actual expected -' - -test_expect_success 'Test user list generation.' ' - cover "$MKPKGLISTS" && - cat <<-EOD >expected && - dev - tu - tu2 - tu3 - tu4 - user - user2 - user3 - user4 - EOD - gunzip users.gz && - sed "/^#/d" users >actual && - test_cmp actual expected -' - -test_done diff --git a/test/test_mkpkglists.py b/test/test_mkpkglists.py new file mode 100644 index 00000000..ee66e4e1 --- /dev/null +++ b/test/test_mkpkglists.py @@ -0,0 +1,215 @@ +import json + +from typing import List, Union +from unittest import mock + +import pytest + +from aurweb import config, db, util +from aurweb.models import License, Package, PackageBase, PackageDependency, PackageLicense, User +from aurweb.models.account_type import USER_ID +from aurweb.models.dependency_type import DEPENDS_ID +from aurweb.testing import noop + + +class FakeFile: + data = str() + __exit__ = noop + + def __init__(self, modes: str) -> "FakeFile": + self.modes = modes + + def __enter__(self, *args, **kwargs) -> "FakeFile": + return self + + def write(self, data: Union[str, bytes]) -> None: + if isinstance(data, bytes): + data = data.decode() + self.data += data + + def writelines(self, dataset: List[Union[str, bytes]]) -> None: + util.apply_all(dataset, self.write) + + def close(self) -> None: + return + + +class MockGzipOpen: + def __init__(self): + self.gzips = dict() + + def open(self, archive: str, modes: str): + self.gzips[archive] = FakeFile(modes) + return self.gzips.get(archive) + + def get(self, key: str) -> FakeFile: + return self.gzips.get(key) + + def __getitem__(self, key: str) -> FakeFile: + return self.get(key) + + def __contains__(self, key: str) -> bool: + return key in self.gzips + + def data(self, archive: str): + return self.get(archive).data + + +@pytest.fixture(autouse=True) +def setup(db_test): + config.rehash() + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def packages(user: User) -> List[Package]: + output = [] + with db.begin(): + lic = db.create(License, Name="GPL") + for i in range(5): + # Create the package. + pkgbase = db.create(PackageBase, Name=f"pkgbase_{i}", + Packager=user) + pkg = db.create(Package, PackageBase=pkgbase, + Name=f"pkg_{i}") + + # Create some related records. + db.create(PackageLicense, Package=pkg, License=lic) + db.create(PackageDependency, DepTypeID=DEPENDS_ID, + Package=pkg, DepName=f"dep_{i}", + DepCondition=">=1.0") + + # Add the package to our output list. + output.append(pkg) + + # Sort output by the package name and return it. + yield sorted(output, key=lambda k: k.Name) + + +@mock.patch("os.makedirs", side_effect=noop) +def test_mkpkglists_empty(makedirs: mock.MagicMock): + gzips = MockGzipOpen() + with mock.patch("gzip.open", side_effect=gzips.open): + from aurweb.scripts import mkpkglists + mkpkglists.main() + + archives = config.get_section("mkpkglists") + archives.pop("archivedir") + archives.pop("packagesmetaextfile") + + for archive in archives.values(): + assert archive in gzips + + # Expect that packagesfile got created, but is empty because + # we have no DB records. + packages_file = archives.get("packagesfile") + assert gzips.data(packages_file) == str() + + # Expect that pkgbasefile got created, but is empty because + # we have no DB records. + users_file = archives.get("pkgbasefile") + assert gzips.data(users_file) == str() + + # Expect that userfile got created, but is empty because + # we have no DB records. + users_file = archives.get("userfile") + assert gzips.data(users_file) == str() + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetafile") + assert gzips.data(meta_file) == "[\n]" + + +@mock.patch("sys.argv", ["mkpkglists", "--extended"]) +@mock.patch("os.makedirs", side_effect=noop) +def test_mkpkglists_extended_empty(makedirs: mock.MagicMock): + gzips = MockGzipOpen() + with mock.patch("gzip.open", side_effect=gzips.open): + from aurweb.scripts import mkpkglists + mkpkglists.main() + + archives = config.get_section("mkpkglists") + archives.pop("archivedir") + + for archive in archives.values(): + assert archive in gzips + + # Expect that packagesfile got created, but is empty because + # we have no DB records. + packages_file = archives.get("packagesfile") + assert gzips.data(packages_file) == str() + + # Expect that pkgbasefile got created, but is empty because + # we have no DB records. + users_file = archives.get("pkgbasefile") + assert gzips.data(users_file) == str() + + # Expect that userfile got created, but is empty because + # we have no DB records. + users_file = archives.get("userfile") + assert gzips.data(users_file) == str() + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetafile") + assert gzips.data(meta_file) == "[\n]" + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetaextfile") + assert gzips.data(meta_file) == "[\n]" + + +@mock.patch("sys.argv", ["mkpkglists", "--extended"]) +@mock.patch("os.makedirs", side_effect=noop) +def test_mkpkglists_extended(makedirs: mock.MagicMock, user: User, + packages: List[Package]): + gzips = MockGzipOpen() + with mock.patch("gzip.open", side_effect=gzips.open): + from aurweb.scripts import mkpkglists + mkpkglists.main() + + archives = config.get_section("mkpkglists") + archives.pop("archivedir") + + for archive in archives.values(): + assert archive in gzips + + # Expect that packagesfile got created, but is empty because + # we have no DB records. + packages_file = archives.get("packagesfile") + expected = "\n".join([p.Name for p in packages]) + "\n" + assert gzips.data(packages_file) == expected + + # Expect that pkgbasefile got created, but is empty because + # we have no DB records. + users_file = archives.get("pkgbasefile") + expected = "\n".join([p.PackageBase.Name for p in packages]) + "\n" + assert gzips.data(users_file) == expected + + # Expect that userfile got created, but is empty because + # we have no DB records. + users_file = archives.get("userfile") + assert gzips.data(users_file) == "test\n" + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetafile") + data = json.loads(gzips.data(meta_file)) + assert len(data) == 5 + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetaextfile") + data = json.loads(gzips.data(meta_file)) + assert len(data) == 5 From 8d5683d3f18004d44cb4277a67da12c0a88e7698 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 10:28:58 -0800 Subject: [PATCH 0733/1451] change(tuvotereminder): converted to use aurweb.db ORM - Removed tuvotereminder sharness test. - Added [tuvotereminder] section to config.defaults. - Added `range_start` option to config.defaults [tuvotereminder]. - Added `range_end` option to config.defaults [tuvotereminder]. Signed-off-by: Kevin Morris --- aurweb/scripts/tuvotereminder.py | 33 ++++++---- conf/config.defaults | 8 +++ test/t2200-tuvotereminder.t | 53 ---------------- test/test_tuvotereminder.py | 102 +++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 65 deletions(-) delete mode 100755 test/t2200-tuvotereminder.t create mode 100644 test/test_tuvotereminder.py diff --git a/aurweb/scripts/tuvotereminder.py b/aurweb/scripts/tuvotereminder.py index eb3874e1..5e860725 100755 --- a/aurweb/scripts/tuvotereminder.py +++ b/aurweb/scripts/tuvotereminder.py @@ -1,27 +1,36 @@ #!/usr/bin/env python3 -import subprocess -import time +from datetime import datetime + +from sqlalchemy import and_ import aurweb.config -import aurweb.db + +from aurweb import db +from aurweb.models import TUVoteInfo +from aurweb.scripts import notify notify_cmd = aurweb.config.get('notifications', 'notify-cmd') def main(): - conn = aurweb.db.Connection() + db.get_engine() - now = int(time.time()) - filter_from = now + 500 - filter_to = now + 172800 + now = int(datetime.utcnow().timestamp()) - cur = conn.execute("SELECT ID FROM TU_VoteInfo " + - "WHERE End >= ? AND End <= ?", - [filter_from, filter_to]) + start = aurweb.config.getint("tuvotereminder", "range_start") + filter_from = now + start - for vote_id in [row[0] for row in cur.fetchall()]: - subprocess.Popen((notify_cmd, 'tu-vote-reminder', str(vote_id))).wait() + end = aurweb.config.getint("tuvotereminder", "range_end") + filter_to = now + end + + query = db.query(TUVoteInfo.ID).filter( + and_(TUVoteInfo.End >= filter_from, + TUVoteInfo.End <= filter_to) + ) + for voteinfo in query: + notif = notify.TUVoteReminderNotification(voteinfo.ID) + notif.send() if __name__ == '__main__': diff --git a/conf/config.defaults b/conf/config.defaults index a589997b..082d51a5 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -121,3 +121,11 @@ commit_url = https://gitlab.archlinux.org/archlinux/aurweb/-/commits/%s ; Example deployment configuration step: ; sed -r "s/^;?(commit_hash) =.*$/\1 = $(git rev-parse HEAD)/" config ;commit_hash = 1234567 + +[tuvotereminder] +; Offsets used to determine when TUs should be reminded about +; votes that they should make. +; Reminders will be sent out for all votes that a TU has not yet +; voted on based on `now + range_start <= End <= now + range_end`. +range_start = 500 +range_end = 172800 diff --git a/test/t2200-tuvotereminder.t b/test/t2200-tuvotereminder.t deleted file mode 100755 index 2f3836de..00000000 --- a/test/t2200-tuvotereminder.t +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -test_description='tuvotereminder tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test Trusted User vote reminders.' ' - now=$(date -d now +%s) && - tomorrow=$(date -d tomorrow +%s) && - threedays=$(date -d "3 days" +%s) && - cat <<-EOD | sqlite3 aur.db && - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (1, "Lorem ipsum.", "user", 0, $now, 0.00, 2); - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (2, "Lorem ipsum.", "user", 0, $tomorrow, 0.00, 2); - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (3, "Lorem ipsum.", "user", 0, $tomorrow, 0.00, 2); - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (4, "Lorem ipsum.", "user", 0, $threedays, 0.00, 2); - EOD - >sendmail.out && - cover "$TUVOTEREMINDER" && - grep -q "Proposal 2" sendmail.out && - grep -q "Proposal 3" sendmail.out && - test_must_fail grep -q "Proposal 1" sendmail.out && - test_must_fail grep -q "Proposal 4" sendmail.out -' - -test_expect_success 'Check that only TUs who did not vote receive reminders.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO TU_Votes (VoteID, UserID) VALUES (1, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (2, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (3, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (4, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (1, 7); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (3, 7); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (2, 8); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (4, 8); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (1, 9); - EOD - >sendmail.out && - cover "$TUVOTEREMINDER" && - cat <<-EOD >expected && - Subject: TU Vote Reminder: Proposal 2 - To: tu2@localhost - Subject: TU Vote Reminder: Proposal 2 - To: tu4@localhost - Subject: TU Vote Reminder: Proposal 3 - To: tu3@localhost - Subject: TU Vote Reminder: Proposal 3 - To: tu4@localhost - EOD - grep "^\(Subject\|To\)" sendmail.out >sendmail.parts && - test_cmp sendmail.parts expected -' - -test_done diff --git a/test/test_tuvotereminder.py b/test/test_tuvotereminder.py new file mode 100644 index 00000000..bb898e3a --- /dev/null +++ b/test/test_tuvotereminder.py @@ -0,0 +1,102 @@ +from datetime import datetime +from typing import Tuple + +import pytest + +from aurweb import config, db +from aurweb.models import TUVote, TUVoteInfo, User +from aurweb.models.account_type import TRUSTED_USER_ID +from aurweb.scripts import tuvotereminder as reminder +from aurweb.testing.email import Email + +aur_location = config.get("options", "aur_location") + + +def create_vote(user: User, voteinfo: TUVoteInfo) -> TUVote: + with db.begin(): + vote = db.create(TUVote, User=user, VoteID=voteinfo.ID) + return vote + + +def create_user(username: str, type_id: int): + with db.begin(): + user = db.create(User, AccountTypeID=type_id, Username=username, + Email=f"{username}@example.org", Passwd=str()) + return user + + +def email_pieces(voteinfo: TUVoteInfo) -> Tuple[str, str]: + """ + Return a (subject, content) tuple based on voteinfo.ID + + :param voteinfo: TUVoteInfo instance + :return: tuple(subject, content) + """ + subject = f"TU Vote Reminder: Proposal {voteinfo.ID}" + content = (f"Please remember to cast your vote on proposal {voteinfo.ID} " + f"[1]. The voting period\nends in less than 48 hours.\n\n" + f"[1] {aur_location}/tu/?id={voteinfo.ID}") + return (subject, content) + + +@pytest.fixture +def user(db_test) -> User: + yield create_user("test", TRUSTED_USER_ID) + + +@pytest.fixture +def user2() -> User: + yield create_user("test2", TRUSTED_USER_ID) + + +@pytest.fixture +def user3() -> User: + yield create_user("test3", TRUSTED_USER_ID) + + +@pytest.fixture +def voteinfo(user: User) -> TUVoteInfo: + now = int(datetime.utcnow().timestamp()) + start = config.getint("tuvotereminder", "range_start") + with db.begin(): + voteinfo = db.create(TUVoteInfo, Agenda="Lorem ipsum.", + User=user.Username, End=(now + start + 1), + Quorum=0.00, Submitter=user, Submitted=0) + yield voteinfo + + +def test_tu_vote_reminders(user: User, user2: User, user3: User, + voteinfo: TUVoteInfo): + reminder.main() + assert Email.count() == 3 + + emails = [Email(i).parse() for i in range(1, 4)] + subject, content = email_pieces(voteinfo) + expectations = [ + # (to, content) + (user.Email, subject, content), + (user2.Email, subject, content), + (user3.Email, subject, content) + ] + for i, element in enumerate(expectations): + email, subject, content = element + assert emails[i].headers.get("To") == email + assert emails[i].headers.get("Subject") == subject + assert emails[i].body == content + + +def test_tu_vote_reminders_only_unvoted(user: User, user2: User, user3: User, + voteinfo: TUVoteInfo): + # Vote with user2 and user3; leaving only user to be notified. + create_vote(user2, voteinfo) + create_vote(user3, voteinfo) + + reminder.main() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + + subject, content = email_pieces(voteinfo) + assert email.headers.get("Subject") == subject + assert email.body == content From d097799b34e5392f577db28920f8fb27b35602f3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 10:51:44 -0800 Subject: [PATCH 0734/1451] change(usermaint): converted to use aurweb.db ORM - Removed usermaint sharness test Signed-off-by: Kevin Morris --- aurweb/scripts/usermaint.py | 34 ++++++++++++------- test/t2700-usermaint.t | 49 --------------------------- test/test_usermaint.py | 67 +++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 61 deletions(-) delete mode 100755 test/t2700-usermaint.t create mode 100644 test/test_usermaint.py diff --git a/aurweb/scripts/usermaint.py b/aurweb/scripts/usermaint.py index 1621d410..aad3e8de 100755 --- a/aurweb/scripts/usermaint.py +++ b/aurweb/scripts/usermaint.py @@ -1,21 +1,31 @@ #!/usr/bin/env python3 -import time +from datetime import datetime -import aurweb.db +from sqlalchemy import update + +from aurweb import db +from aurweb.models import User + + +def _main(): + limit_to = int(datetime.utcnow().timestamp()) - 86400 * 7 + + update_ = update(User).where( + User.LastLogin < limit_to + ).values(LastLoginIPAddress=None) + db.get_session().execute(update_) + + update_ = update(User).where( + User.LastSSHLogin < limit_to + ).values(LastSSHLoginIPAddress=None) + db.get_session().execute(update_) def main(): - conn = aurweb.db.Connection() - - limit_to = int(time.time()) - 86400 * 7 - conn.execute("UPDATE Users SET LastLoginIPAddress = NULL " + - "WHERE LastLogin < ?", [limit_to]) - conn.execute("UPDATE Users SET LastSSHLoginIPAddress = NULL " + - "WHERE LastSSHLogin < ?", [limit_to]) - - conn.commit() - conn.close() + db.get_engine() + with db.begin(): + _main() if __name__ == '__main__': diff --git a/test/t2700-usermaint.t b/test/t2700-usermaint.t deleted file mode 100755 index c119e3f4..00000000 --- a/test/t2700-usermaint.t +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh - -test_description='usermaint tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test removal of login IP addresses.' ' - now=$(date -d now +%s) && - threedaysago=$(date -d "3 days ago" +%s) && - tendaysago=$(date -d "10 days ago" +%s) && - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET LastLogin = $threedaysago, LastLoginIPAddress = "1.2.3.4" WHERE ID = 1; - UPDATE Users SET LastLogin = $tendaysago, LastLoginIPAddress = "2.3.4.5" WHERE ID = 2; - UPDATE Users SET LastLogin = $now, LastLoginIPAddress = "3.4.5.6" WHERE ID = 3; - UPDATE Users SET LastLogin = 0, LastLoginIPAddress = "4.5.6.7" WHERE ID = 4; - UPDATE Users SET LastLogin = 0, LastLoginIPAddress = "5.6.7.8" WHERE ID = 5; - UPDATE Users SET LastLogin = $tendaysago, LastLoginIPAddress = "6.7.8.9" WHERE ID = 6; - EOD - cover "$USERMAINT" && - cat <<-EOD >expected && - 1.2.3.4 - 3.4.5.6 - EOD - echo "SELECT LastLoginIPAddress FROM Users WHERE LastLoginIPAddress IS NOT NULL;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_expect_success 'Test removal of SSH login IP addresses.' ' - now=$(date -d now +%s) && - threedaysago=$(date -d "3 days ago" +%s) && - tendaysago=$(date -d "10 days ago" +%s) && - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET LastSSHLogin = $now, LastSSHLoginIPAddress = "1.2.3.4" WHERE ID = 1; - UPDATE Users SET LastSSHLogin = $threedaysago, LastSSHLoginIPAddress = "2.3.4.5" WHERE ID = 2; - UPDATE Users SET LastSSHLogin = $tendaysago, LastSSHLoginIPAddress = "3.4.5.6" WHERE ID = 3; - UPDATE Users SET LastSSHLogin = 0, LastSSHLoginIPAddress = "4.5.6.7" WHERE ID = 4; - UPDATE Users SET LastSSHLogin = 0, LastSSHLoginIPAddress = "5.6.7.8" WHERE ID = 5; - UPDATE Users SET LastSSHLogin = $tendaysago, LastSSHLoginIPAddress = "6.7.8.9" WHERE ID = 6; - EOD - cover "$USERMAINT" && - cat <<-EOD >expected && - 1.2.3.4 - 2.3.4.5 - EOD - echo "SELECT LastSSHLoginIPAddress FROM Users WHERE LastSSHLoginIPAddress IS NOT NULL;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_done diff --git a/test/test_usermaint.py b/test/test_usermaint.py new file mode 100644 index 00000000..f1af59e1 --- /dev/null +++ b/test/test_usermaint.py @@ -0,0 +1,67 @@ +from datetime import datetime + +import pytest + +from aurweb import db +from aurweb.models import User +from aurweb.models.account_type import USER_ID +from aurweb.scripts import usermaint + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd="testPassword", AccountTypeID=USER_ID) + yield user + + +def test_usermaint_noop(user: User): + """ Last[SSH]Login isn't expired in this test: usermaint is noop. """ + + now = int(datetime.utcnow().timestamp()) + with db.begin(): + user.LastLoginIPAddress = "127.0.0.1" + user.LastLogin = now - 10 + user.LastSSHLoginIPAddress = "127.0.0.1" + user.LastSSHLogin = now - 10 + + usermaint.main() + + assert user.LastLoginIPAddress == "127.0.0.1" + assert user.LastSSHLoginIPAddress == "127.0.0.1" + + +def test_usermaint(user: User): + """ + In this case, we first test that only the expired record gets + updated, but the non-expired record remains untouched. After, + we update the login time on the non-expired record and exercise + its code path. + """ + + now = int(datetime.utcnow().timestamp()) + limit_to = now - 86400 * 7 + with db.begin(): + user.LastLoginIPAddress = "127.0.0.1" + user.LastLogin = limit_to - 666 + user.LastSSHLoginIPAddress = "127.0.0.1" + user.LastSSHLogin = now - 10 + + usermaint.main() + + assert user.LastLoginIPAddress is None + assert user.LastSSHLoginIPAddress == "127.0.0.1" + + with db.begin(): + user.LastSSHLogin = limit_to - 666 + + usermaint.main() + + assert user.LastLoginIPAddress is None + assert user.LastSSHLoginIPAddress is None From f4ef02fa5b744598e366930e63adec4cdbac6706 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 11:07:31 -0800 Subject: [PATCH 0735/1451] fix(fastapi): fix Package's PackageBase backref cascade Signed-off-by: Kevin Morris --- aurweb/models/package.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aurweb/models/package.py b/aurweb/models/package.py index 8f82dadd..cfb27634 100644 --- a/aurweb/models/package.py +++ b/aurweb/models/package.py @@ -12,7 +12,8 @@ class Package(Base): __mapper_args__ = {"primary_key": [__table__.c.ID]} PackageBase = relationship( - _PackageBase, backref=backref("packages", lazy="dynamic"), + _PackageBase, backref=backref("packages", lazy="dynamic", + cascade="all, delete"), foreign_keys=[__table__.c.PackageBaseID]) def __init__(self, **kwargs): From b72bd38f76ed290a4e5607dfca177dcf0c1d9864 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 11:08:15 -0800 Subject: [PATCH 0736/1451] change(pkgmaint): converted to use aurweb.db ORM - Replaced time.time() usage with datetime.utcnow().timestamp() - Removed pkgmaint sharness test Signed-off-by: Kevin Morris --- aurweb/scripts/pkgmaint.py | 28 ++++++++++------ test/t2300-pkgmaint.t | 26 --------------- test/test_pkgmaint.py | 65 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 36 deletions(-) delete mode 100755 test/t2300-pkgmaint.t create mode 100644 test/test_pkgmaint.py diff --git a/aurweb/scripts/pkgmaint.py b/aurweb/scripts/pkgmaint.py index 36da126f..b3992e5c 100755 --- a/aurweb/scripts/pkgmaint.py +++ b/aurweb/scripts/pkgmaint.py @@ -1,19 +1,27 @@ #!/usr/bin/env python3 -import time +from datetime import datetime -import aurweb.db +from sqlalchemy import and_ + +from aurweb import db +from aurweb.models import PackageBase + + +def _main(): + # One day behind. + limit_to = int(datetime.utcnow().timestamp()) - 86400 + + query = db.query(PackageBase).filter( + and_(PackageBase.SubmittedTS < limit_to, + PackageBase.PackagerUID.is_(None))) + db.delete_all(query) def main(): - conn = aurweb.db.Connection() - - limit_to = int(time.time()) - 86400 - conn.execute("DELETE FROM PackageBases WHERE " + - "SubmittedTS < ? AND PackagerUID IS NULL", [limit_to]) - - conn.commit() - conn.close() + db.get_engine() + with db.begin(): + _main() if __name__ == '__main__': diff --git a/test/t2300-pkgmaint.t b/test/t2300-pkgmaint.t deleted file mode 100755 index 997f95b0..00000000 --- a/test/t2300-pkgmaint.t +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -test_description='pkgmaint tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test package base cleanup script.' ' - now=$(date -d now +%s) && - threedaysago=$(date -d "3 days ago" +%s) && - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1, "foobar", 1, $now, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (2, "foobar2", 2, $threedaysago, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (3, "foobar3", NULL, $now, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (4, "foobar4", NULL, $threedaysago, 0, ""); - EOD - cover "$PKGMAINT" && - cat <<-EOD >expected && - foobar - foobar2 - foobar3 - EOD - echo "SELECT Name FROM PackageBases;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_done diff --git a/test/test_pkgmaint.py b/test/test_pkgmaint.py new file mode 100644 index 00000000..921a6330 --- /dev/null +++ b/test/test_pkgmaint.py @@ -0,0 +1,65 @@ +from datetime import datetime +from typing import List + +import pytest + +from aurweb import db +from aurweb.models import Package, PackageBase, User +from aurweb.models.account_type import USER_ID +from aurweb.scripts import pkgmaint + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd="testPassword", AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def packages(user: User) -> List[Package]: + output = [] + + now = int(datetime.utcnow().timestamp()) + with db.begin(): + for i in range(5): + pkgbase = db.create(PackageBase, Name=f"pkg_{i}", + SubmittedTS=now, + ModifiedTS=now) + pkg = db.create(Package, PackageBase=pkgbase, + Name=f"pkg_{i}", Version=f"{i}.0") + output.append(pkg) + yield output + + +def test_pkgmaint_noop(packages: List[Package]): + assert len(packages) == 5 + pkgmaint.main() + packages = db.query(Package).all() + assert len(packages) == 5 + + +def test_pkgmaint(packages: List[Package]): + assert len(packages) == 5 + + # Modify the first package so it's out of date and gets deleted. + with db.begin(): + # Reduce SubmittedTS by a day + 10 seconds. + packages[0].PackageBase.SubmittedTS -= (86400 + 10) + + # Run pkgmaint. + pkgmaint.main() + + # Query package objects again and assert that the + # first package was deleted but all others are intact. + packages = db.query(Package).all() + assert len(packages) == 4 + expected = ["pkg_1", "pkg_2", "pkg_3", "pkg_4"] + for i, pkgname in enumerate(expected): + assert packages[i].Name == pkgname From 9fb1fbe32cd2d7652f4dee9df29002a36b9ff38c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 15:00:22 -0800 Subject: [PATCH 0737/1451] feat(testing): add email testing utilities Changes: - util/sendmail now populates email files in the 'test-emails' directory. - util/sendmail does this in a serialized fashion based off of the test suite and name retrieved from PYTEST_CURRENT_TEST in the format: `_.n.txt` where n is increased by one every time sendmail is run. - pytest conftest fixtures have been added for test email setup; it wipes out old emails for the particular test function being run. - New aurweb.testing.email.Email class allows developers to test against emails stored by util/sendmail. Simple pass the serial you want to test against, starting at serial = 1; e.g. Email(serial). Signed-off-by: Kevin Morris --- aurweb/testing/email.py | 120 ++++++++++++++++++++++++++++++++++++++++ test/conftest.py | 20 ++++++- util/sendmail | 23 ++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 aurweb/testing/email.py diff --git a/aurweb/testing/email.py b/aurweb/testing/email.py new file mode 100644 index 00000000..6ff9df99 --- /dev/null +++ b/aurweb/testing/email.py @@ -0,0 +1,120 @@ +import base64 +import copy +import email +import os +import re + + +class Email: + """ + An email class used for testing. + + This class targets a specific serial of emails for PYTEST_CURRENT_TEST. + As emails are sent out with util/sendmail, the serial number increases, + starting at 1. + + Email content sent out by aurweb is always base64-encoded. Email.parse() + decodes that for us and puts it into Email.body. + + Example: + + # Get the {test_suite}_{test_function}.1.txt email. + email = Email(1).parse() + print(email.body) + print(email.headers) + + """ + TEST_DIR = "test-emails" + + def __init__(self, serial: int = 1): + self.serial = serial + self.content = self._get() + + @staticmethod + def email_prefix(suite: bool = False) -> str: + """ + Get the email prefix. + + We find the email prefix by reducing PYTEST_CURRENT_TEST to + either {test_suite}_{test_function}. If `suite` is set, we + reduce it to {test_suite} only. + + :param suite: Reduce PYTEST_CURRENT_TEST to {test_suite} + :return: Email prefix with '/', '.', ',', and ':' chars replaced by '_' + """ + value = os.environ.get("PYTEST_CURRENT_TEST", "email").split(" ")[0] + if suite: + value = value.split(":")[0] + return re.sub(r'(\/|\.|,|:)', "_", value) + + @staticmethod + def count() -> int: + """ + Count the current number of emails sent from the test. + + This function is **only** supported inside of pytest functions. + Do not use it elsewhere as data races will occur. + + :return: Number of emails sent by the current test + """ + files = os.listdir(Email.TEST_DIR) + prefix = Email.email_prefix() + expr = "^" + prefix + r"\.\d+\.txt$" + subset = filter(lambda e: re.match(expr, e), files) + return len(list(subset)) + + def _email_path(self) -> str: + filename = self.email_prefix() + f".{self.serial}.txt" + return os.path.join(Email.TEST_DIR, filename) + + def _get(self) -> str: + """ + Get this email's content by reading its file. + + :return: Email content + """ + path = self._email_path() + with open(path) as f: + return f.read() + + def parse(self) -> "Email": + """ + Parse this email and base64-decode the body. + + This function populates Email.message, Email.headers and Email.body. + + Additionally, after parsing, we write over our email file with + self.glue()'d content (base64-decoded). This is done for ease + of inspection by users. + + :return: self + """ + self.message = email.message_from_string(self.content) + self.headers = dict(self.message) + + # aurweb email notifications always have base64 encoded content. + # Decode it here so self.body is human readable. + self.body = base64.b64decode(self.message.get_payload()).decode() + + path = self._email_path() + with open(path, "w") as f: + f.write(self.glue()) + + return self + + def glue(self) -> str: + """ + Glue parsed content back into a complete email document, but + base64-decoded this time. + + :return: Email document as a string + """ + headers = copy.copy(self.headers) + del headers["Content-Transfer-Encoding"] + + output = [] + for k, v in headers.items(): + output.append(f"{k}: {v}") + output.append("") + output.append(self.body) + return "\n".join(output) diff --git a/test/conftest.py b/test/conftest.py index db2e5997..01131109 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -37,6 +37,8 @@ then clears the database for each test function run in that module. It is done this way because migration has a large cost; migrating ahead of each function takes too long when compared to this method. """ +import os + import pytest from filelock import FileLock @@ -50,6 +52,7 @@ import aurweb.config import aurweb.db from aurweb import initdb, logging, testing +from aurweb.testing.email import Email logger = logging.get_logger(__name__) @@ -120,6 +123,18 @@ def _drop_database(engine: Engine, dbname: str) -> None: conn.close() +def setup_email(): + if not os.path.exists(Email.TEST_DIR): + os.makedirs(Email.TEST_DIR) + + # Cleanup all email files for this test suite. + prefix = Email.email_prefix(suite=True) + files = os.listdir(Email.TEST_DIR) + for file in files: + if file.startswith(prefix): + os.remove(os.path.join(Email.TEST_DIR, file)) + + @pytest.fixture(scope="module") def setup_database(tmp_path_factory: pytest.fixture, worker_id: pytest.fixture) -> None: @@ -129,6 +144,7 @@ def setup_database(tmp_path_factory: pytest.fixture, if worker_id == "master": # pragma: no cover # If we're not running tests through multiproc pytest-xdist. + setup_email() yield _create_database(engine, dbname) _drop_database(engine, dbname) return @@ -143,12 +159,13 @@ def setup_database(tmp_path_factory: pytest.fixture, else: # Otherwise, create the data file and create the database. fn.write_text("1") + setup_email() yield _create_database(engine, dbname) _drop_database(engine, dbname) @pytest.fixture(scope="module") -def db_session(setup_database: pytest.fixture) -> scoped_session: +def db_session(setup_database: None) -> scoped_session: """ Yield a database session based on aurweb.db.name(). @@ -158,6 +175,7 @@ def db_session(setup_database: pytest.fixture) -> scoped_session: # configured database, because PYTEST_CURRENT_TEST is removed. dbname = aurweb.db.name() session = aurweb.db.get_session() + yield session # Close the session and pop it. diff --git a/util/sendmail b/util/sendmail index 06bd9865..9356851a 100755 --- a/util/sendmail +++ b/util/sendmail @@ -1,2 +1,25 @@ #!/bin/bash +# Send email to temporary filesystem for tests. +dir='test-emails' +filename='email.txt' +if [ ! -z ${PYTEST_CURRENT_TEST+x} ]; then + filename="$(echo $PYTEST_CURRENT_TEST | cut -d ' ' -f 1 | sed -r 's/(\/|\.|,|:)/_/g')" +fi +mkdir -p "$dir" + +path="${dir}/${filename}" +serial_file="${path}.serial" +if [ ! -f $serial_file ]; then + echo 0 > $serial_file +fi + +# Increment and update $serial_file. +serial=$(($(cat $serial_file) + 1)) +echo $serial > $serial_file + +# Use the serial we're on to mark the email file. +# Emails have the format: PYTEST_CURRENT_TEST.s.txt +# where s is the current serial for PYTEST_CURRENT_TEST. +cat > "${path}.${serial}.txt" + exit 0 From d8e3ca1abbac6a897f262b1eba0695f4323a13d8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 12:03:53 -0800 Subject: [PATCH 0738/1451] change(notify): converted to use aurweb.db ORM - Removed notify sharness test Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 7 +- aurweb/routers/accounts.py | 6 +- aurweb/routers/packages.py | 30 +- aurweb/scripts/notify.py | 402 +++++++++++++---------- aurweb/testing/smtp.py | 42 +++ test/t2500-notify.t | 431 ------------------------- test/test_notify.py | 643 +++++++++++++++++++++++++++++++++++++ 7 files changed, 934 insertions(+), 627 deletions(-) create mode 100644 aurweb/testing/smtp.py delete mode 100755 test/t2500-notify.t create mode 100644 test/test_notify.py diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 55af3a34..3bb3ae5f 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -239,7 +239,6 @@ def remove_comaintainers(pkgbase: models.PackageBase, :param usernames: Iterable of username strings :return: None """ - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) notifications = [] with db.begin(): for username in usernames: @@ -250,8 +249,7 @@ def remove_comaintainers(pkgbase: models.PackageBase, ).first() notifications.append( notify.ComaintainerRemoveNotification( - conn, comaintainer.User.ID, pkgbase.ID - ) + comaintainer.User.ID, pkgbase.ID) ) db.delete(comaintainer) @@ -283,7 +281,6 @@ def add_comaintainers(request: Request, pkgbase: models.PackageBase, memo[username] = user # Alright, now that we got past the check, add them all to the DB. - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) notifications = [] with db.begin(): for username in usernames: @@ -302,7 +299,7 @@ def add_comaintainers(request: Request, pkgbase: models.PackageBase, notifications.append( notify.ComaintainerAddNotification( - conn, comaintainer.User.ID, pkgbase.ID) + comaintainer.User.ID, pkgbase.ID) ) # Send out notifications. diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index ddee1764..545811f0 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -97,8 +97,7 @@ async def passreset_post(request: Request, with db.begin(): user.ResetKey = resetkey - executor = db.ConnectionExecutor(db.get_engine().raw_connection()) - ResetKeyNotification(executor, user.ID).send() + ResetKeyNotification(user.ID).send() # Render ?step=confirm. return RedirectResponse(url="/passreset?step=confirm", @@ -323,8 +322,7 @@ async def account_register_post(request: Request, Fingerprint=fingerprint) # Send a reset key notification to the new user. - executor = db.ConnectionExecutor(db.get_engine().raw_connection()) - WelcomeNotification(executor, user.ID).send() + WelcomeNotification(user.ID).send() context["complete"] = True context["user"] = user diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index eab75e5a..b5f8478e 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -146,7 +146,6 @@ def delete_package(deleter: models.User, package: models.Package): requests = [] bases_to_delete = [] - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) # In all cases, though, just delete the Package in question. if package.PackageBase.packages.count() == 1: reqtype = db.query(models.RequestType).filter( @@ -162,7 +161,7 @@ def delete_package(deleter: models.User, package: models.Package): # Prepare DeleteNotification. notifications.append( - notify.DeleteNotification(conn, deleter.ID, package.PackageBase.ID) + notify.DeleteNotification(deleter.ID, package.PackageBase.ID) ) # For each PackageRequest created, mock up an open and close notification. @@ -170,12 +169,12 @@ def delete_package(deleter: models.User, package: models.Package): for pkgreq in requests: notifications.append( notify.RequestOpenNotification( - conn, deleter.ID, pkgreq.ID, reqtype.Name, + deleter.ID, pkgreq.ID, reqtype.Name, pkgreq.PackageBase.ID, merge_into=basename or None) ) notifications.append( notify.RequestCloseNotification( - conn, deleter.ID, pkgreq.ID, pkgreq.status_display()) + deleter.ID, pkgreq.ID, pkgreq.status_display()) ) # Perform all the deletions. @@ -666,10 +665,9 @@ async def pkgbase_request_post(request: Request, name: str, Comments=comments, ClosureComment=str()) - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) # Prepare notification object. notif = notify.RequestOpenNotification( - conn, request.user.ID, pkgreq.ID, reqtype.Name, + request.user.ID, pkgreq.ID, reqtype.Name, pkgreq.PackageBase.ID, merge_into=merge_into or None) # Send the notification now that we're out of the DB scope. @@ -688,7 +686,7 @@ async def pkgbase_request_post(request: Request, name: str, pkgreq.Status = ACCEPTED_ID db.refresh(pkgreq) notif = notify.RequestCloseNotification( - conn, request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display()) notif.send() elif type == "deletion" and is_maintainer and outdated: packages = pkgbase.packages.all() @@ -742,9 +740,8 @@ async def requests_close_post(request: Request, id: int, pkgreq.Status = reason pkgreq.ClosureComment = comments - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) notify_ = notify.RequestCloseNotification( - conn, request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display()) notify_.send() return RedirectResponse("/requests", status_code=HTTPStatus.SEE_OTHER) @@ -936,9 +933,7 @@ async def pkgbase_unvote(request: Request, name: str): def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase): disowner = request.user - - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - notif = notify.DisownNotification(conn, disowner.ID, pkgbase.ID) + notif = notify.DisownNotification(disowner.ID, pkgbase.ID) if disowner != pkgbase.Maintainer: with db.begin(): @@ -1003,8 +998,7 @@ def pkgbase_adopt_instance(request: Request, pkgbase: models.PackageBase): with db.begin(): pkgbase.Maintainer = request.user - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) - notif = notify.AdoptNotification(conn, request.user.ID, pkgbase.ID) + notif = notify.AdoptNotification(request.user.ID, pkgbase.ID) notif.send() @@ -1366,7 +1360,7 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase, f"{request.user.Username}.") rejected_closure_comment = ("Rejected because another merge request " "for the same package base was accepted.") - conn = db.ConnectionExecutor(db.get_engine().raw_connection()) + if not requests: # If there are no requests, create one owned by request.user. with db.begin(): @@ -1383,7 +1377,7 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase, # Add a notification about the opening to our notifs array. notif = notify.RequestOpenNotification( - conn, request.user.ID, pkgreq.ID, MERGE, + request.user.ID, pkgreq.ID, MERGE, pkgbase.ID, merge_into=target.Name) notifs.append(notif) @@ -1417,11 +1411,9 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase, for pkgreq in all_requests: # Create notifications for request closure. notif = notify.RequestCloseNotification( - conn, request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display()) notifs.append(notif) - conn.close() - # Log this out for accountability purposes. logger.info(f"Trusted User '{request.user.Username}' merged " f"'{pkgbasename}' into '{target.Name}'.") diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index ba4ec9eb..e49024d9 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -7,10 +7,16 @@ import subprocess import sys import textwrap +from sqlalchemy import and_, or_ + import aurweb.config import aurweb.db import aurweb.l10n +from aurweb import db +from aurweb.models import (PackageBase, PackageComaintainer, PackageComment, PackageNotification, PackageRequest, RequestType, + TUVote, User) + aur_location = aurweb.config.get('options', 'aur_location') @@ -22,23 +28,6 @@ def headers_reply(thread_id): return {'In-Reply-To': thread_id, 'References': thread_id} -def username_from_id(conn, uid): - cur = conn.execute('SELECT UserName FROM Users WHERE ID = ?', [uid]) - return cur.fetchone()[0] - - -def pkgbase_from_id(conn, pkgbase_id): - cur = conn.execute('SELECT Name FROM PackageBases WHERE ID = ?', - [pkgbase_id]) - return cur.fetchone()[0] - - -def pkgbase_from_pkgreq(conn, reqid): - cur = conn.execute('SELECT PackageBaseID FROM PackageRequests ' + - 'WHERE ID = ?', [reqid]) - return cur.fetchone()[0] - - class Notification: def get_refs(self): return () @@ -52,8 +41,8 @@ class Notification: def get_body_fmt(self, lang): body = '' for line in self.get_body(lang).splitlines(): - if line == '-- ': - body += '-- \n' + if line == '--': + body += '--\n' continue body += textwrap.fill(line, break_long_words=False) + '\n' for i, ref in enumerate(self.get_refs()): @@ -103,10 +92,11 @@ class Notification: user = aurweb.config.get('notifications', 'smtp-user') passwd = aurweb.config.get('notifications', 'smtp-password') - if use_ssl: - server = smtplib.SMTP_SSL(server_addr, server_port) - else: - server = smtplib.SMTP(server_addr, server_port) + classes = { + False: smtplib.SMTP, + True: smtplib.SMTP_SSL, + } + server = classes[use_ssl](server_addr, server_port) if use_starttls: server.ehlo() @@ -123,12 +113,24 @@ class Notification: class ResetKeyNotification(Notification): - def __init__(self, conn, uid): - cur = conn.execute('SELECT UserName, Email, BackupEmail, ' + - 'LangPreference, ResetKey ' + - 'FROM Users WHERE ID = ? AND Suspended = 0', [uid]) - self._username, self._to, self._backup, self._lang, self._resetkey = \ - cur.fetchone() + def __init__(self, uid): + + user = db.query(User).filter( + and_(User.ID == uid, User.Suspended == 0) + ).with_entities( + User.Username, + User.Email, + User.BackupEmail, + User.LangPreference, + User.ResetKey + ).order_by(User.Username.asc()).first() + + self._username = user.Username + self._to = user.Email + self._backup = user.BackupEmail + self._lang = user.LangPreference + self._resetkey = user.ResetKey + super().__init__() def get_recipients(self): @@ -167,21 +169,28 @@ class WelcomeNotification(ResetKeyNotification): class CommentNotification(Notification): - def __init__(self, conn, uid, pkgbase_id, comment_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, Users.LangPreference ' - 'FROM Users INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'Users.CommentNotify = 1 AND ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ? AND ' + - 'Users.Suspended = 0', - [uid, pkgbase_id]) - self._recipients = cur.fetchall() - cur = conn.execute('SELECT Comments FROM PackageComments WHERE ID = ?', - [comment_id]) - self._text = cur.fetchone()[0] + def __init__(self, uid, pkgbase_id, comment_id): + + self._user = db.query(User.Username).filter( + User.ID == uid).first().Username + self._pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == pkgbase_id).first().Name + + query = db.query(User).join(PackageNotification).filter( + and_(User.CommentNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0) + ).with_entities( + User.Email, + User.LangPreference + ).distinct() + self._recipients = [(u.Email, u.LangPreference) for u in query] + + pkgcomment = db.query(PackageComment.Comments).filter( + PackageComment.ID == comment_id).first() + self._text = pkgcomment.Comments + super().__init__() def get_recipients(self): @@ -196,7 +205,7 @@ class CommentNotification(Notification): body = aurweb.l10n.translator.translate( '{user} [1] added the following comment to {pkgbase} [2]:', lang).format(user=self._user, pkgbase=self._pkgbase) - body += '\n\n' + self._text + '\n\n-- \n' + body += '\n\n' + self._text + '\n\n--\n' dnlabel = aurweb.l10n.translator.translate( 'Disable notifications', lang) body += aurweb.l10n.translator.translate( @@ -216,19 +225,24 @@ class CommentNotification(Notification): class UpdateNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'Users.UpdateNotify = 1 AND ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ? AND ' + - 'Users.Suspended = 0', - [uid, pkgbase_id]) - self._recipients = cur.fetchall() + def __init__(self, uid, pkgbase_id): + + self._user = db.query(User.Username).filter( + User.ID == uid).first().Username + self._pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == pkgbase_id).first().Name + + query = db.query(User).join(PackageNotification).filter( + and_(User.UpdateNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0) + ).with_entities( + User.Email, + User.LangPreference + ).distinct() + self._recipients = [(u.Email, u.LangPreference) for u in query] + super().__init__() def get_recipients(self): @@ -243,7 +257,7 @@ class UpdateNotification(Notification): body = aurweb.l10n.translator.translate( '{user} [1] pushed a new commit to {pkgbase} [2].', lang).format(user=self._user, pkgbase=self._pkgbase) - body += '\n\n-- \n' + body += '\n\n--\n' dnlabel = aurweb.l10n.translator.translate( 'Disable notifications', lang) body += aurweb.l10n.translator.translate( @@ -263,23 +277,30 @@ class UpdateNotification(Notification): class FlagNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute( - 'SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'LEFT JOIN PackageComaintainers ' + - 'ON PackageComaintainers.UsersID = Users.ID ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.MaintainerUID = Users.ID OR ' + - 'PackageBases.ID = PackageComaintainers.PackageBaseID ' + - 'WHERE PackageBases.ID = ? AND ' + - 'Users.Suspended = 0', [pkgbase_id]) - self._recipients = cur.fetchall() - cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' + - 'ID = ?', [pkgbase_id]) - self._text = cur.fetchone()[0] + def __init__(self, uid, pkgbase_id): + + self._user = db.query(User.Username).filter( + User.ID == uid).first().Username + self._pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == pkgbase_id).first().Name + + query = db.query(User).join(PackageComaintainer, isouter=True).join( + PackageBase, + or_(PackageBase.MaintainerUID == User.ID, + PackageBase.ID == PackageComaintainer.PackageBaseID) + ).filter( + and_(PackageBase.ID == pkgbase_id, + User.Suspended == 0) + ).with_entities( + User.Email, + User.LangPreference + ).distinct() + self._recipients = [(u.Email, u.LangPreference) for u in query] + + pkgbase = db.query(PackageBase.FlaggerComment).filter( + PackageBase.ID == pkgbase_id).first() + self._text = pkgbase.FlaggerComment + super().__init__() def get_recipients(self): @@ -304,22 +325,28 @@ class FlagNotification(Notification): class OwnershipEventNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'Users.OwnershipNotify = 1 AND ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ? AND ' + - 'Users.Suspended = 0', - [uid, pkgbase_id]) - self._recipients = cur.fetchall() - cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' + - 'ID = ?', [pkgbase_id]) - self._text = cur.fetchone()[0] + def __init__(self, uid, pkgbase_id): + + self._user = db.query(User.Username).filter( + User.ID == uid).first().Username + self._pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == pkgbase_id).first().Name + + query = db.query(User).join(PackageNotification).filter( + and_(User.OwnershipNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0) + ).with_entities( + User.Email, + User.LangPreference + ).distinct() + self._recipients = [(u.Email, u.LangPreference) for u in query] + + pkgbase = db.query(PackageBase.FlaggerComment).filter( + PackageBase.ID == pkgbase_id).first() + self._text = pkgbase.FlaggerComment + super().__init__() def get_recipients(self): @@ -351,11 +378,22 @@ class DisownNotification(OwnershipEventNotification): class ComaintainershipEventNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT Email, LangPreference FROM Users ' + - 'WHERE ID = ? AND Suspended = 0', [uid]) - self._to, self._lang = cur.fetchone() + def __init__(self, uid, pkgbase_id): + + self._pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == pkgbase_id).first().Name + + user = db.query(User).filter( + and_(User.ID == uid, + User.Suspended == 0) + ).with_entities( + User.Email, + User.LangPreference + ).first() + + self._to = user.Email + self._lang = user.LangPreference + super().__init__() def get_recipients(self): @@ -385,22 +423,28 @@ class ComaintainerRemoveNotification(ComaintainershipEventNotification): class DeleteNotification(Notification): - def __init__(self, conn, uid, old_pkgbase_id, new_pkgbase_id=None): - self._user = username_from_id(conn, uid) - self._old_pkgbase = pkgbase_from_id(conn, old_pkgbase_id) + def __init__(self, uid, old_pkgbase_id, new_pkgbase_id=None): + + self._user = db.query(User.Username).filter( + User.ID == uid).first().Username + self._old_pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == old_pkgbase_id).first().Name + + self._new_pkgbase = None if new_pkgbase_id: - self._new_pkgbase = pkgbase_from_id(conn, new_pkgbase_id) - else: - self._new_pkgbase = None - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ? AND ' + - 'Users.Suspended = 0', - [uid, old_pkgbase_id]) - self._recipients = cur.fetchall() + self._new_pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == new_pkgbase_id).first().Name + + query = db.query(User).join(PackageNotification).filter( + and_(PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == old_pkgbase_id, + User.Suspended == 0) + ).with_entities( + User.Email, + User.LangPreference + ).distinct() + self._recipients = [(u.Email, u.LangPreference) for u in query] + super().__init__() def get_recipients(self): @@ -417,7 +461,7 @@ class DeleteNotification(Notification): 'Disable notifications', lang) return aurweb.l10n.translator.translate( '{user} [1] merged {old} [2] into {new} [3].\n\n' - '-- \n' + '--\n' 'If you no longer wish receive notifications about the ' 'new package, please go to [3] and click "{label}".', lang).format(user=self._user, old=self._old_pkgbase, @@ -438,26 +482,36 @@ class DeleteNotification(Notification): class RequestOpenNotification(Notification): - def __init__(self, conn, uid, reqid, reqtype, pkgbase_id, merge_into=None): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute( - 'SELECT DISTINCT Users.Email FROM PackageRequests ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + - 'LEFT JOIN PackageComaintainers ' + - 'ON PackageComaintainers.PackageBaseID = PackageRequests.PackageBaseID ' + - 'INNER JOIN Users ' + - 'ON Users.ID = PackageRequests.UsersID ' + - 'OR Users.ID = PackageBases.MaintainerUID ' + - 'OR Users.ID = PackageComaintainers.UsersID ' + - 'WHERE PackageRequests.ID = ? AND ' + - 'Users.Suspended = 0', [reqid]) + def __init__(self, uid, reqid, reqtype, pkgbase_id, merge_into=None): + + self._user = db.query(User.Username).filter( + User.ID == uid).first().Username + self._pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == pkgbase_id).first().Name + self._to = aurweb.config.get('options', 'aur_request_ml') - self._cc = [row[0] for row in cur.fetchall()] - cur = conn.execute('SELECT Comments FROM PackageRequests WHERE ID = ?', - [reqid]) - self._text = cur.fetchone()[0] + + query = db.query(PackageRequest).join(PackageBase).join( + PackageComaintainer, + PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, + isouter=True + ).join( + User, + or_(User.ID == PackageRequest.UsersID, + User.ID == PackageBase.MaintainerUID, + User.ID == PackageComaintainer.UsersID) + ).filter( + and_(PackageRequest.ID == reqid, + User.Suspended == 0) + ).with_entities( + User.Email + ).distinct() + self._cc = [u.Email for u in query] + + pkgreq = db.query(PackageRequest.Comments).filter( + PackageRequest.ID == reqid).first() + + self._text = pkgreq.Comments self._reqid = int(reqid) self._reqtype = reqtype self._merge_into = merge_into @@ -500,31 +554,41 @@ class RequestOpenNotification(Notification): class RequestCloseNotification(Notification): - def __init__(self, conn, uid, reqid, reason): - self._user = username_from_id(conn, uid) if int(uid) else None + def __init__(self, uid, reqid, reason): + user = db.query(User.Username).filter(User.ID == uid).first() + self._user = user.Username if user else None - cur = conn.execute( - 'SELECT DISTINCT Users.Email FROM PackageRequests ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + - 'LEFT JOIN PackageComaintainers ' + - 'ON PackageComaintainers.PackageBaseID = PackageRequests.PackageBaseID ' + - 'INNER JOIN Users ' + - 'ON Users.ID = PackageRequests.UsersID ' + - 'OR Users.ID = PackageBases.MaintainerUID ' + - 'OR Users.ID = PackageComaintainers.UsersID ' + - 'WHERE PackageRequests.ID = ? AND ' + - 'Users.Suspended = 0', [reqid]) self._to = aurweb.config.get('options', 'aur_request_ml') - self._cc = [row[0] for row in cur.fetchall()] - cur = conn.execute('SELECT PackageRequests.ClosureComment, ' + - 'RequestTypes.Name, ' + - 'PackageRequests.PackageBaseName ' + - 'FROM PackageRequests ' + - 'INNER JOIN RequestTypes ' + - 'ON RequestTypes.ID = PackageRequests.ReqTypeID ' + - 'WHERE PackageRequests.ID = ?', [reqid]) - self._text, self._reqtype, self._pkgbase = cur.fetchone() + + query = db.query(PackageRequest).join(PackageBase).join( + PackageComaintainer, + PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, + isouter=True + ).join( + User, + or_(User.ID == PackageRequest.UsersID, + User.ID == PackageBase.MaintainerUID, + User.ID == PackageComaintainer.UsersID) + ).filter( + and_(PackageRequest.ID == reqid, + User.Suspended == 0) + ).with_entities( + User.Email + ).distinct() + self._cc = [u.Email for u in query] + + pkgreq = db.query(PackageRequest).join(RequestType).filter( + PackageRequest.ID == reqid + ).with_entities( + PackageRequest.ClosureComment, + RequestType.Name, + PackageRequest.PackageBaseName + ).first() + + self._text = pkgreq.ClosureComment + self._reqtype = pkgreq.Name + self._pkgbase = pkgreq.PackageBaseName + self._reqid = int(reqid) self._reason = reason @@ -567,14 +631,19 @@ class RequestCloseNotification(Notification): class TUVoteReminderNotification(Notification): - def __init__(self, conn, vote_id): + def __init__(self, vote_id): self._vote_id = int(vote_id) - cur = conn.execute('SELECT Email, LangPreference FROM Users ' + - 'WHERE AccountTypeID IN (2, 4) AND ID NOT IN ' + - '(SELECT UserID FROM TU_Votes ' + - 'WHERE TU_Votes.VoteID = ?) AND ' + - 'Users.Suspended = 0', [vote_id]) - self._recipients = cur.fetchall() + + subquery = db.query(TUVote.UserID).filter(TUVote.VoteID == vote_id) + query = db.query(User).filter( + and_(User.AccountTypeID.in_((2, 4)), + ~User.ID.in_(subquery), + User.Suspended == 0) + ).with_entities( + User.Email, User.LangPreference + ) + self._recipients = [(u.Email, u.LangPreference) for u in query] + super().__init__() def get_recipients(self): @@ -596,6 +665,7 @@ class TUVoteReminderNotification(Notification): def main(): + db.get_engine() action = sys.argv[1] action_map = { 'send-resetkey': ResetKeyNotification, @@ -613,14 +683,10 @@ def main(): 'tu-vote-reminder': TUVoteReminderNotification, } - conn = aurweb.db.Connection() - - notification = action_map[action](conn, *sys.argv[2:]) + with db.begin(): + notification = action_map[action](*sys.argv[2:]) notification.send() - conn.commit() - conn.close() - if __name__ == '__main__': main() diff --git a/aurweb/testing/smtp.py b/aurweb/testing/smtp.py new file mode 100644 index 00000000..da64c93f --- /dev/null +++ b/aurweb/testing/smtp.py @@ -0,0 +1,42 @@ +""" Fake SMTP clients that can be used for testing. """ + + +class FakeSMTP: + """ A fake version of smtplib.SMTP used for testing. """ + + starttls_enabled = False + use_ssl = False + + def __init__(self): + self.emails = [] + self.count = 0 + self.ehlo_count = 0 + self.quit_count = 0 + self.set_debuglevel_count = 0 + self.user = None + self.passwd = None + + def ehlo(self) -> None: + self.ehlo_count += 1 + + def starttls(self) -> None: + self.starttls_enabled = True + + def set_debuglevel(self, level: int = 0) -> None: + self.set_debuglevel_count += 1 + + def login(self, user: str, passwd: str) -> None: + self.user = user + self.passwd = passwd + + def sendmail(self, sender: str, to: str, msg: bytes) -> None: + self.emails.append((sender, to, msg.decode())) + self.count += 1 + + def quit(self) -> None: + self.quit_count += 1 + + +class FakeSMTP_SSL(FakeSMTP): + """ A fake version of smtplib.SMTP_SSL used for testing. """ + use_ssl = True diff --git a/test/t2500-notify.t b/test/t2500-notify.t deleted file mode 100755 index a908f125..00000000 --- a/test/t2500-notify.t +++ /dev/null @@ -1,431 +0,0 @@ -#!/bin/sh - -test_description='notify tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test out-of-date notifications.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package base IDs which can be distinguished from user IDs. */ - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1001, "foobar", 1, 0, 0, "This is a test OOD comment."); - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1002, "foobar2", 2, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1003, "foobar3", NULL, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1004, "foobar4", 1, 0, 0, ""); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1001, 2, 1); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1001, 4, 2); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1002, 3, 1); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1002, 5, 2); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1003, 4, 1); - EOD - >sendmail.out && - cover "$NOTIFY" flag 1 1001 && - cat <<-EOD >expected && - Subject: AUR Out-of-date Notification for foobar - To: tu@localhost - Subject: AUR Out-of-date Notification for foobar - To: user2@localhost - Subject: AUR Out-of-date Notification for foobar - To: user@localhost - EOD - grep "^\(Subject\|To\)" sendmail.out >sendmail.parts && - test_cmp sendmail.parts expected && - cat <<-EOD | sqlite3 aur.db - DELETE FROM PackageComaintainers; - EOD -' - -test_expect_success 'Test subject and body of reset key notifications.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET ResetKey = "12345678901234567890123456789012" WHERE ID = 1; - EOD - >sendmail.out && - cover "$NOTIFY" send-resetkey 1 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Password Reset - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - A password reset request was submitted for the account user associated - with your email address. If you wish to reset your password follow the - link [1] below, otherwise ignore this message and nothing will happen. - - [1] https://aur.archlinux.org/passreset/?resetkey=12345678901234567890123456789012 - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of welcome notifications.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET ResetKey = "12345678901234567890123456789012" WHERE ID = 1; - EOD - >sendmail.out && - cover "$NOTIFY" welcome 1 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: Welcome to the Arch User Repository - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Welcome to the Arch User Repository! In order to set an initial - password for your new account, please click the link [1] below. If the - link does not work, try copying and pasting it into your browser. - - [1] https://aur.archlinux.org/passreset/?resetkey=12345678901234567890123456789012 - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of comment notifications.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package comments IDs which can be distinguished from other IDs. */ - INSERT INTO PackageComments (ID, PackageBaseID, UsersID, Comments, RenderedComment) VALUES (2001, 1001, 1, "This is a test comment.", "This is a test comment."); - INSERT INTO PackageNotifications (PackageBaseID, UserID) VALUES (1001, 2); - UPDATE Users SET CommentNotify = 1 WHERE ID = 2; - EOD - >sendmail.out && - cover "$NOTIFY" comment 1 1001 2001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Comment for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] added the following comment to foobar [2]: - - This is a test comment. - - -- - If you no longer wish to receive notifications about this package, - please go to the package page [2] and select "Disable notifications". - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of update notifications.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET UpdateNotify = 1 WHERE ID = 2; - EOD - >sendmail.out && - cover "$NOTIFY" update 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Package Update: foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] pushed a new commit to foobar [2]. - - -- - If you no longer wish to receive notifications about this package, - please go to the package page [2] and select "Disable notifications". - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of out-of-date notifications.' ' - >sendmail.out && - cover "$NOTIFY" flag 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Out-of-date Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Your package foobar [1] has been flagged out-of-date by user [2]: - - This is a test OOD comment. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - [2] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of adopt notifications.' ' - >sendmail.out && - cover "$NOTIFY" adopt 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Ownership Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - The package foobar [1] was adopted by user [2]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - [2] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of disown notifications.' ' - >sendmail.out && - cover "$NOTIFY" disown 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Ownership Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - The package foobar [1] was disowned by user [2]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - [2] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of co-maintainer addition notifications.' ' - >sendmail.out && - cover "$NOTIFY" comaintainer-add 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Co-Maintainer Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - You were added to the co-maintainer list of foobar [1]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of co-maintainer removal notifications.' ' - >sendmail.out && - cover "$NOTIFY" comaintainer-remove 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Co-Maintainer Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - You were removed from the co-maintainer list of foobar [1]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of delete notifications.' ' - >sendmail.out && - cover "$NOTIFY" delete 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Package deleted: foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] deleted foobar [2]. - - You will no longer receive notifications about this package. - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of merge notifications.' ' - >sendmail.out && - cover "$NOTIFY" delete 1 1001 1002 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Package deleted: foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] merged foobar [2] into foobar2 [3]. - - -- - If you no longer wish receive notifications about the new package, - please go to [3] and click "Disable notifications". - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - [3] https://aur.archlinux.org/pkgbase/foobar2/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test Cc, subject and body of request open notifications.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package request IDs which can be distinguished from other IDs. */ - INSERT INTO PackageRequests (ID, PackageBaseID, PackageBaseName, UsersID, ReqTypeID, Comments, ClosureComment) VALUES (3001, 1001, "foobar", 2, 1, "This is a request test comment.", ""); - EOD - >sendmail.out && - cover "$NOTIFY" request-open 1 3001 orphan 1001 && - grep ^Cc: sendmail.out >actual && - cat <<-EOD >expected && - Cc: user@localhost, tu@localhost - EOD - test_cmp actual expected && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Orphan Request for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] filed an orphan request for foobar [2]: - - This is a request test comment. - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of request open notifications for merge requests.' ' - >sendmail.out && - cover "$NOTIFY" request-open 1 3001 merge 1001 foobar2 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Merge Request for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] filed a request to merge foobar [2] into foobar2 [3]: - - This is a request test comment. - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - [3] https://aur.archlinux.org/pkgbase/foobar2/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test Cc, subject and body of request close notifications.' ' - >sendmail.out && - cover "$NOTIFY" request-close 1 3001 accepted && - grep ^Cc: sendmail.out >actual && - cat <<-EOD >expected && - Cc: user@localhost, tu@localhost - EOD - test_cmp actual expected && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Deletion Request for foobar Accepted - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Request #3001 has been accepted by user [1]. - - [1] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of request close notifications (auto-accept).' ' - >sendmail.out && - cover "$NOTIFY" request-close 0 3001 accepted && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Deletion Request for foobar Accepted - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Request #3001 has been accepted automatically by the Arch User - Repository package request system. - EOD - test_cmp actual expected -' - -test_expect_success 'Test Cc of request close notification with co-maintainer.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package base IDs which can be distinguished from user IDs. */ - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1001, 3, 1); - EOD - >sendmail.out && - "$NOTIFY" request-close 0 3001 accepted && - grep ^Cc: sendmail.out >actual && - cat <<-EOD >expected && - Cc: user@localhost, tu@localhost, dev@localhost - EOD - test_cmp actual expected && - cat <<-EOD | sqlite3 aur.db - DELETE FROM PackageComaintainers; - EOD -' - -test_expect_success 'Test subject and body of request close notifications with closure comment.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE PackageRequests SET ClosureComment = "This is a test closure comment." WHERE ID = 3001; - EOD - >sendmail.out && - cover "$NOTIFY" request-close 1 3001 accepted && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Deletion Request for foobar Accepted - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Request #3001 has been accepted by user [1]: - - This is a test closure comment. - - [1] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of TU vote reminders.' ' - >sendmail.out && - cover "$NOTIFY" tu-vote-reminder 1 && - grep ^Subject: sendmail.out | head -1 >actual && - cat <<-EOD >expected && - Subject: TU Vote Reminder: Proposal 1 - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | head -4 | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Please remember to cast your vote on proposal 1 [1]. The voting period - ends in less than 48 hours. - - [1] https://aur.archlinux.org/tu/?id=1 - EOD - test_cmp actual expected -' - -test_done diff --git a/test/test_notify.py b/test/test_notify.py new file mode 100644 index 00000000..45a1df31 --- /dev/null +++ b/test/test_notify.py @@ -0,0 +1,643 @@ +from datetime import datetime +from typing import List +from unittest import mock + +import pytest + +from aurweb import config, db, models +from aurweb.models import Package, PackageBase, PackageRequest, User +from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID +from aurweb.models.request_type import ORPHAN_ID +from aurweb.scripts import notify, rendercomment +from aurweb.testing.email import Email +from aurweb.testing.smtp import FakeSMTP, FakeSMTP_SSL + +aur_location = config.get("options", "aur_location") +aur_request_ml = config.get("options", "aur_request_ml") + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd=str(), AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def user1() -> User: + with db.begin(): + user1 = db.create(User, Username="user1", Email="user1@example.org", + Passwd=str(), AccountTypeID=USER_ID) + yield user1 + + +@pytest.fixture +def user2() -> User: + with db.begin(): + user2 = db.create(User, Username="user2", Email="user2@example.org", + Passwd=str(), AccountTypeID=USER_ID) + yield user2 + + +@pytest.fixture +def pkgbases(user: User) -> List[PackageBase]: + now = int(datetime.utcnow().timestamp()) + + output = [] + with db.begin(): + for i in range(5): + output.append( + db.create(PackageBase, Name=f"pkgbase_{i}", + Maintainer=user, SubmittedTS=now, + ModifiedTS=now)) + db.create(models.PackageNotification, PackageBase=output[-1], + User=user) + yield output + + +@pytest.fixture +def pkgreq(user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + with db.begin(): + pkgreq_ = db.create(PackageRequest, PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, User=user2, + ReqTypeID=ORPHAN_ID, + Comments="This is a request test comment.", + ClosureComment=str()) + yield pkgreq_ + + +@pytest.fixture +def packages(pkgbases: List[PackageBase]) -> List[Package]: + output = [] + with db.begin(): + for i, pkgbase in enumerate(pkgbases): + output.append( + db.create(Package, PackageBase=pkgbase, + Name=f"pkg_{i}", Version=f"{i}.0")) + yield output + + +def test_out_of_date(user: User, user1: User, user2: User, + pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + # Create two comaintainers. We'll pass the maintainer uid to + # FlagNotification, so we should expect to get two emails. + with db.begin(): + db.create(models.PackageComaintainer, + PackageBase=pkgbase, User=user1, Priority=1) + db.create(models.PackageComaintainer, + PackageBase=pkgbase, User=user2, Priority=2) + + # Send the notification for pkgbases[0]. + notif = notify.FlagNotification(user.ID, pkgbases[0].ID) + notif.send() + + # Should've gotten three emails: maintainer + the two comaintainers. + assert Email.count() == 3 + + # Comaintainer 1. + first = Email(1).parse() + assert first.headers.get("To") == user1.Email + + expected = f"AUR Out-of-date Notification for {pkgbase.Name}" + assert first.headers.get("Subject") == expected + + # Comaintainer 2. + second = Email(2).parse() + assert second.headers.get("To") == user2.Email + + # Maintainer. + third = Email(3).parse() + assert third.headers.get("To") == user.Email + + +def test_reset(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + notif = notify.ResetKeyNotification(user.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + expected = "AUR Password Reset" + assert email.headers.get("Subject") == expected + + expected = f"""\ +A password reset request was submitted for the account test associated +with your email address. If you wish to reset your password follow the +link [1] below, otherwise ignore this message and nothing will happen. + +[1] {aur_location}/passreset/?resetkey=12345678901234567890123456789012\ +""" + assert email.body == expected + + +def test_welcome(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + notif = notify.WelcomeNotification(user.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + expected = "Welcome to the Arch User Repository" + assert email.headers.get("Subject") == expected + + expected = f"""\ +Welcome to the Arch User Repository! In order to set an initial +password for your new account, please click the link [1] below. If the +link does not work, try copying and pasting it into your browser. + +[1] {aur_location}/passreset/?resetkey=12345678901234567890123456789012\ +""" + assert email.body == expected + + +def test_comment(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + + with db.begin(): + comment = db.create(models.PackageComment, PackageBase=pkgbase, + User=user2, Comments="This is a test comment.") + rendercomment.update_comment_render_fastapi(comment) + + notif = notify.CommentNotification(user2.ID, pkgbase.ID, comment.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Comment for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] added the following comment to {pkgbase.Name} [2]: + +This is a test comment. + +-- +If you no longer wish to receive notifications about this package, +please go to the package page [2] and select "Disable notifications". + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert expected == email.body + + +def test_update(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + with db.begin(): + user.UpdateNotify = 1 + + notif = notify.UpdateNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Package Update: {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] pushed a new commit to {pkgbase.Name} [2]. + +-- +If you no longer wish to receive notifications about this package, +please go to the package page [2] and select "Disable notifications". + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert expected == email.body + + +def test_adopt(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.AdoptNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Ownership Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +The package {pkgbase.Name} [1] was adopted by {user2.Username} [2]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/ +[2] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + +def test_disown(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.DisownNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Ownership Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +The package {pkgbase.Name} [1] was disowned by {user2.Username} [2]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/ +[2] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + +def test_comaintainer_addition(user: User, pkgbases: List[PackageBase]): + # TODO: Add this in fastapi code! + pkgbase = pkgbases[0] + notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Co-Maintainer Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +You were added to the co-maintainer list of {pkgbase.Name} [1]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + +def test_comaintainer_removal(user: User, pkgbases: List[PackageBase]): + # TODO: Add this in fastapi code! + pkgbase = pkgbases[0] + notif = notify.ComaintainerRemoveNotification(user.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Co-Maintainer Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +You were removed from the co-maintainer list of {pkgbase.Name} [1]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + +def test_delete(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.DeleteNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Package deleted: {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] deleted {pkgbase.Name} [2]. + +You will no longer receive notifications about this package. + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + +def test_merge(user: User, user2: User, pkgbases: List[PackageBase]): + source, target = pkgbases[:2] + notif = notify.DeleteNotification(user2.ID, source.ID, target.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Package deleted: {source.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] merged {source.Name} [2] into {target.Name} [3]. + +-- +If you no longer wish receive notifications about the new package, +please go to [3] and click "Disable notifications". + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{source.Name}/ +[3] {aur_location}/pkgbase/{target.Name}/\ +""" + assert email.body == expected + + +def set_tu(users: List[User]) -> User: + with db.begin(): + for user in users: + user.AccountTypeID = TRUSTED_USER_ID + + +def test_open_close_request(user: User, user2: User, + pkgreq: PackageRequest, + pkgbases: List[PackageBase]): + set_tu([user]) + pkgbase = pkgbases[0] + + # Send an open request notification. + notif = notify.RequestOpenNotification( + user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == aur_request_ml + assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) + expected = f"[PRQ#{pkgreq.ID}] Orphan Request for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] filed an orphan request for {pkgbase.Name} [2]: + +This is a request test comment. + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + # Now send a closure notification on the pkgbase we just opened. + notif = notify.RequestCloseNotification(user2.ID, pkgreq.ID, "rejected") + notif.send() + assert Email.count() == 2 + + email = Email(2).parse() + assert email.headers.get("To") == aur_request_ml + assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) + expected = f"[PRQ#{pkgreq.ID}] Orphan Request for {pkgbase.Name} Rejected" + assert email.headers.get("Subject") == expected + + expected = f"""\ +Request #{pkgreq.ID} has been rejected by {user2.Username} [1]. + +[1] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + # Test auto-accept. + notif = notify.RequestCloseNotification(0, pkgreq.ID, "accepted") + notif.send() + assert Email.count() == 3 + + email = Email(3).parse() + assert email.headers.get("To") == aur_request_ml + assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) + expected = (f"[PRQ#{pkgreq.ID}] Orphan Request for " + f"{pkgbase.Name} Accepted") + assert email.headers.get("Subject") == expected + + expected = (f"Request #{pkgreq.ID} has been accepted automatically " + "by the Arch User Repository\npackage request system.") + assert email.body == expected + + +def test_close_request_auto_accept(): + pass + + +def test_close_request_comaintainer_cc(user: User, user2: User, + pkgreq: PackageRequest, + pkgbases: List[PackageBase]): + # TODO: Check this in fastapi code! + pkgbase = pkgbases[0] + with db.begin(): + db.create(models.PackageComaintainer, PackageBase=pkgbase, + User=user2, Priority=1) + + notif = notify.RequestCloseNotification(0, pkgreq.ID, "accepted") + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == aur_request_ml + assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) + + +def test_close_request_closure_comment(user: User, user2: User, + pkgreq: PackageRequest, + pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + with db.begin(): + pkgreq.ClosureComment = "This is a test closure comment." + + notif = notify.RequestCloseNotification(user2.ID, pkgreq.ID, "accepted") + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == aur_request_ml + assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) + expected = f"[PRQ#{pkgreq.ID}] Orphan Request for {pkgbase.Name} Accepted" + assert email.headers.get("Subject") == expected + + expected = f"""\ +Request #{pkgreq.ID} has been accepted by {user2.Username} [1]: + +This is a test closure comment. + +[1] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + +def test_tu_vote_reminders(user: User): + set_tu([user]) + + vote_id = 1 + notif = notify.TUVoteReminderNotification(vote_id) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"TU Vote Reminder: Proposal {vote_id}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +Please remember to cast your vote on proposal {vote_id} [1]. The voting period +ends in less than 48 hours. + +[1] {aur_location}/tu/?id={vote_id}\ +""" + assert email.body == expected + + +def test_notify_main(user: User): + """ Test TU vote reminder through aurweb.notify.main(). """ + set_tu([user]) + + vote_id = 1 + args = ["aurweb-notify", "tu-vote-reminder", str(vote_id)] + with mock.patch("sys.argv", args): + notify.main() + + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"TU Vote Reminder: Proposal {vote_id}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +Please remember to cast your vote on proposal {vote_id} [1]. The voting period +ends in less than 48 hours. + +[1] {aur_location}/tu/?id={vote_id}\ +""" + assert email.body == expected + + +# Save original config.get; we're going to mock it and need +# to be able to fallback when we are not overriding. +config_get = config.get + + +def mock_smtp_config(cls): + def _mock_smtp_config(section: str, key: str): + if section == "notifications": + if key == "sendmail": + return cls() + elif key == "smtp-use-ssl": + return cls(0) + elif key == "smtp-use-starttls": + return cls(0) + elif key == "smtp-user": + return cls() + elif key == "smtp-password": + return cls() + return cls(config_get(section, key)) + return _mock_smtp_config + + +def test_smtp(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + SMTP = FakeSMTP() + + get = "aurweb.config.get" + getboolean = "aurweb.config.getboolean" + with mock.patch(get, side_effect=mock_smtp_config(str)): + with mock.patch(getboolean, side_effect=mock_smtp_config(bool)): + with mock.patch("smtplib.SMTP", side_effect=lambda a, b: SMTP): + config.rehash() + notif = notify.WelcomeNotification(user.ID) + notif.send() + config.rehash() + assert len(SMTP.emails) == 1 + + +def mock_smtp_starttls_config(cls): + def _mock_smtp_starttls_config(section: str, key: str): + if section == "notifications": + if key == "sendmail": + return cls() + elif key == "smtp-use-ssl": + return cls(0) + elif key == "smtp-use-starttls": + return cls(1) + elif key == "smtp-user": + return cls("test") + elif key == "smtp-password": + return cls("password") + return cls(config_get(section, key)) + return _mock_smtp_starttls_config + + +def test_smtp_starttls(user: User): + # This test does two things: test starttls path and test + # path where we have a backup email. + + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + user.BackupEmail = "backup@example.org" + + SMTP = FakeSMTP() + + get = "aurweb.config.get" + getboolean = "aurweb.config.getboolean" + with mock.patch(get, side_effect=mock_smtp_starttls_config(str)): + with mock.patch( + getboolean, side_effect=mock_smtp_starttls_config(bool)): + with mock.patch("smtplib.SMTP", side_effect=lambda a, b: SMTP): + notif = notify.WelcomeNotification(user.ID) + notif.send() + assert SMTP.starttls_enabled + assert SMTP.user + assert SMTP.passwd + + assert len(SMTP.emails) == 2 + to = SMTP.emails[0][1] + assert to == [user.Email] + + to = SMTP.emails[1][1] + assert to == [user.BackupEmail] + + +def mock_smtp_ssl_config(cls): + def _mock_smtp_ssl_config(section: str, key: str): + if section == "notifications": + if key == "sendmail": + return cls() + elif key == "smtp-use-ssl": + return cls(1) + elif key == "smtp-use-starttls": + return cls(0) + elif key == "smtp-user": + return cls("test") + elif key == "smtp-password": + return cls("password") + return cls(config_get(section, key)) + return _mock_smtp_ssl_config + + +def test_smtp_ssl(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + SMTP = FakeSMTP_SSL() + + get = "aurweb.config.get" + getboolean = "aurweb.config.getboolean" + with mock.patch(get, side_effect=mock_smtp_ssl_config(str)): + with mock.patch(getboolean, side_effect=mock_smtp_ssl_config(bool)): + with mock.patch("smtplib.SMTP_SSL", side_effect=lambda a, b: SMTP): + notif = notify.WelcomeNotification(user.ID) + notif.send() + assert len(SMTP.emails) == 1 + assert SMTP.use_ssl + assert SMTP.user + assert SMTP.passwd + + +def test_notification_defaults(): + notif = notify.Notification() + assert notif.get_refs() == tuple() + assert notif.get_headers() == dict() + assert notif.get_cc() == list() From 155aa47a1acf9143f8bce49a81f05f56f7e61042 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 25 Nov 2021 12:03:25 -0800 Subject: [PATCH 0739/1451] feat(poetry): add posix_ipc Signed-off-by: Kevin Morris --- poetry.lock | 18 +++++++++++++++++- pyproject.toml | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index ab559b77..d56ce458 100644 --- a/poetry.lock +++ b/poetry.lock @@ -619,6 +619,14 @@ dunamai = ">=1.5,<2.0" jinja2 = {version = ">=2.11.1,<4", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} tomlkit = ">=0.4" +[[package]] +name = "posix-ipc" +version = "1.0.5" +description = "POSIX IPC primitives (semaphores, shared memory and message queues) for Python" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "priority" version = "2.0.0" @@ -1056,7 +1064,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.10" -content-hash = "ca42bd35717062d6784025ed3956423502ac66adba059ccc080bcaaa666651cd" +content-hash = "43b3dd494890ef9d419260a06ccd0190638573fdf0b92a226016f1dd5ee87579" [metadata.files] aiofiles = [ @@ -1555,6 +1563,14 @@ poetry-dynamic-versioning = [ {file = "poetry-dynamic-versioning-0.13.1.tar.gz", hash = "sha256:5c0e7b22560db76812057ef95dadad662ecc63eb270145787eabe73da7c222f9"}, {file = "poetry_dynamic_versioning-0.13.1-py3-none-any.whl", hash = "sha256:6d79f76436c624653fc06eb9bb54fb4f39b1d54362bc366ad2496855711d3a78"}, ] +posix-ipc = [ + {file = "posix_ipc-1.0.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ccb36ba90efec56a1796f1566eee9561f355a4f45babbc4d18ac46fb2d0b246b"}, + {file = "posix_ipc-1.0.5-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:613bf1afe90e84c06255ec1a6f52c9b24062492de66e5f0dbe068adf67fc3454"}, + {file = "posix_ipc-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6095bb4faa2bba8b8d0e833b804e0aedc352d5ed921edeb715010cbcd361e038"}, + {file = "posix_ipc-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:621918abe7ec68591c5839b0771d163a9809bc232bf413b9a681bf986ab68d4d"}, + {file = "posix_ipc-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f71587ad3a50e82583987f62bfd4ac2343ab6a206d1032e3fc560e8d55fe0346"}, + {file = "posix_ipc-1.0.5.tar.gz", hash = "sha256:6cddb1ce2cf4aae383f2a0079c26c69bee257fe2720f372201ef047f8ceb8b97"}, +] priority = [ {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, diff --git a/pyproject.toml b/pyproject.toml index 82c439bc..f164967c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ mysql-connector = "^2.2.9" prometheus-fastapi-instrumentator = "^5.7.1" pytest-xdist = "^2.4.0" filelock = "^3.3.2" +posix-ipc = "^1.0.5" [tool.poetry.dev-dependencies] flake8 = "^4.0.1" From 4b0cb0721d8fc4fe2414e37a8a8fbf78949481a5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Nov 2021 20:06:50 -0800 Subject: [PATCH 0740/1451] fix(conftest): use synchronization locks for setup_database We were running into data race issues where the `fn.is_file()` check would occur twice before writing the file in the `else` clause. For this reason, a new aurweb.lock.Lock class has been added which doubles as a thread and process lock. We can use this elsewhere in the future, but we are also able to use it to solve this kind of data race issue. That being said, we still need the lock file state to tell us when the first caller acquired the lock. Signed-off-by: Kevin Morris --- aurweb/testing/filelock.py | 32 ++++++++++++++++++++ test/conftest.py | 61 +++++++++++++++++++++++--------------- test/test_filelock.py | 26 ++++++++++++++++ 3 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 aurweb/testing/filelock.py create mode 100644 test/test_filelock.py diff --git a/aurweb/testing/filelock.py b/aurweb/testing/filelock.py new file mode 100644 index 00000000..3a18c153 --- /dev/null +++ b/aurweb/testing/filelock.py @@ -0,0 +1,32 @@ +import hashlib +import os + +from typing import Callable + +from posix_ipc import O_CREAT, Semaphore + +from aurweb import logging + +logger = logging.get_logger(__name__) + + +def default_on_create(path): + logger.info(f"Filelock at {path} acquired.") + + +class FileLock: + def __init__(self, tmpdir, name: str): + self.root = tmpdir + self.path = str(self.root / name) + self._file = str(self.root / (f"{name}.1")) + + def lock(self, on_create: Callable = default_on_create): + hash = hashlib.sha1(self.path.encode()).hexdigest() + with Semaphore(f"/{hash}-lock", flags=O_CREAT, initial_value=1): + retval = os.path.exists(self._file) + if not retval: + with open(self._file, "w") as f: + f.write("1") + on_create(self.path) + + return retval diff --git a/test/conftest.py b/test/conftest.py index 01131109..80f77c9a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -38,10 +38,14 @@ It is done this way because migration has a large cost; migrating ahead of each function takes too long when compared to this method. """ import os +import pathlib +from multiprocessing import Lock + +import py import pytest -from filelock import FileLock +from posix_ipc import O_CREAT, Semaphore from sqlalchemy import create_engine from sqlalchemy.engine import URL from sqlalchemy.engine.base import Engine @@ -53,9 +57,13 @@ import aurweb.db from aurweb import initdb, logging, testing from aurweb.testing.email import Email +from aurweb.testing.filelock import FileLock logger = logging.get_logger(__name__) +# Synchronization lock for database setup. +setup_lock = Lock() + def test_engine() -> Engine: """ @@ -105,7 +113,12 @@ def _create_database(engine: Engine, dbname: str) -> None: try: conn.execute(f"CREATE DATABASE {dbname}") except ProgrammingError: # pragma: no cover - pass + # The database most likely already existed if we hit + # a ProgrammingError. Just drop the database and try + # again. If at that point things still fail, any + # exception will be propogated up to the caller. + conn.execute(f"DROP DATABASE {dbname}") + conn.execute(f"CREATE DATABASE {dbname}") conn.close() initdb.run(AlembicArgs) @@ -124,20 +137,24 @@ def _drop_database(engine: Engine, dbname: str) -> None: def setup_email(): - if not os.path.exists(Email.TEST_DIR): - os.makedirs(Email.TEST_DIR) + # TODO: Fix this data race! This try/catch is ugly; why is it even + # racing here? Perhaps we need to multiproc + multithread lock + # inside of setup_database to block the check? + with Semaphore("/test-emails", flags=O_CREAT, initial_value=1): + if not os.path.exists(Email.TEST_DIR): + # Create the directory. + os.makedirs(Email.TEST_DIR) - # Cleanup all email files for this test suite. - prefix = Email.email_prefix(suite=True) - files = os.listdir(Email.TEST_DIR) - for file in files: - if file.startswith(prefix): - os.remove(os.path.join(Email.TEST_DIR, file)) + # Cleanup all email files for this test suite. + prefix = Email.email_prefix(suite=True) + files = os.listdir(Email.TEST_DIR) + for file in files: + if file.startswith(prefix): + os.remove(os.path.join(Email.TEST_DIR, file)) @pytest.fixture(scope="module") -def setup_database(tmp_path_factory: pytest.fixture, - worker_id: pytest.fixture) -> None: +def setup_database(tmp_path_factory: pathlib.Path, worker_id: str) -> None: """ Create and drop a database for the suite this fixture is used in. """ engine = test_engine() dbname = aurweb.db.name() @@ -149,19 +166,15 @@ def setup_database(tmp_path_factory: pytest.fixture, _drop_database(engine, dbname) return - root_tmp_dir = tmp_path_factory.getbasetemp().parent - fn = root_tmp_dir / dbname + def setup(path): + setup_email() + _create_database(engine, dbname) - with FileLock(str(fn) + ".lock"): - if fn.is_file(): - # If the data file exists, skip database creation. - yield - else: - # Otherwise, create the data file and create the database. - fn.write_text("1") - setup_email() - yield _create_database(engine, dbname) - _drop_database(engine, dbname) + tmpdir = tmp_path_factory.getbasetemp().parent + file_lock = FileLock(tmpdir, dbname) + file_lock.lock(on_create=setup) + yield # Run the test function depending on this fixture. + _drop_database(engine, dbname) # Cleanup the database. @pytest.fixture(scope="module") diff --git a/test/test_filelock.py b/test/test_filelock.py new file mode 100644 index 00000000..70aa7580 --- /dev/null +++ b/test/test_filelock.py @@ -0,0 +1,26 @@ +import py + +from _pytest.logging import LogCaptureFixture + +from aurweb.testing.filelock import FileLock + + +def test_filelock(tmpdir: py.path.local): + cb_path = None + + def setup(path: str): + nonlocal cb_path + cb_path = str(path) + + flock = FileLock(tmpdir, "test") + assert not flock.lock(on_create=setup) + assert cb_path == str(tmpdir / "test") + assert flock.lock() + + +def test_filelock_default(caplog: LogCaptureFixture, tmpdir: py.path.local): + # Test default_on_create here. + flock = FileLock(tmpdir, "test") + assert not flock.lock() + assert caplog.messages[0] == f"Filelock at {flock.path} acquired." + assert flock.lock() From 2d0e09cd63bc2a1c0baee0727aa5ede60e3475b1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Nov 2021 06:20:50 -0800 Subject: [PATCH 0741/1451] change(rendercomment): converted to use aurweb.db ORM - Added aurweb.util.git_search. - Decoupled away from rendercomment for easier testability. - Added aurweb.testing.git.GitRepository. - Added templates/testing/{PKGBUILD,SRCINFO}.j2. - Added aurweb.testing.git.GitRepository + `git` pytest fixture Signed-off-by: Kevin Morris --- aurweb/scripts/rendercomment.py | 57 ++++----- aurweb/testing/git.py | 110 +++++++++++++++++ aurweb/util.py | 17 +++ templates/testing/PKGBUILD.j2 | 14 +++ templates/testing/SRCINFO.j2 | 10 ++ test/conftest.py | 6 + test/t2600-rendercomment.t | 160 ------------------------- test/test_rendercomment.py | 202 ++++++++++++++++++++++++++++++++ test/test_util.py | 17 +++ 9 files changed, 398 insertions(+), 195 deletions(-) create mode 100644 aurweb/testing/git.py create mode 100644 templates/testing/PKGBUILD.j2 create mode 100644 templates/testing/SRCINFO.j2 delete mode 100755 test/t2600-rendercomment.t create mode 100644 test/test_rendercomment.py diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index efa5357f..33349432 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -7,13 +7,11 @@ import markdown import pygit2 import aurweb.config -import aurweb.db -from aurweb import logging +from aurweb import db, logging, util +from aurweb.models import PackageComment logger = logging.get_logger(__name__) -repo_path = aurweb.config.get('serve', 'repo-path') -commit_uri = aurweb.config.get('options', 'commit_uri') class LinkifyExtension(markdown.extensions.Extension): @@ -64,6 +62,7 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): """ def __init__(self, md, head): + repo_path = aurweb.config.get('serve', 'repo-path') self._repo = pygit2.Repository(repo_path) self._head = head super().__init__(r'\b([0-9a-f]{7,40})\b', md) @@ -74,13 +73,9 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): # Unkwown OID; preserve the orginal text. return (None, None, None) - prefixlen = 12 - while prefixlen < 40: - if oid[:prefixlen] in self._repo: - break - prefixlen += 1 - el = markdown.util.etree.Element('a') + commit_uri = aurweb.config.get("options", "commit_uri") + prefixlen = util.git_search(self._repo, oid) el.set('href', commit_uri % (self._head, oid[:prefixlen])) el.text = markdown.util.AtomicString(oid[:prefixlen]) return (el, m.start(0), m.end(0)) @@ -116,49 +111,41 @@ class HeadingExtension(markdown.extensions.Extension): md.treeprocessors.register(HeadingTreeprocessor(md), 'heading', 30) -def get_comment(conn, commentid): - cur = conn.execute('SELECT PackageComments.Comments, PackageBases.Name ' - 'FROM PackageComments INNER JOIN PackageBases ' - 'ON PackageBases.ID = PackageComments.PackageBaseID ' - 'WHERE PackageComments.ID = ?', [commentid]) - return cur.fetchone() +def save_rendered_comment(comment: PackageComment, html: str): + with db.begin(): + comment.RenderedComment = html -def save_rendered_comment(conn, commentid, html): - conn.execute('UPDATE PackageComments SET RenderedComment = ? WHERE ID = ?', - [html, commentid]) +def update_comment_render_fastapi(comment: PackageComment) -> None: + update_comment_render(comment) -def update_comment_render_fastapi(comment): - conn = aurweb.db.ConnectionExecutor( - aurweb.db.get_engine().raw_connection()) - update_comment_render(conn, comment.ID) - aurweb.db.refresh(comment) +def update_comment_render(comment: PackageComment) -> None: + text = comment.Comments + pkgbasename = comment.PackageBase.Name - -def update_comment_render(conn, commentid): - text, pkgbase = get_comment(conn, commentid) html = markdown.markdown(text, extensions=[ 'fenced_code', LinkifyExtension(), FlysprayLinksExtension(), - GitCommitsExtension(pkgbase), + GitCommitsExtension(pkgbasename), HeadingExtension() ]) allowed_tags = (bleach.sanitizer.ALLOWED_TAGS + ['p', 'pre', 'h4', 'h5', 'h6', 'br', 'hr']) html = bleach.clean(html, tags=allowed_tags) - save_rendered_comment(conn, commentid, html) - - conn.commit() - conn.close() + save_rendered_comment(comment, html) + db.refresh(comment) def main(): - commentid = int(sys.argv[1]) - conn = aurweb.db.Connection() - update_comment_render(conn, commentid) + db.get_engine() + comment_id = int(sys.argv[1]) + comment = db.query(PackageComment).filter( + PackageComment.ID == comment_id + ).first() + update_comment_render(comment) if __name__ == '__main__': diff --git a/aurweb/testing/git.py b/aurweb/testing/git.py new file mode 100644 index 00000000..019d870f --- /dev/null +++ b/aurweb/testing/git.py @@ -0,0 +1,110 @@ +import os +import shlex + +from subprocess import PIPE, Popen +from typing import Tuple + +import py + +from aurweb.models import Package +from aurweb.templates import base_template +from aurweb.testing.filelock import FileLock + + +class GitRepository: + """ + A Git repository class to be used for testing. + + Expects a `tmpdir` fixture on construction, which an 'aur.git' + git repository will be created in. After this class is constructed, + users can call GitRepository.exec for git repository operations. + """ + + def __init__(self, tmpdir: py.path.local): + self.file_lock = FileLock(tmpdir, "aur.git") + self.file_lock.lock(on_create=self._setup) + + def _exec(self, cmdline: str, cwd: str) -> Tuple[int, str, str]: + args = shlex.split(cmdline) + proc = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + return (proc.returncode, out.decode().strip(), err.decode().strip()) + + def _exec_repository(self, cmdline: str) -> Tuple[int, str, str]: + return self._exec(cmdline, cwd=str(self.file_lock.path)) + + def exec(self, cmdline: str) -> Tuple[int, str, str]: + return self._exec_repository(cmdline) + + def _setup(self, path: str) -> None: + """ + Setup the git repository from scratch. + + Create the `path` directory and run the INSTALL recommended + git initialization commands inside of it. Additionally, install + aurweb.git.update to {path}/hooks/update. + + :param path: Repository path not yet created + """ + + os.makedirs(path) + + commands = [ + "git init -q", + "git config --local transfer.hideRefs '^refs/'", + "git config --local --add transfer.hideRefs '!refs/'", + "git config --local --add transfer.hideRefs '!HEAD'", + "git config --local commit.gpgsign false", + "git config --local user.name 'Test User'", + "git config --local user.email 'test@example.org'", + ] + for cmdline in commands: + return_code, out, err = self.exec(cmdline) + assert return_code == 0 + + # This is also done in the INSTALL script to give the `aur` + # ssh user permissions on the repository. We don't need it + # during testing, since our testing user will be controlling + # the repository. It is left here as a note. + # self.exec("chown -R aur .") + + def commit(self, pkg: Package, message: str): + """ + Commit a Package record to the git repository. + + This function generates a PKGBUILD and .SRCINFO based on + `pkg`, then commits them to the repository with the + `message` commit message. + + :param pkg: Package instance + :param message: Commit message + :return: Output of `git rev-parse HEAD` after committing + """ + ref = f"refs/namespaces/{pkg.Name}/refs/heads/master" + rc, out, err = self.exec(f"git checkout -q --orphan {ref}") + assert rc == 0, f"{(rc, out, err)}" + + # Path to aur.git repository. + repo = os.path.join(self.file_lock.path) + + licenses = [f"'{p.License.Name}'" for p in pkg.package_licenses] + depends = [f"'{p.DepName}'" for p in pkg.package_dependencies] + pkgbuild = base_template("testing/PKGBUILD.j2") + pkgbuild_path = os.path.join(repo, "PKGBUILD") + with open(pkgbuild_path, "w") as f: + data = pkgbuild.render(pkg=pkg, licenses=licenses, depends=depends) + f.write(data) + + srcinfo = base_template("testing/SRCINFO.j2") + srcinfo_path = os.path.join(repo, ".SRCINFO") + with open(srcinfo_path, "w") as f: + f.write(srcinfo.render(pkg=pkg)) + + rc, out, err = self.exec("git add PKGBUILD .SRCINFO") + assert rc == 0, f"{(rc, out, err)}" + + rc, out, err = self.exec(f"git commit -q -m '{message}'") + assert rc == 0, f"{(rc, out, err)}" + + # Return stdout of `git rev-parse HEAD`, which is the new commit hash. + return self.exec("git rev-parse HEAD")[1] diff --git a/aurweb/util.py b/aurweb/util.py index bf2d6e4b..f5ced259 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -13,6 +13,7 @@ from urllib.parse import urlencode, urlparse from zoneinfo import ZoneInfo import fastapi +import pygit2 from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email from jinja2 import pass_context @@ -193,3 +194,19 @@ def file_hash(filepath: str, hash_function: Callable) -> str: with open(filepath, "rb") as f: hash_ = hash_function(f.read()) return hash_.hexdigest() + + +def git_search(repo: pygit2.Repository, commit_hash: str) -> int: + """ + Return the shortest prefix length matching `commit_hash` found. + + :param repo: pygit2.Repository instance + :param commit_hash: Full length commit hash + :return: Shortest unique prefix length found + """ + prefixlen = 12 + while prefixlen < len(commit_hash): + if commit_hash[:prefixlen] in repo: + break + prefixlen += 1 + return prefixlen diff --git a/templates/testing/PKGBUILD.j2 b/templates/testing/PKGBUILD.j2 new file mode 100644 index 00000000..29d3a1d9 --- /dev/null +++ b/templates/testing/PKGBUILD.j2 @@ -0,0 +1,14 @@ +pkgname={{ pkg.PackageBase.Name }} +pkgver={{ pkg.Version }} +pkgrel=1 +pkgdesc='{{ pkg.Description }}' +url='{{ pkg.URL }}' +arch='any' +license=({{ licenses | join(" ") }}) +depends=({{ depends | join(" ") }}) +source=() +md5sums=() + +package() { + {{ body }} +} diff --git a/templates/testing/SRCINFO.j2 b/templates/testing/SRCINFO.j2 new file mode 100644 index 00000000..873b9c1b --- /dev/null +++ b/templates/testing/SRCINFO.j2 @@ -0,0 +1,10 @@ +pkgbase = {{ pkg.PackageBase.name }} + pkgver = {{ pkg.Version }} + pkgrel = 1 + pkgdesc = {{ pkg.Description }} + url = {{ pkg.URL }} + arch='any' + license = {{ pkg.package_licenses | join(", ", attribute="License.Name") }} + depends = {{ pkg.package_dependencies | join(", ", attribute="DepName") }} + +pkgname = {{ pkg.Name }} diff --git a/test/conftest.py b/test/conftest.py index 80f77c9a..fc7f77dc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -58,6 +58,7 @@ import aurweb.db from aurweb import initdb, logging, testing from aurweb.testing.email import Email from aurweb.testing.filelock import FileLock +from aurweb.testing.git import GitRepository logger = logging.get_logger(__name__) @@ -211,3 +212,8 @@ def db_test(db_session: scoped_session) -> None: session via aurweb.db.get_session(). """ testing.setup_test_db() + + +@pytest.fixture +def git(tmpdir: py.path.local) -> GitRepository: + yield GitRepository(tmpdir) diff --git a/test/t2600-rendercomment.t b/test/t2600-rendercomment.t deleted file mode 100755 index bb84fcfe..00000000 --- a/test/t2600-rendercomment.t +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/sh - -test_description='rendercomment tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test comment rendering.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1, "foobar", 1, 0, 0, ""); - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (1, 1, "Hello world! - This is a comment.", ""); - EOD - cover "$RENDERCOMMENT" 1 && - cat <<-EOD >expected && -

    Hello world! - This is a comment.

    - EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 1; - EOD - test_cmp actual expected -' - -test_expect_success 'Test Markdown conversion.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (2, 1, "*Hello* [world](https://www.archlinux.org/)!", ""); - EOD - cover "$RENDERCOMMENT" 2 && - cat <<-EOD >expected && -

    Hello world!

    - EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 2; - EOD - test_cmp actual expected -' - -test_expect_success 'Test HTML sanitizing.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (3, 1, "", ""); - EOD - cover "$RENDERCOMMENT" 3 && - cat <<-EOD >expected && - <script>alert("XSS!");</script> - EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 3; - EOD - test_cmp actual expected -' - -test_expect_success 'Test link conversion.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (4, 1, " - Visit https://www.archlinux.org/#_test_. - Visit *https://www.archlinux.org/*. - Visit . - Visit \`https://www.archlinux.org/\`. - Visit [Arch Linux](https://www.archlinux.org/). - Visit [Arch Linux][arch]. - [arch]: https://www.archlinux.org/ - ", ""); - EOD - cover "$RENDERCOMMENT" 4 && - cat <<-EOD >expected && -

    Visit https://www.archlinux.org/#_test_. - Visit https://www.archlinux.org/. - Visit https://www.archlinux.org/. - Visit https://www.archlinux.org/. - Visit Arch Linux. - Visit Arch Linux.

    - EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 4; - EOD - test_cmp actual expected -' - -test_expect_success 'Test Git commit linkification.' ' - local oid=`git -C aur.git rev-parse --verify HEAD` - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (5, 1, " - $oid - ${oid:0:7} - x.$oid.x - ${oid}x - 0123456789abcdef - \`$oid\` - http://example.com/$oid - ", ""); - EOD - cover "$RENDERCOMMENT" 5 && - cat <<-EOD >expected && -

    ${oid:0:12} - ${oid:0:7} - x.${oid:0:12}.x - ${oid}x - 0123456789abcdef - $oid - http://example.com/$oid

    - EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 5; - EOD - test_cmp actual expected -' - -test_expect_success 'Test Flyspray issue linkification.' ' - sqlite3 aur.db <<-EOD && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (6, 1, " - FS#1234567. - *FS#1234* - FS# - XFS#1 - \`FS#1234\` - https://archlinux.org/?test=FS#1234 - ", ""); - EOD - cover "$RENDERCOMMENT" 6 && - cat <<-EOD >expected && -

    FS#1234567. - FS#1234 - FS# - XFS#1 - FS#1234 - https://archlinux.org/?test=FS#1234

    - EOD - sqlite3 aur.db <<-EOD >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 6; - EOD - test_cmp actual expected -' - -test_expect_success 'Test headings lowering.' ' - sqlite3 aur.db <<-EOD && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (7, 1, " - # One - ## Two - ### Three - #### Four - ##### Five - ###### Six - ", ""); - EOD - cover "$RENDERCOMMENT" 7 && - cat <<-EOD >expected && -
    One
    -
    Two
    -
    Three
    -
    Four
    -
    Five
    -
    Six
    - EOD - sqlite3 aur.db <<-EOD >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 7; - EOD - test_cmp actual expected -' - -test_done diff --git a/test/test_rendercomment.py b/test/test_rendercomment.py new file mode 100644 index 00000000..c45d4235 --- /dev/null +++ b/test/test_rendercomment.py @@ -0,0 +1,202 @@ +from datetime import datetime +from unittest import mock + +import pytest + +from aurweb import config, db, logging +from aurweb.models import Package, PackageBase, PackageComment, User +from aurweb.models.account_type import USER_ID +from aurweb.scripts import rendercomment +from aurweb.scripts.rendercomment import update_comment_render +from aurweb.testing.git import GitRepository + +logger = logging.get_logger(__name__) +aur_location = config.get("options", "aur_location") + + +@pytest.fixture(autouse=True) +def setup(db_test, git: GitRepository): + config_get = config.get + + def mock_config_get(section: str, key: str) -> str: + if section == "serve" and key == "repo-path": + return git.file_lock.path + elif section == "options" and key == "commit_uri": + return "/cgit/aur.git/log/?h=%s&id=%s" + return config_get(section, key) + + with mock.patch("aurweb.config.get", side_effect=mock_config_get): + yield + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd=str(), AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + now = int(datetime.utcnow().timestamp()) + with db.begin(): + pkgbase = db.create(PackageBase, Packager=user, Name="pkgbase_0", + SubmittedTS=now, ModifiedTS=now) + yield pkgbase + + +@pytest.fixture +def package(pkgbase: PackageBase) -> Package: + with db.begin(): + package = db.create(Package, PackageBase=pkgbase, + Name=pkgbase.Name, Version="1.0") + yield package + + +def create_comment(user: User, pkgbase: PackageBase, comments: str, + render: bool = True): + with db.begin(): + comment = db.create(PackageComment, User=user, + PackageBase=pkgbase, Comments=comments) + if render: + update_comment_render(comment) + return comment + + +def test_comment_rendering(user: User, pkgbase: PackageBase): + text = "Hello world! This is a comment." + comment = create_comment(user, pkgbase, text) + expected = f"

    {text}

    " + assert comment.RenderedComment == expected + + +def test_rendercomment_main(user: User, pkgbase: PackageBase): + text = "Hello world! This is a comment." + comment = create_comment(user, pkgbase, text, False) + + args = ["aurweb-rendercomment", str(comment.ID)] + with mock.patch("sys.argv", args): + rendercomment.main() + db.refresh(comment) + + expected = f"

    {text}

    " + assert comment.RenderedComment == expected + + +def test_markdown_conversion(user: User, pkgbase: PackageBase): + text = "*Hello* [world](https://aur.archlinux.org)!" + comment = create_comment(user, pkgbase, text) + expected = ('

    Hello ' + 'world!

    ') + assert comment.RenderedComment == expected + + +def test_html_sanitization(user: User, pkgbase: PackageBase): + text = '' + comment = create_comment(user, pkgbase, text) + expected = '<script>alert("XSS!")</script>' + assert comment.RenderedComment == expected + + +def test_link_conversion(user: User, pkgbase: PackageBase): + text = """\ +Visit https://www.archlinux.org/#_test_. +Visit *https://www.archlinux.org/*. +Visit . +Visit `https://www.archlinux.org/`. +Visit [Arch Linux](https://www.archlinux.org/). +Visit [Arch Linux][arch]. +[arch]: https://www.archlinux.org/\ +""" + comment = create_comment(user, pkgbase, text) + expected = '''\ +

    Visit \ +https://www.archlinux.org/#_test_. +Visit https://www.archlinux.org/. +Visit https://www.archlinux.org/. +Visit https://www.archlinux.org/. +Visit Arch Linux. +Visit Arch Linux.

    \ +''' + assert comment.RenderedComment == expected + + +def test_git_commit_link(git: GitRepository, user: User, package: Package): + commit_hash = git.commit(package, "Initial commit.") + logger.info(f"Created commit: {commit_hash}") + logger.info(f"Short hash: {commit_hash[:7]}") + + text = f"""\ +{commit_hash} +{commit_hash[:7]} +x.{commit_hash}.x +{commit_hash}x +0123456789abcdef +`{commit_hash}` +http://example.com/{commit_hash}\ +""" + comment = create_comment(user, package.PackageBase, text) + + pkgname = package.PackageBase.Name + cgit_path = f"/cgit/aur.git/log/?h={pkgname}&" + expected = f"""\ +

    {commit_hash[:12]} +{commit_hash[:7]} +x.{commit_hash[:12]}.x +{commit_hash}x +0123456789abcdef +{commit_hash} +\ +http://example.com/{commit_hash}\ +\ +

    \ +""" + assert comment.RenderedComment == expected + + +def test_flyspray_issue_link(user: User, pkgbase: PackageBase): + text = """\ +FS#1234567. +*FS#1234* +FS# +XFS#1 +`FS#1234` +https://archlinux.org/?test=FS#1234\ +""" + comment = create_comment(user, pkgbase, text) + + expected = """\ +

    FS#1234567. +FS#1234 +FS# +XFS#1 +FS#1234 +\ +https://archlinux.org/?test=FS#1234\ +\ +

    \ +""" + assert comment.RenderedComment == expected + + +def test_lower_headings(user: User, pkgbase: PackageBase): + text = """\ +# One +## Two +### Three +#### Four +##### Five +###### Six\ +""" + comment = create_comment(user, pkgbase, text) + + expected = """\ +
    One
    +
    Two
    +
    Three
    +
    Four
    +
    Five
    +
    Six
    \ +""" + assert comment.RenderedComment == expected diff --git a/test/test_util.py b/test/test_util.py index 99b77a78..2529ed1f 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -40,3 +40,20 @@ def test_round(): assert filters.do_round(1.3) == 1 assert filters.do_round(1.5) == 2 assert filters.do_round(2.0) == 2 + + +def test_git_search(): + """ Test that git_search matches the full commit if necessary. """ + commit_hash = "0123456789abcdef" + repo = {commit_hash} + prefixlen = util.git_search(repo, commit_hash) + assert prefixlen == 16 + + +def test_git_search_double_commit(): + """ Test that git_search matches a shorter prefix length. """ + commit_hash = "0123456789abcdef" + repo = {commit_hash[:13]} + # Locate the shortest prefix length that matches commit_hash. + prefixlen = util.git_search(repo, commit_hash) + assert prefixlen == 13 From bc1cf8b1f6089d0776dc753bc9255032aaf83a8f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 24 Nov 2021 20:30:16 -0800 Subject: [PATCH 0742/1451] fix(rendercomment): markdown.util.etree -> xml.etree.ElementTree This removes a deprecation warning. Signed-off-by: Kevin Morris --- aurweb/scripts/rendercomment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 33349432..2af5384e 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -2,6 +2,8 @@ import sys +from xml.etree.ElementTree import Element + import bleach import markdown import pygit2 @@ -40,7 +42,7 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): """ def handleMatch(self, m, data): - el = markdown.util.etree.Element('a') + el = Element('a') el.set('href', f'https://bugs.archlinux.org/task/{m.group(1)}') el.text = markdown.util.AtomicString(m.group(0)) return (el, m.start(0), m.end(0)) @@ -73,7 +75,7 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): # Unkwown OID; preserve the orginal text. return (None, None, None) - el = markdown.util.etree.Element('a') + el = Element('a') commit_uri = aurweb.config.get("options", "commit_uri") prefixlen = util.git_search(self._repo, oid) el.set('href', commit_uri % (self._head, oid[:prefixlen])) From 67a6b8360eccb02ac4a65940fde7fa6801ac2398 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 24 Nov 2021 20:33:42 -0800 Subject: [PATCH 0743/1451] fix(docker): remove update and build steps from poetry `install` includes dependencies present in poetry.lock and we must stick to them if we wish to pin dependencies. Signed-off-by: Kevin Morris --- docker/scripts/install-python-deps.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker/scripts/install-python-deps.sh b/docker/scripts/install-python-deps.sh index 3ab87742..3d5f28f0 100755 --- a/docker/scripts/install-python-deps.sh +++ b/docker/scripts/install-python-deps.sh @@ -6,8 +6,6 @@ pip install --upgrade pip # Install the aurweb package and deps system-wide via poetry. poetry config virtualenvs.create false -poetry update -poetry build poetry install --no-interaction --no-ansi exec "$@" From 4426c639ceb59ea5d6414b70157f2c0e7cea4f6b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 24 Nov 2021 20:34:45 -0800 Subject: [PATCH 0744/1451] fix(logging): remove `test` logger definition Like the `aurweb` logger definiton was previously, the `test` logger is being redundant with the root logger. Use root for all aurweb-local logging. Signed-off-by: Kevin Morris --- logging.conf | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/logging.conf b/logging.conf index 3b96e827..deb79cf5 100644 --- a/logging.conf +++ b/logging.conf @@ -1,5 +1,5 @@ [loggers] -keys=root,test,uvicorn,hypercorn,alembic +keys=root,uvicorn,hypercorn,alembic [handlers] keys=simpleHandler,detailedHandler @@ -10,12 +10,7 @@ keys=simpleFormatter,detailedFormatter [logger_root] level=INFO handlers=detailedHandler - -[logger_test] -level=DEBUG -handlers=detailedHandler -qualname=test -propagate=1 +propogate=1 [logger_uvicorn] level=DEBUG From 436d7420179043baf4814b2d10bc1cb460132d97 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Nov 2021 14:02:47 -0800 Subject: [PATCH 0745/1451] fix(fastapi): use CRED_TU_LIST_VOTES for "Trusted User" navigation item Closes #189 Signed-off-by: Kevin Morris --- aurweb/auth.py | 2 +- templates/partials/archdev-navbar.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 98a43fd5..4d6dafc6 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -329,7 +329,7 @@ cred_filters = { CRED_PKGREQ_CLOSE: trusted_user_or_dev, CRED_PKGREQ_LIST: trusted_user_or_dev, CRED_TU_ADD_VOTE: trusted_user, - CRED_TU_LIST_VOTES: trusted_user, + CRED_TU_LIST_VOTES: trusted_user_or_dev, CRED_TU_VOTE: trusted_user, CRED_ACCOUNT_EDIT_DEV: developer, CRED_PKGBASE_MERGE: trusted_user_or_dev, diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index ead0d8e2..81695951 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -36,8 +36,8 @@ - {# Only CRED_TU_VOTE privileged users see Trusted User #} - {% if request.user.has_credential("CRED_TU_VOTE") %} + {# Only CRED_TU_LIST_VOTES privileged users see Trusted User #} + {% if request.user.has_credential("CRED_TU_LIST_VOTES") %}
  • {% trans %}Trusted User{% endtrans %}
  • From 44f2366675f0ca81af443dfa293d5251b1f2eb02 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Nov 2021 16:20:36 -0800 Subject: [PATCH 0746/1451] fix: remove TODO comments and noop tests from test_notify Signed-off-by: Kevin Morris --- test/test_notify.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/test_notify.py b/test/test_notify.py index 45a1df31..dc6e7e3e 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -262,7 +262,6 @@ The package {pkgbase.Name} [1] was disowned by {user2.Username} [2]. def test_comaintainer_addition(user: User, pkgbases: List[PackageBase]): - # TODO: Add this in fastapi code! pkgbase = pkgbases[0] notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID) notif.send() @@ -282,7 +281,6 @@ You were added to the co-maintainer list of {pkgbase.Name} [1]. def test_comaintainer_removal(user: User, pkgbases: List[PackageBase]): - # TODO: Add this in fastapi code! pkgbase = pkgbases[0] notif = notify.ComaintainerRemoveNotification(user.ID, pkgbase.ID) notif.send() @@ -417,14 +415,9 @@ Request #{pkgreq.ID} has been rejected by {user2.Username} [1]. assert email.body == expected -def test_close_request_auto_accept(): - pass - - def test_close_request_comaintainer_cc(user: User, user2: User, pkgreq: PackageRequest, pkgbases: List[PackageBase]): - # TODO: Check this in fastapi code! pkgbase = pkgbases[0] with db.begin(): db.create(models.PackageComaintainer, PackageBase=pkgbase, From 69eb17cb0d659d7de7a51715d2113e806663eb44 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Nov 2021 16:51:16 -0800 Subject: [PATCH 0747/1451] change(fastapi): remove the GET /logout route; replaced with POST Had to add some additional CSS in to style a form button the same as links are styled. Closes #188 Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 15 ++------------- templates/partials/archdev-navbar.html | 9 ++++++--- test/test_auth_routes.py | 3 ++- web/html/css/aurweb.css | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index c5a99419..fdc421f5 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -77,14 +77,9 @@ async def login_post(request: Request, return response -@router.get("/logout") +@router.post("/logout") @auth_required() -async def logout(request: Request, next: str = "/"): - """ A GET and POST route for logging out. - - @param request FastAPI request - @param next Route to redirect to - """ +async def logout(request: Request, next: str = Form(default="/")): if request.user.is_authenticated(): request.user.logout(request) @@ -95,9 +90,3 @@ async def logout(request: Request, next: str = "/"): response.delete_cookie("AURSID") response.delete_cookie("AURTZ") return response - - -@router.post("/logout") -@auth_required() -async def logout_post(request: Request, next: str = "/"): - return await logout(request=request, next=next) diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index 81695951..2e01eeab 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -45,9 +45,12 @@ {# All logged in users see Logout #}
  • - - {% trans %}Logout{% endtrans %} - +
  • {% else %} {# All guest users see Register #} diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index a0bb8a7c..dffd1b94 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -154,8 +154,9 @@ def test_unauthenticated_logout_unauthorized(): with client as request: # Alright, let's verify that attempting to /logout when not # authenticated returns 401 Unauthorized. - response = request.get("/logout", allow_redirects=False) + response = request.post("/logout", allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location").startswith("/login") def test_login_missing_username(): diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 62179769..cd81160d 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -229,3 +229,18 @@ input#search-action-submit { .success { color: green; } + +/* Styling used to clone styles for a form.link button. */ +form.link, form.link > button { + display: inline-block; +} +form.link > button { + padding: 0 0.5em; + color: #07b; + background: none; + border: none; +} +form.link > button:hover { + cursor: pointer; + text-decoration: underline; +} From fd8d23a37937f1bab5dfffea393ea30f0656df03 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Nov 2021 19:04:55 -0800 Subject: [PATCH 0748/1451] fix(fastapi): fix new Logout nav item css Signed-off-by: Kevin Morris --- web/html/css/aurweb.css | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index cd81160d..dafa8c91 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -231,16 +231,19 @@ input#search-action-submit { } /* Styling used to clone styles for a form.link button. */ -form.link, form.link > button { +form.link, form.link button { display: inline-block; + font-family: sans-serif; } -form.link > button { +form.link button { padding: 0 0.5em; color: #07b; background: none; border: none; + font-family: inherit; + font-size: inherit; } -form.link > button:hover { +form.link button:hover { cursor: pointer; text-decoration: underline; } From 9bfe2b07ba712f1667f93a4527b6a76b0b3cfcc1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Nov 2021 19:39:27 -0800 Subject: [PATCH 0749/1451] fix(fastapi): render Logged-in as page on authenticated /login This was missed during the initial porting of the /login route. Modifications: ------------- - A form is now used for the [Logout] link and some css was needed to deal with positioning. Closes #186 Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 1 - templates/login.html | 162 +++++++++++++++++++++------------------ test/test_auth_routes.py | 10 ++- web/html/css/aurweb.css | 10 ++- 4 files changed, 104 insertions(+), 79 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index fdc421f5..1e0b026a 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -24,7 +24,6 @@ async def login_template(request: Request, next: str, errors: list = None): @router.get("/login", response_class=HTMLResponse) -@auth_required(False, login=False) async def login_get(request: Request, next: str = "/"): return await login_template(request, next) diff --git a/templates/login.html b/templates/login.html index 2c028936..c62de43e 100644 --- a/templates/login.html +++ b/templates/login.html @@ -5,81 +5,95 @@

    AUR {% trans %}Login{% endtrans %}

    - {% if request.url.scheme == "http" and config.getboolean("options", "disable_http_login") %} - {% set https_login = url_base.replace("http://", "https://") + "/login" %} -

    - {{ "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." - | tr - | format( - '' | format(https_login), - "") - | safe - }} -

    - {% elif request.user.is_authenticated() %} -

    - {{ "Logged-in as: %s" - | tr - | format("%s" | format(request.user.Username)) - | safe - }} - [{% trans %}Logout{% endtrans %}] -

    - {% else %} -
    -
    - {% trans %}Enter login credentials{% endtrans %} - - {% if errors %} -
      - {% for error in errors %} -
    • {{ error }}
    • - {% endfor %} -
    - {% endif %} - -

    - - - -

    - -

    - - -

    - -

    - - -

    - -

    - - - [{% trans %}Forgot Password{% endtrans %}] - - - -

    - -
    + {% if request.user.is_authenticated() %} + +

    + {{ + "Logged-in as: %s" | tr + | format("%s" | format(request.user.Username)) + | safe + }} + + +

    + {% else %} + {% if request.url.scheme == "http" and config.getboolean("options", "disable_http_login") %} + {% set https_login = url_base.replace("http://", "https://") + "/login" %} +

    + {{ "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." + | tr + | format( + '' | format(https_login), + "") + | safe + }} +

    + {% elif request.user.is_authenticated() %} +

    + {{ "Logged-in as: %s" + | tr + | format("%s" | format(request.user.Username)) + | safe + }} + [{% trans %}Logout{% endtrans %}] +

    + {% else %} +
    +
    + {% trans %}Enter login credentials{% endtrans %} + + {% if errors %} +
      + {% for error in errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% endif %} + +

    + + + +

    + +

    + + +

    + +

    + + +

    + +

    + + + [{% trans %}Forgot Password{% endtrans %}] + + + +

    + +
    +
    + {% endif %} {% endif %}
    diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index dffd1b94..0157fcc8 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -131,7 +131,7 @@ def test_secure_login(mock): assert user.session == record -def test_authenticated_login_forbidden(): +def test_authenticated_login(): post_data = { "user": "test", "passwd": "testPassword", @@ -139,15 +139,19 @@ def test_authenticated_login_forbidden(): } with client as request: - # Login. + # Try to login. response = request.post("/login", data=post_data, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + # Now, let's verify that we get the logged in rendering + # when requesting GET /login as an authenticated user. # Now, let's verify that we receive 403 Forbidden when we # try to get /login as an authenticated user. response = request.get("/login", allow_redirects=False) - assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.status_code == int(HTTPStatus.OK) + assert "Logged-in as: test" in response.text def test_unauthenticated_logout_unauthorized(): diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index dafa8c91..739ac7b7 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -232,7 +232,7 @@ input#search-action-submit { /* Styling used to clone styles for a form.link button. */ form.link, form.link button { - display: inline-block; + display: inline; font-family: sans-serif; } form.link button { @@ -247,3 +247,11 @@ form.link button:hover { cursor: pointer; text-decoration: underline; } + +/* Customize form.link when used inside of a page. */ +div.box form.link p { + margin: .33em 0 1em; +} +div.box form.link button { + padding: 0; +} From 001e86317fd19b68491e61e8fad811a89a17ad3e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Nov 2021 19:44:18 -0800 Subject: [PATCH 0750/1451] fix(rpc): fix ordering of related records They were being ordered by IDs; they should be ordered by Names. Signed-off-by: Kevin Morris --- aurweb/rpc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index c70ddf1a..7bdae638 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -193,7 +193,7 @@ class RPC: models.DependencyType.Name.label("Type"), models.PackageDependency.DepName.label("Name"), models.PackageDependency.DepCondition.label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # PackageRelation db.query( @@ -205,7 +205,7 @@ class RPC: models.RelationType.Name.label("Type"), models.PackageRelation.RelName.label("Name"), models.PackageRelation.RelCondition.label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # Groups db.query(models.PackageGroup).join( @@ -217,7 +217,7 @@ class RPC: literal("Groups").label("Type"), models.Group.Name.label("Name"), literal(str()).label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # Licenses db.query(models.PackageLicense).join( @@ -230,7 +230,7 @@ class RPC: literal("License").label("Type"), models.License.Name.label("Name"), literal(str()).label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # Keywords db.query(models.PackageKeyword).join( @@ -242,7 +242,7 @@ class RPC: literal("Keywords").label("Type"), models.PackageKeyword.Keyword.label("Name"), literal(str()).label("Cond") - ).distinct().order_by("ID") + ).distinct().order_by("Name") ] # Union all subqueries together. From a6ac5f0dbf5b2e8d0d0c55a58e7a41ee7d5ad5dc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 29 Nov 2021 19:44:18 -0800 Subject: [PATCH 0751/1451] fix(rpc): fix ordering of related records They were being ordered by IDs; they should be ordered by Names. Signed-off-by: Kevin Morris --- aurweb/rpc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index c70ddf1a..7bdae638 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -193,7 +193,7 @@ class RPC: models.DependencyType.Name.label("Type"), models.PackageDependency.DepName.label("Name"), models.PackageDependency.DepCondition.label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # PackageRelation db.query( @@ -205,7 +205,7 @@ class RPC: models.RelationType.Name.label("Type"), models.PackageRelation.RelName.label("Name"), models.PackageRelation.RelCondition.label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # Groups db.query(models.PackageGroup).join( @@ -217,7 +217,7 @@ class RPC: literal("Groups").label("Type"), models.Group.Name.label("Name"), literal(str()).label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # Licenses db.query(models.PackageLicense).join( @@ -230,7 +230,7 @@ class RPC: literal("License").label("Type"), models.License.Name.label("Name"), literal(str()).label("Cond") - ).distinct().order_by("ID"), + ).distinct().order_by("Name"), # Keywords db.query(models.PackageKeyword).join( @@ -242,7 +242,7 @@ class RPC: literal("Keywords").label("Type"), models.PackageKeyword.Keyword.label("Name"), literal(str()).label("Cond") - ).distinct().order_by("ID") + ).distinct().order_by("Name") ] # Union all subqueries together. From ecbab8546b68574c939b269dac1ac2d77e53766b Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Thu, 21 Oct 2021 17:48:29 -0400 Subject: [PATCH 0752/1451] fix(FastAPI): access AccountType ID directly Signed-off-by: Steven Guikal --- aurweb/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/auth.py b/aurweb/auth.py index 4d6dafc6..1527f0a3 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -244,7 +244,7 @@ def account_type_required(one_of: set): def decorator(func): @functools.wraps(func) async def wrapper(request: fastapi.Request, *args, **kwargs): - if request.user.AccountType.ID not in one_of: + if request.user.AccountTypeID not in one_of: return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER)) return await func(request, *args, **kwargs) From 125b244f4478146ae293f76ca4a53a77160feda3 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Thu, 21 Oct 2021 17:49:10 -0400 Subject: [PATCH 0753/1451] fix(FastAPI): use account type vars instead of strings Signed-off-by: Steven Guikal --- aurweb/routers/trusted_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 7c0a0404..f0cea61e 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -228,7 +228,7 @@ async def trusted_user_proposal_post(request: Request, @router.get("/addvote") @auth_required(True, redirect="/addvote") -@account_type_required({"Trusted User", "Trusted User & Developer"}) +@account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote(request: Request, user: str = str(), type: str = "add_tu", From a10f8663fd9bc4ddb990f1dfba31cb3331f4c9e9 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Tue, 30 Nov 2021 15:44:18 -0500 Subject: [PATCH 0754/1451] fix(FastAPI): reorganize credential checkin into dedicated file Signed-off-by: Steven Guikal --- aurweb/{auth.py => auth/__init__.py} | 98 ------------------------ aurweb/auth/creds.py | 76 ++++++++++++++++++ aurweb/models/user.py | 9 ++- aurweb/routers/accounts.py | 6 +- aurweb/routers/packages.py | 56 +++++++------- aurweb/templates.py | 3 +- templates/partials/account/comment.html | 2 +- templates/partials/account_form.html | 2 +- templates/partials/archdev-navbar.html | 4 +- templates/partials/comment_actions.html | 8 +- templates/partials/packages/actions.html | 8 +- templates/partials/packages/comment.html | 2 +- templates/partials/packages/details.html | 4 +- test/test_auth.py | 12 +-- test/test_user.py | 25 +++--- 15 files changed, 143 insertions(+), 172 deletions(-) rename aurweb/{auth.py => auth/__init__.py} (75%) create mode 100644 aurweb/auth/creds.py diff --git a/aurweb/auth.py b/aurweb/auth/__init__.py similarity index 75% rename from aurweb/auth.py rename to aurweb/auth/__init__.py index 1527f0a3..82192cc2 100644 --- a/aurweb/auth.py +++ b/aurweb/auth/__init__.py @@ -250,101 +250,3 @@ def account_type_required(one_of: set): return await func(request, *args, **kwargs) return wrapper return decorator - - -CRED_ACCOUNT_CHANGE_TYPE = 1 -CRED_ACCOUNT_EDIT = 2 -CRED_ACCOUNT_EDIT_DEV = 3 -CRED_ACCOUNT_LAST_LOGIN = 4 -CRED_ACCOUNT_SEARCH = 5 -CRED_ACCOUNT_LIST_COMMENTS = 28 -CRED_COMMENT_DELETE = 6 -CRED_COMMENT_UNDELETE = 27 -CRED_COMMENT_VIEW_DELETED = 22 -CRED_COMMENT_EDIT = 25 -CRED_COMMENT_PIN = 26 -CRED_PKGBASE_ADOPT = 7 -CRED_PKGBASE_SET_KEYWORDS = 8 -CRED_PKGBASE_DELETE = 9 -CRED_PKGBASE_DISOWN = 10 -CRED_PKGBASE_EDIT_COMAINTAINERS = 24 -CRED_PKGBASE_FLAG = 11 -CRED_PKGBASE_LIST_VOTERS = 12 -CRED_PKGBASE_NOTIFY = 13 -CRED_PKGBASE_UNFLAG = 15 -CRED_PKGBASE_VOTE = 16 -CRED_PKGREQ_FILE = 23 -CRED_PKGREQ_CLOSE = 17 -CRED_PKGREQ_LIST = 18 -CRED_TU_ADD_VOTE = 19 -CRED_TU_LIST_VOTES = 20 -CRED_TU_VOTE = 21 -CRED_PKGBASE_MERGE = 29 - - -def has_any(user, *account_types): - return str(user.AccountType) in set(account_types) - - -def user_developer_or_trusted_user(user): - return True - - -def trusted_user(user): - return has_any(user, "Trusted User", "Trusted User & Developer") - - -def developer(user): - return has_any(user, "Developer", "Trusted User & Developer") - - -def trusted_user_or_dev(user): - return has_any(user, "Trusted User", "Developer", - "Trusted User & Developer") - - -# A mapping of functions that users must pass to have credentials. -cred_filters = { - CRED_PKGBASE_FLAG: user_developer_or_trusted_user, - CRED_PKGBASE_NOTIFY: user_developer_or_trusted_user, - CRED_PKGBASE_VOTE: user_developer_or_trusted_user, - CRED_PKGREQ_FILE: user_developer_or_trusted_user, - CRED_ACCOUNT_CHANGE_TYPE: trusted_user_or_dev, - CRED_ACCOUNT_EDIT: trusted_user_or_dev, - CRED_ACCOUNT_LAST_LOGIN: trusted_user_or_dev, - CRED_ACCOUNT_LIST_COMMENTS: trusted_user_or_dev, - CRED_ACCOUNT_SEARCH: trusted_user_or_dev, - CRED_COMMENT_DELETE: trusted_user_or_dev, - CRED_COMMENT_UNDELETE: trusted_user_or_dev, - CRED_COMMENT_VIEW_DELETED: trusted_user_or_dev, - CRED_COMMENT_EDIT: trusted_user_or_dev, - CRED_COMMENT_PIN: trusted_user_or_dev, - CRED_PKGBASE_ADOPT: trusted_user_or_dev, - CRED_PKGBASE_SET_KEYWORDS: trusted_user_or_dev, - CRED_PKGBASE_DELETE: trusted_user_or_dev, - CRED_PKGBASE_EDIT_COMAINTAINERS: trusted_user_or_dev, - CRED_PKGBASE_DISOWN: trusted_user_or_dev, - CRED_PKGBASE_LIST_VOTERS: trusted_user_or_dev, - CRED_PKGBASE_UNFLAG: trusted_user_or_dev, - CRED_PKGREQ_CLOSE: trusted_user_or_dev, - CRED_PKGREQ_LIST: trusted_user_or_dev, - CRED_TU_ADD_VOTE: trusted_user, - CRED_TU_LIST_VOTES: trusted_user_or_dev, - CRED_TU_VOTE: trusted_user, - CRED_ACCOUNT_EDIT_DEV: developer, - CRED_PKGBASE_MERGE: trusted_user_or_dev, -} - - -def has_credential(user: User, - credential: int, - approved_users: list = tuple()): - - if user in approved_users: - return True - - if credential in cred_filters: - cred_filter = cred_filters.get(credential) - return cred_filter(user) - - return False diff --git a/aurweb/auth/creds.py b/aurweb/auth/creds.py new file mode 100644 index 00000000..100aad8c --- /dev/null +++ b/aurweb/auth/creds.py @@ -0,0 +1,76 @@ +from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.user import User + +ACCOUNT_CHANGE_TYPE = 1 +ACCOUNT_EDIT = 2 +ACCOUNT_EDIT_DEV = 3 +ACCOUNT_LAST_LOGIN = 4 +ACCOUNT_SEARCH = 5 +ACCOUNT_LIST_COMMENTS = 28 +COMMENT_DELETE = 6 +COMMENT_UNDELETE = 27 +COMMENT_VIEW_DELETED = 22 +COMMENT_EDIT = 25 +COMMENT_PIN = 26 +PKGBASE_ADOPT = 7 +PKGBASE_SET_KEYWORDS = 8 +PKGBASE_DELETE = 9 +PKGBASE_DISOWN = 10 +PKGBASE_EDIT_COMAINTAINERS = 24 +PKGBASE_FLAG = 11 +PKGBASE_LIST_VOTERS = 12 +PKGBASE_NOTIFY = 13 +PKGBASE_UNFLAG = 15 +PKGBASE_VOTE = 16 +PKGREQ_FILE = 23 +PKGREQ_CLOSE = 17 +PKGREQ_LIST = 18 +TU_ADD_VOTE = 19 +TU_LIST_VOTES = 20 +TU_VOTE = 21 +PKGBASE_MERGE = 29 + +user_developer_or_trusted_user = set([USER_ID, TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) +trusted_user_or_dev = set([TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) +developer = set([DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) +trusted_user = set([TRUSTED_USER_ID, TRUSTED_USER_AND_DEV_ID]) + +cred_filters = { + PKGBASE_FLAG: user_developer_or_trusted_user, + PKGBASE_NOTIFY: user_developer_or_trusted_user, + PKGBASE_VOTE: user_developer_or_trusted_user, + PKGREQ_FILE: user_developer_or_trusted_user, + ACCOUNT_CHANGE_TYPE: trusted_user_or_dev, + ACCOUNT_EDIT: trusted_user_or_dev, + ACCOUNT_LAST_LOGIN: trusted_user_or_dev, + ACCOUNT_LIST_COMMENTS: trusted_user_or_dev, + ACCOUNT_SEARCH: trusted_user_or_dev, + COMMENT_DELETE: trusted_user_or_dev, + COMMENT_UNDELETE: trusted_user_or_dev, + COMMENT_VIEW_DELETED: trusted_user_or_dev, + COMMENT_EDIT: trusted_user_or_dev, + COMMENT_PIN: trusted_user_or_dev, + PKGBASE_ADOPT: trusted_user_or_dev, + PKGBASE_SET_KEYWORDS: trusted_user_or_dev, + PKGBASE_DELETE: trusted_user_or_dev, + PKGBASE_EDIT_COMAINTAINERS: trusted_user_or_dev, + PKGBASE_DISOWN: trusted_user_or_dev, + PKGBASE_LIST_VOTERS: trusted_user_or_dev, + PKGBASE_UNFLAG: trusted_user_or_dev, + PKGREQ_CLOSE: trusted_user_or_dev, + PKGREQ_LIST: trusted_user_or_dev, + TU_ADD_VOTE: trusted_user, + TU_LIST_VOTES: trusted_user_or_dev, + TU_VOTE: trusted_user, + ACCOUNT_EDIT_DEV: developer, + PKGBASE_MERGE: trusted_user_or_dev, +} + + +def has_credential(user: User, + credential: int, + approved_users: list = tuple()): + + if user in approved_users: + return True + return user.AccountTypeID in cred_filters[credential] diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 03634a36..f0724202 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -1,6 +1,7 @@ import hashlib from datetime import datetime +from typing import List, Set import bcrypt @@ -136,10 +137,10 @@ class User(Base): request.cookies["AURSID"] = self.session.SessionID return self.session.SessionID - def has_credential(self, credential: str, approved: list = tuple()): - import aurweb.auth - cred = getattr(aurweb.auth, credential) - return aurweb.auth.has_credential(self, cred, approved) + def has_credential(self, credential: Set[int], + approved: List["User"] = list()): + from aurweb.auth.creds import has_credential + return has_credential(self, credential, approved) def logout(self, request): del request.cookies["AURSID"] diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 545811f0..360857e8 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -10,7 +10,7 @@ from sqlalchemy import and_, or_ import aurweb.config from aurweb import cookies, db, l10n, logging, models, util -from aurweb.auth import account_type_required, auth_required +from aurweb.auth import account_type_required, auth_required, creds from aurweb.captcha import get_captcha_salts from aurweb.exceptions import ValidationError from aurweb.l10n import get_translator_for_request @@ -176,7 +176,7 @@ def make_account_form_context(context: dict, user_account_type_id = context.get("account_types")[0][0] - if request.user.has_credential("CRED_ACCOUNT_EDIT_DEV"): + if request.user.has_credential(creds.ACCOUNT_EDIT_DEV): context["account_types"].append((at.DEVELOPER_ID, at.DEVELOPER)) context["account_types"].append((at.TRUSTED_USER_AND_DEV_ID, at.TRUSTED_USER_AND_DEV)) @@ -332,7 +332,7 @@ async def account_register_post(request: Request, def cannot_edit(request, user): """ Return a 401 HTMLResponse if the request user doesn't have authorization, otherwise None. """ - has_dev_cred = request.user.has_credential("CRED_ACCOUNT_EDIT_DEV", + has_dev_cred = request.user.has_credential(creds.ACCOUNT_EDIT_DEV, approved=[user]) if not has_dev_cred: return HTMLResponse(status_code=HTTPStatus.UNAUTHORIZED) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index b5f8478e..2bf04949 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -10,7 +10,7 @@ import aurweb.filters import aurweb.packages.util from aurweb import db, defaults, l10n, logging, models, util -from aurweb.auth import auth_required +from aurweb.auth import auth_required, creds from aurweb.exceptions import ValidationError from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID @@ -413,7 +413,7 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - authorized = request.user.has_credential("CRED_COMMENT_DELETE", + authorized = request.user.has_credential(creds.COMMENT_DELETE, [comment.User]) if not authorized: _ = l10n.get_translator_for_request(request) @@ -439,7 +439,7 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential("CRED_COMMENT_UNDELETE", + has_cred = request.user.has_credential(creds.COMMENT_UNDELETE, approved=[comment.User]) if not has_cred: _ = l10n.get_translator_for_request(request) @@ -464,7 +464,7 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential("CRED_COMMENT_PIN", + has_cred = request.user.has_credential(creds.COMMENT_PIN, approved=[pkgbase.Maintainer]) if not has_cred: _ = l10n.get_translator_for_request(request) @@ -489,7 +489,7 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential("CRED_COMMENT_PIN", + has_cred = request.user.has_credential(creds.COMMENT_PIN, approved=[pkgbase.Maintainer]) if not has_cred: _ = l10n.get_translator_for_request(request) @@ -514,7 +514,7 @@ async def package_base_comaintainers(request: Request, name: str) -> Response: # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential("CRED_PKGBASE_EDIT_COMAINTAINERS", + has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]) if not has_creds: return RedirectResponse(f"/pkgbase/{name}", @@ -541,7 +541,7 @@ async def package_base_comaintainers_post( # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential("CRED_PKGBASE_EDIT_COMAINTAINERS", + has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]) if not has_creds: return RedirectResponse(f"/pkgbase/{name}", @@ -779,7 +779,7 @@ async def pkgbase_keywords(request: Request, name: str, async def pkgbase_flag_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_FLAG") + has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if not has_cred or pkgbase.Flagger is not None: return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -803,7 +803,7 @@ async def pkgbase_flag_post(request: Request, name: str, return render_template(request, "packages/flag.html", context, status_code=HTTPStatus.BAD_REQUEST) - has_cred = request.user.has_credential("CRED_PKGBASE_FLAG") + has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if has_cred and not pkgbase.Flagger: now = int(datetime.utcnow().timestamp()) with db.begin(): @@ -830,7 +830,7 @@ async def pkgbase_flag_comment(request: Request, name: str): def pkgbase_unflag_instance(request: Request, pkgbase: models.PackageBase): has_cred = request.user.has_credential( - "CRED_PKGBASE_UNFLAG", approved=[pkgbase.Flagger, pkgbase.Maintainer]) + creds.PKGBASE_UNFLAG, approved=[pkgbase.Flagger, pkgbase.Maintainer]) if has_cred: with db.begin(): pkgbase.OutOfDateTS = None @@ -851,7 +851,7 @@ def pkgbase_notify_instance(request: Request, pkgbase: models.PackageBase): notif = db.query(pkgbase.notifications.filter( models.PackageNotification.UserID == request.user.ID ).exists()).scalar() - has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and not notif: with db.begin(): db.create(models.PackageNotification, @@ -872,7 +872,7 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: models.PackageBase): notif = pkgbase.notifications.filter( models.PackageNotification.UserID == request.user.ID ).first() - has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and notif: with db.begin(): db.delete(notif) @@ -895,7 +895,7 @@ async def pkgbase_vote(request: Request, name: str): vote = pkgbase.package_votes.filter( models.PackageVote.UsersID == request.user.ID ).first() - has_cred = request.user.has_credential("CRED_PKGBASE_VOTE") + has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and not vote: now = int(datetime.utcnow().timestamp()) with db.begin(): @@ -919,7 +919,7 @@ async def pkgbase_unvote(request: Request, name: str): vote = pkgbase.package_votes.filter( models.PackageVote.UsersID == request.user.ID ).first() - has_cred = request.user.has_credential("CRED_PKGBASE_VOTE") + has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and vote: with db.begin(): db.delete(vote) @@ -958,7 +958,7 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase): async def pkgbase_disown_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) if not has_cred: return RedirectResponse(f"/pkgbase/{name}", @@ -975,7 +975,7 @@ async def pkgbase_disown_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) if not has_cred: return RedirectResponse(f"/pkgbase/{name}", @@ -1007,7 +1007,7 @@ def pkgbase_adopt_instance(request: Request, pkgbase: models.PackageBase): async def pkgbase_adopt_post(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_ADOPT") + has_cred = request.user.has_credential(creds.PKGBASE_ADOPT) if has_cred or not pkgbase.Maintainer: # If the user has credentials, they'll adopt the package regardless # of maintainership. Otherwise, we'll promote the user to maintainer @@ -1021,7 +1021,7 @@ async def pkgbase_adopt_post(request: Request, name: str): @router.get("/pkgbase/{name}/delete") @auth_required(True, redirect="/pkgbase/{name}/delete") async def pkgbase_delete_get(request: Request, name: str): - if not request.user.has_credential("CRED_PKGBASE_DELETE"): + if not request.user.has_credential(creds.PKGBASE_DELETE): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -1036,7 +1036,7 @@ async def pkgbase_delete_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) - if not request.user.has_credential("CRED_PKGBASE_DELETE"): + if not request.user.has_credential(creds.PKGBASE_DELETE): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -1070,7 +1070,7 @@ async def packages_unflag(request: Request, package_ids: List[int] = [], models.Package.ID.in_(package_ids)).all() for pkg in packages: has_cred = request.user.has_credential( - "CRED_PKGBASE_UNFLAG", approved=[pkg.PackageBase.Flagger]) + creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger]) if not has_cred: return (False, ["You did not select any packages to unflag."]) @@ -1106,7 +1106,7 @@ async def packages_notify(request: Request, package_ids: List[int] = [], notif = db.query(pkgbase.notifications.filter( models.PackageNotification.UserID == request.user.ID ).exists()).scalar() - has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) # If the request user either does not have credentials # or the notification already exists: @@ -1178,7 +1178,7 @@ async def packages_adopt(request: Request, package_ids: List[int] = [], # Check that the user has credentials for every package they selected. for pkgbase in bases: - has_cred = request.user.has_credential("CRED_PKGBASE_ADOPT") + has_cred = request.user.has_credential(creds.PKGBASE_ADOPT) if not (has_cred or not pkgbase.Maintainer): # TODO: This error needs to be translated. return (False, ["You are not allowed to adopt one of the " @@ -1211,7 +1211,7 @@ async def packages_disown(request: Request, package_ids: List[int] = [], # Check that the user has credentials for every package they selected. for pkgbase in bases: - has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) if not has_cred: # TODO: This error needs to be translated. @@ -1235,7 +1235,7 @@ async def packages_delete(request: Request, package_ids: List[int] = [], return (False, ["The selected packages have not been deleted, " "check the confirmation checkbox."]) - if not request.user.has_credential("CRED_PKGBASE_DELETE"): + if not request.user.has_credential(creds.PKGBASE_DELETE): return (False, ["You do not have permission to delete packages."]) # A "memo" used to store names of packages that we delete. @@ -1329,10 +1329,10 @@ async def pkgbase_merge_get(request: Request, name: str, status_code = HTTPStatus.OK # TODO: Lookup errors from credential instead of hardcoding them. - # Idea: Something like credential_errors("CRED_PKGBASE_MERGE"). - # Perhaps additionally: bad_credential_status_code("CRED_PKGBASE_MERGE"). + # Idea: Something like credential_errors(creds.PKGBASE_MERGE). + # Perhaps additionally: bad_credential_status_code(creds.PKGBASE_MERGE). # Don't take these examples verbatim. We should find good naming. - if not request.user.has_credential("CRED_PKGBASE_MERGE"): + if not request.user.has_credential(creds.PKGBASE_MERGE): context["errors"] = [ "Only Trusted Users and Developers can merge packages."] status_code = HTTPStatus.UNAUTHORIZED @@ -1434,7 +1434,7 @@ async def pkgbase_merge_post(request: Request, name: str, context["pkgbase"] = pkgbase # TODO: Lookup errors from credential instead of hardcoding them. - if not request.user.has_credential("CRED_PKGBASE_MERGE"): + if not request.user.has_credential(creds.PKGBASE_MERGE): context["errors"] = [ "Only Trusted Users and Developers can merge packages."] return render_template(request, "pkgbase/merge.html", context, diff --git a/aurweb/templates.py b/aurweb/templates.py index a7102ae1..635b22b4 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -16,7 +16,7 @@ from fastapi.responses import HTMLResponse import aurweb.config -from aurweb import captcha, cookies, l10n, time, util +from aurweb import auth, captcha, cookies, l10n, time, util # Prepare jinja2 objects. _loader = jinja2.FileSystemLoader(os.path.join( @@ -107,6 +107,7 @@ def make_context(request: Request, title: str, next: str = None): "now": datetime.now(tz=zoneinfo.ZoneInfo(timezone)), "utcnow": int(datetime.utcnow().timestamp()), "config": aurweb.config, + "creds": auth.creds, "next": next if next else request.url.path } diff --git a/templates/partials/account/comment.html b/templates/partials/account/comment.html index bc167cf7..8c310738 100644 --- a/templates/partials/account/comment.html +++ b/templates/partials/account/comment.html @@ -3,7 +3,7 @@ {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} {% endif %} -{% if not comment.Deleter or request.user.has_credential("CRED_COMMENT_VIEW_DELETED", approved=[comment.Deleter]) %} +{% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %} {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %}

    diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 2e47a932..f3c293d8 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -53,7 +53,7 @@

    {% endif %} - {% if request.user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") %} + {% if request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) %}

  • {% trans %}Accounts{% endtrans %} @@ -37,7 +37,7 @@
  • {# Only CRED_TU_LIST_VOTES privileged users see Trusted User #} - {% if request.user.has_credential("CRED_TU_LIST_VOTES") %} + {% if request.user.has_credential(creds.TU_LIST_VOTES) %}
  • {% trans %}Trusted User{% endtrans %}
  • diff --git a/templates/partials/comment_actions.html b/templates/partials/comment_actions.html index b8ccf945..78c4cc22 100644 --- a/templates/partials/comment_actions.html +++ b/templates/partials/comment_actions.html @@ -1,7 +1,7 @@ {% set pkgbasename = comment.PackageBase.Name %} {% if not comment.Deleter %} - {% if request.user.has_credential('CRED_COMMENT_DELETE', approved=[comment.User]) %} + {% if request.user.has_credential(creds.COMMENT_DELETE, approved=[comment.User]) %}
    {% endif %} - {% if request.user.has_credential('CRED_COMMENT_EDIT', approved=[comment.User]) %} + {% if request.user.has_credential(creds.COMMENT_EDIT, approved=[comment.User]) %} {% endif %} {% endif %} -{% elif request.user.has_credential("CRED_COMMENT_UNDELETE", approved=[comment.User]) %} +{% elif request.user.has_credential(creds.COMMENT_UNDELETE, approved=[comment.User]) %} {% endif %} - {% if request.user.has_credential('CRED_PKGBASE_EDIT_COMAINTAINERS', approved=[pkgbase.Maintainer]) %} + {% if request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]) %}
  • {{ "Manage Co-Maintainers" | tr }} @@ -107,14 +107,14 @@ {{ "Submit Request" | tr }}
  • - {% if request.user.has_credential("CRED_PKGBASE_DELETE") %} + {% if request.user.has_credential(creds.PKGBASE_DELETE) %}
  • {{ "Delete Package" | tr }}
  • {% endif %} - {% if request.user.has_credential("CRED_PKGBASE_MERGE") %} + {% if request.user.has_credential(creds.PKGBASE_MERGE) %}
  • {{ "Merge Package" | tr }} @@ -130,7 +130,7 @@ />
  • - {% elif request.user.has_credential("CRED_PKGBASE_DISOWN", approved=[pkgbase.Maintainer]) %} + {% elif request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) %}
  • {{ "Disown Package" | tr }} diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index 676a7a73..1427e0a0 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -5,7 +5,7 @@ {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} {% endif %} -{% if not comment.Deleter or request.user.has_credential("CRED_COMMENT_VIEW_DELETED", approved=[comment.Deleter]) %} +{% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %}

    {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} {% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %} diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index dbb81c19..78e0ad1c 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -33,10 +33,10 @@ {% endif %} - {% if pkgbase.keywords.count() or request.user.has_credential("CRED_PKGBASE_SET_KEYWORDS", approved=[pkgbase.Maintainer]) %} + {% if pkgbase.keywords.count() or request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} {{ "Keywords" | tr }}: - {% if request.user.has_credential("CRED_PKGBASE_SET_KEYWORDS", approved=[pkgbase.Maintainer]) %} + {% if request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %}
    Date: Thu, 18 Nov 2021 14:17:46 -0500 Subject: [PATCH 0755/1451] fix(FastAPI): remove login and redirect parameters from auth_required Signed-off-by: Steven Guikal --- aurweb/auth/__init__.py | 27 ++++++-------- aurweb/routers/accounts.py | 22 ++++++------ aurweb/routers/auth.py | 2 +- aurweb/routers/packages.py | 60 ++++++++++++++++---------------- aurweb/routers/trusted_user.py | 10 +++--- test/test_trusted_user_routes.py | 5 +-- 6 files changed, 61 insertions(+), 65 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index 82192cc2..7aa4b526 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -1,5 +1,4 @@ import functools -import re from datetime import datetime from http import HTTPStatus @@ -122,17 +121,12 @@ class BasicAuthBackend(AuthenticationBackend): def auth_required(is_required: bool = True, - login: bool = True, - redirect: str = "/", template: tuple = None, status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): """ Authentication route decorator. - If redirect is given, the user will be redirected if the auth state - does not match is_required. - If template is given, it will be rendered with Unauthorized if - is_required does not match and take priority over redirect. + is_required does not match. A precondition of this function is that, if template is provided, it **must** match the following format: @@ -152,8 +146,6 @@ def auth_required(is_required: bool = True, applying any format operations. :param is_required: A boolean indicating whether the function requires auth - :param login: Redirect to `/login`, passing `next=` - :param redirect: Path to redirect to if is_required isn't True :param template: A three-element template tuple: (path, title_iterable, variable_iterable) :param status_code: An optional status_code for template render. @@ -166,14 +158,17 @@ def auth_required(is_required: bool = True, if request.user.is_authenticated() != is_required: url = "/" - if redirect: - path_params_expr = re.compile(r'\{(\w+)\}') - match = re.findall(path_params_expr, redirect) - args = {k: request.path_params.get(k) for k in match} - url = redirect.format(**args) + if is_required: + if request.method == "GET": + url = request.url.path + elif request.method == "POST" and (referer := request.headers.get("Referer")): + aur = aurweb.config.get("options", "aur_location") + "/" + if not referer.startswith(aur): + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, + detail=_("Bad Referer header.")) + url = referer[len(aur) - 1:] - if login: - url = "/login?" + util.urlencode({"next": url}) + url = "/login?" + util.urlencode({"next": url}) if template: # template=("template.html", diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 360857e8..dade92bb 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -27,14 +27,14 @@ logger = logging.get_logger(__name__) @router.get("/passreset", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def passreset(request: Request): context = await make_variable_context(request, "Password Reset") return render_template(request, "passreset.html", context) @router.post("/passreset", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def passreset_post(request: Request, user: str = Form(...), resetkey: str = Form(default=None), @@ -226,7 +226,7 @@ def make_account_form_context(context: dict, @router.get("/register", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def account_register(request: Request, U: str = Form(default=str()), # Username E: str = Form(default=str()), # Email @@ -252,7 +252,7 @@ async def account_register(request: Request, @router.post("/register", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def account_register_post(request: Request, U: str = Form(default=str()), # Username E: str = Form(default=str()), # Email @@ -340,7 +340,7 @@ def cannot_edit(request, user): @router.get("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True, redirect="/account/{username}") +@auth_required(True) async def account_edit(request: Request, username: str): user = db.query(models.User, models.User.Username == username).first() @@ -356,7 +356,7 @@ async def account_edit(request: Request, username: str): @router.post("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True, redirect="/account/{username}") +@auth_required(True) async def account_edit_post(request: Request, username: str, U: str = Form(default=str()), # Username @@ -443,7 +443,7 @@ async def account(request: Request, username: str): @router.get("/account/{username}/comments") -@auth_required(redirect="/account/{username}/comments") +@auth_required() async def account_comments(request: Request, username: str): user = get_user_by_name(username) context = make_context(request, "Accounts") @@ -454,7 +454,7 @@ async def account_comments(request: Request, username: str): @router.get("/accounts") -@auth_required(True, redirect="/accounts") +@auth_required(True) @account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) @@ -464,7 +464,7 @@ async def accounts(request: Request): @router.post("/accounts") -@auth_required(True, redirect="/accounts") +@auth_required(True) @account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) @@ -548,7 +548,7 @@ def render_terms_of_service(request: Request, @router.get("/tos") -@auth_required(True, redirect="/tos") +@auth_required(True) async def terms_of_service(request: Request): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. @@ -572,7 +572,7 @@ async def terms_of_service(request: Request): @router.post("/tos") -@auth_required(True, redirect="/tos") +@auth_required(True) async def terms_of_service_post(request: Request, accept: bool = Form(default=False)): # Query the database for terms that were previously accepted, diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 1e0b026a..74763667 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -29,7 +29,7 @@ async def login_get(request: Request, next: str = "/"): @router.post("/login", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def login_post(request: Request, next: str = Form(...), user: str = Form(default=str()), diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 2bf04949..4a2cdce3 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -295,7 +295,7 @@ async def package_base_voters(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comments") -@auth_required(True, redirect="/pkgbase/{name}/comments") +@auth_required(True) async def pkgbase_comments_post( request: Request, name: str, comment: str = Form(default=str()), @@ -327,7 +327,7 @@ async def pkgbase_comments_post( @router.get("/pkgbase/{name}/comments/{id}/form") -@auth_required(True, login=False) +@auth_required(True) async def pkgbase_comment_form(request: Request, name: str, id: int, next: str = Query(default=None)): """ Produce a comment form for comment {id}. """ @@ -353,7 +353,7 @@ async def pkgbase_comment_form(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}") +@auth_required(True) async def pkgbase_comment_post( request: Request, name: str, id: int, comment: str = Form(default=str()), @@ -392,7 +392,7 @@ async def pkgbase_comment_post( @router.get("/pkgbase/{name}/comments/{id}/edit") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/edit") +@auth_required(True) async def pkgbase_comment_edit(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -407,7 +407,7 @@ async def pkgbase_comment_edit(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/delete") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/delete") +@auth_required(True) async def pkgbase_comment_delete(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -433,7 +433,7 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/undelete") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/undelete") +@auth_required(True) async def pkgbase_comment_undelete(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -458,7 +458,7 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/pin") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/pin") +@auth_required(True) async def pkgbase_comment_pin(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -483,7 +483,7 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/unpin") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/unpin") +@auth_required(True) async def pkgbase_comment_unpin(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -507,7 +507,7 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, @router.get("/pkgbase/{name}/comaintainers") -@auth_required(True, redirect="/pkgbase/{name}/comaintainers") +@auth_required(True) async def package_base_comaintainers(request: Request, name: str) -> Response: # Get the PackageBase. pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -532,7 +532,7 @@ async def package_base_comaintainers(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comaintainers") -@auth_required(True, redirect="/pkgbase/{name}/comaintainers") +@auth_required(True) async def package_base_comaintainers_post( request: Request, name: str, users: str = Form(default=str())) -> Response: @@ -584,7 +584,7 @@ async def package_base_comaintainers_post( @router.get("/requests") -@auth_required(True, redirect="/requests") +@auth_required(True) async def requests(request: Request, O: int = Query(default=defaults.O), PP: int = Query(default=defaults.PP)): @@ -618,7 +618,7 @@ async def requests(request: Request, @router.get("/pkgbase/{name}/request") -@auth_required(True, redirect="/pkgbase/{name}/request") +@auth_required(True) async def package_request(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) context = await make_variable_context(request, "Submit Request") @@ -627,7 +627,7 @@ async def package_request(request: Request, name: str): @router.post("/pkgbase/{name}/request") -@auth_required(True, redirect="/pkgbase/{name}/request") +@auth_required(True) async def pkgbase_request_post(request: Request, name: str, type: str = Form(...), merge_into: str = Form(default=None), @@ -699,7 +699,7 @@ async def pkgbase_request_post(request: Request, name: str, @router.get("/requests/{id}/close") -@auth_required(True, redirect="/requests/{id}/close") +@auth_required(True) async def requests_close(request: Request, id: int): pkgreq = get_pkgreq_by_id(id) if not request.user.is_elevated() and request.user != pkgreq.User: @@ -712,7 +712,7 @@ async def requests_close(request: Request, id: int): @router.post("/requests/{id}/close") -@auth_required(True, redirect="/requests/{id}/close") +@auth_required(True) async def requests_close_post(request: Request, id: int, reason: int = Form(default=0), comments: str = Form(default=str())): @@ -775,7 +775,7 @@ async def pkgbase_keywords(request: Request, name: str, @router.get("/pkgbase/{name}/flag") -@auth_required(True, redirect="/pkgbase/{name}/flag") +@auth_required(True) async def pkgbase_flag_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -790,7 +790,7 @@ async def pkgbase_flag_get(request: Request, name: str): @router.post("/pkgbase/{name}/flag") -@auth_required(True, redirect="/pkgbase/{name}/flag") +@auth_required(True) async def pkgbase_flag_post(request: Request, name: str, comments: str = Form(default=str())): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -839,7 +839,7 @@ def pkgbase_unflag_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/unflag") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required(True) async def pkgbase_unflag(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_unflag_instance(request, pkgbase) @@ -860,7 +860,7 @@ def pkgbase_notify_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/notify") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required(True) async def pkgbase_notify(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_notify_instance(request, pkgbase) @@ -879,7 +879,7 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/unnotify") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required(True) async def pkgbase_unnotify(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_unnotify_instance(request, pkgbase) @@ -888,7 +888,7 @@ async def pkgbase_unnotify(request: Request, name: str): @router.post("/pkgbase/{name}/vote") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required(True) async def pkgbase_vote(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -912,7 +912,7 @@ async def pkgbase_vote(request: Request, name: str): @router.post("/pkgbase/{name}/unvote") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required(True) async def pkgbase_unvote(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -954,7 +954,7 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase): @router.get("/pkgbase/{name}/disown") -@auth_required(True, redirect="/pkgbase/{name}/disown") +@auth_required(True) async def pkgbase_disown_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -970,7 +970,7 @@ async def pkgbase_disown_get(request: Request, name: str): @router.post("/pkgbase/{name}/disown") -@auth_required(True, redirect="/pkgbase/{name}/disown") +@auth_required(True) async def pkgbase_disown_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -1003,7 +1003,7 @@ def pkgbase_adopt_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/adopt") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required(True) async def pkgbase_adopt_post(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -1019,7 +1019,7 @@ async def pkgbase_adopt_post(request: Request, name: str): @router.get("/pkgbase/{name}/delete") -@auth_required(True, redirect="/pkgbase/{name}/delete") +@auth_required(True) async def pkgbase_delete_get(request: Request, name: str): if not request.user.has_credential(creds.PKGBASE_DELETE): return RedirectResponse(f"/pkgbase/{name}", @@ -1031,7 +1031,7 @@ async def pkgbase_delete_get(request: Request, name: str): @router.post("/pkgbase/{name}/delete") -@auth_required(True, redirect="/pkgbase/{name}/delete") +@auth_required(True) async def pkgbase_delete_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -1279,7 +1279,7 @@ PACKAGE_ACTIONS = { @router.post("/packages") -@auth_required(redirect="/packages") +@auth_required() async def packages_post(request: Request, IDs: List[int] = Form(default=[]), action: str = Form(default=str()), @@ -1311,7 +1311,7 @@ async def packages_post(request: Request, @router.get("/pkgbase/{name}/merge") -@auth_required(redirect="/pkgbase/{name}/merge") +@auth_required() async def pkgbase_merge_get(request: Request, name: str, into: str = Query(default=str()), next: str = Query(default=str())): @@ -1423,7 +1423,7 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase, @router.post("/pkgbase/{name}/merge") -@auth_required(redirect="/pkgbase/{name}/merge") +@auth_required() async def pkgbase_merge_post(request: Request, name: str, into: str = Form(default=str()), confirm: bool = Form(default=False), diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index f0cea61e..09de58fe 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -41,7 +41,7 @@ ADDVOTE_SPECIFICS = { @router.get("/tu") -@auth_required(True, redirect="/tu") +@auth_required(True) @account_type_required(REQUIRED_TYPES) async def trusted_user(request: Request, coff: int = 0, # current offset @@ -147,7 +147,7 @@ def render_proposal(request: Request, @router.get("/tu/{proposal}") -@auth_required(True, redirect="/tu/{proposal}") +@auth_required(True) @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal(request: Request, proposal: int): context = await make_variable_context(request, "Trusted User") @@ -176,7 +176,7 @@ async def trusted_user_proposal(request: Request, proposal: int): @router.post("/tu/{proposal}") -@auth_required(True, redirect="/tu/{proposal}") +@auth_required(True) @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal_post(request: Request, proposal: int, @@ -227,7 +227,7 @@ async def trusted_user_proposal_post(request: Request, @router.get("/addvote") -@auth_required(True, redirect="/addvote") +@auth_required(True) @account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote(request: Request, user: str = str(), @@ -247,7 +247,7 @@ async def trusted_user_addvote(request: Request, @router.post("/addvote") -@auth_required(True, redirect="/addvote") +@auth_required(True) @account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote_post(request: Request, user: str = Form(default=str()), diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index 43a3443b..ac7f82d5 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -9,7 +9,7 @@ import pytest from fastapi.testclient import TestClient -from aurweb import db, util +from aurweb import config, db, util from aurweb.models.account_type import AccountType from aurweb.models.tu_vote import TUVote from aurweb.models.tu_voteinfo import TUVoteInfo @@ -124,8 +124,9 @@ def proposal(user, tu_user): def test_tu_index_guest(client): + headers = {"referer": config.get("options", "aur_location") + "/tu"} with client as request: - response = request.get("/tu", allow_redirects=False) + response = request.get("/tu", allow_redirects=False, headers=headers) assert response.status_code == int(HTTPStatus.SEE_OTHER) params = util.urlencode({"next": "/tu"}) From 0b30216229f561cfdcffd27231f200c3901ce26d Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Thu, 18 Nov 2021 15:18:17 -0500 Subject: [PATCH 0756/1451] fix(FastAPI): remove unnecessary arguments to auth_required Signed-off-by: Steven Guikal --- aurweb/auth/__init__.py | 59 ++-------------------------------- aurweb/routers/accounts.py | 24 ++++++-------- aurweb/routers/packages.py | 54 +++++++++++++++---------------- aurweb/routers/trusted_user.py | 10 +++--- 4 files changed, 43 insertions(+), 104 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index 7aa4b526..8ceb136c 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -5,6 +5,7 @@ from http import HTTPStatus import fastapi +from fastapi import HTTPException from fastapi.responses import RedirectResponse from sqlalchemy import and_ from starlette.authentication import AuthCredentials, AuthenticationBackend @@ -15,7 +16,6 @@ import aurweb.config from aurweb import db, l10n, util from aurweb.models import Session, User from aurweb.models.account_type import ACCOUNT_TYPE_ID -from aurweb.templates import make_variable_context, render_template class StubQuery: @@ -125,29 +125,7 @@ def auth_required(is_required: bool = True, status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): """ Authentication route decorator. - If template is given, it will be rendered with Unauthorized if - is_required does not match. - - A precondition of this function is that, if template is provided, - it **must** match the following format: - - template=("template.html", ["Some Template For", "{}"], ["username"]) - - Where `username` is a FastAPI request path parameter, fitting - a route like: `/some_route/{username}`. - - If you wish to supply a non-formatted template, just omit any Python - format strings (with the '{}' substring). The third tuple element - will not be used, and so anything can be supplied. - - template=("template.html", ["Some Page"], None) - - All title shards and format parameters will be translated before - applying any format operations. - :param is_required: A boolean indicating whether the function requires auth - :param template: A three-element template tuple: - (path, title_iterable, variable_iterable) :param status_code: An optional status_code for template render. Redirects are always SEE_OTHER. """ @@ -164,45 +142,12 @@ def auth_required(is_required: bool = True, elif request.method == "POST" and (referer := request.headers.get("Referer")): aur = aurweb.config.get("options", "aur_location") + "/" if not referer.startswith(aur): + _ = l10n.get_translator_for_request(request) raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=_("Bad Referer header.")) url = referer[len(aur) - 1:] url = "/login?" + util.urlencode({"next": url}) - - if template: - # template=("template.html", - # ["Some Title", "someFormatted {}"], - # ["variable"]) - # => render template.html with title: - # "Some Title someFormatted variables" - path, title_parts, variables = template - _ = l10n.get_translator_for_request(request) - - # Step through title_parts; for each part which contains - # a '{}' in it, apply .format(var) where var = the current - # iteration of variables. - # - # This implies that len(variables) is equal to - # len([part for part in title_parts if '{}' in part]) - # and this must always be true. - # - sanitized = [] - _variables = iter(variables) - for part in title_parts: - if "{}" in part: # If this part is formattable. - key = next(_variables) - var = request.path_params.get(key) - sanitized.append(_(part.format(var))) - else: # Otherwise, just add the translated part. - sanitized.append(_(part)) - - # Glue all title parts together, separated by spaces. - title = " ".join(sanitized) - - context = await make_variable_context(request, title) - return render_template(request, path, context, - status_code=status_code) return RedirectResponse(url, status_code=int(HTTPStatus.SEE_OTHER)) return await func(request, *args, **kwargs) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index dade92bb..388daf84 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -340,7 +340,7 @@ def cannot_edit(request, user): @router.get("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True) +@auth_required() async def account_edit(request: Request, username: str): user = db.query(models.User, models.User.Username == username).first() @@ -356,7 +356,7 @@ async def account_edit(request: Request, username: str): @router.post("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True) +@auth_required() async def account_edit_post(request: Request, username: str, U: str = Form(default=str()), # Username @@ -424,20 +424,14 @@ async def account_edit_post(request: Request, aurtz=TZ, aurlang=L) -account_template = ( - "account/show.html", - ["Account", "{}"], - ["username"] # Query parameters to replace in the title string. -) - - @router.get("/account/{username}") -@auth_required(True, template=account_template, - status_code=HTTPStatus.UNAUTHORIZED) async def account(request: Request, username: str): _ = l10n.get_translator_for_request(request) context = await make_variable_context( request, _("Account") + " " + username) + if not request.user.is_authenticated(): + return render_template(request, "account/show.html", context, + status_code=HTTPStatus.UNAUTHORIZED) context["user"] = get_user_by_name(username) return render_template(request, "account/show.html", context) @@ -454,7 +448,7 @@ async def account_comments(request: Request, username: str): @router.get("/accounts") -@auth_required(True) +@auth_required() @account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) @@ -464,7 +458,7 @@ async def accounts(request: Request): @router.post("/accounts") -@auth_required(True) +@auth_required() @account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) @@ -548,7 +542,7 @@ def render_terms_of_service(request: Request, @router.get("/tos") -@auth_required(True) +@auth_required() async def terms_of_service(request: Request): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. @@ -572,7 +566,7 @@ async def terms_of_service(request: Request): @router.post("/tos") -@auth_required(True) +@auth_required() async def terms_of_service_post(request: Request, accept: bool = Form(default=False)): # Query the database for terms that were previously accepted, diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 4a2cdce3..c06ec51f 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -295,7 +295,7 @@ async def package_base_voters(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comments") -@auth_required(True) +@auth_required() async def pkgbase_comments_post( request: Request, name: str, comment: str = Form(default=str()), @@ -327,7 +327,7 @@ async def pkgbase_comments_post( @router.get("/pkgbase/{name}/comments/{id}/form") -@auth_required(True) +@auth_required() async def pkgbase_comment_form(request: Request, name: str, id: int, next: str = Query(default=None)): """ Produce a comment form for comment {id}. """ @@ -353,7 +353,7 @@ async def pkgbase_comment_form(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}") -@auth_required(True) +@auth_required() async def pkgbase_comment_post( request: Request, name: str, id: int, comment: str = Form(default=str()), @@ -392,7 +392,7 @@ async def pkgbase_comment_post( @router.get("/pkgbase/{name}/comments/{id}/edit") -@auth_required(True) +@auth_required() async def pkgbase_comment_edit(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -407,7 +407,7 @@ async def pkgbase_comment_edit(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/delete") -@auth_required(True) +@auth_required() async def pkgbase_comment_delete(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -433,7 +433,7 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/undelete") -@auth_required(True) +@auth_required() async def pkgbase_comment_undelete(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -458,7 +458,7 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/pin") -@auth_required(True) +@auth_required() async def pkgbase_comment_pin(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -483,7 +483,7 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/unpin") -@auth_required(True) +@auth_required() async def pkgbase_comment_unpin(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -507,7 +507,7 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, @router.get("/pkgbase/{name}/comaintainers") -@auth_required(True) +@auth_required() async def package_base_comaintainers(request: Request, name: str) -> Response: # Get the PackageBase. pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -532,7 +532,7 @@ async def package_base_comaintainers(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comaintainers") -@auth_required(True) +@auth_required() async def package_base_comaintainers_post( request: Request, name: str, users: str = Form(default=str())) -> Response: @@ -584,7 +584,7 @@ async def package_base_comaintainers_post( @router.get("/requests") -@auth_required(True) +@auth_required() async def requests(request: Request, O: int = Query(default=defaults.O), PP: int = Query(default=defaults.PP)): @@ -618,7 +618,7 @@ async def requests(request: Request, @router.get("/pkgbase/{name}/request") -@auth_required(True) +@auth_required() async def package_request(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) context = await make_variable_context(request, "Submit Request") @@ -627,7 +627,7 @@ async def package_request(request: Request, name: str): @router.post("/pkgbase/{name}/request") -@auth_required(True) +@auth_required() async def pkgbase_request_post(request: Request, name: str, type: str = Form(...), merge_into: str = Form(default=None), @@ -699,7 +699,7 @@ async def pkgbase_request_post(request: Request, name: str, @router.get("/requests/{id}/close") -@auth_required(True) +@auth_required() async def requests_close(request: Request, id: int): pkgreq = get_pkgreq_by_id(id) if not request.user.is_elevated() and request.user != pkgreq.User: @@ -712,7 +712,7 @@ async def requests_close(request: Request, id: int): @router.post("/requests/{id}/close") -@auth_required(True) +@auth_required() async def requests_close_post(request: Request, id: int, reason: int = Form(default=0), comments: str = Form(default=str())): @@ -775,7 +775,7 @@ async def pkgbase_keywords(request: Request, name: str, @router.get("/pkgbase/{name}/flag") -@auth_required(True) +@auth_required() async def pkgbase_flag_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -790,7 +790,7 @@ async def pkgbase_flag_get(request: Request, name: str): @router.post("/pkgbase/{name}/flag") -@auth_required(True) +@auth_required() async def pkgbase_flag_post(request: Request, name: str, comments: str = Form(default=str())): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -839,7 +839,7 @@ def pkgbase_unflag_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/unflag") -@auth_required(True) +@auth_required() async def pkgbase_unflag(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_unflag_instance(request, pkgbase) @@ -860,7 +860,7 @@ def pkgbase_notify_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/notify") -@auth_required(True) +@auth_required() async def pkgbase_notify(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_notify_instance(request, pkgbase) @@ -879,7 +879,7 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/unnotify") -@auth_required(True) +@auth_required() async def pkgbase_unnotify(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_unnotify_instance(request, pkgbase) @@ -888,7 +888,7 @@ async def pkgbase_unnotify(request: Request, name: str): @router.post("/pkgbase/{name}/vote") -@auth_required(True) +@auth_required() async def pkgbase_vote(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -912,7 +912,7 @@ async def pkgbase_vote(request: Request, name: str): @router.post("/pkgbase/{name}/unvote") -@auth_required(True) +@auth_required() async def pkgbase_unvote(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -954,7 +954,7 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase): @router.get("/pkgbase/{name}/disown") -@auth_required(True) +@auth_required() async def pkgbase_disown_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -970,7 +970,7 @@ async def pkgbase_disown_get(request: Request, name: str): @router.post("/pkgbase/{name}/disown") -@auth_required(True) +@auth_required() async def pkgbase_disown_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -1003,7 +1003,7 @@ def pkgbase_adopt_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/adopt") -@auth_required(True) +@auth_required() async def pkgbase_adopt_post(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -1019,7 +1019,7 @@ async def pkgbase_adopt_post(request: Request, name: str): @router.get("/pkgbase/{name}/delete") -@auth_required(True) +@auth_required() async def pkgbase_delete_get(request: Request, name: str): if not request.user.has_credential(creds.PKGBASE_DELETE): return RedirectResponse(f"/pkgbase/{name}", @@ -1031,7 +1031,7 @@ async def pkgbase_delete_get(request: Request, name: str): @router.post("/pkgbase/{name}/delete") -@auth_required(True) +@auth_required() async def pkgbase_delete_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 09de58fe..fac68f04 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -41,7 +41,7 @@ ADDVOTE_SPECIFICS = { @router.get("/tu") -@auth_required(True) +@auth_required() @account_type_required(REQUIRED_TYPES) async def trusted_user(request: Request, coff: int = 0, # current offset @@ -147,7 +147,7 @@ def render_proposal(request: Request, @router.get("/tu/{proposal}") -@auth_required(True) +@auth_required() @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal(request: Request, proposal: int): context = await make_variable_context(request, "Trusted User") @@ -176,7 +176,7 @@ async def trusted_user_proposal(request: Request, proposal: int): @router.post("/tu/{proposal}") -@auth_required(True) +@auth_required() @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal_post(request: Request, proposal: int, @@ -227,7 +227,7 @@ async def trusted_user_proposal_post(request: Request, @router.get("/addvote") -@auth_required(True) +@auth_required() @account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote(request: Request, user: str = str(), @@ -247,7 +247,7 @@ async def trusted_user_addvote(request: Request, @router.post("/addvote") -@auth_required(True) +@auth_required() @account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote_post(request: Request, user: str = Form(default=str()), From 2fee6205a6d11ad6ecae4b003991fc3aea1e992f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 01:42:19 -0800 Subject: [PATCH 0757/1451] housekeep(fastapi): rewrite test_rpc with fixtures Signed-off-by: Kevin Morris --- test/test_rpc.py | 532 +++++++++++++++++++++++++++-------------------- 1 file changed, 308 insertions(+), 224 deletions(-) diff --git a/test/test_rpc.py b/test/test_rpc.py index b61a7e4e..acb82cad 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1,6 +1,8 @@ import re +from datetime import datetime from http import HTTPStatus +from typing import List from unittest import mock import orjson @@ -9,10 +11,11 @@ import pytest from fastapi.testclient import TestClient from redis.client import Pipeline -from aurweb import asgi, config, scripts -from aurweb.db import begin, create, query -from aurweb.models.account_type import AccountType -from aurweb.models.dependency_type import DependencyType +import aurweb.models.dependency_type as dt +import aurweb.models.relation_type as rt + +from aurweb import asgi, config, db, scripts +from aurweb.models.account_type import USER_ID from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -21,7 +24,6 @@ from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_license import PackageLicense from aurweb.models.package_relation import PackageRelation from aurweb.models.package_vote import PackageVote -from aurweb.models.relation_type import RelationType from aurweb.models.user import User from aurweb.redis import redis_connection @@ -31,163 +33,172 @@ def client() -> TestClient: yield TestClient(app=asgi.app) -@pytest.fixture(autouse=True) -def setup(db_test): - # TODO: Rework this into organized fixtures. +@pytest.fixture +def user(db_test) -> User: + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User 1", Passwd=str(), + AccountTypeID=USER_ID) + yield user - # Create test package details. - with begin(): - # Get ID types. - account_type = query(AccountType, AccountType.AccountType == "User").first() - dependency_depends = query(DependencyType, DependencyType.Name == "depends").first() - dependency_optdepends = query(DependencyType, DependencyType.Name == "optdepends").first() - dependency_makedepends = query(DependencyType, DependencyType.Name == "makedepends").first() - dependency_checkdepends = query(DependencyType, DependencyType.Name == "checkdepends").first() +@pytest.fixture +def user2() -> User: + with db.begin(): + user = db.create(User, Username="user2", Email="user2@example.org", + RealName="Test User 2", Passwd=str(), + AccountTypeID=USER_ID) + yield user - relation_conflicts = query(RelationType, RelationType.Name == "conflicts").first() - relation_provides = query(RelationType, RelationType.Name == "provides").first() - relation_replaces = query(RelationType, RelationType.Name == "replaces").first() - # Create database info. - user1 = create(User, - Username="user1", - Email="user1@example.com", - RealName="Test User 1", - Passwd="testPassword", - AccountType=account_type) +@pytest.fixture +def user3() -> User: + with db.begin(): + user = db.create(User, Username="user3", Email="user3@example.org", + RealName="Test User 3", Passwd=str(), + AccountTypeID=USER_ID) + yield user - user2 = create(User, - Username="user2", - Email="user2@example.com", - RealName="Test User 2", - Passwd="testPassword", - AccountType=account_type) - user3 = create(User, - Username="user3", - Email="user3@example.com", - RealName="Test User 3", - Passwd="testPassword", - AccountType=account_type) +@pytest.fixture +def packages(user: User, user2: User, user3: User) -> List[Package]: + output = [] - pkgbase1 = create(PackageBase, Name="big-chungus", - Maintainer=user1, - Packager=user1) + # Create package records used in our tests. + with db.begin(): + pkgbase = db.create(PackageBase, Name="big-chungus", + Maintainer=user, Packager=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, + Description="Bunny bunny around bunny", + URL="https://example.com/") + output.append(pkg) - pkgname1 = create(Package, - PackageBase=pkgbase1, - Name=pkgbase1.Name, - Description="Bunny bunny around bunny", - URL="https://example.com/") + pkgbase = db.create(PackageBase, Name="chungy-chungus", + Maintainer=user, Packager=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, + Description="Wubby wubby on wobba wuubu", + URL="https://example.com/") + output.append(pkg) - pkgbase2 = create(PackageBase, Name="chungy-chungus", - Maintainer=user1, - Packager=user1) + pkgbase = db.create(PackageBase, Name="gluggly-chungus", + Maintainer=user, Packager=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, + Description="glurrba glurrba gur globba", + URL="https://example.com/") + output.append(pkg) - pkgname2 = create(Package, - PackageBase=pkgbase2, - Name=pkgbase2.Name, - Description="Wubby wubby on wobba wuubu", - URL="https://example.com/") - - pkgbase3 = create(PackageBase, Name="gluggly-chungus", - Maintainer=user1, - Packager=user1) - - pkgbase4 = create(PackageBase, Name="fugly-chungus", - Maintainer=user1, - Packager=user1) + pkgbase = db.create(PackageBase, Name="fugly-chungus", + Maintainer=user, Packager=user) desc = "A Package belonging to a PackageBase with another name." - create(Package, - PackageBase=pkgbase4, - Name="other-pkg", - Description=desc, - URL="https://example.com") + pkg = db.create(Package, PackageBase=pkgbase, Name="other-pkg", + Description=desc, URL="https://example.com") + output.append(pkg) - create(Package, - PackageBase=pkgbase3, - Name=pkgbase3.Name, - Description="glurrba glurrba gur globba", - URL="https://example.com/") + pkgbase = db.create(PackageBase, Name="woogly-chungus") + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, + Description="wuggla woblabeloop shemashmoop", + URL="https://example.com/") + output.append(pkg) - pkgbase4 = create(PackageBase, Name="woogly-chungus") + # Setup a few more related records on the first package: + # a license, some keywords and some votes. + with db.begin(): + lic = db.create(License, Name="GPL") + db.create(PackageLicense, Package=output[0], License=lic) - create(Package, - PackageBase=pkgbase4, - Name=pkgbase4.Name, - Description="wuggla woblabeloop shemashmoop", - URL="https://example.com/") + for keyword in ["big-chungus", "smol-chungus", "sizeable-chungus"]: + db.create(PackageKeyword, + PackageBase=output[0].PackageBase, + Keyword=keyword) - # Dependencies. - create(PackageDependency, - Package=pkgname1, - DependencyType=dependency_depends, - DepName="chungus-depends") + now = int(datetime.utcnow().timestamp()) + for user_ in [user, user2, user3]: + db.create(PackageVote, User=user_, + PackageBase=output[0].PackageBase, VoteTS=now) + scripts.popupdate.run_single(output[0].PackageBase) - create(PackageDependency, - Package=pkgname2, - DependencyType=dependency_depends, - DepName="chungy-depends") + yield output - create(PackageDependency, - Package=pkgname1, - DependencyType=dependency_optdepends, - DepName="chungus-optdepends", - DepCondition="=50") - create(PackageDependency, - Package=pkgname1, - DependencyType=dependency_makedepends, - DepName="chungus-makedepends") +@pytest.fixture +def depends(packages: List[Package]) -> List[PackageDependency]: + output = [] - create(PackageDependency, - Package=pkgname1, - DependencyType=dependency_checkdepends, - DepName="chungus-checkdepends") + with db.begin(): + dep = db.create(PackageDependency, + Package=packages[0], + DepTypeID=dt.DEPENDS_ID, + DepName="chungus-depends") + output.append(dep) - # Relations. - create(PackageRelation, - Package=pkgname1, - RelationType=relation_conflicts, - RelName="chungus-conflicts") + dep = db.create(PackageDependency, + Package=packages[1], + DepTypeID=dt.DEPENDS_ID, + DepName="chungy-depends") + output.append(dep) - create(PackageRelation, - Package=pkgname2, - RelationType=relation_conflicts, - RelName="chungy-conflicts") + dep = db.create(PackageDependency, + Package=packages[0], + DepTypeID=dt.OPTDEPENDS_ID, + DepName="chungus-optdepends", + DepCondition="=50") + output.append(dep) - create(PackageRelation, - Package=pkgname1, - RelationType=relation_provides, - RelName="chungus-provides", - RelCondition="<=200") + dep = db.create(PackageDependency, + Package=packages[0], + DepTypeID=dt.MAKEDEPENDS_ID, + DepName="chungus-makedepends") + output.append(dep) - create(PackageRelation, - Package=pkgname1, - RelationType=relation_replaces, - RelName="chungus-replaces", - RelCondition="<=200") + dep = db.create(PackageDependency, + Package=packages[0], + DepTypeID=dt.CHECKDEPENDS_ID, + DepName="chungus-checkdepends") + output.append(dep) - license = create(License, Name="GPL") + yield output - create(PackageLicense, - Package=pkgname1, - License=license) - for i in ["big-chungus", "smol-chungus", "sizeable-chungus"]: - create(PackageKeyword, - PackageBase=pkgbase1, - Keyword=i) +@pytest.fixture +def relations(user: User, packages: List[Package]) -> List[PackageRelation]: + output = [] - for i in [user1, user2, user3]: - create(PackageVote, - User=i, - PackageBase=pkgbase1, - VoteTS=5000) + with db.begin(): + rel = db.create(PackageRelation, + Package=packages[0], + RelTypeID=rt.CONFLICTS_ID, + RelName="chungus-conflicts") + output.append(rel) - scripts.popupdate.run_single(pkgbase1) + rel = db.create(PackageRelation, + Package=packages[1], + RelTypeID=rt.CONFLICTS_ID, + RelName="chungy-conflicts") + output.append(rel) + + rel = db.create(PackageRelation, + Package=packages[0], + RelTypeID=rt.PROVIDES_ID, + RelName="chungus-provides", + RelCondition="<=200") + output.append(rel) + + rel = db.create(PackageRelation, + Package=packages[0], + RelTypeID=rt.REPLACES_ID, + RelName="chungus-replaces", + RelCondition="<=200") + output.append(rel) + + # Finally, yield the packages. + yield output + + +@pytest.fixture(autouse=True) +def setup(db_test): + # Create some extra package relationships. + pass @pytest.fixture @@ -195,28 +206,35 @@ def pipeline(): redis = redis_connection() pipeline = redis.pipeline() + # The 'testclient' host is used when requesting the app + # via fastapi.testclient.TestClient. pipeline.delete("ratelimit-ws:testclient") pipeline.delete("ratelimit:testclient") - one, two = pipeline.execute() + pipeline.execute() yield pipeline -def test_rpc_singular_info(client: TestClient): +def test_rpc_singular_info(client: TestClient, + user: User, + packages: List[Package], + depends: List[PackageDependency], + relations: List[PackageRelation]): # Define expected response. + pkg = packages[0] expected_data = { "version": 5, "results": [{ - "Name": "big-chungus", - "Version": "", - "Description": "Bunny bunny around bunny", - "URL": "https://example.com/", - "PackageBase": "big-chungus", - "NumVotes": 3, - "Popularity": 0.0, + "Name": pkg.Name, + "Version": pkg.Version, + "Description": pkg.Description, + "URL": pkg.URL, + "PackageBase": pkg.PackageBase.Name, + "NumVotes": pkg.PackageBase.NumVotes, + "Popularity": float(pkg.PackageBase.Popularity), "OutOfDate": None, - "Maintainer": "user1", - "URLPath": "/cgit/aur.git/snapshot/big-chungus.tar.gz", + "Maintainer": user.Username, + "URLPath": f"/cgit/aur.git/snapshot/{pkg.Name}.tar.gz", "Depends": ["chungus-depends"], "OptDepends": ["chungus-optdepends=50"], "MakeDepends": ["chungus-makedepends"], @@ -224,7 +242,7 @@ def test_rpc_singular_info(client: TestClient): "Conflicts": ["chungus-conflicts"], "Provides": ["chungus-provides<=200"], "Replaces": ["chungus-replaces<=200"], - "License": ["GPL"], + "License": [pkg.package_licenses.first().License.Name], "Keywords": [ "big-chungus", "sizeable-chungus", @@ -237,20 +255,23 @@ def test_rpc_singular_info(client: TestClient): # Make dummy request. with client as request: - response_arg = request.get( - "/rpc/?v=5&type=info&arg=chungy-chungus&arg=big-chungus") + resp = request.get("/rpc", params={ + "v": 5, + "type": "info", + "arg": ["chungy-chungus", "big-chungus"], + }) # Load request response into Python dictionary. - response_info_arg = orjson.loads(response_arg.content.decode()) + response_data = orjson.loads(resp.text) # Remove the FirstSubmitted LastModified, ID and PackageBaseID keys from # reponse, as the key's values aren't guaranteed to match between the two # (the keys are already removed from 'expected_data'). for i in ["FirstSubmitted", "LastModified", "ID", "PackageBaseID"]: - response_info_arg["results"][0].pop(i) + response_data["results"][0].pop(i) # Validate that the new dictionaries are the same. - assert response_info_arg == expected_data + assert response_data == expected_data def test_rpc_nonexistent_package(client: TestClient): @@ -265,12 +286,13 @@ def test_rpc_nonexistent_package(client: TestClient): assert response_data["resultcount"] == 0 -def test_rpc_multiinfo(client: TestClient): +def test_rpc_multiinfo(client: TestClient, packages: List[Package]): # Make dummy request. request_packages = ["big-chungus", "chungy-chungus"] with client as request: - response = request.get( - "/rpc/?v=5&type=info&arg[]=big-chungus&arg[]=chungy-chungus") + response = request.get("/rpc", params={ + "v": 5, "type": "info", "arg[]": request_packages + }) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -282,19 +304,21 @@ def test_rpc_multiinfo(client: TestClient): assert request_packages == [] -def test_rpc_mixedargs(client: TestClient): +def test_rpc_mixedargs(client: TestClient, packages: List[Package]): # Make dummy request. response1_packages = ["gluggly-chungus"] response2_packages = ["gluggly-chungus", "chungy-chungus"] with client as request: + # Supply all of the args in the url to enforce ordering. response1 = request.get( "/rpc?v=5&arg[]=big-chungus&arg=gluggly-chungus&type=info") assert response1.status_code == int(HTTPStatus.OK) with client as request: response2 = request.get( - "/rpc?v=5&arg=big-chungus&arg[]=gluggly-chungus&type=info&arg[]=chungy-chungus") + "/rpc?v=5&arg=big-chungus&arg[]=gluggly-chungus" + "&type=info&arg[]=chungy-chungus") assert response1.status_code == int(HTTPStatus.OK) # Load request response into Python dictionary. @@ -312,22 +336,27 @@ def test_rpc_mixedargs(client: TestClient): assert i == [] -def test_rpc_no_dependencies(client: TestClient): - """This makes sure things like 'MakeDepends' get removed from JSON strings - when they don't have set values.""" - +def test_rpc_no_dependencies_omits_key(client: TestClient, user: User, + packages: List[Package], + depends: List[PackageDependency], + relations: List[PackageRelation]): + """ + This makes sure things like 'MakeDepends' get removed from JSON strings + when they don't have set values. + """ + pkg = packages[1] expected_response = { 'version': 5, 'results': [{ - 'Name': 'chungy-chungus', - 'Version': '', - 'Description': 'Wubby wubby on wobba wuubu', - 'URL': 'https://example.com/', - 'PackageBase': 'chungy-chungus', - 'NumVotes': 0, - 'Popularity': 0.0, + 'Name': pkg.Name, + 'Version': pkg.Version, + 'Description': pkg.Description, + 'URL': pkg.URL, + 'PackageBase': pkg.PackageBase.Name, + 'NumVotes': pkg.PackageBase.NumVotes, + 'Popularity': int(pkg.PackageBase.Popularity), 'OutOfDate': None, - 'Maintainer': 'user1', + 'Maintainer': user.Username, 'URLPath': '/cgit/aur.git/snapshot/chungy-chungus.tar.gz', 'Depends': ['chungy-depends'], 'Conflicts': ['chungy-conflicts'], @@ -340,7 +369,9 @@ def test_rpc_no_dependencies(client: TestClient): # Make dummy request. with client as request: - response = request.get("/rpc/?v=5&type=info&arg=chungy-chungus") + response = request.get("/rpc", params={ + "v": 5, "type": "info", "arg": "chungy-chungus" + }) response_data = orjson.loads(response.content.decode()) # Remove inconsistent keys. @@ -362,7 +393,9 @@ def test_rpc_bad_type(client: TestClient): # Make dummy request. with client as request: - response = request.get("/rpc/?v=5&type=invalid-type&arg=big-chungus") + response = request.get("/rpc", params={ + "v": 5, "type": "invalid-type", "arg": "big-chungus" + }) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -383,7 +416,9 @@ def test_rpc_bad_version(client: TestClient): # Make dummy request. with client as request: - response = request.get("/rpc/?v=0&type=info&arg=big-chungus") + response = request.get("/rpc", params={ + "v": 0, "type": "info", "arg": "big-chungus" + }) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -404,7 +439,10 @@ def test_rpc_no_version(client: TestClient): # Make dummy request. with client as request: - response = request.get("/rpc/?type=info&arg=big-chungus") + response = request.get("/rpc", params={ + "type": "info", + "arg": "big-chungus" + }) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -425,7 +463,7 @@ def test_rpc_no_type(client: TestClient): # Make dummy request. with client as request: - response = request.get("/rpc/?v=5&arg=big-chungus") + response = request.get("/rpc", params={"v": 5, "arg": "big-chungus"}) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -446,7 +484,7 @@ def test_rpc_no_args(client: TestClient): # Make dummy request. with client as request: - response = request.get("/rpc/?v=5&type=info") + response = request.get("/rpc", params={"v": 5, "type": "info"}) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -455,10 +493,12 @@ def test_rpc_no_args(client: TestClient): assert expected_data == response_data -def test_rpc_no_maintainer(client: TestClient): +def test_rpc_no_maintainer(client: TestClient, packages: List[Package]): # Make dummy request. with client as request: - response = request.get("/rpc/?v=5&type=info&arg=woogly-chungus") + response = request.get("/rpc", params={ + "v": 5, "type": "info", "arg": "woogly-chungus" + }) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -467,39 +507,45 @@ def test_rpc_no_maintainer(client: TestClient): assert response_data["results"][0]["Maintainer"] is None -def test_rpc_suggest_pkgbase(client: TestClient): +def test_rpc_suggest_pkgbase(client: TestClient, packages: List[Package]): + params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} with client as request: - response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") + response = request.get("/rpc", params=params) data = response.json() assert data == ["big-chungus"] + params["arg"] = "chungy" with client as request: - response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=chungy") + response = request.get("/rpc", params=params) data = response.json() assert data == ["chungy-chungus"] # Test no arg supplied. + del params["arg"] with client as request: - response = request.get("/rpc?v=5&type=suggest-pkgbase") + response = request.get("/rpc", params=params) data = response.json() assert data == [] -def test_rpc_suggest(client: TestClient): +def test_rpc_suggest(client: TestClient, packages: List[Package]): + params = {"v": 5, "type": "suggest", "arg": "other"} with client as request: - response = request.get("/rpc?v=5&type=suggest&arg=other") + response = request.get("/rpc", params=params) data = response.json() assert data == ["other-pkg"] # Test non-existent Package. + params["arg"] = "nonexistent" with client as request: - response = request.get("/rpc?v=5&type=suggest&arg=nonexistent") + response = request.get("/rpc", params=params) data = response.json() assert data == [] # Test no arg supplied. + del params["arg"] with client as request: - response = request.get("/rpc?v=5&type=suggest") + response = request.get("/rpc", params=params) data = response.json() assert data == [] @@ -514,16 +560,18 @@ def mock_config_getint(section: str, key: str): @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, - pipeline: Pipeline): + pipeline: Pipeline, packages: List[Package]): + params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} + for i in range(4): # The first 4 requests should be good. with client as request: - response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") + response = request.get("/rpc", params=params) assert response.status_code == int(HTTPStatus.OK) # The fifth request should be banned. with client as request: - response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") + response = request.get("/rpc", params=params) assert response.status_code == int(HTTPStatus.TOO_MANY_REQUESTS) # Delete the cached records. @@ -534,124 +582,155 @@ def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, # The new first request should be good. with client as request: - response = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") + response = request.get("/rpc", params=params) assert response.status_code == int(HTTPStatus.OK) -def test_rpc_etag(client: TestClient): - with client as request: - response1 = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") +def test_rpc_etag(client: TestClient, packages: List[Package]): + params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} with client as request: - response2 = request.get("/rpc?v=5&type=suggest-pkgbase&arg=big") + response1 = request.get("/rpc", params=params) + with client as request: + response2 = request.get("/rpc", params=params) + assert response1.headers.get("ETag") is not None assert response1.headers.get("ETag") != str() assert response1.headers.get("ETag") == response2.headers.get("ETag") def test_rpc_search_arg_too_small(client: TestClient): + params = {"v": 5, "type": "search", "arg": "b"} with client as request: - response = request.get("/rpc?v=5&type=search&arg=b") + response = request.get("/rpc", params=params) assert response.status_code == int(HTTPStatus.OK) assert response.json().get("error") == "Query arg too small." -def test_rpc_search(client: TestClient): +def test_rpc_search(client: TestClient, packages: List[Package]): + params = {"v": 5, "type": "search", "arg": "big"} with client as request: - response = request.get("/rpc?v=5&type=search&arg=big") + response = request.get("/rpc", params=params) assert response.status_code == int(HTTPStatus.OK) data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] - assert result.get("Name") == "big-chungus" + assert result.get("Name") == packages[0].Name # Test the If-None-Match headers. etag = response.headers.get("ETag").strip('"') headers = {"If-None-Match": etag} - response = request.get("/rpc?v=5&type=search&arg=big", headers=headers) + response = request.get("/rpc", params=params, headers=headers) assert response.status_code == int(HTTPStatus.NOT_MODIFIED) assert response.content == b'' # No args on non-m by types return an error. - response = request.get("/rpc?v=5&type=search") + del params["arg"] + with client as request: + response = request.get("/rpc", params=params) assert response.json().get("error") == "No request type/data specified." -def test_rpc_msearch(client: TestClient): +def test_rpc_msearch(client: TestClient, user: User, packages: List[Package]): + params = {"v": 5, "type": "msearch", "arg": user.Username} with client as request: - response = request.get("/rpc?v=5&type=msearch&arg=user1") + response = request.get("/rpc", params=params) data = response.json() # user1 maintains 4 packages; assert that we got them all. assert data.get("resultcount") == 4 names = list(sorted(r.get("Name") for r in data.get("results"))) - expected_results = list(sorted([ + expected_results = [ "big-chungus", "chungy-chungus", "gluggly-chungus", "other-pkg" - ])) + ] assert names == expected_results # Search for a non-existent maintainer, giving us zero packages. - response = request.get("/rpc?v=5&type=msearch&arg=blah-blah") + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) data = response.json() assert data.get("resultcount") == 0 # A missing arg still succeeds, but it returns all orphans. # Just verify that we receive no error and the orphaned result. - response = request.get("/rpc?v=5&type=msearch") + params.pop("arg") + response = request.get("/rpc", params=params) data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] assert result.get("Name") == "woogly-chungus" -def test_rpc_search_depends(client: TestClient): +def test_rpc_search_depends(client: TestClient, packages: List[Package], + depends: List[PackageDependency]): + params = { + "v": 5, "type": "search", "by": "depends", "arg": "chungus-depends" + } with client as request: - response = request.get( - "/rpc?v=5&type=search&by=depends&arg=chungus-depends") + response = request.get("/rpc", params=params) data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] - assert result.get("Name") == "big-chungus" + assert result.get("Name") == packages[0].Name -def test_rpc_search_makedepends(client: TestClient): +def test_rpc_search_makedepends(client: TestClient, packages: List[Package], + depends: List[PackageDependency]): + params = { + "v": 5, + "type": "search", + "by": "makedepends", + "arg": "chungus-makedepends" + } with client as request: - response = request.get( - "/rpc?v=5&type=search&by=makedepends&arg=chungus-makedepends") + response = request.get("/rpc", params=params) data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] - assert result.get("Name") == "big-chungus" + assert result.get("Name") == packages[0].Name -def test_rpc_search_optdepends(client: TestClient): +def test_rpc_search_optdepends(client: TestClient, packages: List[Package], + depends: List[PackageDependency]): + params = { + "v": 5, + "type": "search", + "by": "optdepends", + "arg": "chungus-optdepends" + } with client as request: - response = request.get( - "/rpc?v=5&type=search&by=optdepends&arg=chungus-optdepends") + response = request.get("/rpc", params=params) data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] - assert result.get("Name") == "big-chungus" + assert result.get("Name") == packages[0].Name -def test_rpc_search_checkdepends(client: TestClient): +def test_rpc_search_checkdepends(client: TestClient, packages: List[Package], + depends: List[PackageDependency]): + params = { + "v": 5, + "type": "search", + "by": "checkdepends", + "arg": "chungus-checkdepends" + } with client as request: - response = request.get( - "/rpc?v=5&type=search&by=checkdepends&arg=chungus-checkdepends") + response = request.get("/rpc", params=params) data = response.json() assert data.get("resultcount") == 1 result = data.get("results")[0] - assert result.get("Name") == "big-chungus" + assert result.get("Name") == packages[0].Name def test_rpc_incorrect_by(client: TestClient): + params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} with client as request: - response = request.get("/rpc?v=5&type=search&by=fake&arg=big") + response = request.get("/rpc", params=params) assert response.json().get("error") == "Incorrect by field specified." @@ -661,15 +740,20 @@ def test_rpc_jsonp_callback(client: TestClient): For end-to-end verification, the `examples/jsonp.html` file can be used to submit jsonp callback requests to the RPC. """ + params = { + "v": 5, + "type": "search", + "arg": "big", + "callback": "jsonCallback" + } with client as request: - response = request.get( - "/rpc?v=5&type=search&arg=big&callback=jsonCallback") + response = request.get("/rpc", params=params) assert response.headers.get("content-type") == "text/javascript" assert re.search(r'^/\*\*/jsonCallback\(.*\)$', response.text) is not None # Test an invalid callback name; we get an application/json error. + params["callback"] = "jsonCallback!" with client as request: - response = request.get( - "/rpc?v=5&type=search&arg=big&callback=jsonCallback!") + response = request.get("/rpc", params=params) assert response.headers.get("content-type") == "application/json" assert response.json().get("error") == "Invalid callback name." From 604df50b88ac6a1b7babfeebb7ee2ae2844fba2a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 01:49:35 -0800 Subject: [PATCH 0758/1451] housekeep(fastapi): rewrite test_package_comment with fixtures Signed-off-by: Kevin Morris --- test/test_package_comment.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test/test_package_comment.py b/test/test_package_comment.py index b00e08c3..c89e23af 100644 --- a/test/test_package_comment.py +++ b/test/test_package_comment.py @@ -8,21 +8,29 @@ from aurweb.models.package_base import PackageBase from aurweb.models.package_comment import PackageComment from aurweb.models.user import User -user = pkgbase = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase -def test_package_comment_creation(): +def test_package_comment_creation(user: User, pkgbase: PackageBase): with db.begin(): package_comment = db.create(PackageComment, PackageBase=pkgbase, User=user, Comments="Test comment.", @@ -30,26 +38,28 @@ def test_package_comment_creation(): assert bool(package_comment.ID) -def test_package_comment_null_package_base_raises_exception(): +def test_package_comment_null_pkgbase_raises(user: User): with pytest.raises(IntegrityError): PackageComment(User=user, Comments="Test comment.", RenderedComment="Test rendered comment.") -def test_package_comment_null_user_raises_exception(): +def test_package_comment_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageComment(PackageBase=pkgbase, Comments="Test comment.", RenderedComment="Test rendered comment.") -def test_package_comment_null_comments_raises_exception(): +def test_package_comment_null_comments_raises(user: User, + pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageComment(PackageBase=pkgbase, User=user, RenderedComment="Test rendered comment.") -def test_package_comment_null_renderedcomment_defaults(): +def test_package_comment_null_renderedcomment_defaults(user: User, + pkgbase: PackageBase): with db.begin(): record = db.create(PackageComment, PackageBase=pkgbase, User=user, Comments="Test comment.") From 012dd24fd85d24369556d4a3eb7e0675fa0c5856 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 01:53:02 -0800 Subject: [PATCH 0759/1451] housekeep(fastapi): rewrite test_tu_vote with fixtures Signed-off-by: Kevin Morris --- test/test_tu_vote.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test/test_tu_vote.py b/test/test_tu_vote.py index 1dd33387..9bb344b1 100644 --- a/test/test_tu_vote.py +++ b/test/test_tu_vote.py @@ -10,28 +10,33 @@ from aurweb.models.tu_vote import TUVote from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User -user = tu_voteinfo = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, tu_voteinfo + return - ts = int(datetime.utcnow().timestamp()) + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=TRUSTED_USER_ID) + yield user - tu_voteinfo = db.create(TUVoteInfo, - Agenda="Blah blah.", + +@pytest.fixture +def tu_voteinfo(user: User) -> TUVoteInfo: + ts = int(datetime.utcnow().timestamp()) + with db.begin(): + tu_voteinfo = db.create(TUVoteInfo, Agenda="Blah blah.", User=user.Username, Submitted=ts, End=ts + 5, - Quorum=0.5, - Submitter=user) + Quorum=0.5, Submitter=user) + yield tu_voteinfo -def test_tu_vote_creation(): +def test_tu_vote_creation(user: User, tu_voteinfo: TUVoteInfo): with db.begin(): tu_vote = db.create(TUVote, User=user, VoteInfo=tu_voteinfo) @@ -41,11 +46,11 @@ def test_tu_vote_creation(): assert tu_vote in tu_voteinfo.tu_votes -def test_tu_vote_null_user_raises_exception(): +def test_tu_vote_null_user_raises_exception(tu_voteinfo: TUVoteInfo): with pytest.raises(IntegrityError): TUVote(VoteInfo=tu_voteinfo) -def test_tu_vote_null_voteinfo_raises_exception(): +def test_tu_vote_null_voteinfo_raises_exception(user: User): with pytest.raises(IntegrityError): TUVote(User=user) From adafa6ebc14769b8ab3d8564b3c59d6ae441b81a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 01:56:49 -0800 Subject: [PATCH 0760/1451] housekeep(fastapi): rewrite test_package_request with fixtures Signed-off-by: Kevin Morris --- test/test_package_request.py | 37 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/test/test_package_request.py b/test/test_package_request.py index 4b5dfb2b..1ba48e09 100644 --- a/test/test_package_request.py +++ b/test/test_package_request.py @@ -12,21 +12,29 @@ from aurweb.models.package_request import (ACCEPTED, ACCEPTED_ID, CLOSED, CLOSED from aurweb.models.request_type import MERGE_ID from aurweb.models.user import User -user = pkgbase = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase -def test_package_request_creation(): +def test_package_request_creation(user: User, pkgbase: PackageBase): with db.begin(): package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, User=user, PackageBase=pkgbase, @@ -45,7 +53,7 @@ def test_package_request_creation(): assert package_request in pkgbase.requests -def test_package_request_closed(): +def test_package_request_closed(user: User, pkgbase: PackageBase): ts = int(datetime.utcnow().timestamp()) with db.begin(): package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, @@ -61,49 +69,54 @@ def test_package_request_closed(): assert package_request in user.closed_requests -def test_package_request_null_request_type_raises_exception(): +def test_package_request_null_request_type_raises(user: User, + pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageRequest(User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, Comments=str(), ClosureComment=str()) -def test_package_request_null_user_raises_exception(): +def test_package_request_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageRequest(ReqTypeID=MERGE_ID, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, Comments=str(), ClosureComment=str()) -def test_package_request_null_package_base_raises_exception(): +def test_package_request_null_package_base_raises(user: User, + pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageRequest(ReqTypeID=MERGE_ID, User=user, PackageBaseName=pkgbase.Name, Comments=str(), ClosureComment=str()) -def test_package_request_null_package_base_name_raises_exception(): +def test_package_request_null_package_base_name_raises(user: User, + pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageRequest(ReqTypeID=MERGE_ID, User=user, PackageBase=pkgbase, Comments=str(), ClosureComment=str()) -def test_package_request_null_comments_raises_exception(): +def test_package_request_null_comments_raises(user: User, + pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageRequest(ReqTypeID=MERGE_ID, User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, ClosureComment=str()) -def test_package_request_null_closure_comment_raises_exception(): +def test_package_request_null_closure_comment_raises(user: User, + pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageRequest(ReqTypeID=MERGE_ID, User=user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, Comments=str()) -def test_package_request_status_display(): +def test_package_request_status_display(user: User, pkgbase: PackageBase): """ Test status_display() based on the Status column value. """ with db.begin(): pkgreq = db.create(PackageRequest, ReqTypeID=MERGE_ID, From 735c5f57cb467174f966c5030c6f13cb5481756b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 16:25:04 -0800 Subject: [PATCH 0761/1451] housekeep(fastapi): rewrite test_package_blacklist Signed-off-by: Kevin Morris --- test/test_package_blacklist.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/test/test_package_blacklist.py b/test/test_package_blacklist.py index 6f4c36d7..427c3be4 100644 --- a/test/test_package_blacklist.py +++ b/test/test_package_blacklist.py @@ -3,21 +3,12 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.models.package_base import PackageBase from aurweb.models.package_blacklist import PackageBlacklist -from aurweb.models.user import User - -user = pkgbase = None @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase - - with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") - pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + return def test_package_blacklist_creation(): From d6cb3b9fac73d531b399ddcb86710df765260bc5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 16:30:33 -0800 Subject: [PATCH 0762/1451] housekeep(fastapi): rewrite test_auth with fixtures Signed-off-by: Kevin Morris --- test/test_auth.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/test/test_auth.py b/test/test_auth.py index 0dc26f86..b63fb96f 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -11,35 +11,40 @@ from aurweb.models.session import Session from aurweb.models.user import User from aurweb.testing.requests import Request -user = backend = request = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, backend, request + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.com", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) + yield user - backend = BasicAuthBackend() - request = Request() + +@pytest.fixture +def backend() -> BasicAuthBackend: + yield BasicAuthBackend() @pytest.mark.asyncio -async def test_auth_backend_missing_sid(): +async def test_auth_backend_missing_sid(backend: BasicAuthBackend): # The request has no AURSID cookie, so authentication fails, and # AnonymousUser is returned. - _, result = await backend.authenticate(request) + _, result = await backend.authenticate(Request()) assert not result.is_authenticated() @pytest.mark.asyncio -async def test_auth_backend_invalid_sid(): +async def test_auth_backend_invalid_sid(backend: BasicAuthBackend): # Provide a fake AURSID that won't be found in the database. # This results in our path going down the invalid sid route, # which gives us an AnonymousUser. + request = Request() request.cookies["AURSID"] = "fake" _, result = await backend.authenticate(request) assert not result.is_authenticated() @@ -55,13 +60,15 @@ async def test_auth_backend_invalid_user_id(): @pytest.mark.asyncio -async def test_basic_auth_backend(): +async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): # This time, everything matches up. We expect the user to # equal the real_user. now_ts = datetime.utcnow().timestamp() with db.begin(): db.create(Session, UsersID=user.ID, SessionID="realSession", LastUpdateTS=now_ts + 5) + + request = Request() request.cookies["AURSID"] = "realSession" _, result = await backend.authenticate(request) assert result == user From 91f65911414423fc9dae8cf509beb57e3fadabea Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 16:35:52 -0800 Subject: [PATCH 0763/1451] housekeep(fastapi): rewrite test_accepted_term with fixtures Signed-off-by: Kevin Morris --- test/test_accepted_term.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py index de18c61a..2af7127b 100644 --- a/test/test_accepted_term.py +++ b/test/test_accepted_term.py @@ -8,39 +8,48 @@ from aurweb.models.account_type import USER_ID from aurweb.models.term import Term from aurweb.models.user import User -user = term = accepted_term = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, term + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) + yield user + +@pytest.fixture +def term() -> Term: + with db.begin(): term = db.create(Term, Description="Test term", URL="https://test.term") - yield term -def test_accepted_term(): +@pytest.fixture +def accepted_term(user: User, term: Term) -> AcceptedTerm: with db.begin(): accepted_term = db.create(AcceptedTerm, User=user, Term=term) + yield accepted_term + +def test_accepted_term(user: User, term: Term, accepted_term: AcceptedTerm): # Make sure our AcceptedTerm relationships got initialized properly. assert accepted_term.User == user assert accepted_term in user.accepted_terms assert accepted_term in term.accepted_terms -def test_accepted_term_null_user_raises_exception(): +def test_accepted_term_null_user_raises_exception(term: Term): with pytest.raises(IntegrityError): AcceptedTerm(Term=term) -def test_accepted_term_null_term_raises_exception(): +def test_accepted_term_null_term_raises_exception(user: User): with pytest.raises(IntegrityError): AcceptedTerm(User=user) From b20ec9925a5f92c020b8705bc733cdc4831ca5e4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 17:11:10 -0800 Subject: [PATCH 0764/1451] housekeep(fastapi): rewrite test_ssh_pub_key with fixtures Signed-off-by: Kevin Morris --- test/test_ssh_pub_key.py | 48 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index e17af5a7..68b6e7a0 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -16,47 +16,53 @@ lfv98Kr0NUp51zpf55Arxn9j0Rz9xTA7FiODQgCn6iQ0SDtzUNL0IKTCw26xJY5gzMxbfpvzPQGeul\ x/ioM= kevr@volcano """ -user = ssh_pub_key = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, ssh_pub_key + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) + yield user + +@pytest.fixture +def pubkey(user: User) -> SSHPubKey: with db.begin(): - ssh_pub_key = db.create(SSHPubKey, UserID=user.ID, - Fingerprint="testFingerprint", - PubKey="testPubKey") + pubkey = db.create(SSHPubKey, User=user, + Fingerprint="testFingerprint", + PubKey="testPubKey") + yield pubkey -def test_ssh_pub_key(): - assert ssh_pub_key.UserID == user.ID - assert ssh_pub_key.User == user - assert ssh_pub_key.Fingerprint == "testFingerprint" - assert ssh_pub_key.PubKey == "testPubKey" +def test_pubkey(user: User, pubkey: SSHPubKey): + assert pubkey.UserID == user.ID + assert pubkey.User == user + assert pubkey.Fingerprint == "testFingerprint" + assert pubkey.PubKey == "testPubKey" -def test_ssh_pub_key_cs(): +def test_pubkey_cs(user: User): """ Test case sensitivity of the database table. """ with db.begin(): - ssh_pub_key_cs = db.create(SSHPubKey, UserID=user.ID, - Fingerprint="TESTFINGERPRINT", - PubKey="TESTPUBKEY") + pubkey_cs = db.create(SSHPubKey, User=user, + Fingerprint="TESTFINGERPRINT", + PubKey="TESTPUBKEY") - assert ssh_pub_key_cs.Fingerprint == "TESTFINGERPRINT" - assert ssh_pub_key_cs.PubKey == "TESTPUBKEY" - assert ssh_pub_key.Fingerprint == "testFingerprint" - assert ssh_pub_key.PubKey == "testPubKey" + assert pubkey_cs.Fingerprint == "TESTFINGERPRINT" + assert pubkey_cs.Fingerprint != "testFingerprint" + assert pubkey_cs.PubKey == "TESTPUBKEY" + assert pubkey_cs.PubKey != "testPubKey" -def test_ssh_pub_key_fingerprint(): +def test_pubkey_fingerprint(): assert get_fingerprint(TEST_SSH_PUBKEY) is not None -def test_ssh_pub_key_invalid_fingerprint(): +def test_pubkey_invalid_fingerprint(): assert get_fingerprint("ssh-rsa fake and invalid") is None From a082de5244f55aa4098608e2e6b3341463669c01 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 17:21:42 -0800 Subject: [PATCH 0765/1451] housekeep(fastapi): rewrite test_package_keyword with fixtures Signed-off-by: Kevin Morris --- test/test_package_keyword.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py index 88ccb734..ff466efc 100644 --- a/test/test_package_keyword.py +++ b/test/test_package_keyword.py @@ -8,26 +8,32 @@ from aurweb.models.package_base import PackageBase from aurweb.models.package_keyword import PackageKeyword from aurweb.models.user import User -user = pkgbase = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) - pkgbase = db.create(PackageBase, - Name="beautiful-package", - Maintainer=user) + yield user -def test_package_keyword(): +@pytest.fixture +def pkgbase(user: User) -> PackageBase: with db.begin(): - pkg_keyword = db.create(PackageKeyword, - PackageBase=pkgbase, + pkgbase = db.create(PackageBase, Name="beautiful-package", + Maintainer=user) + yield pkgbase + + +def test_package_keyword(pkgbase: PackageBase): + with db.begin(): + pkg_keyword = db.create(PackageKeyword, PackageBase=pkgbase, Keyword="test") assert pkg_keyword in pkgbase.keywords assert pkgbase == pkg_keyword.PackageBase From 655b98d19e5e82817146f3f8a00215d10fd0da0a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 17:29:03 -0800 Subject: [PATCH 0766/1451] housekeep(fastapi): rewrite test_package_license with fixtures Signed-off-by: Kevin Morris --- test/test_package_license.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/test/test_package_license.py b/test/test_package_license.py index 965d0c6f..c43423b8 100644 --- a/test/test_package_license.py +++ b/test/test_package_license.py @@ -10,25 +10,37 @@ from aurweb.models.package_base import PackageBase from aurweb.models.package_license import PackageLicense from aurweb.models.user import User -user = license = pkgbase = package = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, license, pkgbase, package + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) - license = db.create(License, Name="Test License") + yield user + +@pytest.fixture +def license() -> License: + with db.begin(): + license = db.create(License, Name="Test License") + yield license + + +@pytest.fixture +def package(user: User, license: License): with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + yield package -def test_package_license(): +def test_package_license(license: License, package: Package): with db.begin(): package_license = db.create(PackageLicense, Package=package, License=license) @@ -36,11 +48,11 @@ def test_package_license(): assert package_license.Package == package -def test_package_license_null_package_raises_exception(): +def test_package_license_null_package_raises(license: License): with pytest.raises(IntegrityError): PackageLicense(License=license) -def test_package_license_null_license_raises_exception(): +def test_package_license_null_license_raises(package: Package): with pytest.raises(IntegrityError): PackageLicense(Package=package) From ff3931e43506a466d01dde48fa647c4d13740ce3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 18:10:40 -0800 Subject: [PATCH 0767/1451] housekeep(fastapi): rewrite test_package_notification with fixtures Signed-off-by: Kevin Morris --- test/test_package_notification.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/test_package_notification.py b/test/test_package_notification.py index 2e505dd8..e7a72a43 100644 --- a/test/test_package_notification.py +++ b/test/test_package_notification.py @@ -7,20 +7,28 @@ from aurweb.models.package_base import PackageBase from aurweb.models.package_notification import PackageNotification from aurweb.models.user import User -user = pkgbase = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword") + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase -def test_package_notification_creation(): +def test_package_notification_creation(user: User, pkgbase: PackageBase): with db.begin(): package_notification = db.create( PackageNotification, User=user, PackageBase=pkgbase) @@ -29,11 +37,11 @@ def test_package_notification_creation(): assert package_notification.PackageBase == pkgbase -def test_package_notification_null_user_raises_exception(): +def test_package_notification_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageNotification(PackageBase=pkgbase) -def test_package_notification_null_pkgbase_raises_exception(): +def test_package_notification_null_pkgbase_raises(user: User): with pytest.raises(IntegrityError): PackageNotification(User=user) From 14d80d756fe73039d7e5b7836ad1c343668a7e9f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 18:18:48 -0800 Subject: [PATCH 0768/1451] housekeep(fastapi): rewrite test_package_comaintainer with fixtures Signed-off-by: Kevin Morris --- test/test_package_comaintainer.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/test/test_package_comaintainer.py b/test/test_package_comaintainer.py index ff74cddf..e377edc0 100644 --- a/test/test_package_comaintainer.py +++ b/test/test_package_comaintainer.py @@ -3,24 +3,34 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db +from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.user import User -user = pkgbase = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase -def test_package_comaintainer_creation(): +def test_package_comaintainer_creation(user: User, pkgbase: PackageBase): with db.begin(): package_comaintainer = db.create(PackageComaintainer, User=user, PackageBase=pkgbase, Priority=5) @@ -30,16 +40,17 @@ def test_package_comaintainer_creation(): assert package_comaintainer.Priority == 5 -def test_package_comaintainer_null_user_raises_exception(): +def test_package_comaintainer_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageComaintainer(PackageBase=pkgbase, Priority=1) -def test_package_comaintainer_null_pkgbase_raises_exception(): +def test_package_comaintainer_null_pkgbase_raises(user: User): with pytest.raises(IntegrityError): PackageComaintainer(User=user, Priority=1) -def test_package_comaintainer_null_priority_raises_exception(): +def test_package_comaintainer_null_priority_raises(user: User, + pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageComaintainer(User=user, PackageBase=pkgbase) From 31a093ba063168825595fe82d3c42d34d9d67a15 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 18:51:50 -0800 Subject: [PATCH 0769/1451] housekeep(fastapi): rewrite test_package_relation with fixtures Signed-off-by: Kevin Morris --- test/test_package_relation.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/test/test_package_relation.py b/test/test_package_relation.py index e5f7f453..6e9a5545 100644 --- a/test/test_package_relation.py +++ b/test/test_package_relation.py @@ -10,28 +10,32 @@ from aurweb.models.package_relation import PackageRelation from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.models.user import User -user = pkgbase = package = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase, package + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) - pkgbase = db.create(PackageBase, - Name="test-package", - Maintainer=user) - package = db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, + yield user + + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, Description="Test description.", URL="https://test.package") + yield package -def test_package_relation(): +def test_package_relation(package: Package): with db.begin(): pkgrel = db.create(PackageRelation, Package=package, RelTypeID=CONFLICTS_ID, @@ -48,16 +52,16 @@ def test_package_relation(): pkgrel.RelTypeID = REPLACES_ID -def test_package_relation_null_package_raises_exception(): +def test_package_relation_null_package_raises(): with pytest.raises(IntegrityError): PackageRelation(RelTypeID=CONFLICTS_ID, RelName="test-relation") -def test_package_relation_null_relation_type_raises_exception(): +def test_package_relation_null_relation_type_raises(package: Package): with pytest.raises(IntegrityError): PackageRelation(Package=package, RelName="test-relation") -def test_package_relation_null_relname_raises_exception(): +def test_package_relation_null_relname_raises(package: Package): with pytest.raises(IntegrityError): PackageRelation(Package=package, RelTypeID=CONFLICTS_ID) From a0e1a1641d08d533b1919ffb60298476f21de237 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:12:06 -0800 Subject: [PATCH 0770/1451] fix(fastapi): support UsersID and User columns in the Session model Signed-off-by: Kevin Morris --- aurweb/models/session.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/aurweb/models/session.py b/aurweb/models/session.py index 7a06eddc..37ab4bce 100644 --- a/aurweb/models/session.py +++ b/aurweb/models/session.py @@ -18,10 +18,16 @@ class Session(Base): def __init__(self, **kwargs): super().__init__(**kwargs) - user_exists = db.query( - db.query(_User).filter(_User.ID == self.UsersID).exists() - ).scalar() - if not user_exists: + # We'll try to either use UsersID or User.ID if we can. + # If neither exist, an AttributeError is raised, in which case + # we set the uid to 0, which triggers IntegrityError below. + try: + uid = self.UsersID or self.User.ID + except AttributeError: + uid = 0 + + user_exists = db.query(_User).filter(_User.ID == uid).exists() + if not db.query(user_exists).scalar(): raise IntegrityError( statement=("Foreign key UsersID cannot be null and " "must be a valid user's ID."), From ca25595022e4a5c525ecce9de0c009bf19c6fd2c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:12:31 -0800 Subject: [PATCH 0771/1451] housekeep(fastapi): rewrite test_sesion with fixtures Also, added a new test function which tests the IntegrityError exception. Signed-off-by: Kevin Morris --- test/test_session.py | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/test/test_session.py b/test/test_session.py index 7d3037a1..67b1ada0 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -4,31 +4,37 @@ from unittest import mock import pytest +from sqlalchemy.exc import IntegrityError + from aurweb import db -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.session import Session, generate_unique_sid from aurweb.models.user import User -account_type = user = session = None - @pytest.fixture(autouse=True) def setup(db_test): - global account_type, user, session + return - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", ResetKey="testReset", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) + yield user + +@pytest.fixture +def session(user: User) -> Session: with db.begin(): - session = db.create(Session, UsersID=user.ID, SessionID="testSession", + session = db.create(Session, User=user, SessionID="testSession", LastUpdateTS=datetime.utcnow().timestamp()) + yield session -def test_session(): +def test_session(user: User, session: Session): assert session.SessionID == "testSession" assert session.UsersID == user.ID @@ -38,22 +44,27 @@ def test_session_cs(): with db.begin(): user2 = db.create(User, Username="test2", Email="test2@example.org", ResetKey="testReset2", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) with db.begin(): - session_cs = db.create(Session, UsersID=user2.ID, - SessionID="TESTSESSION", + session_cs = db.create(Session, User=user2, SessionID="TESTSESSION", LastUpdateTS=datetime.utcnow().timestamp()) + assert session_cs.SessionID == "TESTSESSION" - assert session.SessionID == "testSession" + assert session_cs.SessionID != "testSession" -def test_session_user_association(): +def test_session_user_association(user: User, session: Session): # Make sure that the Session user attribute is correct. assert session.User == user -def test_generate_unique_sid(): +def test_session_null_user_raises(): + with pytest.raises(IntegrityError): + Session() + + +def test_generate_unique_sid(session: Session): # Mock up aurweb.models.session.generate_sid by returning # sids[i % 2] from 0 .. n. This will swap between each sid # between each call. From ae728179506bdd39ac0c929f36324626cea53a91 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:17:14 -0800 Subject: [PATCH 0772/1451] housekeep(fastapi): rewrite test_routes with fixtures Signed-off-by: Kevin Morris --- test/test_routes.py | 46 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/test/test_routes.py b/test/test_routes.py index 32f507f3..85d30c02 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -14,30 +14,34 @@ from aurweb.models.account_type import USER_ID from aurweb.models.user import User from aurweb.testing.requests import Request -user = client = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, client + return + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=app) + + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) - - client = TestClient(app) + yield user -def test_index(): +def test_index(client: TestClient): """ Test the index route at '/'. """ - # Use `with` to trigger FastAPI app events. with client as req: response = req.get("/") assert response.status_code == int(HTTPStatus.OK) -def test_index_security_headers(): +def test_index_security_headers(client: TestClient): """ Check for the existence of CSP, XCTO, XFO and RP security headers. CSP: Content-Security-Policy @@ -55,15 +59,16 @@ def test_index_security_headers(): assert response.headers.get("X-Frame-Options") == "SAMEORIGIN" -def test_favicon(): +def test_favicon(client: TestClient): """ Test the favicon route at '/favicon.ico'. """ - response1 = client.get("/static/images/favicon.ico") - response2 = client.get("/favicon.ico") + with client as request: + response1 = request.get("/static/images/favicon.ico") + response2 = request.get("/favicon.ico") assert response1.status_code == int(HTTPStatus.OK) assert response1.content == response2.content -def test_language(): +def test_language(client: TestClient): """ Test the language post route as a guest user. """ post_data = { "set_lang": "de", @@ -74,7 +79,7 @@ def test_language(): assert response.status_code == int(HTTPStatus.SEE_OTHER) -def test_language_invalid_next(): +def test_language_invalid_next(client: TestClient): """ Test an invalid next route at '/language'. """ post_data = { "set_lang": "de", @@ -85,7 +90,7 @@ def test_language_invalid_next(): assert response.status_code == int(HTTPStatus.BAD_REQUEST) -def test_user_language(): +def test_user_language(client: TestClient, user: User): """ Test the language post route as an authenticated user. """ post_data = { "set_lang": "de", @@ -102,7 +107,7 @@ def test_user_language(): assert user.LangPreference == "de" -def test_language_query_params(): +def test_language_query_params(client: TestClient): """ Test the language post route with query params. """ next = urllib.parse.quote_plus("/") post_data = { @@ -117,14 +122,15 @@ def test_language_query_params(): assert response.status_code == int(HTTPStatus.SEE_OTHER) -def test_error_messages(): - response1 = client.get("/thisroutedoesnotexist") - response2 = client.get("/raisefivethree") +def test_error_messages(client: TestClient): + with client as request: + response1 = request.get("/thisroutedoesnotexist") + response2 = request.get("/raisefivethree") assert response1.status_code == int(HTTPStatus.NOT_FOUND) assert response2.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) -def test_nonce_csp(): +def test_nonce_csp(client: TestClient): with client as request: response = request.get("/") data = response.headers.get("Content-Security-Policy") @@ -146,7 +152,7 @@ def test_nonce_csp(): assert nonce_verified is True -def test_id_redirect(): +def test_id_redirect(client: TestClient): with client as request: response = request.get("/", params={ "id": "test", # This param will be rewritten into Location. From 93bc91cce252ab789493aa5d0bc0c796f5133ae2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:25:10 -0800 Subject: [PATCH 0773/1451] housekeep(fastapi): rewrite test_tu_voteinfo with fixtures Signed-off-by: Kevin Morris --- test/test_tu_voteinfo.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py index 5926fbf9..26fa9522 100644 --- a/test/test_tu_voteinfo.py +++ b/test/test_tu_voteinfo.py @@ -5,27 +5,27 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.db import create, query, rollback -from aurweb.models.account_type import AccountType +from aurweb.db import create, rollback +from aurweb.models.account_type import TRUSTED_USER_ID from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User -user = None - @pytest.fixture(autouse=True) def setup(db_test): - global user + return - tu_type = query(AccountType, - AccountType.AccountType == "Trusted User").first() + +@pytest.fixture +def user() -> User: with db.begin(): user = create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - AccountType=tu_type) + AccountTypeID=TRUSTED_USER_ID) + yield user -def test_tu_voteinfo_creation(): +def test_tu_voteinfo_creation(user: User): ts = int(datetime.utcnow().timestamp()) with db.begin(): tu_voteinfo = create(TUVoteInfo, @@ -49,7 +49,7 @@ def test_tu_voteinfo_creation(): assert tu_voteinfo in user.tu_voteinfo_set -def test_tu_voteinfo_is_running(): +def test_tu_voteinfo_is_running(user: User): ts = int(datetime.utcnow().timestamp()) with db.begin(): tu_voteinfo = create(TUVoteInfo, @@ -65,7 +65,7 @@ def test_tu_voteinfo_is_running(): assert tu_voteinfo.is_running() is False -def test_tu_voteinfo_total_votes(): +def test_tu_voteinfo_total_votes(user: User): ts = int(datetime.utcnow().timestamp()) with db.begin(): tu_voteinfo = create(TUVoteInfo, @@ -83,7 +83,7 @@ def test_tu_voteinfo_total_votes(): assert tu_voteinfo.total_votes() == 9 -def test_tu_voteinfo_null_submitter_raises_exception(): +def test_tu_voteinfo_null_submitter_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): create(TUVoteInfo, @@ -94,7 +94,7 @@ def test_tu_voteinfo_null_submitter_raises_exception(): rollback() -def test_tu_voteinfo_null_agenda_raises_exception(): +def test_tu_voteinfo_null_agenda_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): create(TUVoteInfo, @@ -105,7 +105,7 @@ def test_tu_voteinfo_null_agenda_raises_exception(): rollback() -def test_tu_voteinfo_null_user_raises_exception(): +def test_tu_voteinfo_null_user_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): create(TUVoteInfo, @@ -116,7 +116,7 @@ def test_tu_voteinfo_null_user_raises_exception(): rollback() -def test_tu_voteinfo_null_submitted_raises_exception(): +def test_tu_voteinfo_null_submitted_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): create(TUVoteInfo, @@ -128,7 +128,7 @@ def test_tu_voteinfo_null_submitted_raises_exception(): rollback() -def test_tu_voteinfo_null_end_raises_exception(): +def test_tu_voteinfo_null_end_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): create(TUVoteInfo, @@ -140,7 +140,7 @@ def test_tu_voteinfo_null_end_raises_exception(): rollback() -def test_tu_voteinfo_null_quorum_raises_exception(): +def test_tu_voteinfo_null_quorum_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): create(TUVoteInfo, From 171b347dadddf92f90e16f98fab388112174053a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:38:49 -0800 Subject: [PATCH 0774/1451] housekeep(fastapi): rewrite test_package_base with fixtures Signed-off-by: Kevin Morris --- test/test_package_base.py | 57 +++++++++++++++------------------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/test/test_package_base.py b/test/test_package_base.py index 8e4b2edf..5be7e40b 100644 --- a/test/test_package_base.py +++ b/test/test_package_base.py @@ -2,35 +2,36 @@ import pytest from sqlalchemy.exc import IntegrityError -import aurweb.config - from aurweb import db -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase from aurweb.models.user import User -user = None - @pytest.fixture(autouse=True) def setup(db_test): - global user + return - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) + yield user -def test_package_base(): +@pytest.fixture +def pkgbase(user: User) -> PackageBase: with db.begin(): - pkgbase = db.create(PackageBase, - Name="beautiful-package", + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) - assert pkgbase in user.maintained_bases + yield pkgbase + +def test_package_base(user: User, pkgbase: PackageBase): + assert pkgbase in user.maintained_bases assert not pkgbase.OutOfDateTS assert pkgbase.SubmittedTS > 0 assert pkgbase.ModifiedTS > 0 @@ -42,33 +43,19 @@ def test_package_base(): assert pkgbase.Popularity == 0.0 -def test_package_base_ci(): +def test_package_base_ci(user: User, pkgbase: PackageBase): """ Test case insensitivity of the database table. """ - if aurweb.config.get("database", "backend") == "sqlite": - return None # SQLite doesn't seem handle this. - - with db.begin(): - pkgbase = db.create(PackageBase, - Name="beautiful-package", - Maintainer=user) - assert bool(pkgbase.ID) - with pytest.raises(IntegrityError): with db.begin(): - db.create(PackageBase, - Name="Beautiful-Package", - Maintainer=user) + db.create(PackageBase, Name=pkgbase.Name.upper(), Maintainer=user) db.rollback() -def test_package_base_relationships(): +def test_package_base_relationships(user: User, pkgbase: PackageBase): with db.begin(): - pkgbase = db.create(PackageBase, - Name="beautiful-package", - Flagger=user, - Maintainer=user, - Submitter=user, - Packager=user) + pkgbase.Flagger = user + pkgbase.Submitter = user + pkgbase.Packager = user assert pkgbase in user.flagged_bases assert pkgbase in user.maintained_bases assert pkgbase in user.submitted_bases @@ -77,6 +64,4 @@ def test_package_base_relationships(): def test_package_base_null_name_raises_exception(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(PackageBase) - db.rollback() + PackageBase() From df530d8a7358f71c6579e6bfd0fea1143dfc89b7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:42:50 -0800 Subject: [PATCH 0775/1451] housekeep(fastapi): rewrite test_package_source with fixtures Signed-off-by: Kevin Morris --- test/test_package_source.py | 51 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/test/test_package_source.py b/test/test_package_source.py index b83c9d48..e5797f90 100644 --- a/test/test_package_source.py +++ b/test/test_package_source.py @@ -2,46 +2,45 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb.db import begin, create, query, rollback -from aurweb.models.account_type import AccountType +from aurweb import db +from aurweb.models.account_type import USER_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_source import PackageSource from aurweb.models.user import User -from aurweb.testing import setup_test_db - -user = pkgbase = package = None @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase, package - - setup_test_db("PackageSources", "Packages", "PackageBases", "Users") - - account_type = query(AccountType, - AccountType.AccountType == "User").first() - with begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - pkgbase = create(PackageBase, - Name="test-package", - Maintainer=user) - package = create(Package, PackageBase=pkgbase, Name="test-package") + return -def test_package_source(): - with begin(): - pkgsource = create(PackageSource, Package=package) +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + package = db.create(Package, PackageBase=pkgbase, Name="test-package") + yield package + + +def test_package_source(package: Package): + with db.begin(): + pkgsource = db.create(PackageSource, Package=package) assert pkgsource.Package == package # By default, PackageSources.Source assigns the string '/dev/null'. assert pkgsource.Source == "/dev/null" assert pkgsource.SourceArch is None -def test_package_source_null_package_raises_exception(): +def test_package_source_null_package_raises(): with pytest.raises(IntegrityError): - with begin(): - create(PackageSource) - rollback() + PackageSource() From 150c944758bb67f97625c566cd635500d27ef7ef Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:45:08 -0800 Subject: [PATCH 0776/1451] housekeep(fastapi): rewrite test_package_group with fixtures Signed-off-by: Kevin Morris --- test/test_package_group.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/test/test_package_group.py b/test/test_package_group.py index 2c91e0b1..0cb83ee2 100644 --- a/test/test_package_group.py +++ b/test/test_package_group.py @@ -10,36 +10,48 @@ from aurweb.models.package_base import PackageBase from aurweb.models.package_group import PackageGroup from aurweb.models.user import User -user = group = pkgbase = package = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, group, pkgbase, package + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", AccountTypeID=USER_ID) - group = db.create(Group, Name="Test Group") + yield user + +@pytest.fixture +def group() -> Group: + with db.begin(): + group = db.create(Group, Name="Test Group") + yield group + + +@pytest.fixture +def package(user: User) -> Package: with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + yield package -def test_package_group(): +def test_package_group(package: Package, group: Group): with db.begin(): package_group = db.create(PackageGroup, Package=package, Group=group) assert package_group.Group == group assert package_group.Package == package -def test_package_group_null_package_raises_exception(): +def test_package_group_null_package_raises(group: Group): with pytest.raises(IntegrityError): PackageGroup(Group=group) -def test_package_group_null_group_raises_exception(): +def test_package_group_null_group_raises(package: Package): with pytest.raises(IntegrityError): PackageGroup(Package=package) From 05bd6e9076d03006358c6862bfb2d95d78eb93b9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:48:01 -0800 Subject: [PATCH 0777/1451] housekeep(fastapi): rewrite test_package_vote with fixtures Signed-off-by: Kevin Morris --- test/test_package_vote.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test/test_package_vote.py b/test/test_package_vote.py index d1ec203b..08edb92d 100644 --- a/test/test_package_vote.py +++ b/test/test_package_vote.py @@ -5,24 +5,34 @@ import pytest from sqlalchemy.exc import IntegrityError from aurweb import db +from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase from aurweb.models.package_vote import PackageVote from aurweb.models.user import User -user = pkgbase = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") + RealName="Test User", Passwd=str(), + AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase -def test_package_vote_creation(): +def test_package_vote_creation(user: User, pkgbase: PackageBase): ts = int(datetime.utcnow().timestamp()) with db.begin(): @@ -34,16 +44,16 @@ def test_package_vote_creation(): assert package_vote.VoteTS == ts -def test_package_vote_null_user_raises_exception(): +def test_package_vote_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageVote(PackageBase=pkgbase, VoteTS=1) -def test_package_vote_null_pkgbase_raises_exception(): +def test_package_vote_null_pkgbase_raises(user: User): with pytest.raises(IntegrityError): PackageVote(User=user, VoteTS=1) -def test_package_vote_null_votets_raises_exception(): +def test_package_vote_null_votets_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageVote(User=user, PackageBase=pkgbase) From 140f9b1fb225e00163662290de426e8c2a064264 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 20:50:17 -0800 Subject: [PATCH 0778/1451] housekeep(fastapi): rewrite test_package_dependency with fixtures Signed-off-by: Kevin Morris --- test/test_package_dependency.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py index e6125669..7297abe4 100644 --- a/test/test_package_dependency.py +++ b/test/test_package_dependency.py @@ -10,28 +10,32 @@ from aurweb.models.package_base import PackageBase from aurweb.models.package_dependency import PackageDependency from aurweb.models.user import User -user = pkgbase = package = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase, package + return + +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", + RealName="Test User", Passwd=str(), AccountTypeID=USER_ID) - pkgbase = db.create(PackageBase, - Name="test-package", - Maintainer=user) - package = db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, + yield user + + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, Description="Test description.", URL="https://test.package") + yield package -def test_package_dependencies(): +def test_package_dependencies(user: User, package: Package): with db.begin(): pkgdep = db.create(PackageDependency, Package=package, DepTypeID=DEPENDS_ID, DepName="test-dep") @@ -57,16 +61,16 @@ def test_package_dependencies(): assert pkgdep.is_package() -def test_package_dependencies_null_package_raises_exception(): +def test_package_dependencies_null_package_raises(): with pytest.raises(IntegrityError): PackageDependency(DepTypeID=DEPENDS_ID, DepName="test-dep") -def test_package_dependencies_null_dependency_type_raises_exception(): +def test_package_dependencies_null_dependency_type_raises(package: Package): with pytest.raises(IntegrityError): PackageDependency(Package=package, DepName="test-dep") -def test_package_dependencies_null_depname_raises_exception(): +def test_package_dependencies_null_depname_raises(package: Package): with pytest.raises(IntegrityError): PackageDependency(DepTypeID=DEPENDS_ID, Package=package) From 5b14ad406560b87773b7bf81b0ef0fbb6a362898 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 21:16:49 -0800 Subject: [PATCH 0779/1451] housekeep(fastapi): rewrite test_user with fixtures Signed-off-by: Kevin Morris --- test/test_user.py | 110 +++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 66 deletions(-) diff --git a/test/test_user.py b/test/test_user.py index dbb45166..52cdc89e 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -8,10 +8,10 @@ import pytest import aurweb.auth import aurweb.config +import aurweb.models.account_type as at from aurweb import db from aurweb.auth import creds -from aurweb.models.account_type import AccountType from aurweb.models.ban import Ban from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -22,23 +22,30 @@ from aurweb.models.ssh_pub_key import SSHPubKey from aurweb.models.user import User from aurweb.testing.requests import Request -account_type = user = None - @pytest.fixture(autouse=True) def setup(db_test): - global account_type, user + return - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=at.USER_ID) + yield user -def test_user_login_logout(): +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + yield pkg + + +def test_user_login_logout(user: User): """ Test creating a user and reading its columns. """ # Assert that make_user created a valid user. assert bool(user.ID) @@ -47,8 +54,6 @@ def test_user_login_logout(): assert user.valid_password("testPassword") assert not user.valid_password("badPassword") - assert user in account_type.users - # Make a raw request. request = Request() assert not user.login(request, "badPassword") @@ -81,10 +86,6 @@ def test_user_login_logout(): assert result.valid_password("testPassword") assert result.is_authenticated() - # Ensure we've got the correct account type. - assert user.AccountType.ID == account_type.ID - assert user.AccountType.AccountType == account_type.AccountType - # Test out user string functions. assert repr(user) == f"" @@ -95,13 +96,13 @@ def test_user_login_logout(): assert not user.is_authenticated() -def test_user_login_twice(): +def test_user_login_twice(user: User): request = Request() assert user.login(request, "testPassword") assert user.login(request, "testPassword") -def test_user_login_banned(): +def test_user_login_banned(user: User): # Add ban for the next 30 seconds. banned_timestamp = datetime.utcnow() + timedelta(seconds=30) with db.begin(): @@ -112,13 +113,13 @@ def test_user_login_banned(): assert not user.login(request, "testPassword") -def test_user_login_suspended(): +def test_user_login_suspended(user: User): with db.begin(): user.Suspended = True assert not user.login(Request(), "testPassword") -def test_legacy_user_authentication(): +def test_legacy_user_authentication(user: User): with db.begin(): user.Salt = bcrypt.gensalt().decode() user.Passwd = hashlib.md5( @@ -132,7 +133,7 @@ def test_legacy_user_authentication(): assert not user.valid_password(None) -def test_user_login_with_outdated_sid(): +def test_user_login_with_outdated_sid(user: User): # Make a session with a LastUpdateTS 5 seconds ago, causing # user.login to update it with a new sid. with db.begin(): @@ -143,7 +144,7 @@ def test_user_login_with_outdated_sid(): assert sid != "stub" -def test_user_update_password(): +def test_user_update_password(user: User): user.update_password("secondPassword") assert not user.valid_password("testPassword") assert user.valid_password("secondPassword") @@ -154,11 +155,11 @@ def test_user_minimum_passwd_length(): assert User.minimum_passwd_length() == passwd_min_len -def test_user_has_credential(): - assert not user.has_credential(aurweb.auth.creds.ACCOUNT_CHANGE_TYPE) +def test_user_has_credential(user: User): + assert not user.has_credential(creds.ACCOUNT_CHANGE_TYPE) -def test_user_ssh_pub_key(): +def test_user_ssh_pub_key(user: User): assert user.ssh_pub_key is None with db.begin(): @@ -169,34 +170,26 @@ def test_user_ssh_pub_key(): assert user.ssh_pub_key == ssh_pub_key -def test_user_credential_types(): +def test_user_credential_types(user: User): assert user.AccountTypeID in creds.user_developer_or_trusted_user assert user.AccountTypeID not in creds.trusted_user assert user.AccountTypeID not in creds.developer assert user.AccountTypeID not in creds.trusted_user_or_dev - trusted_user_type = db.query(AccountType).filter( - AccountType.AccountType == "Trusted User" - ).first() with db.begin(): - user.AccountType = trusted_user_type + user.AccountTypeID = at.TRUSTED_USER_ID assert user.AccountTypeID in creds.trusted_user assert user.AccountTypeID in creds.trusted_user_or_dev - developer_type = db.query(AccountType, - AccountType.AccountType == "Developer").first() with db.begin(): - user.AccountType = developer_type + user.AccountTypeID = at.DEVELOPER_ID assert user.AccountTypeID in creds.developer assert user.AccountTypeID in creds.trusted_user_or_dev - type_str = "Trusted User & Developer" - elevated_type = db.query(AccountType, - AccountType.AccountType == type_str).first() with db.begin(): - user.AccountType = elevated_type + user.AccountTypeID = at.TRUSTED_USER_AND_DEV_ID assert user.AccountTypeID in creds.trusted_user assert user.AccountTypeID in creds.developer @@ -208,7 +201,7 @@ def test_user_credential_types(): assert user.is_developer() -def test_user_json(): +def test_user_json(user: User): data = json.loads(user.json()) assert data.get("ID") == user.ID assert data.get("Username") == user.Username @@ -217,7 +210,7 @@ def test_user_json(): assert isinstance(data.get("RegistrationTS"), int) -def test_user_as_dict(): +def test_user_as_dict(user: User): data = user.as_dict() assert data.get("ID") == user.ID assert data.get("Username") == user.Username @@ -226,57 +219,42 @@ def test_user_as_dict(): assert isinstance(data.get("RegistrationTS"), datetime) -def test_user_is_trusted_user(): - tu_type = db.query(AccountType, - AccountType.AccountType == "Trusted User").first() +def test_user_is_trusted_user(user: User): with db.begin(): - user.AccountType = tu_type + user.AccountTypeID = at.TRUSTED_USER_ID assert user.is_trusted_user() is True # Do it again with the combined role. - tu_type = db.query( - AccountType, - AccountType.AccountType == "Trusted User & Developer").first() with db.begin(): - user.AccountType = tu_type + user.AccountTypeID = at.TRUSTED_USER_AND_DEV_ID assert user.is_trusted_user() is True -def test_user_is_developer(): - dev_type = db.query(AccountType, - AccountType.AccountType == "Developer").first() +def test_user_is_developer(user: User): with db.begin(): - user.AccountType = dev_type + user.AccountTypeID = at.DEVELOPER_ID assert user.is_developer() is True # Do it again with the combined role. - dev_type = db.query( - AccountType, - AccountType.AccountType == "Trusted User & Developer").first() with db.begin(): - user.AccountType = dev_type + user.AccountTypeID = at.TRUSTED_USER_AND_DEV_ID assert user.is_developer() is True -def test_user_voted_for(): +def test_user_voted_for(user: User, package: Package): + pkgbase = package.PackageBase now = int(datetime.utcnow().timestamp()) with db.begin(): - pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) db.create(PackageVote, PackageBase=pkgbase, User=user, VoteTS=now) - assert user.voted_for(pkg) + assert user.voted_for(package) -def test_user_notified(): +def test_user_notified(user: User, package: Package): + pkgbase = package.PackageBase with db.begin(): - pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) db.create(PackageNotification, PackageBase=pkgbase, User=user) - assert user.notified(pkg) + assert user.notified(package) -def test_user_packages(): - with db.begin(): - pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) - assert pkg in user.packages() +def test_user_packages(user: User, package: Package): + assert package in user.packages() From eb396813a89c29bfbb96d6d5f7c884d1c83117bc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 21:27:02 -0800 Subject: [PATCH 0780/1451] housekeep(fastapi): rewrite test_package with fixtures Signed-off-by: Kevin Morris --- test/test_package.py | 54 ++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/test/test_package.py b/test/test_package.py index c2afa660..1408a182 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -4,7 +4,7 @@ from sqlalchemy import and_ from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.user import User @@ -14,28 +14,30 @@ user = pkgbase = package = None @pytest.fixture(autouse=True) def setup(db_test): - global user, pkgbase, package + return - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() +@pytest.fixture +def user() -> User: with db.begin(): user = db.create(User, Username="test", Email="test@example.org", RealName="Test User", Passwd="testPassword", - AccountType=account_type) + AccountTypeID=USER_ID) + yield user - pkgbase = db.create(PackageBase, - Name="beautiful-package", + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) - package = db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name, + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, Description="Test description.", URL="https://test.package") + yield package -def test_package(): - assert pkgbase == package.PackageBase +def test_package(package: Package): assert package.Name == "beautiful-package" assert package.Description == "Test description." assert package.Version == str() # Default version. @@ -46,27 +48,21 @@ def test_package(): package.Version = "1.2.3" # Make sure it got updated in the database. - record = db.query(Package, - and_(Package.ID == package.ID, - Package.Version == "1.2.3")).first() + record = db.query(Package).filter( + and_(Package.ID == package.ID, + Package.Version == "1.2.3") + ).first() assert record is not None -def test_package_null_pkgbase_raises_exception(): +def test_package_null_pkgbase_raises(): with pytest.raises(IntegrityError): - with db.begin(): - db.create(Package, - Name="some-package", - Description="Some description.", - URL="https://some.package") - db.rollback() + Package(Name="some-package", Description="Some description.", + URL="https://some.package") -def test_package_null_name_raises_exception(): +def test_package_null_name_raises(package: Package): + pkgbase = package.PackageBase with pytest.raises(IntegrityError): - with db.begin(): - db.create(Package, - PackageBase=pkgbase, - Description="Some description.", - URL="https://some.package") - db.rollback() + Package(PackageBase=pkgbase, Description="Some description.", + URL="https://some.package") From de0f9190778b160fcb05cf4a6945eb4dd56b1aa1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 22:06:32 -0800 Subject: [PATCH 0781/1451] housekeep(fastapi): rewrite test_ban with fixtures Signed-off-by: Kevin Morris --- test/test_ban.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/test_ban.py b/test/test_ban.py index 2c705410..ff49f7e2 100644 --- a/test/test_ban.py +++ b/test/test_ban.py @@ -11,20 +11,21 @@ from aurweb.db import create from aurweb.models.ban import Ban, is_banned from aurweb.testing.requests import Request -ban = request = None - @pytest.fixture(autouse=True) def setup(db_test): - global ban, request + return + +@pytest.fixture +def ban() -> Ban: ts = datetime.utcnow() + timedelta(seconds=30) with db.begin(): ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) - request = Request() + yield ban -def test_ban(): +def test_ban(ban: Ban): assert ban.IPAddress == "127.0.0.1" assert bool(ban.BanTS) @@ -45,11 +46,13 @@ def test_invalid_ban(): db.rollback() -def test_banned(): +def test_banned(ban: Ban): + request = Request() request.client.host = "127.0.0.1" assert is_banned(request) -def test_not_banned(): +def test_not_banned(ban: Ban): + request = Request() request.client.host = "192.168.0.1" assert not is_banned(request) From 7ef3e3438681b29d6a32aed54c92f3965737c7af Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 22:43:29 -0800 Subject: [PATCH 0782/1451] housekeep(fastapi): rewrite test_accounts_routes with fixtures Signed-off-by: Kevin Morris --- test/test_accounts_routes.py | 233 ++++++++++++++++++----------------- 1 file changed, 118 insertions(+), 115 deletions(-) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index be929e97..f08efcd2 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -23,16 +23,12 @@ from aurweb.models.user import User from aurweb.testing.html import get_errors from aurweb.testing.requests import Request +logger = logging.get_logger(__name__) + # Some test global constants. TEST_USERNAME = "test" TEST_EMAIL = "test@example.org" -# Global mutables. -client = TestClient(app) -user = None - -logger = logging.get_logger(__name__) - def make_ssh_pubkey(): # Create a public key with ssh-keygen (this adds ssh-keygen as a @@ -50,29 +46,32 @@ def make_ssh_pubkey(): @pytest.fixture(autouse=True) def setup(db_test): - global user + return - account_type = query(AccountType, - AccountType.AccountType == "User").first() +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=app) + + +@pytest.fixture +def user() -> User: with db.begin(): user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, RealName="Test UserZ", Passwd="testPassword", - IRCNick="testZ", AccountType=account_type) + IRCNick="testZ", AccountTypeID=USER_ID) yield user @pytest.fixture -def tu_user(): +def tu_user(user: User): with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == TRUSTED_USER_AND_DEV_ID - ).first() + user.AccountTypeID = TRUSTED_USER_AND_DEV_ID yield user -def test_get_passreset_authed_redirects(): +def test_get_passreset_authed_redirects(client: TestClient, user: User): sid = user.login(Request(), "testPassword") assert sid is not None @@ -84,39 +83,39 @@ def test_get_passreset_authed_redirects(): assert response.headers.get("location") == "/" -def test_get_passreset(): +def test_get_passreset(client: TestClient): with client as request: response = request.get("/passreset") assert response.status_code == int(HTTPStatus.OK) -def test_get_passreset_translation(): +def test_get_passreset_translation(client: TestClient): # Test that translation works; set it to de. with client as request: response = request.get("/passreset", cookies={"AURLANG": "de"}) # The header title should be translated. - assert "Passwort zurücksetzen".encode("utf-8") in response.content + assert "Passwort zurücksetzen" in response.text # The form input label should be translated. - assert "Benutzername oder primäre E-Mail-Adresse eingeben:".encode( - "utf-8") in response.content + expected = "Benutzername oder primäre E-Mail-Adresse eingeben:" + assert expected in response.text # And the button. - assert "Weiter".encode("utf-8") in response.content + assert "Weiter" in response.text # Restore english. with client as request: response = request.get("/passreset", cookies={"AURLANG": "en"}) -def test_get_passreset_with_resetkey(): +def test_get_passreset_with_resetkey(client: TestClient): with client as request: response = request.get("/passreset", data={"resetkey": "abcd"}) assert response.status_code == int(HTTPStatus.OK) -def test_post_passreset_authed_redirects(): +def test_post_passreset_authed_redirects(client: TestClient, user: User): sid = user.login(Request(), "testPassword") assert sid is not None @@ -130,7 +129,7 @@ def test_post_passreset_authed_redirects(): assert response.headers.get("location") == "/" -def test_post_passreset_user(): +def test_post_passreset_user(client: TestClient, user: User): # With username. with client as request: response = request.post("/passreset", data={"user": TEST_USERNAME}) @@ -144,7 +143,7 @@ def test_post_passreset_user(): assert response.headers.get("location") == "/passreset?step=confirm" -def test_post_passreset_resetkey(): +def test_post_passreset_resetkey(client: TestClient, user: User): with db.begin(): user.session = Session(UsersID=user.ID, SessionID="blah", LastUpdateTS=datetime.utcnow().timestamp()) @@ -171,28 +170,7 @@ def test_post_passreset_resetkey(): assert response.headers.get("location") == "/passreset?step=complete" -def test_post_passreset_error_invalid_email(): - # First, test with a user that doesn't even exist. - with client as request: - response = request.post("/passreset", data={"user": "invalid"}) - assert response.status_code == int(HTTPStatus.NOT_FOUND) - - error = "Invalid e-mail." - assert error in response.content.decode("utf-8") - - # Then, test with an invalid resetkey for a real user. - _ = make_resetkey() - post_data = make_passreset_data("fake") - post_data["password"] = "abcd1234" - post_data["confirm"] = "abcd1234" - - with client as request: - response = request.post("/passreset", data=post_data) - assert response.status_code == int(HTTPStatus.NOT_FOUND) - assert error in response.content.decode("utf-8") - - -def make_resetkey(): +def make_resetkey(client: TestClient, user: User): with client as request: response = request.post("/passreset", data={"user": TEST_USERNAME}) assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -200,18 +178,37 @@ def make_resetkey(): return user.ResetKey -def make_passreset_data(resetkey): +def make_passreset_data(user: User, resetkey: str): return { "user": user.Username, "resetkey": resetkey } -def test_post_passreset_error_missing_field(): +def test_post_passreset_error_invalid_email(client: TestClient, user: User): + # First, test with a user that doesn't even exist. + with client as request: + response = request.post("/passreset", data={"user": "invalid"}) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + assert "Invalid e-mail." in response.text + + # Then, test with an invalid resetkey for a real user. + _ = make_resetkey(client, user) + post_data = make_passreset_data(user, "fake") + post_data["password"] = "abcd1234" + post_data["confirm"] = "abcd1234" + + with client as request: + response = request.post("/passreset", data=post_data) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + assert "Invalid e-mail." in response.text + + +def test_post_passreset_error_missing_field(client: TestClient, user: User): # Now that we've prepared the password reset, prepare a POST # request with the user's ResetKey. - resetkey = make_resetkey() - post_data = make_passreset_data(resetkey) + resetkey = make_resetkey(client, user) + post_data = make_passreset_data(user, resetkey) with client as request: response = request.post("/passreset", data=post_data) @@ -222,9 +219,10 @@ def test_post_passreset_error_missing_field(): assert error in response.content.decode("utf-8") -def test_post_passreset_error_password_mismatch(): - resetkey = make_resetkey() - post_data = make_passreset_data(resetkey) +def test_post_passreset_error_password_mismatch(client: TestClient, + user: User): + resetkey = make_resetkey(client, user) + post_data = make_passreset_data(user, resetkey) post_data["password"] = "abcd1234" post_data["confirm"] = "mismatched" @@ -238,9 +236,10 @@ def test_post_passreset_error_password_mismatch(): assert error in response.content.decode("utf-8") -def test_post_passreset_error_password_requirements(): - resetkey = make_resetkey() - post_data = make_passreset_data(resetkey) +def test_post_passreset_error_password_requirements(client: TestClient, + user: User): + resetkey = make_resetkey(client, user) + post_data = make_passreset_data(user, resetkey) passwd_min_len = User.minimum_passwd_length() assert passwd_min_len >= 4 @@ -257,7 +256,7 @@ def test_post_passreset_error_password_requirements(): assert error in response.content.decode("utf-8") -def test_get_register(): +def test_get_register(client: TestClient): with client as request: response = request.get("/register") assert response.status_code == int(HTTPStatus.OK) @@ -288,7 +287,7 @@ def post_register(request, **kwargs): return request.post("/register", data=data, allow_redirects=False) -def test_post_register(): +def test_post_register(client: TestClient): with client as request: response = post_register(request) assert response.status_code == int(HTTPStatus.OK) @@ -298,7 +297,7 @@ def test_post_register(): assert expected in response.content.decode() -def test_post_register_rejects_case_insensitive_spoof(): +def test_post_register_rejects_case_insensitive_spoof(client: TestClient): with client as request: response = post_register(request, U="newUser", E="newUser@example.org") assert response.status_code == int(HTTPStatus.OK) @@ -319,7 +318,7 @@ def test_post_register_rejects_case_insensitive_spoof(): assert expected in response.content.decode() -def test_post_register_error_expired_captcha(): +def test_post_register_error_expired_captcha(client: TestClient): with client as request: response = post_register(request, captcha_salt="invalid-salt") @@ -329,7 +328,7 @@ def test_post_register_error_expired_captcha(): assert "This CAPTCHA has expired. Please try again." in content -def test_post_register_error_missing_captcha(): +def test_post_register_error_missing_captcha(client: TestClient): with client as request: response = post_register(request, captcha=None) @@ -339,7 +338,7 @@ def test_post_register_error_missing_captcha(): assert "The CAPTCHA is missing." in content -def test_post_register_error_invalid_captcha(): +def test_post_register_error_invalid_captcha(client: TestClient): with client as request: response = post_register(request, captcha="invalid blah blah") @@ -349,7 +348,7 @@ def test_post_register_error_invalid_captcha(): assert "The entered CAPTCHA answer is invalid." in content -def test_post_register_error_ip_banned(): +def test_post_register_error_ip_banned(client: TestClient): # 'testclient' is used as request.client.host via FastAPI TestClient. with db.begin(): create(Ban, IPAddress="testclient", BanTS=datetime.utcnow()) @@ -365,7 +364,7 @@ def test_post_register_error_ip_banned(): "inconvenience.") in content -def test_post_register_error_missing_username(): +def test_post_register_error_missing_username(client: TestClient): with client as request: response = post_register(request, U="") @@ -375,7 +374,7 @@ def test_post_register_error_missing_username(): assert "Missing a required field." in content -def test_post_register_error_missing_email(): +def test_post_register_error_missing_email(client: TestClient): with client as request: response = post_register(request, E="") @@ -385,7 +384,7 @@ def test_post_register_error_missing_email(): assert "Missing a required field." in content -def test_post_register_error_invalid_username(): +def test_post_register_error_invalid_username(client: TestClient): with client as request: # Our test config requires at least three characters for a # valid username, so test against two characters: 'ba'. @@ -397,7 +396,7 @@ def test_post_register_error_invalid_username(): assert "The username is invalid." in content -def test_post_register_invalid_password(): +def test_post_register_invalid_password(client: TestClient): with client as request: response = post_register(request, P="abc", C="abc") @@ -408,7 +407,7 @@ def test_post_register_invalid_password(): assert re.search(expected, content) -def test_post_register_error_missing_confirm(): +def test_post_register_error_missing_confirm(client: TestClient): with client as request: response = post_register(request, C=None) @@ -418,7 +417,7 @@ def test_post_register_error_missing_confirm(): assert "Please confirm your new password." in content -def test_post_register_error_mismatched_confirm(): +def test_post_register_error_mismatched_confirm(client: TestClient): with client as request: response = post_register(request, C="mismatched") @@ -428,7 +427,7 @@ def test_post_register_error_mismatched_confirm(): assert "Password fields do not match." in content -def test_post_register_error_invalid_email(): +def test_post_register_error_invalid_email(client: TestClient): with client as request: response = post_register(request, E="bad@email") @@ -438,7 +437,7 @@ def test_post_register_error_invalid_email(): assert "The email address is invalid." in content -def test_post_register_error_undeliverable_email(): +def test_post_register_error_undeliverable_email(client: TestClient): with client as request: # At the time of writing, webchat.freenode.net does not contain # mx records; if it ever does, it'll break this test. @@ -450,7 +449,7 @@ def test_post_register_error_undeliverable_email(): assert "The email address is invalid." in content -def test_post_register_invalid_backup_email(): +def test_post_register_invalid_backup_email(client: TestClient): with client as request: response = post_register(request, BE="bad@email") @@ -460,7 +459,7 @@ def test_post_register_invalid_backup_email(): assert "The backup email address is invalid." in content -def test_post_register_error_invalid_homepage(): +def test_post_register_error_invalid_homepage(client: TestClient): with client as request: response = post_register(request, HP="bad") @@ -471,7 +470,7 @@ def test_post_register_error_invalid_homepage(): assert expected in content -def test_post_register_error_invalid_pgp_fingerprints(): +def test_post_register_error_invalid_pgp_fingerprints(client: TestClient): with client as request: response = post_register(request, K="bad") @@ -492,7 +491,7 @@ def test_post_register_error_invalid_pgp_fingerprints(): assert expected in content -def test_post_register_error_invalid_ssh_pubkeys(): +def test_post_register_error_invalid_ssh_pubkeys(client: TestClient): with client as request: response = post_register(request, PK="bad") @@ -510,7 +509,7 @@ def test_post_register_error_invalid_ssh_pubkeys(): assert "The SSH public key is invalid." in content -def test_post_register_error_unsupported_language(): +def test_post_register_error_unsupported_language(client: TestClient): with client as request: response = post_register(request, L="bad") @@ -521,7 +520,7 @@ def test_post_register_error_unsupported_language(): assert expected in content -def test_post_register_error_unsupported_timezone(): +def test_post_register_error_unsupported_timezone(client: TestClient): with client as request: response = post_register(request, TZ="ABCDEFGH") @@ -532,7 +531,7 @@ def test_post_register_error_unsupported_timezone(): assert expected in content -def test_post_register_error_username_taken(): +def test_post_register_error_username_taken(client: TestClient, user: User): with client as request: response = post_register(request, U="test") @@ -543,7 +542,7 @@ def test_post_register_error_username_taken(): assert re.search(expected, content) -def test_post_register_error_email_taken(): +def test_post_register_error_email_taken(client: TestClient, user: User): with client as request: response = post_register(request, E="test@example.org") @@ -554,7 +553,7 @@ def test_post_register_error_email_taken(): assert re.search(expected, content) -def test_post_register_error_ssh_pubkey_taken(): +def test_post_register_error_ssh_pubkey_taken(client: TestClient, user: User): pk = str() # Create a public key with ssh-keygen (this adds ssh-keygen as a @@ -584,7 +583,7 @@ def test_post_register_error_ssh_pubkey_taken(): assert re.search(expected, content) -def test_post_register_with_ssh_pubkey(): +def test_post_register_with_ssh_pubkey(client: TestClient): pk = str() # Create a public key with ssh-keygen (this adds ssh-keygen as a @@ -605,7 +604,7 @@ def test_post_register_with_ssh_pubkey(): assert response.status_code == int(HTTPStatus.OK) -def test_get_account_edit(): +def test_get_account_edit(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -617,7 +616,7 @@ def test_get_account_edit(): assert response.status_code == int(HTTPStatus.OK) -def test_get_account_edit_unauthorized(): +def test_get_account_edit_unauthorized(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -633,7 +632,7 @@ def test_get_account_edit_unauthorized(): assert response.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_post_account_edit(): +def test_post_account_edit(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -655,7 +654,7 @@ def test_post_account_edit(): assert expected in response.content.decode() -def test_post_account_edit_dev(): +def test_post_account_edit_dev(client: TestClient, user: User): # Modify our user to be a "Trusted User & Developer" name = "Trusted User & Developer" tu_or_dev = query(AccountType, AccountType.AccountType == name).first() @@ -683,7 +682,7 @@ def test_post_account_edit_dev(): assert expected in response.content.decode() -def test_post_account_edit_language(): +def test_post_account_edit_language(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -710,7 +709,7 @@ def test_post_account_edit_language(): assert lang_nodes[0] == "selected" -def test_post_account_edit_timezone(): +def test_post_account_edit_timezone(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -729,7 +728,8 @@ def test_post_account_edit_timezone(): assert response.status_code == int(HTTPStatus.OK) -def test_post_account_edit_error_missing_password(): +def test_post_account_edit_error_missing_password(client: TestClient, + user: User): request = Request() sid = user.login(request, "testPassword") @@ -751,7 +751,8 @@ def test_post_account_edit_error_missing_password(): assert "Invalid password." in content -def test_post_account_edit_error_invalid_password(): +def test_post_account_edit_error_invalid_password(client: TestClient, + user: User): request = Request() sid = user.login(request, "testPassword") @@ -773,7 +774,8 @@ def test_post_account_edit_error_invalid_password(): assert "Invalid password." in content -def test_post_account_edit_inactivity_unauthorized(): +def test_post_account_edit_inactivity_unauthorized(client: TestClient, + user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} post_data = { "U": "test", @@ -791,7 +793,7 @@ def test_post_account_edit_inactivity_unauthorized(): assert errors[0].text.strip() == expected -def test_post_account_edit_inactivity(): +def test_post_account_edit_inactivity(client: TestClient, user: User): with db.begin(): user.AccountTypeID = TRUSTED_USER_ID assert not user.Suspended @@ -822,7 +824,7 @@ def test_post_account_edit_inactivity(): assert user.InactivityTS == 0 -def test_post_account_edit_error_unauthorized(): +def test_post_account_edit_error_unauthorized(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -845,7 +847,7 @@ def test_post_account_edit_error_unauthorized(): assert response.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_post_account_edit_ssh_pub_key(): +def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -874,7 +876,7 @@ def test_post_account_edit_ssh_pub_key(): assert response.status_code == int(HTTPStatus.OK) -def test_post_account_edit_missing_ssh_pubkey(): +def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -907,7 +909,7 @@ def test_post_account_edit_missing_ssh_pubkey(): assert response.status_code == int(HTTPStatus.OK) -def test_post_account_edit_invalid_ssh_pubkey(): +def test_post_account_edit_invalid_ssh_pubkey(client: TestClient, user: User): pubkey = "ssh-rsa fake key" request = Request() @@ -930,7 +932,7 @@ def test_post_account_edit_invalid_ssh_pubkey(): assert response.status_code == int(HTTPStatus.BAD_REQUEST) -def test_post_account_edit_password(): +def test_post_account_edit_password(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -952,7 +954,7 @@ def test_post_account_edit_password(): assert user.valid_password("newPassword") -def test_post_account_edit_account_types(): +def test_post_account_edit_account_types(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") cookies = {"AURSID": sid} @@ -1066,7 +1068,7 @@ def test_post_account_edit_account_types(): assert user.AccountTypeID == DEVELOPER_ID -def test_get_account(): +def test_get_account(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -1077,7 +1079,7 @@ def test_get_account(): assert response.status_code == int(HTTPStatus.OK) -def test_get_account_not_found(): +def test_get_account_not_found(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -1088,7 +1090,7 @@ def test_get_account_not_found(): assert response.status_code == int(HTTPStatus.NOT_FOUND) -def test_get_account_unauthenticated(): +def test_get_account_unauthenticated(client: TestClient, user: User): with client as request: response = request.get("/account/test", allow_redirects=False) assert response.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -1097,7 +1099,7 @@ def test_get_account_unauthenticated(): assert "You must log in to view user information." in content -def test_get_accounts(tu_user): +def test_get_accounts(client: TestClient, user: User, tu_user: User): """ Test that we can GET request /accounts and receive a form which can be used to POST /accounts. """ sid = user.login(Request(), "testPassword") @@ -1156,7 +1158,7 @@ def get_rows(html): return root.xpath('//table[contains(@class, "users")]/tbody/tr') -def test_post_accounts(tu_user): +def test_post_accounts(client: TestClient, user: User, tu_user: User): # Set a PGPKey. with db.begin(): user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" @@ -1211,7 +1213,7 @@ def test_post_accounts(tu_user): % (_user.ID, _user.Username)) -def test_post_accounts_username(tu_user): +def test_post_accounts_username(client: TestClient, user: User, tu_user: User): # Test the U parameter path. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1231,7 +1233,8 @@ def test_post_accounts_username(tu_user): assert username.text.strip() == user.Username -def test_post_accounts_account_type(tu_user): +def test_post_accounts_account_type(client: TestClient, user: User, + tu_user: User): # Check the different account type options. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1324,7 +1327,7 @@ def test_post_accounts_account_type(tu_user): assert type.text.strip() == "Trusted User & Developer" -def test_post_accounts_status(tu_user): +def test_post_accounts_status(client: TestClient, user: User, tu_user: User): # Test the functionality of Suspended. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1356,7 +1359,7 @@ def test_post_accounts_status(tu_user): assert status.text.strip() == "Suspended" -def test_post_accounts_email(tu_user): +def test_post_accounts_email(client: TestClient, user: User, tu_user: User): sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1370,7 +1373,7 @@ def test_post_accounts_email(tu_user): assert len(rows) == 1 -def test_post_accounts_realname(tu_user): +def test_post_accounts_realname(client: TestClient, user: User, tu_user: User): # Test the R parameter path. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1384,7 +1387,7 @@ def test_post_accounts_realname(tu_user): assert len(rows) == 1 -def test_post_accounts_irc(tu_user): +def test_post_accounts_irc(client: TestClient, user: User, tu_user: User): # Test the I parameter path. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1398,7 +1401,7 @@ def test_post_accounts_irc(tu_user): assert len(rows) == 1 -def test_post_accounts_sortby(tu_user): +def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Create a second user so we can compare sorts. account_type = query(AccountType, AccountType.ID == DEVELOPER_ID).first() @@ -1481,7 +1484,7 @@ def test_post_accounts_sortby(tu_user): assert compare_text_values(1, first_rows, reversed(rows)) -def test_post_accounts_pgp_key(tu_user): +def test_post_accounts_pgp_key(client: TestClient, user: User, tu_user: User): with db.begin(): user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" @@ -1498,7 +1501,7 @@ def test_post_accounts_pgp_key(tu_user): assert len(rows) == 1 -def test_post_accounts_paged(tu_user): +def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): # Create 150 users. users = [user] account_type = query(AccountType, @@ -1572,7 +1575,7 @@ def test_post_accounts_paged(tu_user): assert page_next.attrib["disabled"] == "disabled" -def test_get_terms_of_service(): +def test_get_terms_of_service(client: TestClient, user: User): with db.begin(): term = create(Term, Description="Test term.", URL="http://localhost", Revision=1) @@ -1624,7 +1627,7 @@ def test_get_terms_of_service(): assert response.status_code == int(HTTPStatus.SEE_OTHER) -def test_post_terms_of_service(): +def test_post_terms_of_service(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -1682,7 +1685,7 @@ def test_post_terms_of_service(): assert response.headers.get("location") == "/" -def test_account_comments_not_found(): +def test_account_comments_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: resp = request.get("/account/non-existent/comments", cookies=cookies) From fccd8b63d271785e741cae0ec453b349ee0d6d38 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 23:07:34 -0800 Subject: [PATCH 0783/1451] housekeep(fastapi): rewrite test_auth_routes with fixtures Signed-off-by: Kevin Morris --- test/test_auth_routes.py | 95 ++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 0157fcc8..a8d0db11 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -8,11 +8,12 @@ from fastapi.testclient import TestClient import aurweb.config +from aurweb import db from aurweb.asgi import app -from aurweb.db import begin, create, query -from aurweb.models.account_type import AccountType +from aurweb.models.account_type import USER_ID from aurweb.models.session import Session from aurweb.models.user import User +from aurweb.testing.requests import Request # Some test global constants. TEST_USERNAME = "test" @@ -21,30 +22,32 @@ TEST_REFERER = { "referer": aurweb.config.get("options", "aur_location") + "/login", } -# Global mutables. -user = client = None - @pytest.fixture(autouse=True) def setup(db_test): - global user, client + return - account_type = query(AccountType, - AccountType.AccountType == "User").first() - with begin(): - user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, - RealName="Test User", Passwd="testPassword", - AccountType=account_type) - - client = TestClient(app) +@pytest.fixture +def client() -> TestClient: + client = TestClient(app=app) # Necessary for forged login CSRF protection on the login route. Set here # instead of only on the necessary requests for convenience. client.headers.update(TEST_REFERER) + yield client -def test_login_logout(): +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + yield user + + +def test_login_logout(client: TestClient, user: User): post_data = { "user": "test", "passwd": "testPassword", @@ -83,7 +86,7 @@ def mock_getboolean(a, b): @mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean) -def test_secure_login(mock): +def test_secure_login(getboolean: bool, client: TestClient, user: User): """ In this test, we check to verify the course of action taken by starlette when providing secure=True to a response cookie. This is achieved by mocking aurweb.config.getboolean to return @@ -94,11 +97,11 @@ def test_secure_login(mock): on such a request. """ # Create a local TestClient here since we mocked configuration. - client = TestClient(app) + # client = TestClient(app) # Necessary for forged login CSRF protection on the login route. Set here # instead of only on the necessary requests for convenience. - client.headers.update(TEST_REFERER) + # client.headers.update(TEST_REFERER) # Data used for our upcoming http post request. post_data = { @@ -126,18 +129,19 @@ def test_secure_login(mock): # Let's make sure we actually have a session relationship # with the AURSID we ended up with. - record = query(Session, Session.SessionID == cookie.value).first() + record = db.query(Session, Session.SessionID == cookie.value).first() assert record is not None and record.User == user assert user.session == record -def test_authenticated_login(): +def test_authenticated_login(client: TestClient, user: User): post_data = { "user": "test", "passwd": "testPassword", "next": "/" } + cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: # Try to login. response = request.post("/login", data=post_data, @@ -149,12 +153,13 @@ def test_authenticated_login(): # when requesting GET /login as an authenticated user. # Now, let's verify that we receive 403 Forbidden when we # try to get /login as an authenticated user. - response = request.get("/login", allow_redirects=False) + response = request.get("/login", cookies=cookies, + allow_redirects=False) assert response.status_code == int(HTTPStatus.OK) assert "Logged-in as: test" in response.text -def test_unauthenticated_logout_unauthorized(): +def test_unauthenticated_logout_unauthorized(client: TestClient): with client as request: # Alright, let's verify that attempting to /logout when not # authenticated returns 401 Unauthorized. @@ -163,7 +168,7 @@ def test_unauthenticated_logout_unauthorized(): assert response.headers.get("location").startswith("/login") -def test_login_missing_username(): +def test_login_missing_username(client: TestClient): post_data = { "passwd": "testPassword", "next": "/" @@ -179,7 +184,7 @@ def test_login_missing_username(): assert "checked" not in content -def test_login_remember_me(): +def test_login_remember_me(client: TestClient, user: User): post_data = { "user": "test", "passwd": "testPassword", @@ -197,16 +202,15 @@ def test_login_remember_me(): "options", "persistent_cookie_timeout") expected_ts = datetime.utcnow().timestamp() + cookie_timeout - _session = query(Session, - Session.UsersID == user.ID).first() + session = db.query(Session).filter(Session.UsersID == user.ID).first() # Expect that LastUpdateTS was within 5 seconds of the expected_ts, # which is equal to the current timestamp + persistent_cookie_timeout. - assert _session.LastUpdateTS > expected_ts - 5 - assert _session.LastUpdateTS < expected_ts + 5 + assert session.LastUpdateTS > expected_ts - 5 + assert session.LastUpdateTS < expected_ts + 5 -def test_login_incorrect_password_remember_me(): +def test_login_incorrect_password_remember_me(client: TestClient, user: User): post_data = { "user": "test", "passwd": "badPassword", @@ -218,15 +222,14 @@ def test_login_incorrect_password_remember_me(): response = request.post("/login", data=post_data) assert "AURSID" not in response.cookies - # Make sure username is prefilled, password isn't prefilled, and remember_me - # is checked. - content = response.content.decode() - assert post_data["user"] in content - assert post_data["passwd"] not in content - assert "checked" in content + # Make sure username is prefilled, password isn't prefilled, + # and remember_me is checked. + assert post_data["user"] in response.text + assert post_data["passwd"] not in response.text + assert "checked" in response.text -def test_login_missing_password(): +def test_login_missing_password(client: TestClient): post_data = { "user": "test", "next": "/" @@ -237,12 +240,11 @@ def test_login_missing_password(): assert "AURSID" not in response.cookies # Make sure username is prefilled and remember_me isn't checked. - content = response.content.decode() - assert post_data["user"] in content - assert "checked" not in content + assert post_data["user"] in response.text + assert "checked" not in response.text -def test_login_incorrect_password(): +def test_login_incorrect_password(client: TestClient): post_data = { "user": "test", "passwd": "badPassword", @@ -253,15 +255,14 @@ def test_login_incorrect_password(): response = request.post("/login", data=post_data) assert "AURSID" not in response.cookies - # Make sure username is prefilled, password isn't prefilled and remember_me - # isn't checked. - content = response.content.decode() - assert post_data["user"] in content - assert post_data["passwd"] not in content - assert "checked" not in content + # Make sure username is prefilled, password isn't prefilled + # and remember_me isn't checked. + assert post_data["user"] in response.text + assert post_data["passwd"] not in response.text + assert "checked" not in response.text -def test_login_bad_referer(): +def test_login_bad_referer(client: TestClient): post_data = { "user": "test", "passwd": "testPassword", From 043ac7fe9211b96222274fcab6797df8a48d55b1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 30 Nov 2021 23:24:42 -0800 Subject: [PATCH 0784/1451] fix(test_aurblup): use correct type hint for tmpdir Signed-off-by: Kevin Morris --- test/test_aurblup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_aurblup.py b/test/test_aurblup.py index 7eaae556..0b499d57 100644 --- a/test/test_aurblup.py +++ b/test/test_aurblup.py @@ -2,6 +2,7 @@ import tempfile from unittest import mock +import py import pytest from aurweb import config, db @@ -17,12 +18,12 @@ def tempdir() -> str: @pytest.fixture -def alpm_db(tempdir: str) -> AlpmDatabase: +def alpm_db(tempdir: py.path.local) -> AlpmDatabase: yield AlpmDatabase(tempdir) @pytest.fixture(autouse=True) -def setup(db_test, alpm_db: AlpmDatabase, tempdir: str) -> None: +def setup(db_test, alpm_db: AlpmDatabase, tempdir: py.path.local) -> None: config_get = config.get def mock_config_get(section: str, key: str) -> str: From 112837e0e99c3d6c84660ece76240d959760dcdf Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 1 Dec 2021 11:53:43 -0800 Subject: [PATCH 0785/1451] fix(test_auth): cover mismatched referer situation Signed-off-by: Kevin Morris --- aurweb/testing/requests.py | 12 ++++++++++-- test/test_auth.py | 22 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py index a8c077db..76f7afca 100644 --- a/aurweb/testing/requests.py +++ b/aurweb/testing/requests.py @@ -1,3 +1,5 @@ +from typing import Dict + import aurweb.config @@ -27,7 +29,13 @@ class URL: class Request: """ A fake Request object which mimics a FastAPI Request for tests. """ client = Client() - cookies = dict() - headers = dict() user = User() url = URL() + + def __init__(self, + method: str = "GET", + headers: Dict[str, str] = dict(), + cookies: Dict[str, str] = dict()) -> "Request": + self.method = method.upper() + self.headers = headers + self.cookies = cookies diff --git a/test/test_auth.py b/test/test_auth.py index b63fb96f..b607a038 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -1,11 +1,13 @@ from datetime import datetime +import fastapi import pytest +from fastapi import HTTPException from sqlalchemy.exc import IntegrityError from aurweb import db -from aurweb.auth import AnonymousUser, BasicAuthBackend, account_type_required +from aurweb.auth import AnonymousUser, BasicAuthBackend, account_type_required, auth_required from aurweb.models.account_type import USER, USER_ID from aurweb.models.session import Session from aurweb.models.user import User @@ -74,6 +76,24 @@ async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): assert result == user +@pytest.mark.asyncio +async def test_auth_required_redirection_bad_referrer(): + # Create a fake route function which can be wrapped by auth_required. + def bad_referrer_route(request: fastapi.Request): + pass + + # Get down to the nitty gritty internal wrapper. + bad_referrer_route = auth_required()(bad_referrer_route) + + # Execute the route with a "./blahblahblah" Referer, which does not + # match aur_location; `./` has been used as a prefix to attempt to + # ensure we're providing a fake referer. + with pytest.raises(HTTPException) as exc: + request = Request(method="POST", headers={"Referer": "./blahblahblah"}) + await bad_referrer_route(request) + assert exc.detail == "Bad Referer header." + + def test_account_type_required(): """ This test merely asserts that a few different paths do not raise exceptions. """ From c09784d58f600a249f321e6d2a80f9073cd12d49 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 1 Dec 2021 11:56:44 -0800 Subject: [PATCH 0786/1451] fix(auth.auth_required): remove unused keyword arguments Signed-off-by: Kevin Morris --- aurweb/auth/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index 8ceb136c..18356ac2 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -120,9 +120,7 @@ class BasicAuthBackend(AuthenticationBackend): return (AuthCredentials(["authenticated"]), user) -def auth_required(is_required: bool = True, - template: tuple = None, - status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): +def auth_required(is_required: bool = True): """ Authentication route decorator. :param is_required: A boolean indicating whether the function requires auth From 0435c56a41c8e9cf3c286ffbbf4ae21ad33bd6e6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 1 Dec 2021 12:27:14 -0800 Subject: [PATCH 0787/1451] update test/README.md to be more aligned with the current state Signed-off-by: Kevin Morris --- test/README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/test/README.md b/test/README.md index 13fb0a0c..0d86a879 100644 --- a/test/README.md +++ b/test/README.md @@ -117,13 +117,12 @@ To run `sharness` shell test suites (requires Arch Linux): To run `pytest` Python test suites: - $ make -C test pytest + $ pytest **Note:** For SQLite tests, users may want to use `eatmydata` to improve speed: $ eatmydata -- make -C test sh - $ eatmydata -- make -C test pytest To produce coverage reports related to Python when running tests manually, use the following method: @@ -147,11 +146,9 @@ Almost all of our `pytest` suites use the database in some way. There are a few particular testing utilities in `aurweb` that one should keep aware of to aid testing code: -- `aurweb.testing.setup_init_db(*tables)` - - Prepares test database tables to be cleared before a test - is run. Be careful not to specify any tables we depend on - for constant records, like `AccountTypes`, `DependencyTypes`, - `RelationTypes` and `RequestTypes`. +- `db_test` pytest fixture + - Prepares test databases for the module and cleans out database + tables for each test function requiring this fixture. - `aurweb.testing.requests.Request` - A fake stripped down version of `fastapi.Request` that can be passed to any functions in our codebase which use @@ -168,14 +165,16 @@ Example code: @pytest.fixture(autouse=True) - def setup(): - setup_test_db(User.__tablename__) + def setup(db_test): + return @pytest.fixture def user(): - yield db.create(User, Passwd="testPassword", ...) + with db.begin(): + user = db.create(User, Passwd="testPassword", ...) + yield user - def test_user_login(user): + def test_user_login(user: User): assert isinstance(user, User) is True fake_request = Request() From 42701514e75ac373c01d787508e092b7ed0d144d Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Wed, 1 Dec 2021 02:16:08 -0500 Subject: [PATCH 0788/1451] fix(FastAPI): Use HTTPStatus instead of raw number Signed-off-by: Steven Guikal --- aurweb/routers/errors.py | 8 ++++++-- aurweb/routers/html.py | 2 +- aurweb/routers/sso.py | 11 +++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/aurweb/routers/errors.py b/aurweb/routers/errors.py index eb935b57..9ed1e80d 100644 --- a/aurweb/routers/errors.py +++ b/aurweb/routers/errors.py @@ -1,14 +1,18 @@ +from http import HTTPStatus + from aurweb.templates import make_context, render_template async def not_found(request, exc): context = make_context(request, "Page Not Found") - return render_template(request, "errors/404.html", context, 404) + return render_template(request, "errors/404.html", context, + HTTPStatus.NOT_FOUND) async def service_unavailable(request, exc): context = make_context(request, "Service Unavailable") - return render_template(request, "errors/503.html", context, 503) + return render_template(request, "errors/503.html", context, + HTTPStatus.SERVICE_UNAVAILABLE) # Maps HTTP errors to functions exceptions = { diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 525fb626..337acce6 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -221,4 +221,4 @@ async def metrics(request: Request): @router.get("/raisefivethree", response_class=HTMLResponse) async def raise_service_unavailable(request: Request): - raise HTTPException(status_code=503) + raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE) diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index edeb7c6b..eff1c63f 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -1,6 +1,7 @@ import time import uuid +from http import HTTPStatus from urllib.parse import urlencode import fastapi @@ -59,7 +60,8 @@ def open_session(request, conn, user_id): """ if is_account_suspended(conn, user_id): _ = get_translator_for_request(request) - raise HTTPException(status_code=403, detail=_('Account suspended')) + raise HTTPException(status_code=HTTPStatus.FORBIDDEN, + detail=_('Account suspended')) # TODO This is a terrible message because it could imply the attempt at # logging in just caused the suspension. @@ -104,7 +106,7 @@ async def authenticate(request: Request, redirect: str = None, conn=Depends(aurw if is_ip_banned(conn, request.client.host): _ = get_translator_for_request(request) raise HTTPException( - status_code=403, + status_code=HTTPStatus.FORBIDDEN, detail=_('The login form is currently disabled for your IP address, ' 'probably due to sustained spam attacks. Sorry for the ' 'inconvenience.')) @@ -117,13 +119,14 @@ async def authenticate(request: Request, redirect: str = None, conn=Depends(aurw # Let’s give attackers as little information as possible. _ = get_translator_for_request(request) raise HTTPException( - status_code=400, + status_code=HTTPStatus.BAD_REQUEST, detail=_('Bad OAuth token. Please retry logging in from the start.')) sub = user.get("sub") # this is the SSO account ID in JWT terminology if not sub: _ = get_translator_for_request(request) - raise HTTPException(status_code=400, detail=_("JWT is missing its `sub` field.")) + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, + detail=_("JWT is missing its `sub` field.")) aur_accounts = conn.execute(select([Users.c.ID]).where(Users.c.SSOAccountID == sub)) \ .fetchall() From e1bf6dd56256eddfbed2909fd658406755ce06f2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 2 Dec 2021 17:09:37 -0800 Subject: [PATCH 0789/1451] fix(fastapi): restore stripped whitespace in archdev-navbar Signed-off-by: Kevin Morris --- templates/partials/archdev-navbar.html | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index dc2377fe..98bb1841 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -54,17 +54,10 @@

  • {% else %} {# All guest users see Register #} -
  • - - {% trans %}Register{% endtrans %} - -
  • +
  • {% trans %}Register{% endtrans %}
  • + {# All guest users see Login #} -
  • - - {% trans %}Login{% endtrans %} - -
  • +
  • {% trans %}Login{% endtrans %}
  • {% endif %} From abfd41f31e76a11619f6aa233058aa0bb25c2dec Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 2 Dec 2021 23:22:31 -0800 Subject: [PATCH 0790/1451] change(fastapi): centralize HTTPException Signed-off-by: Kevin Morris --- aurweb/asgi.py | 24 +++++++++++++----------- aurweb/routers/errors.py | 21 --------------------- templates/errors/404.html | 8 -------- templates/errors/503.html | 8 -------- templates/errors/detail.html | 8 ++++++++ test/test_asgi.py | 10 +++++++--- 6 files changed, 28 insertions(+), 51 deletions(-) delete mode 100644 aurweb/routers/errors.py delete mode 100644 templates/errors/404.html delete mode 100644 templates/errors/503.html create mode 100644 templates/errors/detail.html diff --git a/aurweb/asgi.py b/aurweb/asgi.py index b399cfb1..ef8d5933 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -6,8 +6,8 @@ import typing from urllib.parse import quote_plus -from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi import FastAPI, HTTPException, Request, Response +from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from prometheus_client import multiprocess from sqlalchemy import and_, or_ @@ -21,10 +21,11 @@ from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine, query from aurweb.models import AcceptedTerm, Term from aurweb.prometheus import http_api_requests_total, http_requests_total, instrumentator -from aurweb.routers import accounts, auth, errors, html, packages, rpc, rss, sso, trusted_user +from aurweb.routers import accounts, auth, html, packages, rpc, rss, sso, trusted_user +from aurweb.templates import make_context, render_template # Setup the FastAPI app. -app = FastAPI(exception_handlers=errors.exceptions) +app = FastAPI() # Instrument routes with the prometheus-fastapi-instrumentator # library with custom collectors and expose /metrics. @@ -93,14 +94,15 @@ def child_exit(server, worker): # pragma: no cover @app.exception_handler(HTTPException) -async def http_exception_handler(request, exc): - """ - Dirty HTML error page to replace the default JSON error responses. - In the future this should use a proper Arch-themed HTML template. - """ +async def http_exception_handler(request: Request, exc: HTTPException) \ + -> Response: + """ Handle an HTTPException thrown in a route. """ phrase = http.HTTPStatus(exc.status_code).phrase - return HTMLResponse(f"

    {exc.status_code} {phrase}

    {exc.detail}

    ", - status_code=exc.status_code) + context = make_context(request, phrase) + context["exc"] = exc + context["phrase"] = phrase + return render_template(request, "errors/detail.html", context, + exc.status_code) @app.middleware("http") diff --git a/aurweb/routers/errors.py b/aurweb/routers/errors.py deleted file mode 100644 index 9ed1e80d..00000000 --- a/aurweb/routers/errors.py +++ /dev/null @@ -1,21 +0,0 @@ -from http import HTTPStatus - -from aurweb.templates import make_context, render_template - - -async def not_found(request, exc): - context = make_context(request, "Page Not Found") - return render_template(request, "errors/404.html", context, - HTTPStatus.NOT_FOUND) - - -async def service_unavailable(request, exc): - context = make_context(request, "Service Unavailable") - return render_template(request, "errors/503.html", context, - HTTPStatus.SERVICE_UNAVAILABLE) - -# Maps HTTP errors to functions -exceptions = { - 404: not_found, - 503: service_unavailable -} diff --git a/templates/errors/404.html b/templates/errors/404.html deleted file mode 100644 index 4926aff6..00000000 --- a/templates/errors/404.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends 'partials/layout.html' %} - -{% block pageContent %} -
    -

    404 - {% trans %}Page Not Found{% endtrans %}

    -

    {% trans %}Sorry, the page you've requested does not exist.{% endtrans %}

    -
    -{% endblock %} diff --git a/templates/errors/503.html b/templates/errors/503.html deleted file mode 100644 index 9a0ed56a..00000000 --- a/templates/errors/503.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends 'partials/layout.html' %} - -{% block pageContent %} -
    -

    503 - {% trans %}Service Unavailable{% endtrans %}

    -

    {% trans %}Don't panic! This site is down due to maintenance. We will be back soon.{% endtrans %}

    -
    -{% endblock %} diff --git a/templates/errors/detail.html b/templates/errors/detail.html new file mode 100644 index 00000000..f382a9bb --- /dev/null +++ b/templates/errors/detail.html @@ -0,0 +1,8 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +
    +

    {{ "%d" | format(exc.status_code) }} - {{ phrase }}

    +

    {{ exc.detail }}

    +
    +{% endblock %} diff --git a/test/test_asgi.py b/test/test_asgi.py index fa2df5a1..16b07c31 100644 --- a/test/test_asgi.py +++ b/test/test_asgi.py @@ -11,6 +11,8 @@ import aurweb.asgi import aurweb.config import aurweb.redis +from aurweb.testing.requests import Request + @pytest.mark.asyncio async def test_asgi_startup_session_secret_exception(monkeypatch): @@ -42,9 +44,11 @@ async def test_asgi_startup_exception(monkeypatch): async def test_asgi_http_exception_handler(): exc = HTTPException(status_code=422, detail="EXCEPTION!") phrase = http.HTTPStatus(exc.status_code).phrase - response = await aurweb.asgi.http_exception_handler(None, exc) - assert response.body.decode() == \ - f"

    {exc.status_code} {phrase}

    {exc.detail}

    " + response = await aurweb.asgi.http_exception_handler(Request(), exc) + assert response.status_code == 422 + content = response.body.decode() + assert f"{exc.status_code} - {phrase}" in content + assert "EXCEPTION!" in content @pytest.mark.asyncio From 806a19b91a3f2e254e1956243a2fff89b123ff4f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 2 Dec 2021 23:26:42 -0800 Subject: [PATCH 0791/1451] feat(fastapi): render a 500 html response when unique SID generation fails We've seen a bug in the past where unique SID generation fails and still ends up raising an exception. This commit reworks how we deal with database exceptions internally, tries for 36 iterations to set a fresh unique SID, and raises a 500 HTTPException if we were unable to. Signed-off-by: Kevin Morris --- aurweb/models/user.py | 67 ++++++++++++++++++++++++---------------- test/test_auth_routes.py | 45 +++++++++++++++++++++++++++ test/test_user.py | 2 -- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index f0724202..dcf5f519 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -1,12 +1,14 @@ import hashlib from datetime import datetime +from http import HTTPStatus from typing import List, Set import bcrypt -from fastapi import Request +from fastapi import HTTPException, Request from sqlalchemy import or_ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship import aurweb.config @@ -108,33 +110,45 @@ class User(Base): if not self.authenticated: return None - now_ts = datetime.utcnow().timestamp() - session_ts = now_ts + ( - session_time if session_time - else aurweb.config.getint("options", "login_timeout") - ) + # Maximum number of iterations where we attempt to generate + # a unique SID. In cases where the Session table has + # exhausted all possible values, this will catch exceptions + # instead of raising them and include details about failing + # generation in an HTTPException. + tries = 36 - sid = None + exc = None + for i in range(tries): + exc = None + now_ts = datetime.utcnow().timestamp() + session_ts = now_ts + ( + session_time if session_time + else aurweb.config.getint("options", "login_timeout") + ) + try: + with db.begin(): + self.LastLogin = now_ts + self.LastLoginIPAddress = request.client.host + if not self.session: + sid = generate_unique_sid() + self.session = db.create(Session, User=self, + SessionID=sid, + LastUpdateTS=session_ts) + else: + last_updated = self.session.LastUpdateTS + if last_updated and last_updated < now_ts: + self.session.SessionID = generate_unique_sid() + self.session.LastUpdateTS = session_ts + break + except IntegrityError as exc_: + exc = exc_ - with db.begin(): - self.LastLogin = now_ts - self.LastLoginIPAddress = request.client.host - if not self.session: - sid = generate_unique_sid() - self.session = Session(UsersID=self.ID, SessionID=sid, - LastUpdateTS=session_ts) - db.add(self.session) - else: - last_updated = self.session.LastUpdateTS - if last_updated and last_updated < now_ts: - self.session.SessionID = sid = generate_unique_sid() - else: - # Session is still valid; retrieve the current SID. - sid = self.session.SessionID + if exc: + detail = ("Unable to generate a unique session ID in " + f"{tries} iterations.") + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=detail) - self.session.LastUpdateTS = session_ts - - request.cookies["AURSID"] = self.session.SessionID return self.session.SessionID def has_credential(self, credential: Set[int], @@ -142,8 +156,7 @@ class User(Base): from aurweb.auth.creds import has_credential return has_credential(self, credential, approved) - def logout(self, request): - del request.cookies["AURSID"] + def logout(self, request: Request): self.authenticated = False if self.session: with db.begin(): diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index a8d0db11..3455a019 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -283,3 +283,48 @@ def test_login_bad_referer(client: TestClient): response = request.post("/login", data=post_data, headers=BAD_REFERER) assert response.status_code == int(HTTPStatus.BAD_REQUEST) assert "AURSID" not in response.cookies + + +def test_generate_unique_sid_exhausted(client: TestClient, user: User): + """ + In this test, we mock up generate_unique_sid() to infinitely return + the same SessionID given to `user`. Within that mocking, we try + to login as `user2` and expect the internal server error rendering + by our error handler. + + This exercises the bad path of /login, where we can't find a unique + SID to assign the user. + """ + now = int(datetime.utcnow().timestamp()) + with db.begin(): + # Create a second user; we'll login with this one. + user2 = db.create(User, Username="test2", Email="test2@example.org", + ResetKey="testReset", Passwd="testPassword", + AccountTypeID=USER_ID) + + # Create a session with ID == "testSession" for `user`. + db.create(Session, User=user, SessionID="testSession", + LastUpdateTS=now) + + # Mock out generate_unique_sid; always return "testSession" which + # causes us to eventually error out and raise an internal error. + def mock_generate_sid(): + return "testSession" + + # Login as `user2`; we expect an internal server error response + # with a relevent detail. + post_data = { + "user": user2.Username, + "passwd": "testPassword", + "next": "/", + } + generate_unique_sid_ = "aurweb.models.session.generate_unique_sid" + with mock.patch(generate_unique_sid_, mock_generate_sid): + with client as request: + # Set cookies = {} to remove any previous login kept by TestClient. + response = request.post("/login", data=post_data, cookies={}) + assert response.status_code == int(HTTPStatus.INTERNAL_SERVER_ERROR) + + expected = "Unable to generate a unique session ID" + assert expected in response.text + assert "500 - Internal Server Error" in response.text diff --git a/test/test_user.py b/test/test_user.py index 52cdc89e..2c8dd847 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -62,7 +62,6 @@ def test_user_login_logout(user: User): sid = user.login(request, "testPassword") assert sid is not None assert user.is_authenticated() - assert "AURSID" in request.cookies # Expect that User session relationships work right. user_session = db.query(Session, @@ -92,7 +91,6 @@ def test_user_login_logout(user: User): # Test logout. user.logout(request) - assert "AURSID" not in request.cookies assert not user.is_authenticated() From 81f8c2326566f993fcbcb2f0ef189c13657b0676 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 2 Dec 2021 23:42:13 -0800 Subject: [PATCH 0792/1451] fix(fastapi): log out IntegrityError from failed SID generation Signed-off-by: Kevin Morris --- aurweb/models/user.py | 5 ++++- test/test_auth_routes.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index dcf5f519..8e66b490 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -15,11 +15,13 @@ import aurweb.config import aurweb.models.account_type import aurweb.schema -from aurweb import db, schema +from aurweb import db, logging, schema from aurweb.models.account_type import AccountType as _AccountType from aurweb.models.ban import is_banned from aurweb.models.declarative import Base +logger = logging.get_logger(__name__) + SALT_ROUNDS_DEFAULT = 12 @@ -146,6 +148,7 @@ class User(Base): if exc: detail = ("Unable to generate a unique session ID in " f"{tries} iterations.") + logger.error(str(exc)) raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=detail) diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 3455a019..3ae8a56c 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -285,7 +285,8 @@ def test_login_bad_referer(client: TestClient): assert "AURSID" not in response.cookies -def test_generate_unique_sid_exhausted(client: TestClient, user: User): +def test_generate_unique_sid_exhausted(client: TestClient, user: User, + caplog: pytest.LogCaptureFixture): """ In this test, we mock up generate_unique_sid() to infinitely return the same SessionID given to `user`. Within that mocking, we try @@ -328,3 +329,6 @@ def test_generate_unique_sid_exhausted(client: TestClient, user: User): expected = "Unable to generate a unique session ID" assert expected in response.text assert "500 - Internal Server Error" in response.text + + # Make sure an IntegrityError from the DB got logged out. + assert "IntegrityError" in caplog.text From 75ad2fb53d04e5f85dc32779bfa0e373f9301e74 Mon Sep 17 00:00:00 2001 From: Steven Guikal Date: Wed, 1 Dec 2021 16:35:24 -0500 Subject: [PATCH 0793/1451] fix(FastAPI): cleanup auth_required decorator Signed-off-by: Steven Guikal --- aurweb/auth/__init__.py | 46 ++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index 18356ac2..b6dd6e3f 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -120,35 +120,39 @@ class BasicAuthBackend(AuthenticationBackend): return (AuthCredentials(["authenticated"]), user) -def auth_required(is_required: bool = True): - """ Authentication route decorator. +def auth_required(auth_goal: bool = True): + """ Enforce a user's authentication status, bringing them to the login page + or homepage if their authentication status does not match the goal. - :param is_required: A boolean indicating whether the function requires auth - :param status_code: An optional status_code for template render. - Redirects are always SEE_OTHER. + :param auth_goal: Whether authentication is required or entirely disallowed + for a user to perform this request. + :return: Return the FastAPI function this decorator wraps. """ def decorator(func): @functools.wraps(func) async def wrapper(request, *args, **kwargs): - if request.user.is_authenticated() != is_required: - url = "/" + if request.user.is_authenticated() == auth_goal: + return await func(request, *args, **kwargs) - if is_required: - if request.method == "GET": - url = request.url.path - elif request.method == "POST" and (referer := request.headers.get("Referer")): - aur = aurweb.config.get("options", "aur_location") + "/" - if not referer.startswith(aur): - _ = l10n.get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, - detail=_("Bad Referer header.")) - url = referer[len(aur) - 1:] + url = "/" + if auth_goal is False: + return RedirectResponse(url, status_code=int(HTTPStatus.SEE_OTHER)) - url = "/login?" + util.urlencode({"next": url}) - return RedirectResponse(url, - status_code=int(HTTPStatus.SEE_OTHER)) - return await func(request, *args, **kwargs) + # Use the request path when the user can visit a page directly but + # is not authenticated and use the Referer header if visiting the + # page itself is not directly possible (e.g. submitting a form). + if request.method in ("GET", "HEAD"): + url = request.url.path + elif (referer := request.headers.get("Referer")): + aur = aurweb.config.get("options", "aur_location") + "/" + if not referer.startswith(aur): + _ = l10n.get_translator_for_request(request) + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, + detail=_("Bad Referer header.")) + url = referer[len(aur) - 1:] + url = "/login?" + util.urlencode({"next": url}) + return RedirectResponse(url, status_code=int(HTTPStatus.SEE_OTHER)) return wrapper return decorator From b0b5e4c9d10ee3c31c7e3a61286c9fdabd3c8ceb Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 3 Dec 2021 15:13:41 -0800 Subject: [PATCH 0794/1451] fix(fastapi): use `secrets` module to generate random strings Signed-off-by: Kevin Morris --- aurweb/util.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aurweb/util.py b/aurweb/util.py index f5ced259..542dfc2e 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,7 +1,6 @@ import base64 import copy import math -import random import re import secrets import string @@ -25,9 +24,9 @@ from aurweb import defaults, logging logger = logging.get_logger(__name__) -def make_random_string(length): - return ''.join(random.choices(string.ascii_lowercase - + string.digits, k=length)) +def make_random_string(length: int) -> str: + alphanumerics = string.ascii_lowercase + string.digits + return ''.join([secrets.choice(alphanumerics) for i in range(length)]) def make_nonce(length: int = 8): From aa717a4ef9d45d0b2a454a75bdd4f55dd18a2225 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 3 Dec 2021 15:41:54 -0800 Subject: [PATCH 0795/1451] change(fastapi): no longer care about ResetKey collisions Signed-off-by: Kevin Morris --- aurweb/models/user.py | 6 +++--- aurweb/routers/accounts.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 8e66b490..d0bdea30 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -15,7 +15,7 @@ import aurweb.config import aurweb.models.account_type import aurweb.schema -from aurweb import db, logging, schema +from aurweb import db, logging, schema, util from aurweb.models.account_type import AccountType as _AccountType from aurweb.models.ban import is_banned from aurweb.models.declarative import Base @@ -249,5 +249,5 @@ class User(Base): self.ID, str(self.AccountType), self.Username) -def generate_unique_resetkey(): - return db.make_random_value(User, User.ResetKey, 32) +def generate_resetkey(): + return util.make_random_string(32) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 388daf84..f61ccdd2 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -16,7 +16,7 @@ from aurweb.exceptions import ValidationError from aurweb.l10n import get_translator_for_request from aurweb.models import account_type as at from aurweb.models.ssh_pub_key import get_fingerprint -from aurweb.models.user import generate_unique_resetkey +from aurweb.models.user import generate_resetkey from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification from aurweb.templates import make_context, make_variable_context, render_template from aurweb.users import update, validate @@ -93,7 +93,7 @@ async def passreset_post(request: Request, status_code=HTTPStatus.SEE_OTHER) # If we got here, we continue with issuing a resetkey for the user. - resetkey = generate_unique_resetkey() + resetkey = generate_resetkey() with db.begin(): user.ResetKey = resetkey @@ -291,7 +291,7 @@ async def account_register_post(request: Request, # Create a user with no password with a resetkey, then send # an email off about it. - resetkey = generate_unique_resetkey() + resetkey = generate_resetkey() # By default, we grab the User account type to associate with. atype = db.query(models.AccountType, From bfa916c7b294fb82f3b935f973b649f42849557b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 3 Dec 2021 23:40:16 -0800 Subject: [PATCH 0796/1451] fix(fastapi): fix PGP Key Fingerprint display for account/show.html There's a space between every 4 characters in the fingerprint in PHP; we were missing it in FastAPI. This commit fixes that inconsistency. Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 11 ++++++++++- templates/account/show.html | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index f61ccdd2..946ffc31 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -432,7 +432,16 @@ async def account(request: Request, username: str): if not request.user.is_authenticated(): return render_template(request, "account/show.html", context, status_code=HTTPStatus.UNAUTHORIZED) - context["user"] = get_user_by_name(username) + + # Get related User record, if possible. + user = get_user_by_name(username) + context["user"] = user + + # Format PGPKey for display with a space between each 4 characters. + k = user.PGPKey or str() + context["pgp_key"] = " ".join([k[i:i + 4] for i in range(0, len(k), 4)]) + + # Render the template. return render_template(request, "account/show.html", context) diff --git a/templates/account/show.html b/templates/account/show.html index 0c99c99f..23b262b0 100644 --- a/templates/account/show.html +++ b/templates/account/show.html @@ -46,7 +46,7 @@ {% trans %}PGP Key Fingerprint{% endtrans %}: - {{ user.PGPKey or '' }} + {{ pgp_key }} {% trans %}Status{% endtrans %}: From d0fc56d53fa1232aa5d5c1b5a7bca12ad1edb773 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Dec 2021 00:14:55 -0800 Subject: [PATCH 0797/1451] fix(python): redirect when the request user can't edit target user Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 24 +++++++++++++++++------- test/test_accounts_routes.py | 30 ++++++++++++++++++------------ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index f61ccdd2..ff2c3040 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -329,13 +329,23 @@ async def account_register_post(request: Request, return render_template(request, "register.html", context) -def cannot_edit(request, user): - """ Return a 401 HTMLResponse if the request user doesn't - have authorization, otherwise None. """ - has_dev_cred = request.user.has_credential(creds.ACCOUNT_EDIT_DEV, - approved=[user]) - if not has_dev_cred: - return HTMLResponse(status_code=HTTPStatus.UNAUTHORIZED) +def cannot_edit(request: Request, user: models.User) \ + -> typing.Optional[RedirectResponse]: + """ + Decide if `request.user` cannot edit `user`. + + If the request user can edit the target user, None is returned. + Otherwise, a redirect is returned to /account/{user.Username}. + + :param request: FastAPI request + :param user: Target user to be edited + :return: RedirectResponse if approval != granted else None + """ + approved = request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user]) + if not approved and (to := "/"): + if user: + to = f"/account/{user.Username}" + return RedirectResponse(to, status_code=HTTPStatus.SEE_OTHER) return None diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index f08efcd2..348a6994 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -620,16 +620,19 @@ def test_get_account_edit_unauthorized(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword") + with db.begin(): + user2 = create(User, Username="test2", Email="test2@example.org", + Passwd="testPassword", AccountTypeID=USER_ID) + endpoint = f"/account/{user2.Username}/edit" with client as request: # Try to edit `test2` while authenticated as `test`. - response = request.get("/account/test2/edit", cookies={ - "AURSID": sid - }, allow_redirects=False) + response = request.get(endpoint, cookies={"AURSID": sid}, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) - assert response.status_code == int(HTTPStatus.UNAUTHORIZED) + expected = f"/account/{user2.Username}" + assert response.headers.get("location") == expected def test_post_account_edit(client: TestClient, user: User): @@ -828,8 +831,9 @@ def test_post_account_edit_error_unauthorized(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - create(User, Username="test2", - Email="test2@example.org", Passwd="testPassword") + with db.begin(): + user2 = create(User, Username="test2", Email="test2@example.org", + Passwd="testPassword", AccountTypeID=USER_ID) post_data = { "U": "test", @@ -838,13 +842,15 @@ def test_post_account_edit_error_unauthorized(client: TestClient, user: User): "passwd": "testPassword" } + endpoint = f"/account/{user2.Username}/edit" with client as request: # Attempt to edit 'test2' while logged in as 'test'. - response = request.post("/account/test2/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post(endpoint, cookies={"AURSID": sid}, + data=post_data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) - assert response.status_code == int(HTTPStatus.UNAUTHORIZED) + expected = f"/account/{user2.Username}" + assert response.headers.get("location") == expected def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): From 973dbf04828c1b5f475c189d12dd8c099ac8b35e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Dec 2021 00:15:34 -0800 Subject: [PATCH 0798/1451] fix(python): use creds to determine account links to display Signed-off-by: Kevin Morris --- templates/account/show.html | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/templates/account/show.html b/templates/account/show.html index 0c99c99f..14a4eccf 100644 --- a/templates/account/show.html +++ b/templates/account/show.html @@ -69,20 +69,24 @@ | safe }} -
  • - {{ "%sEdit this user's account%s" - | tr - | format('' | format(user.Username), "") - | safe - }} -
  • -
  • - {{ "%sList this user's comments%s" - | tr - | format('' | format(user.Username), "") - | safe - }} -
  • + {% if request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user]) %} +
  • + {{ "%sEdit this user's account%s" + | tr + | format('' | format(user.Username), "") + | safe + }} +
  • + {% endif %} + {% if request.user.has_credential(creds.ACCOUNT_LIST_COMMENTS, approved=[user]) %} +
  • + {{ "%sList this user's comments%s" + | tr + | format('' | format(user.Username), "") + | safe + }} +
  • + {% endif %} From 2ea4559b60135b38c07b949d5905c99ec98739dc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Dec 2021 00:50:32 -0800 Subject: [PATCH 0799/1451] fix(python): use correct Status field in account/show.html Signed-off-by: Kevin Morris --- templates/account/show.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/templates/account/show.html b/templates/account/show.html index e1074394..3e36faf0 100644 --- a/templates/account/show.html +++ b/templates/account/show.html @@ -50,6 +50,17 @@ {% trans %}Status{% endtrans %}: + {% if not user.InactivityTS %} + {{ "Active" | tr }} + {% else %} + {% set inactive_ds = user.InactivityTS | dt | as_timezone(timezone) %} + + {{ + "Inactive since %s" | tr + | format(inactive_ds.strftime("%Y-%m-%d %H:%M")) + }} + + {% endif %} {{ "Active" if not user.Suspended else "Suspended" | tr }} From 224a0de784634c1ee5569e12a7f95d1e9bc1bb5f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Dec 2021 01:16:14 -0800 Subject: [PATCH 0800/1451] fix(python): add logged in date field to account/show.html Signed-off-by: Kevin Morris --- aurweb/routers/accounts.py | 7 +++++++ templates/account/show.html | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index fc25a7e8..8eecaa31 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -451,6 +451,13 @@ async def account(request: Request, username: str): k = user.PGPKey or str() context["pgp_key"] = " ".join([k[i:i + 4] for i in range(0, len(k), 4)]) + login_ts = None + session = db.query(models.Session).filter( + models.Session.UsersID == user.ID).first() + if session: + login_ts = user.session.LastUpdateTS + context["login_ts"] = login_ts + # Render the template. return render_template(request, "account/show.html", context) diff --git a/templates/account/show.html b/templates/account/show.html index 3e36faf0..c6a53f4a 100644 --- a/templates/account/show.html +++ b/templates/account/show.html @@ -61,7 +61,6 @@ }} {% endif %} - {{ "Active" if not user.Suspended else "Suspended" | tr }} {% trans %}Registration date{% endtrans %}: @@ -69,6 +68,13 @@ {{ user.RegistrationTS.strftime("%Y-%m-%d") }} + {% if login_ts %} + + {% trans %}Last Login{% endtrans %}: + {% set login_ds = login_ts | dt | as_timezone(timezone) %} + {{ login_ds.strftime("%Y-%m-%d") }} + + {% endif %} {% trans %}Links{% endtrans %}: From 8501bba0ac7e6ad03f80d9f370bcd4cbd63db296 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Dec 2021 02:12:20 -0800 Subject: [PATCH 0801/1451] change(python): rework session timing Previously, we were just relying on the cookie expiration for sessions to expire. We were not cleaning up Session records either. Rework timing to depend on an AURREMEMBER cookie which is now emitted on login during BasicAuthBackend processing. If the SID does still have a session but it's expired, we now delete the session record before returning. Otherwise, we update the session's LastUpdateTS to the current time. In addition, stored the unauthenticated result value in a variable to reduce redundancy. Signed-off-by: Kevin Morris --- aurweb/auth/__init__.py | 22 +++++++++++++++------- aurweb/models/user.py | 8 ++------ aurweb/routers/auth.py | 4 ++++ test/test_auth.py | 24 +++++++++++++++++++++++- test/test_auth_routes.py | 16 ++++++---------- 5 files changed, 50 insertions(+), 24 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index b6dd6e3f..5f55e2fb 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -7,7 +7,6 @@ import fastapi from fastapi import HTTPException from fastapi.responses import RedirectResponse -from sqlalchemy import and_ from starlette.authentication import AuthCredentials, AuthenticationBackend from starlette.requests import HTTPConnection @@ -97,18 +96,27 @@ class AnonymousUser: class BasicAuthBackend(AuthenticationBackend): async def authenticate(self, conn: HTTPConnection): + unauthenticated = (None, AnonymousUser()) sid = conn.cookies.get("AURSID") if not sid: - return (None, AnonymousUser()) + return unauthenticated - now_ts = datetime.utcnow().timestamp() - record = db.query(Session).filter( - and_(Session.SessionID == sid, - Session.LastUpdateTS >= now_ts)).first() + timeout = aurweb.config.getint("options", "login_timeout") + remembered = ("AURREMEMBER" in conn.cookies + and bool(conn.cookies.get("AURREMEMBER"))) + if remembered: + timeout = aurweb.config.getint("options", + "persistent_cookie_timeout") # If no session with sid and a LastUpdateTS now or later exists. + now_ts = int(datetime.utcnow().timestamp()) + record = db.query(Session).filter(Session.SessionID == sid).first() if not record: - return (None, AnonymousUser()) + return unauthenticated + elif record.LastUpdateTS < (now_ts - timeout): + with db.begin(): + db.delete_all([record]) + return unauthenticated # At this point, we cannot have an invalid user if the record # exists, due to ForeignKey constraints in the schema upheld diff --git a/aurweb/models/user.py b/aurweb/models/user.py index d0bdea30..5ead606e 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -123,10 +123,6 @@ class User(Base): for i in range(tries): exc = None now_ts = datetime.utcnow().timestamp() - session_ts = now_ts + ( - session_time if session_time - else aurweb.config.getint("options", "login_timeout") - ) try: with db.begin(): self.LastLogin = now_ts @@ -135,12 +131,12 @@ class User(Base): sid = generate_unique_sid() self.session = db.create(Session, User=self, SessionID=sid, - LastUpdateTS=session_ts) + LastUpdateTS=now_ts) else: last_updated = self.session.LastUpdateTS if last_updated and last_updated < now_ts: self.session.SessionID = generate_unique_sid() - self.session.LastUpdateTS = session_ts + self.session.LastUpdateTS = now_ts break except IntegrityError as exc_: exc = exc_ diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 74763667..8815c896 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -73,6 +73,10 @@ async def login_post(request: Request, response.set_cookie("AURLANG", user.LangPreference, secure=secure, httponly=secure, samesite=cookies.samesite()) + response.set_cookie("AURREMEMBER", remember_me, + expires=expires_at, + secure=secure, httponly=secure, + samesite=cookies.samesite()) return response diff --git a/test/test_auth.py b/test/test_auth.py index b607a038..0094aa25 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -6,7 +6,7 @@ import pytest from fastapi import HTTPException from sqlalchemy.exc import IntegrityError -from aurweb import db +from aurweb import config, db from aurweb.auth import AnonymousUser, BasicAuthBackend, account_type_required, auth_required from aurweb.models.account_type import USER, USER_ID from aurweb.models.session import Session @@ -76,6 +76,28 @@ async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): assert result == user +@pytest.mark.asyncio +async def test_expired_session(backend: BasicAuthBackend, user: User): + """ Login, expire the session manually, then authenticate. """ + # First, build a Request with a logged in user. + request = Request() + request.user = user + sid = request.user.login(Request(), "testPassword") + request.cookies["AURSID"] = sid + + # Set Session.LastUpdateTS to 20 seconds expired. + timeout = config.getint("options", "login_timeout") + now_ts = int(datetime.utcnow().timestamp()) + with db.begin(): + request.user.session.LastUpdateTS = now_ts - timeout - 20 + + # Run through authentication backend and get the session + # deleted due to its expiration. + await backend.authenticate(request) + session = db.query(Session).filter(Session.SessionID == sid).first() + assert session is None + + @pytest.mark.asyncio async def test_auth_required_redirection_bad_referrer(): # Create a fake route function which can be wrapped by auth_required. diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 3ae8a56c..f3e2a011 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -13,7 +13,6 @@ from aurweb.asgi import app from aurweb.models.account_type import USER_ID from aurweb.models.session import Session from aurweb.models.user import User -from aurweb.testing.requests import Request # Some test global constants. TEST_USERNAME = "test" @@ -136,12 +135,11 @@ def test_secure_login(getboolean: bool, client: TestClient, user: User): def test_authenticated_login(client: TestClient, user: User): post_data = { - "user": "test", + "user": user.Username, "passwd": "testPassword", "next": "/" } - cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: # Try to login. response = request.post("/login", data=post_data, @@ -153,7 +151,7 @@ def test_authenticated_login(client: TestClient, user: User): # when requesting GET /login as an authenticated user. # Now, let's verify that we receive 403 Forbidden when we # try to get /login as an authenticated user. - response = request.get("/login", cookies=cookies, + response = request.get("/login", cookies=response.cookies, allow_redirects=False) assert response.status_code == int(HTTPStatus.OK) assert "Logged-in as: test" in response.text @@ -200,14 +198,12 @@ def test_login_remember_me(client: TestClient, user: User): cookie_timeout = aurweb.config.getint( "options", "persistent_cookie_timeout") - expected_ts = datetime.utcnow().timestamp() + cookie_timeout - + now_ts = int(datetime.utcnow().timestamp()) session = db.query(Session).filter(Session.UsersID == user.ID).first() - # Expect that LastUpdateTS was within 5 seconds of the expected_ts, - # which is equal to the current timestamp + persistent_cookie_timeout. - assert session.LastUpdateTS > expected_ts - 5 - assert session.LastUpdateTS < expected_ts + 5 + # Expect that LastUpdateTS is not past the cookie timeout + # for a remembered session. + assert session.LastUpdateTS > (now_ts - cookie_timeout) def test_login_incorrect_password_remember_me(client: TestClient, user: User): From cf978e23aa787a002d66403866140cc4be2617fe Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Dec 2021 00:51:33 -0800 Subject: [PATCH 0802/1451] fix(python): use S argument to decide Suspended Signed-off-by: Kevin Morris --- aurweb/users/update.py | 4 ++-- test/test_accounts_routes.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/aurweb/users/update.py b/aurweb/users/update.py index 60a6184e..fd42a194 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -12,7 +12,7 @@ def simple(U: str = str(), E: str = str(), H: bool = False, BE: str = str(), R: str = str(), HP: str = str(), I: str = str(), K: str = str(), J: bool = False, CN: bool = False, UN: bool = False, ON: bool = False, - user: models.User = None, + S: bool = False, user: models.User = None, **kwargs) -> None: now = int(datetime.utcnow().timestamp()) with db.begin(): @@ -24,7 +24,7 @@ def simple(U: str = str(), E: str = str(), H: bool = False, user.Homepage = HP or user.Homepage user.IRCNick = I or user.IRCNick user.PGPKey = K or user.PGPKey - user.Suspended = strtobool(J) + user.Suspended = strtobool(S) user.InactivityTS = now * int(strtobool(J)) user.CommentNotify = strtobool(CN) user.UpdateNotify = strtobool(UN) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 348a6994..d3435089 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -814,7 +814,6 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): assert resp.status_code == int(HTTPStatus.OK) # Make sure the user record got updated correctly. - assert user.Suspended assert user.InactivityTS > 0 post_data.update({"J": False}) @@ -823,10 +822,37 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) - assert not user.Suspended assert user.InactivityTS == 0 +def test_post_account_edit_suspended(client: TestClient, user: User): + with db.begin(): + user.AccountTypeID = TRUSTED_USER_ID + assert not user.Suspended + + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_data = { + "U": "test", + "E": "test@example.org", + "S": True, + "passwd": "testPassword" + } + endpoint = f"/account/{user.Username}/edit" + with client as request: + resp = request.post(endpoint, data=post_data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # Make sure the user record got updated correctly. + assert user.Suspended + + post_data.update({"S": False}) + with client as request: + resp = request.post(endpoint, data=post_data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + assert not user.Suspended + + def test_post_account_edit_error_unauthorized(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") From 27f8603dc511ad4723c8100829f458b0e5a3c719 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 4 Dec 2021 00:51:59 -0800 Subject: [PATCH 0803/1451] fix(python): fix ordering of fields in partials/account_form.html Signed-off-by: Kevin Morris --- templates/partials/account_form.html | 64 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index f3c293d8..37bb85c4 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -42,6 +42,38 @@ "account is inactive." | tr }}

    + {% if request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) %} +

    + + +

    + +

    + + + +

    + {% endif %} + {% if request.user.is_elevated() %}

    @@ -53,38 +85,6 @@

    {% endif %} - {% if request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) %} -

    - - -

    - -

    - - - -

    - {% endif %} -

    +

    + + +

    +

    +

    + + +

    +

    +

    + + +

    +

    -

    - - -

    -

    +

    +

    -

    - - -

    -

    +

    + {{ + "This action will close any pending package requests " + "related to it. If %sComments%s are omitted, a closure " + "comment will be autogenerated." + | tr | format("", "") | safe + }} +

    +

    {{ "By selecting the checkbox, you confirm that you want to " @@ -38,8 +47,11 @@

    - +

    diff --git a/templates/pkgbase/merge.html b/templates/pkgbase/merge.html index b5129801..981bd649 100644 --- a/templates/pkgbase/merge.html +++ b/templates/pkgbase/merge.html @@ -28,6 +28,15 @@ {% endfor %} +

    + {{ + "This action will close any pending package requests " + "related to it. If %sComments%s are omitted, a closure " + "comment will be autogenerated." + | tr | format("", "") | safe + }} +

    +

    {{ "Once the package has been merged it cannot be reversed. " | tr }} {{ "Enter the package name you wish to merge the package into. " | tr }} @@ -37,6 +46,16 @@

    + +

    + + +

    +

    -

    - - -

    -

    +
    {% set len = comaintainers | length %} {% if comaintainers %} - ({% for co in comaintainers %}{{ co.User }}{% if loop.index < len %}, {% endif %}{% endfor %}) + ({% for co in comaintainers %}{{ co }}{% if loop.index < len %}, {% endif %}{% endfor %}) {% endif %} {% else %} {{ pkgbase.Maintainer.Username | default("None" | tr) }} diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index 03b55063..5edae592 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -534,6 +534,35 @@ def test_pkgbase_comment_undelete_not_found(client: TestClient, assert resp.status_code == int(HTTPStatus.NOT_FOUND) +def test_pkgbase_comment_pin_as_co(client: TestClient, package: Package, + comment: PackageComment): + comaint = create_user("comaint1") + + with db.begin(): + db.create(PackageComaintainer, PackageBase=package.PackageBase, + User=comaint, Priority=1) + + # Pin the comment. + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/pin" + cookies = {"AURSID": comaint.login(Request(), "testPassword")} + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Assert that PinnedTS got set. + assert comment.PinnedTS > 0 + + # Unpin the comment we just pinned. + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/unpin" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Let's assert that PinnedTS was unset. + assert comment.PinnedTS == 0 + + def test_pkgbase_comment_pin(client: TestClient, maintainer: User, package: Package, From 167186895625f57e8fb88713b34d3a989307f8ee Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 13 Feb 2022 17:34:33 -0800 Subject: [PATCH 1019/1451] fix: links to cgit should be url encoded Closes #283 Signed-off-by: Kevin Morris --- aurweb/scripts/rendercomment.py | 8 ++++++-- templates/partials/packages/actions.html | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 2af5384e..87f8b89f 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -2,6 +2,7 @@ import sys +from urllib.parse import quote_plus from xml.etree.ElementTree import Element import bleach @@ -72,13 +73,16 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): def handleMatch(self, m, data): oid = m.group(1) if oid not in self._repo: - # Unkwown OID; preserve the orginal text. + # Unknown OID; preserve the orginal text. return (None, None, None) el = Element('a') commit_uri = aurweb.config.get("options", "commit_uri") prefixlen = util.git_search(self._repo, oid) - el.set('href', commit_uri % (self._head, oid[:prefixlen])) + el.set('href', commit_uri % ( + quote_plus(self._head), + quote_plus(oid[:prefixlen]) + )) el.text = markdown.util.AtomicString(oid[:prefixlen]) return (el, m.start(0), m.end(0)) diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 8d024506..88420222 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -6,21 +6,21 @@

    {{ "Package Actions" | tr }}

    • - + {{ "View PKGBUILD" | tr }} / - + {{ "View Changes" | tr }}
    • - + {{ "Download snapshot" | tr }}
    • - + {{ "Search wiki" | tr }}
    • From 29061c000c2876fd00bdfdf67c0c19d19d2f983d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 14 Feb 2022 15:24:14 -0800 Subject: [PATCH 1020/1451] fix: pkgbase -> package redirection We were redirecting in some error-cases, which this commit sorts out: - package count == 1 and package base name != package name - was redirecting to {name} and not the only associated Package Now, when we have a package base name that mismatches its only package, we display the package base page. Otherwise, we redirect to the first package's page. Closes #282 Signed-off-by: Kevin Morris --- aurweb/routers/pkgbase.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 7825ad7b..845c6372 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -39,14 +39,17 @@ async def pkgbase(request: Request, name: str) -> Response: # Get the PackageBase. pkgbase = get_pkg_or_base(name, PackageBase) - # If this is not a split package, redirect to /packages/{name}. - if pkgbase.packages.count() == 1: - return RedirectResponse(f"/packages/{name}", + # Redirect to /packages if there's only one related Package + # and its name matches its PackageBase. + packages = pkgbase.packages.all() + pkg = packages[0] + if len(packages) == 1 and pkg.Name == pkgbase.Name: + return RedirectResponse(f"/packages/{pkg.Name}", status_code=int(HTTPStatus.SEE_OTHER)) # Add our base information. context = pkgbaseutil.make_context(request, pkgbase) - context["packages"] = pkgbase.packages.all() + context["packages"] = packages return render_template(request, "pkgbase/index.html", context) From 93275949261520ebc81e0b9595a56f000b623900 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 14 Feb 2022 15:42:18 -0800 Subject: [PATCH 1021/1451] upgrade: bump to v6.0.12 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 7bd7abd4..e004daef 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.11" +AURWEB_VERSION = "v6.0.12" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 5afa6d6c..50149b77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.11" +version = "v6.0.12" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 640630fafffb11f409b616f7d571f53470db5e4d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 14 Feb 2022 15:45:59 -0800 Subject: [PATCH 1022/1451] upgrade: bump to v6.0.13 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index e004daef..2a865b9b 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.12" +AURWEB_VERSION = "v6.0.13" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 50149b77..6ba0ff52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.12" +version = "v6.0.13" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 040c9bc3e62a41dde83fa44c53fc36ec12bdcd54 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 17 Feb 2022 15:30:32 -0800 Subject: [PATCH 1023/1451] fix: send up to date flag notifications These were being produced with the db state before the flag was set, which is not what should be done for flag notifications, as the notification contains data about the comment and the current flagger. Closes #292 Signed-off-by: Kevin Morris --- aurweb/routers/pkgbase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 845c6372..4c0b8a67 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -150,13 +150,13 @@ async def pkgbase_flag_post(request: Request, name: str, has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if has_cred and not pkgbase.OutOfDateTS: - notif = notify.FlagNotification(request.user.ID, pkgbase.ID) now = time.utcnow() with db.begin(): pkgbase.OutOfDateTS = now pkgbase.Flagger = request.user pkgbase.FlaggerComment = comments - notif.send() + + notify.FlagNotification(request.user.ID, pkgbase.ID).send() return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) From 361163098f7e074ddca76c7926e69a3fa9b2eae2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 17 Feb 2022 15:48:58 -0800 Subject: [PATCH 1024/1451] fix: /packages search ordering links This was not including other parameters that should be persisted for users. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 4 ++-- templates/partials/packages/search_results.html | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 8f6cb7d6..d4e85d20 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -15,7 +15,7 @@ from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base from aurweb.pkgbase import actions as pkgbase_actions from aurweb.pkgbase import util as pkgbaseutil -from aurweb.templates import make_context, render_template +from aurweb.templates import make_context, make_variable_context, render_template logger = logging.get_logger(__name__) router = APIRouter() @@ -125,7 +125,7 @@ async def packages_get(request: Request, context: Dict[str, Any], @router.get("/packages") async def packages(request: Request) -> Response: - context = make_context(request, "Packages") + context = await make_variable_context(request, "Packages") return await packages_get(request, context) diff --git a/templates/partials/packages/search_results.html b/templates/partials/packages/search_results.html index 680891c4..c3b4427c 100644 --- a/templates/partials/packages/search_results.html +++ b/templates/partials/packages/search_results.html @@ -9,7 +9,7 @@ {% if SB == "n" %} {% set order = "d" if order == "a" else "a" %} {% endif %} - + {{ "Name" | tr }} @@ -19,7 +19,7 @@ {% if SB == "v" %} {% set order = "d" if order == "a" else "a" %} {% endif %} - + {{ "Votes" | tr }} @@ -28,7 +28,7 @@ {% if SB == "p" %} {% set order = "d" if order == "a" else "a" %} {% endif %} - {{ "Popularity" | tr }}? + {{ "Popularity" | tr }}? {% if request.user.is_authenticated() %} @@ -36,7 +36,7 @@ {% if SB == "w" %} {% set order = "d" if order == "a" else "a" %} {% endif %} - + {{ "Voted" | tr }} @@ -45,7 +45,7 @@ {% if SB == "o" %} {% set order = "d" if order == "a" else "a" %} {% endif %} - + {{ "Notify" | tr }} @@ -56,7 +56,7 @@ {% if SB == "m" %} {% set order = "d" if order == "a" else "a" %} {% endif %} - + {{ "Maintainer" | tr }} From e3864d4b7ca21d17f3faea196509e0a5b3f23f82 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 17 Feb 2022 15:54:04 -0800 Subject: [PATCH 1025/1451] fix: set RequestTS when autogenerating requests Signed-off-by: Kevin Morris --- aurweb/packages/requests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aurweb/packages/requests.py b/aurweb/packages/requests.py index 724249d4..6aaa59ab 100644 --- a/aurweb/packages/requests.py +++ b/aurweb/packages/requests.py @@ -201,9 +201,11 @@ def handle_request(request: Request, reqtype_id: int, # This is done to increase tracking of actions occurring # through the website. if not to_accept: + utcnow = time.utcnow() with db.begin(): pkgreq = db.create(PackageRequest, ReqTypeID=reqtype_id, + RequestTS=utcnow, User=request.user, PackageBase=pkgbase, PackageBaseName=pkgbase.Name, From 0bfecb984482720db1b72d47799dc86812b8bfb0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 17 Feb 2022 16:14:31 -0800 Subject: [PATCH 1026/1451] upgrade: bump to v6.0.14 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 2a865b9b..eea156f5 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.13" +AURWEB_VERSION = "v6.0.14" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 6ba0ff52..50691228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.13" +version = "v6.0.14" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From bfd592299c08befaf24733fdbf83b156565fc958 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 17 Feb 2022 17:23:05 -0800 Subject: [PATCH 1027/1451] change: display default package search parameter values in its form The previous behavior was carried over from PHP. It has been requested that we use the true defaults when rendering the default form, making search a bit more sensible. Closes #269 Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 9 +---- templates/partials/packages/search.html | 6 +++ .../partials/packages/search_results.html | 40 +++++++++++-------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index d4e85d20..34c09d86 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -40,7 +40,7 @@ async def packages_get(request: Request, context: Dict[str, Any], search_by = context["SeB"] = request.query_params.get("SeB", "nd") # Query sort by. - sort_by = context["SB"] = request.query_params.get("SB", "p") + sort_by = request.query_params.get("SB", None) # Query sort order. sort_order = request.query_params.get("SO", None) @@ -93,13 +93,6 @@ async def packages_get(request: Request, context: Dict[str, Any], # Apply user-specified specified sort column and ordering. search.sort_by(sort_by, sort_order) - # If no SO was given, default the context SO to 'a' (Ascending). - # By default, if no SO is given, the search should sort by 'd' - # (Descending), but display "Ascending" for the Sort order select. - if sort_order is None: - sort_order = "a" - context["SO"] = sort_order - # Insert search results into the context. results = search.results().with_entities( models.Package.ID, diff --git a/templates/partials/packages/search.html b/templates/partials/packages/search.html index 33bc6132..3db5c7a4 100644 --- a/templates/partials/packages/search.html +++ b/templates/partials/packages/search.html @@ -34,6 +34,9 @@
      + {% if not SB %} + {% set SB = 'p' %} + {% endif %} diff --git a/templates/partials/packages/search_results.html b/templates/partials/packages/search_results.html index c3b4427c..84c39079 100644 --- a/templates/partials/packages/search_results.html +++ b/templates/partials/packages/search_results.html @@ -1,3 +1,5 @@ +{% set reverse_order = "d" if SO == "a" else "a" %} + @@ -5,9 +7,10 @@ {% endif %} {% if request.user.is_authenticated() %} From dcaf407536993cce9ae37482d3fbb9c699477a4b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 17 Feb 2022 17:42:24 -0800 Subject: [PATCH 1028/1451] fix: /packages search result count We need to query for this after we've applied all filters. Signed-off-by: Kevin Morris --- aurweb/routers/packages.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 34c09d86..bc12455d 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -61,11 +61,6 @@ async def packages_get(request: Request, context: Dict[str, Any], for keyword in keywords: search.search_by(search_by, keyword) - # Collect search result count here; we've applied our keywords. - # Including more query operations below, like ordering, will - # increase the amount of time required to collect a count. - num_packages = search.count() - flagged = request.query_params.get("outdated", None) if flagged: # If outdated was given, set it up in the context. @@ -90,7 +85,12 @@ async def packages_get(request: Request, context: Dict[str, Any], search.query = search.query.filter( models.PackageBase.MaintainerUID.is_(None)) - # Apply user-specified specified sort column and ordering. + # Collect search result count here; we've applied our keywords. + # Including more query operations below, like ordering, will + # increase the amount of time required to collect a count. + num_packages = search.count() + + # Apply user-specified sort column and ordering. search.sort_by(sort_by, sort_order) # Insert search results into the context. From b2508e5bf860320d3e2644c0b87bae13acdd9ca9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 17 Feb 2022 18:27:00 -0800 Subject: [PATCH 1029/1451] upgrade: bump to v6.0.15 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index eea156f5..6284f794 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.14" +AURWEB_VERSION = "v6.0.15" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 50691228..cc6e9d2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.14" +version = "v6.0.15" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 4e641d945c3f63dac69a2fff3a0c70b8afa8e60d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Feb 2022 13:53:45 -0800 Subject: [PATCH 1030/1451] fix: unset InactivityTS for users on login Signed-off-by: Kevin Morris --- aurweb/models/user.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 871ff209..c375fcbc 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -135,6 +135,10 @@ class User(Base): if last_updated and last_updated < now_ts: self.session.SessionID = generate_unique_sid() self.session.LastUpdateTS = now_ts + + # Unset InactivityTS, we've logged in! + self.InactivityTS = 0 + break except IntegrityError as exc_: exc = exc_ From 1d86b3e210e1f7eb8ba1cb4bc29c9fc9b5681fd5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Feb 2022 15:42:35 -0800 Subject: [PATCH 1031/1451] fix: use a transaction for package query; remove refresh Closes #284 Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 0a259e1e..e8569f29 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -138,20 +138,20 @@ def updated_packages(limit: int = 0, # If we already have a cache, deserialize it and return. return orjson.loads(packages) - query = db.query(models.Package).join(models.PackageBase).filter( - models.PackageBase.PackagerUID.isnot(None) - ).order_by( - models.PackageBase.ModifiedTS.desc() - ) + with db.begin(): + query = db.query(models.Package).join(models.PackageBase).filter( + models.PackageBase.PackagerUID.isnot(None) + ).order_by( + models.PackageBase.ModifiedTS.desc() + ) - if limit: - query = query.limit(limit) + if limit: + query = query.limit(limit) packages = [] for pkg in query: # For each Package returned by the query, append a dict # containing Package columns we're interested in. - db.refresh(pkg) packages.append({ "Name": pkg.Name, "Version": pkg.Version, From 8387f325f69c4505cbd94c484577f047f06594e8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Feb 2022 13:02:28 -0800 Subject: [PATCH 1032/1451] fix: resolve null VoteTS columns via migration Somehow, many aur.al records of PackageVotes do not have a valid VoteTS value. This migration fixes that issue by setting all null VoteTS columns to the epoch timestamp. Signed-off-by: Kevin Morris --- aurweb/schema.py | 2 +- .../d64e5571bc8d_fix_pkgvote_votets.py | 37 +++++++++++++++++++ schema/gendummydata.py | 27 ++++++++------ 3 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py diff --git a/aurweb/schema.py b/aurweb/schema.py index 39550ff6..d2644541 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -269,7 +269,7 @@ PackageVotes = Table( 'PackageVotes', metadata, Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('VoteTS', BIGINT(unsigned=True)), + Column('VoteTS', BIGINT(unsigned=True), nullable=False), Index('VoteUsersIDPackageID', 'UsersID', 'PackageBaseID', unique=True), Index('VotesPackageBaseID', 'PackageBaseID'), Index('VotesUsersID', 'UsersID'), diff --git a/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py b/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py new file mode 100644 index 00000000..a89d97ef --- /dev/null +++ b/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py @@ -0,0 +1,37 @@ +"""fix pkgvote votets + +Revision ID: d64e5571bc8d +Revises: be7adae47ac3 +Create Date: 2022-02-18 12:47:05.322766 + +""" +from datetime import datetime + +import sqlalchemy as sa + +from alembic import op + +from aurweb import db +from aurweb.models import PackageVote + +# revision identifiers, used by Alembic. +revision = 'd64e5571bc8d' +down_revision = 'be7adae47ac3' +branch_labels = None +depends_on = None + +table = PackageVote.__tablename__ +column = 'VoteTS' +epoch = datetime(1970, 1, 1) + + +def upgrade(): + with db.begin(): + records = db.query(PackageVote).filter(PackageVote.VoteTS.is_(None)) + for record in records: + record.VoteTS = epoch.timestamp() + op.alter_column(table, column, existing_type=sa.BIGINT(), nullable=False) + + +def downgrade(): + op.alter_column(table, column, existing_type=sa.BIGINT(), nullable=True) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 275b3601..aedfda7e 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -16,6 +16,8 @@ import random import sys import time +from datetime import datetime + import bcrypt LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output @@ -203,7 +205,7 @@ for u in user_keys: log.debug("Number of developers: %d" % len(developers)) log.debug("Number of trusted users: %d" % len(trustedusers)) -log.debug("Number of users: %d" % (MAX_USERS-len(developers)-len(trustedusers))) +log.debug("Number of users: %d" % (MAX_USERS - len(developers) - len(trustedusers))) log.debug("Number of packages: %d" % MAX_PKGS) log.debug("Gathering text from fortune file...") @@ -244,26 +246,27 @@ for p in list(seen_pkgs.keys()): # num_comments = random.randrange(PKG_CMNTS[0], PKG_CMNTS[1]) for i in range(0, num_comments): - now = NOW + random.randrange(400, 86400*3) + now = NOW + random.randrange(400, 86400 * 3) s = ("INSERT INTO PackageComments (PackageBaseID, UsersID," " Comments, RenderedComment, CommentTS) VALUES (%d, %d, '%s', '', %d);\n") s = s % (seen_pkgs[p], genUID(), genFortune(), now) out.write(s) # Cast votes -# +utcnow = int(datetime.utcnow().timestamp()) + track_votes = {} log.debug("Casting votes for packages.") for u in user_keys: - num_votes = random.randrange(int(len(seen_pkgs)*VOTING[0]), - int(len(seen_pkgs)*VOTING[1])) + num_votes = random.randrange(int(len(seen_pkgs) * VOTING[0]), + int(len(seen_pkgs) * VOTING[1])) pkgvote = {} for v in range(num_votes): pkg = random.randrange(1, len(seen_pkgs) + 1) if pkg not in pkgvote: - s = ("INSERT INTO PackageVotes (UsersID, PackageBaseID)" - " VALUES (%d, %d);\n") - s = s % (seen_users[u], pkg) + s = ("INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS)" + " VALUES (%d, %d, %d);\n") + s = s % (seen_users[u], pkg, utcnow) pkgvote[pkg] = 1 if pkg not in track_votes: track_votes[pkg] = 0 @@ -318,14 +321,14 @@ for p in seen_pkgs_keys: # log.debug("Creating SQL statements for trusted user proposals.") count = 0 -for t in range(0, OPEN_PROPOSALS+CLOSE_PROPOSALS): +for t in range(0, OPEN_PROPOSALS + CLOSE_PROPOSALS): now = int(time.time()) if count < CLOSE_PROPOSALS: - start = now - random.randrange(3600*24*7, 3600*24*21) - end = now - random.randrange(0, 3600*24*7) + start = now - random.randrange(3600 * 24 * 7, 3600 * 24 * 21) + end = now - random.randrange(0, 3600 * 24 * 7) else: start = now - end = now + random.randrange(3600*24, 3600*24*7) + end = now + random.randrange(3600 * 24, 3600 * 24 * 7) if count % 5 == 0: # Don't make the vote about anyone once in a while user = "" else: From 14347232fdf6c834270df23e923ca0940d7c7ba3 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Feb 2022 16:41:43 -0800 Subject: [PATCH 1033/1451] fix: treat all keywords as lowercase when updating In addition, treat package search by keywords as lowercase. Closes #296, #297, #298, #301 Signed-off-by: Kevin Morris --- aurweb/packages/search.py | 1 + aurweb/routers/pkgbase.py | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index b0e1e891..4a6eb75f 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -124,6 +124,7 @@ class PackageSearch: def _search_by_keywords(self, keywords: Set[str]) -> orm.Query: self._join_user() self._join_keywords() + keywords = set(k.lower() for k in keywords) self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords)) return self diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 4c0b8a67..2cef5436 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -95,24 +95,28 @@ async def pkgbase_flag_comment(request: Request, name: str): async def pkgbase_keywords(request: Request, name: str, keywords: str = Form(default=str())): pkgbase = get_pkg_or_base(name, PackageBase) - keywords = set(keywords.split(" ")) + + # Lowercase all keywords. Our database table is case insensitive, + # and providing CI duplicates of keywords is erroneous. + keywords = set(k.lower() for k in keywords.split(" ")) # Delete all keywords which are not supplied by the user. - other_keywords = pkgbase.keywords.filter( - ~PackageKeyword.Keyword.in_(keywords)) - other_keyword_strings = [kwd.Keyword for kwd in other_keywords] - - existing_keywords = set( - kwd.Keyword for kwd in - pkgbase.keywords.filter( - ~PackageKeyword.Keyword.in_(other_keyword_strings)) - ) with db.begin(): + other_keywords = pkgbase.keywords.filter( + ~PackageKeyword.Keyword.in_(keywords)) + other_keyword_strings = set( + kwd.Keyword.lower() for kwd in other_keywords) + + existing_keywords = set( + kwd.Keyword.lower() for kwd in + pkgbase.keywords.filter( + ~PackageKeyword.Keyword.in_(other_keyword_strings)) + ) + db.delete_all(other_keywords) - for keyword in keywords.difference(existing_keywords): - db.create(PackageKeyword, - PackageBase=pkgbase, - Keyword=keyword) + new_keywords = keywords.difference(existing_keywords) + for keyword in new_keywords: + db.create(PackageKeyword, PackageBase=pkgbase, Keyword=keyword) return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) From e43e1c6d2007de41d1dbc190bb859cb8c5a4c256 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Feb 2022 17:17:14 -0800 Subject: [PATCH 1034/1451] upgrade: bump to v6.0.16 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 6284f794..ba6c0aea 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.15" +AURWEB_VERSION = "v6.0.16" _parser = None diff --git a/pyproject.toml b/pyproject.toml index cc6e9d2e..955e004d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.15" +version = "v6.0.16" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 7cc20cd9a46d149d9b766642d0bd09f0e3c3a633 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Feb 2022 17:44:06 -0800 Subject: [PATCH 1035/1451] fix: suspended users should not be able to login Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 12 +++++++++--- test/test_auth_routes.py | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index fc5209ce..9f465388 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -46,13 +46,19 @@ async def login_post(request: Request, raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=_("Bad Referer header.")) - user = db.query(User).filter( - or_(User.Username == user, User.Email == user) - ).first() + with db.begin(): + user = db.query(User).filter( + or_(User.Username == user, User.Email == user) + ).first() + if not user: return await login_template(request, next, errors=["Bad username or password."]) + if user.Suspended: + return await login_template(request, next, + errors=["Account Suspended"]) + cookie_timeout = cookies.timeout(remember_me) sid = user.login(request, passwd, cookie_timeout) if not sid: diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 79b34b6b..8467adea 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -14,6 +14,7 @@ from aurweb.asgi import app from aurweb.models.account_type import USER_ID from aurweb.models.session import Session from aurweb.models.user import User +from aurweb.testing.html import get_errors # Some test global constants. TEST_USERNAME = "test" @@ -79,6 +80,21 @@ def test_login_logout(client: TestClient, user: User): assert "AURSID" not in response.cookies +def test_login_suspended(client: TestClient, user: User): + with db.begin(): + user.Suspended = 1 + + data = { + "user": user.Username, + "passwd": "testPassword", + "next": "/" + } + with client as request: + resp = request.post("/login", data=data) + errors = get_errors(resp.text) + assert errors[0].text.strip() == "Account Suspended" + + def test_login_email(client: TestClient, user: user): post_data = { "user": user.Email, From 388e64d0aff3a85108f9e0b0bffd4cababbbee6b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 18 Feb 2022 17:54:36 -0800 Subject: [PATCH 1036/1451] upgrade: bump to v6.0.17 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index ba6c0aea..61b60402 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.16" +AURWEB_VERSION = "v6.0.17" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 955e004d..89b149a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.16" +version = "v6.0.17" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From c83c5cdc4227b025ba74b3c5db4d06ea06b33668 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Feb 2022 11:28:16 -0800 Subject: [PATCH 1037/1451] change: log out details about PROMETHEUS_MULTIPROC_DIR Additionally, respond with a 503 if the var is not set when /metrics is requested. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 4 ++++ aurweb/routers/html.py | 10 +++++++--- test/test_asgi.py | 11 +++++++++++ test/test_html.py | 15 +++++++++++++-- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index ad0b7ca0..fa2526ed 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -74,6 +74,10 @@ async def app_startup(): if not session_secret: raise Exception("[fastapi] session_secret must not be empty") + if not os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): + logger.warning("$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " + "endpoint is disabled.") + app.mount("/static/css", StaticFiles(directory="web/html/css"), name="static_css") diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index b9d291d2..d31a32c7 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -13,7 +13,7 @@ from sqlalchemy import and_, case, or_ import aurweb.config import aurweb.models.package_request -from aurweb import cookies, db, models, time, util +from aurweb import cookies, db, logging, models, time, util from aurweb.cache import db_count_cache from aurweb.exceptions import handle_form_exceptions from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID @@ -21,6 +21,7 @@ from aurweb.models.package_request import PENDING_ID from aurweb.packages.util import query_notified, query_voted, updated_packages from aurweb.templates import make_context, render_template +logger = logging.get_logger(__name__) router = APIRouter() @@ -230,9 +231,12 @@ async def archive_sha256(request: Request, archive: str): @router.get("/metrics") async def metrics(request: Request): + if not os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): + return Response("Prometheus metrics are not enabled.", + status_code=HTTPStatus.SERVICE_UNAVAILABLE) + registry = CollectorRegistry() - if os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): # pragma: no cover - multiprocess.MultiProcessCollector(registry) + multiprocess.MultiProcessCollector(registry) data = generate_latest(registry) headers = { "Content-Type": CONTENT_TYPE_LATEST, diff --git a/test/test_asgi.py b/test/test_asgi.py index 667ae871..c693a3a9 100644 --- a/test/test_asgi.py +++ b/test/test_asgi.py @@ -104,6 +104,17 @@ async def test_asgi_app_unsupported_backends(): await aurweb.asgi.app_startup() +@pytest.mark.asyncio +async def test_asgi_app_disabled_metrics(caplog: pytest.LogCaptureFixture): + env = {"PROMETHEUS_MULTIPROC_DIR": str()} + with mock.patch.dict(os.environ, env): + await aurweb.asgi.app_startup() + + expected = ("$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " + "endpoint is disabled.") + assert expected in caplog.text + + @pytest.fixture def use_traceback(): config_getboolean = aurweb.config.getboolean diff --git a/test/test_html.py b/test/test_html.py index b97d3571..ffe2a9f2 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -160,12 +160,23 @@ def test_archive_sig_404(client: TestClient): def test_metrics(client: TestClient): - with client as request: - resp = request.get("/metrics") + with tempfile.TemporaryDirectory() as tmpdir: + env = {"PROMETHEUS_MULTIPROC_DIR": tmpdir} + with mock.patch.dict(os.environ, env): + with client as request: + resp = request.get("/metrics") assert resp.status_code == int(HTTPStatus.OK) assert resp.headers.get("Content-Type").startswith("text/plain") +def test_disabled_metrics(client: TestClient): + env = {"PROMETHEUS_MULTIPROC_DIR": str()} + with mock.patch.dict(os.environ, env): + with client as request: + resp = request.get("/metrics") + assert resp.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) + + def test_rtl(client: TestClient): responses = {} expected = [ From 4a4fd015635c9a392f224eba51ed588c758fa441 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Feb 2022 16:01:06 -0800 Subject: [PATCH 1038/1451] fix: blanking out particular fields when editing accounts Signed-off-by: Kevin Morris --- aurweb/users/update.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aurweb/users/update.py b/aurweb/users/update.py index 8e42765e..5a32fd01 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -19,11 +19,11 @@ def simple(U: str = str(), E: str = str(), H: bool = False, user.Username = U or user.Username user.Email = E or user.Email user.HideEmail = strtobool(H) - user.BackupEmail = BE or user.BackupEmail - user.RealName = R or user.RealName - user.Homepage = HP or user.Homepage - user.IRCNick = I or user.IRCNick - user.PGPKey = K or user.PGPKey + user.BackupEmail = user.BackupEmail if BE is None else BE + user.RealName = user.RealName if R is None else R + user.Homepage = user.Homepage if HP is None else HP + user.IRCNick = user.IRCNick if I is None else I + user.PGPKey = user.PGPKey if K is None else K user.Suspended = strtobool(S) user.InactivityTS = now * int(strtobool(J)) user.CommentNotify = strtobool(CN) From 80622cc96611d36ee8b63eb3c95d952e290668b6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Feb 2022 16:12:15 -0800 Subject: [PATCH 1039/1451] fix: suspend check should check Suspended... This was causing some false negative errors in the update process, and it clearly not correct -- oops :( Signed-off-by: Kevin Morris --- aurweb/users/validate.py | 5 +++-- test/test_accounts_routes.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/aurweb/users/validate.py b/aurweb/users/validate.py index 26f6eec6..de51e3ff 100644 --- a/aurweb/users/validate.py +++ b/aurweb/users/validate.py @@ -15,6 +15,7 @@ from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_to from aurweb.exceptions import ValidationError from aurweb.models.account_type import ACCOUNT_TYPE_NAME from aurweb.models.ssh_pub_key import get_fingerprint +from aurweb.util import strtobool logger = logging.get_logger(__name__) @@ -26,9 +27,9 @@ def invalid_fields(E: str = str(), U: str = str(), **kwargs) -> None: def invalid_suspend_permission(request: Request = None, user: models.User = None, - J: bool = False, + S: str = "False", **kwargs) -> None: - if not request.user.is_elevated() and J != bool(user.InactivityTS): + if not request.user.is_elevated() and strtobool(S) != bool(user.Suspended): raise ValidationError([ "You do not have permission to suspend accounts."]) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index e532e341..37b3d130 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -916,13 +916,13 @@ def test_post_account_edit_error_invalid_password(client: TestClient, assert "Invalid password." in content -def test_post_account_edit_inactivity_unauthorized(client: TestClient, - user: User): +def test_post_account_edit_suspend_unauthorized(client: TestClient, + user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} post_data = { "U": "test", "E": "test@example.org", - "J": True, + "S": True, "passwd": "testPassword" } with client as request: From 1e31db47ab20d86a0b0c943299113c3daaf6089c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 19 Feb 2022 16:32:49 -0800 Subject: [PATCH 1040/1451] upgrade: bump to v6.0.18 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 61b60402..b7aa3027 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.17" +AURWEB_VERSION = "v6.0.18" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 89b149a8..0b21a643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.17" +version = "v6.0.18" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 6e837e0c023404e0a1c43dbdfde0826acb1f3381 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 21 Feb 2022 10:25:01 +0000 Subject: [PATCH 1041/1451] fix: always provide a path https://github.com/stephenhillier/starlette_exporter/commit/891efcd142da5a13f72ec9647ad0b8aca21075a8 --- aurweb/prometheus.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index dae56320..73be3ef6 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -70,9 +70,17 @@ def http_requests_total() -> Callable[[Info], None]: if not (scope.get("endpoint", None) and scope.get("router", None)): return None + root_path = scope.get("root_path", "") + app = scope.get("app", {}) + + if hasattr(app, "root_path"): + app_root_path = getattr(app, "root_path") + if root_path.startswith(app_root_path): + root_path = root_path[len(app_root_path):] + base_scope = { "type": scope.get("type"), - "path": scope.get("root_path", "") + scope.get("path"), + "path": root_path + scope.get("path"), "path_params": scope.get("path_params", {}), "method": scope.get("method") } From 9f452a62e58ee6212e64e476c69f19dbdbfffb48 Mon Sep 17 00:00:00 2001 From: Colin Woodbury Date: Mon, 21 Feb 2022 11:56:57 -0800 Subject: [PATCH 1042/1451] docs: fix link formatting in CONTRIBUTING --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1b0da60..2deaf237 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Before sending patches, you are recommended to run `flake8` and `isort`. You can add a git hook to do this by installing `python-pre-commit` and running `pre-commit install`. -[1] https://lists.archlinux.org/listinfo/aur-dev +[1]: https://lists.archlinux.org/listinfo/aur-dev ### Coding Guidelines From 7c3637971571c1b5757d634aa627a91ff96999da Mon Sep 17 00:00:00 2001 From: Colin Woodbury Date: Mon, 21 Feb 2022 14:18:26 -0800 Subject: [PATCH 1043/1451] docs(docker): basic usage instructions --- docker/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docker/README.md diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..dceee74f --- /dev/null +++ b/docker/README.md @@ -0,0 +1,39 @@ +# Aurweb and Docker + +The `INSTALL` document details a manual Aurweb setup, but Docker images are also +provided here to avoid the complications of database configuration (and so +forth). + +### Setup + +Naturally, both `docker` and `docker-compose` must be installed, and your Docker +service must be started: + +```sh +systemctl start docker.service +``` + +The main image - `aurweb` - must be built manually: + +```sh +docker compose build aurweb-image +``` + +### Starting and Stopping the Services + +With the above steps complete, you can bring up an initial cluster: + +```sh +docker compose up +``` + +Subsequent runs will be done with `start` instead of `up`. The cluster can be +stopped with `docker compose stop`. + +### Testing + +With a running cluster, execute the following in a new terminal: + +```sh +docker compose run test +``` From 27f30212e83609fafe2081f3366d376f5ecf69df Mon Sep 17 00:00:00 2001 From: Colin Woodbury Date: Mon, 21 Feb 2022 14:40:18 -0800 Subject: [PATCH 1044/1451] docs(docker): note ports and `curl` usage --- docker/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docker/README.md b/docker/README.md index dceee74f..6fa2f142 100644 --- a/docker/README.md +++ b/docker/README.md @@ -37,3 +37,14 @@ With a running cluster, execute the following in a new terminal: ```sh docker compose run test ``` + +### Querying the RPC + +The Fast (Python) API runs on Port 8444, while the legacy PHP version runs +on 8443. You can query one like so: + +```sh +curl -k "https://localhost:8444/rpc/?v=5&type=search&arg=python" +``` + +`-k` bypasses local certificate issues that `curl` will otherwise complain about. From 3aa8d523f5ee6beb13a9e981e2f8754f79c54578 Mon Sep 17 00:00:00 2001 From: Colin Woodbury Date: Mon, 21 Feb 2022 16:49:38 -0800 Subject: [PATCH 1045/1451] change(rpc): `search` module reformatting --- aurweb/packages/search.py | 93 +++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 4a6eb75f..5ba72652 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -12,7 +12,7 @@ from aurweb.models.package_vote import PackageVote class PackageSearch: - """ A Package search query builder. """ + """A Package search query builder.""" # A constant mapping of short to full name sort orderings. FULL_SORT_ORDER = {"d": "desc", "a": "asc"} @@ -24,14 +24,18 @@ class PackageSearch: if self.user: self.query = self.query.join( PackageVote, - and_(PackageVote.PackageBaseID == PackageBase.ID, - PackageVote.UsersID == self.user.ID), - isouter=True + and_( + PackageVote.PackageBaseID == PackageBase.ID, + PackageVote.UsersID == self.user.ID, + ), + isouter=True, ).join( PackageNotification, - and_(PackageNotification.PackageBaseID == PackageBase.ID, - PackageNotification.UserID == self.user.ID), - isouter=True + and_( + PackageNotification.PackageBaseID == PackageBase.ID, + PackageNotification.UserID == self.user.ID, + ), + isouter=True, ) self.ordering = "d" @@ -47,7 +51,7 @@ class PackageSearch: "m": self._search_by_maintainer, "c": self._search_by_comaintainer, "M": self._search_by_co_or_maintainer, - "s": self._search_by_submitter + "s": self._search_by_submitter, } # Setup SB (Sort By) callbacks. @@ -58,7 +62,7 @@ class PackageSearch: "w": self._sort_by_voted, "o": self._sort_by_notify, "m": self._sort_by_maintainer, - "l": self._sort_by_last_modified + "l": self._sort_by_last_modified, } self._joined_user = False @@ -66,12 +70,10 @@ class PackageSearch: self._joined_comaint = False def _join_user(self, outer: bool = True) -> orm.Query: - """ Centralized joining of a package base's maintainer. """ + """Centralized joining of a package base's maintainer.""" if not self._joined_user: self.query = self.query.join( - User, - User.ID == PackageBase.MaintainerUID, - isouter=outer + User, User.ID == PackageBase.MaintainerUID, isouter=outer ) self._joined_user = True return self.query @@ -87,7 +89,7 @@ class PackageSearch: self.query = self.query.join( PackageComaintainer, PackageComaintainer.PackageBaseID == PackageBase.ID, - isouter=isouter + isouter=isouter, ) self._joined_comaint = True return self.query @@ -95,8 +97,10 @@ class PackageSearch: def _search_by_namedesc(self, keywords: str) -> orm.Query: self._join_user() self.query = self.query.filter( - or_(Package.Name.like(f"%{keywords}%"), - Package.Description.like(f"%{keywords}%")) + or_( + Package.Name.like(f"%{keywords}%"), + Package.Description.like(f"%{keywords}%"), + ) ) return self @@ -132,8 +136,7 @@ class PackageSearch: self._join_user() if keywords: self.query = self.query.filter( - and_(User.Username == keywords, - User.ID == PackageBase.MaintainerUID) + and_(User.Username == keywords, User.ID == PackageBase.MaintainerUID) ) else: self.query = self.query.filter(PackageBase.MaintainerUID.is_(None)) @@ -197,8 +200,7 @@ class PackageSearch: # in terms of performance. We should improve this; there's no # reason it should take _longer_. column = getattr( - case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), - order + case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), order ) name = getattr(models.Package.Name, order) self.query = self.query.order_by(column(), name()) @@ -209,9 +211,8 @@ class PackageSearch: # in terms of performance. We should improve this; there's no # reason it should take _longer_. column = getattr( - case([(models.PackageNotification.UserID == self.user.ID, 1)], - else_=0), - order + case([(models.PackageNotification.UserID == self.user.ID, 1)], else_=0), + order, ) name = getattr(models.Package.Name, order) self.query = self.query.order_by(column(), name()) @@ -239,16 +240,16 @@ class PackageSearch: return callback(ordering) def count(self) -> int: - """ Return internal query's count. """ + """Return internal query's count.""" return self.query.count() def results(self) -> orm.Query: - """ Return internal query. """ + """Return internal query.""" return self.query class RPCSearch(PackageSearch): - """ A PackageSearch-derived RPC package search query builder. + """A PackageSearch-derived RPC package search query builder. With RPC search, we need a subset of PackageSearch's handlers, with a few additional handlers added. So, within the RPCSearch @@ -270,52 +271,60 @@ class RPCSearch(PackageSearch): # We keep: "nd", "n" and "m". We also overlay four new by params # on top: "depends", "makedepends", "optdepends" and "checkdepends". self.search_by_cb = { - k: v for k, v in self.search_by_cb.items() + k: v + for k, v in self.search_by_cb.items() if k not in RPCSearch.keys_removed } - self.search_by_cb.update({ - "depends": self._search_by_depends, - "makedepends": self._search_by_makedepends, - "optdepends": self._search_by_optdepends, - "checkdepends": self._search_by_checkdepends - }) + self.search_by_cb.update( + { + "depends": self._search_by_depends, + "makedepends": self._search_by_makedepends, + "optdepends": self._search_by_optdepends, + "checkdepends": self._search_by_checkdepends, + } + ) # We always want an optional Maintainer in the RPC. self._join_user() def _join_depends(self, dep_type_id: int) -> orm.Query: - """ Join Package with PackageDependency and filter results + """Join Package with PackageDependency and filter results based on `dep_type_id`. :param dep_type_id: DependencyType ID :returns: PackageDependency-joined orm.Query """ self.query = self.query.join(models.PackageDependency).filter( - models.PackageDependency.DepTypeID == dep_type_id) + models.PackageDependency.DepTypeID == dep_type_id + ) return self.query def _search_by_depends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(DEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) return self def _search_by_makedepends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(MAKEDEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) return self def _search_by_optdepends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(OPTDEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) return self def _search_by_checkdepends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(CHECKDEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) return self def search_by(self, by: str, keywords: str) -> "RPCSearch": - """ Override inherited search_by. In this override, we reduce the + """Override inherited search_by. In this override, we reduce the scope of what we handle within this function. We do not set `by` to a default of "nd" in the RPC, as the RPC returns an error when incorrect `by` fields are specified. @@ -329,6 +338,4 @@ class RPCSearch(PackageSearch): return result def results(self) -> orm.Query: - return self.query.filter( - models.PackageBase.PackagerUID.isnot(None) - ) + return self.query.filter(models.PackageBase.PackagerUID.isnot(None)) From 51d4b7f9935aa3bef34c151a417898985caf69f7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 23 Feb 2022 14:06:07 -0800 Subject: [PATCH 1046/1451] fix(rpc): limit Package results, not relationships ...This was an obvious bug in hindsight. Apologies :( Closes #314 Signed-off-by: Kevin Morris --- aurweb/rpc.py | 14 +++++++------- test/test_rpc.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 6e2a27fe..70d8c2fd 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -202,7 +202,12 @@ class RPC: models.User.ID == models.PackageBase.MaintainerUID, isouter=True ).filter(models.Package.Name.in_(args)) - packages = self._entities(packages) + + max_results = config.getint("options", "max_rpc_results") + packages = self._entities(packages).limit(max_results + 1) + + if packages.count() > max_results: + raise RPCError("Too many package results.") ids = {pkg.ID for pkg in packages} @@ -274,12 +279,7 @@ class RPC: ] # Union all subqueries together. - max_results = config.getint("options", "max_rpc_results") - query = subqueries[0].union_all(*subqueries[1:]).limit( - max_results + 1).all() - - if len(query) > max_results: - raise RPCError("Too many package results.") + query = subqueries[0].union_all(*subqueries[1:]).all() # Store our extra information in a class-wise dictionary, # which contains package id -> extra info dict mappings. diff --git a/test/test_rpc.py b/test/test_rpc.py index a67a026e..0d6b2931 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -15,6 +15,7 @@ import aurweb.models.relation_type as rt from aurweb import asgi, config, db, rpc, scripts, time from aurweb.models.account_type import USER_ID +from aurweb.models.dependency_type import DEPENDS_ID from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -23,6 +24,7 @@ from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_license import PackageLicense from aurweb.models.package_relation import PackageRelation from aurweb.models.package_vote import PackageVote +from aurweb.models.relation_type import PROVIDES_ID from aurweb.models.user import User from aurweb.redis import redis_connection @@ -814,6 +816,16 @@ def test_rpc_too_many_search_results(client: TestClient, def test_rpc_too_many_info_results(client: TestClient, packages: List[Package]): + # Make many of these packages depend and rely on each other. + # This way, we can test to see that the exceeded limit stays true + # regardless of the number of related records. + with db.begin(): + for i in range(len(packages) - 1): + db.create(PackageDependency, DepTypeID=DEPENDS_ID, + Package=packages[i], DepName=packages[i + 1].Name) + db.create(PackageRelation, RelTypeID=PROVIDES_ID, + Package=packages[i], RelName=packages[i + 1].Name) + config_getint = config.getint def mock_config(section: str, key: str): From 07e479ab503b6e2cb2e363ccf731c3ea60281451 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 23 Feb 2022 14:37:41 -0800 Subject: [PATCH 1047/1451] upgrade: bump to v6.0.19 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index b7aa3027..c2b8be79 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.18" +AURWEB_VERSION = "v6.0.19" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 0b21a643..b5da9913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.18" +version = "v6.0.19" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From d92f1838404b426c6e3baad103f4dccac1a40e03 Mon Sep 17 00:00:00 2001 From: Colin Woodbury Date: Wed, 23 Feb 2022 18:12:00 -0800 Subject: [PATCH 1048/1451] docs(docker): explain how to generate dummy data --- docker/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 6fa2f142..89dbb739 100644 --- a/docker/README.md +++ b/docker/README.md @@ -16,7 +16,7 @@ systemctl start docker.service The main image - `aurweb` - must be built manually: ```sh -docker compose build aurweb-image +docker compose build ``` ### Starting and Stopping the Services @@ -38,6 +38,21 @@ With a running cluster, execute the following in a new terminal: docker compose run test ``` +### Generating Dummy Data + +Before you can make meaningful queries to the cluster, it needs some data. +Luckily such data can be generated. First, `docker ps` to discover the ID of the +container running the FastAPI. Then: + +```sh +docker exec -it /bin/bash +./scheme/gendummydata.py dummy.sql +mysql aurweb < dummy.sql +``` + +The generation script may prompt you to install other Arch packages before it +can proceed. + ### Querying the RPC The Fast (Python) API runs on Port 8444, while the legacy PHP version runs From 1bb4daa36ac1e92e68a528727581bb781f3d9c00 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 23 Feb 2022 18:54:35 -0800 Subject: [PATCH 1049/1451] doc: merge CodingGuidelines into CONTRIBUTING.md Signed-off-by: Kevin Morris --- CONTRIBUTING.md | 17 ++++++++++---- doc/CodingGuidelines | 54 -------------------------------------------- 2 files changed, 13 insertions(+), 58 deletions(-) delete mode 100644 doc/CodingGuidelines diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2deaf237..2bb840f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ # Contributing -Patches should be sent to the [aur-dev@lists.archlinux.org][1] mailing list. +Patches should be sent to the [aur-dev@lists.archlinux.org][1] mailing list +or included in a merge request on the [aurweb repository][2]. Before sending patches, you are recommended to run `flake8` and `isort`. @@ -8,12 +9,20 @@ You can add a git hook to do this by installing `python-pre-commit` and running `pre-commit install`. [1]: https://lists.archlinux.org/listinfo/aur-dev +[2]: https://gitlab.archlinunx.org/archlinux/aurweb ### Coding Guidelines -1. All source modified or added within a patchset **must** maintain equivalent - or increased coverage by providing tests that use the functionality. +DISCLAIMER: We realise the code doesn't necessarily follow all the rules. +This is an attempt to establish a standard coding style for future +development. -2. Please keep your source within an 80 column width. +1. All source modified or added within a patchset **must** maintain equivalent + or increased coverage by providing tests that use the functionality +2. Please keep your source within an 80 column width +3. Use four space indentation +4. Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) +5. DRY: Don't Repeat Yourself +6. All code should be tested for good _and_ bad cases Test patches that increase coverage in the codebase are always welcome. diff --git a/doc/CodingGuidelines b/doc/CodingGuidelines deleted file mode 100644 index 46537bb2..00000000 --- a/doc/CodingGuidelines +++ /dev/null @@ -1,54 +0,0 @@ -Coding Guidelines -================= - -DISCLAIMER: We realise the code doesn't necessarily follow all the rules. -This is an attempt to establish a standard coding style for future -development. - -Coding style ------------- - -Column width: 79 columns or less within reason. - -Indentation: tabs (standard eight column width) -Please don't add any mode lines. Adjust your editor to display tabs to your -preferred width. Generally code should work with the standard eight column -tabs. - -No short open tags. '' -Try embedding as little XHTML in the PHP as possible. -Consider creating templates for XHTML. - -All markup should conform to XHTML 1.0 Strict requirements. -You can use http://validator.w3.org to check the markup. - -Prevent PHP Notices by using isset() or empty() in conditionals that -reference $_GET, $_POST, or $_REQUEST variables. - -MySQL queries should generally go into functions. - -Submitting patches ------------------- - -!!! PLEASE TEST YOUR PATCHES BEFORE SUBMITTING !!! -Submit uncompressed git-formatted patches to aur-dev@archlinux.org. - -You will need to register on the mailing list before submitting: -https://mailman.archlinux.org/mailman/listinfo/aur-dev - -Base your patches on the master branch as forward development is done there. -When writing patches please keep unnecessary changes to a minimum. - -Try to keep your commits small and focused. -Smaller patches are much easier to review and have a better chance of being -pushed more quickly into the main repo. Smaller commits also makes reviewing -the commit history and tracking down specific changes much easier. - -Try to make your commit messages brief but descriptive. - -Glossary --------- -git-formatted patch: - A patch that is produced via `git format-patch` and is sent via - `git send-email` or as an inline attachment of an email. From 9204b76110daaeb582f373836696f05f9674ce94 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Feb 2022 19:24:29 -0800 Subject: [PATCH 1050/1451] fix: ...do not add to ActiveTUs when voting on a proposal Straight up bug. Closes #324 Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 1 - test/test_trusted_user_routes.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 2d6ea92c..53bcecb7 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -220,7 +220,6 @@ async def trusted_user_proposal_post(request: Request, proposal: int, with db.begin(): vote = db.create(models.TUVote, User=request.user, VoteInfo=voteinfo) - voteinfo.ActiveTUs += 1 context["error"] = "You've already voted for this proposal." return render_proposal(request, context, proposal, voteinfo, voters, vote) diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index e2bf6497..a5c4c5e8 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -650,7 +650,6 @@ def test_tu_proposal_vote(client, proposal): # Store the current related values. yes = voteinfo.Yes - active_tus = voteinfo.ActiveTUs cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: @@ -661,7 +660,6 @@ def test_tu_proposal_vote(client, proposal): # Check that the proposal record got updated. assert voteinfo.Yes == yes + 1 - assert voteinfo.ActiveTUs == active_tus + 1 # Check that the new TUVote exists. vote = db.query(TUVote, TUVote.VoteInfo == voteinfo, From c7c79a152b50b5536d54332c0d45e57e0462aed6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 25 Feb 2022 19:44:10 -0800 Subject: [PATCH 1051/1451] upgrade: bump to v6.0.20 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index c2b8be79..ad8ea100 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.19" +AURWEB_VERSION = "v6.0.20" _parser = None diff --git a/pyproject.toml b/pyproject.toml index b5da9913..88f182be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.19" +version = "v6.0.20" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From b80d914cba0158a559162f1d019f836c004dee6b Mon Sep 17 00:00:00 2001 From: Matt Harrison Date: Mon, 7 Mar 2022 12:37:54 -0500 Subject: [PATCH 1052/1451] fix click to copy when there is more than one copy link on the page. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes issue reported on the mailing list here: https://lists.archlinux.org/pipermail/aur-general/2022-March/036833.html Thanks to Henry-Joseph Audéoud for diagnosing the issue https://lists.archlinux.org/pipermail/aur-general/2022-March/036836.html Also update the event variable to use the local copy instead of the deprecated global version https://stackoverflow.com/questions/58341832/event-is-deprecated-what-should-be-used-instead --- web/html/js/copy.js | 4 ++-- web/template/pkg_details.php | 4 ++-- web/template/pkgbase_details.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/html/js/copy.js b/web/html/js/copy.js index f46299b3..21324ccb 100644 --- a/web/html/js/copy.js +++ b/web/html/js/copy.js @@ -1,6 +1,6 @@ document.addEventListener('DOMContentLoaded', function() { - document.querySelector('.copy').addEventListener('click', function(e) { + document.querySelectorAll('.copy').addEventListener('click', function(e) { e.preventDefault(); - navigator.clipboard.writeText(event.target.text); + navigator.clipboard.writeText(e.target.text); }); }); diff --git a/web/template/pkg_details.php b/web/template/pkg_details.php index 047de9a7..25d85b78 100644 --- a/web/template/pkg_details.php +++ b/web/template/pkg_details.php @@ -309,9 +309,9 @@ endif; diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php index 35ad217a..bde29c1c 100644 --- a/web/template/pkgbase_details.php +++ b/web/template/pkgbase_details.php @@ -138,9 +138,9 @@ endif; From 6a243e90dbf08c3a9db8f757c11471661d18bcc1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 7 Mar 2022 23:23:49 -0800 Subject: [PATCH 1053/1451] fix: only reject addvote for users with running proposals This was incorrectly indiscriminately targetting _any_ proposal for a particular user. Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 53bcecb7..cbe3e47d 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -274,8 +274,10 @@ async def trusted_user_addvote_post(request: Request, context["error"] = "Username does not exist." return render_addvote(context, HTTPStatus.NOT_FOUND) + utcnow = time.utcnow() voteinfo = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.User == user).count() + and_(models.TUVoteInfo.User == user, + models.TUVoteInfo.End > utcnow)).count() if voteinfo: _ = l10n.get_translator_for_request(request) context["error"] = _( From f11e8de251af54e78043d8016b12feda38a9ec55 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 7 Mar 2022 23:32:14 -0800 Subject: [PATCH 1054/1451] upgrade: bump to v6.0.21 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index ad8ea100..9931e7d2 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.20" +AURWEB_VERSION = "v6.0.21" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 88f182be..ce081ce6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.20" +version = "v6.0.21" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 5045f0f3e464fc0fbb3229968cb07617ec48314f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 7 Mar 2022 23:53:57 -0800 Subject: [PATCH 1055/1451] fix: copy.js javascript initialization Not sure where this works, but it doesn't seem to work on my browser. Achieved the same by forEaching through the array returned by querySelectorAll instead. Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 1 - web/html/js/copy.js | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 22d519b9..e0eda54c 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -160,4 +160,3 @@
      - {% set order = SO %} {% if SB == "n" %} - {% set order = "d" if order == "a" else "a" %} + {% set order = reverse_order %} + {% else %} + {% set order = SO %} {% endif %} {{ "Name" | tr }} @@ -15,35 +18,39 @@ {{ "Version" | tr }} - {% set order = SO %} {% if SB == "v" %} - {% set order = "d" if order == "a" else "a" %} + {% set order = reverse_order %} + {% else %} + {% set order = SO %} {% endif %} {{ "Votes" | tr }} - {% set order = SO %} {% if SB == "p" %} - {% set order = "d" if order == "a" else "a" %} + {% set order = reverse_order %} + {% else %} + {% set order = SO %} {% endif %} {{ "Popularity" | tr }}? - {% set order = SO %} {% if SB == "w" %} - {% set order = "d" if order == "a" else "a" %} + {% set order = reverse_order %} + {% else %} + {% set order = SO %} {% endif %} {{ "Voted" | tr }} - {% set order = SO %} {% if SB == "o" %} - {% set order = "d" if order == "a" else "a" %} + {% set order = reverse_order %} + {% else %} + {% set order = SO %} {% endif %} {{ "Notify" | tr }} @@ -52,13 +59,14 @@ {% endif %} {{ "Description" | tr }} + {% if SB == "m" %} + {% set order = reverse_order %} + {% else %} {% set order = SO %} - {% if SB == "m" %} - {% set order = "d" if order == "a" else "a" %} - {% endif %} - - {{ "Maintainer" | tr }} - + {% endif %} + + {{ "Maintainer" | tr }} +
      - diff --git a/web/html/js/copy.js b/web/html/js/copy.js index 21324ccb..3b659270 100644 --- a/web/html/js/copy.js +++ b/web/html/js/copy.js @@ -1,6 +1,9 @@ document.addEventListener('DOMContentLoaded', function() { - document.querySelectorAll('.copy').addEventListener('click', function(e) { - e.preventDefault(); - navigator.clipboard.writeText(e.target.text); - }); + let elements = document.querySelectorAll('.copy'); + elements.forEach(function(el) { + el.addEventListener('click', function(e) { + e.preventDefault(); + navigator.clipboard.writeText(e.target.text); + }); + }); }); From e2a17fef95385f0a7cae4216d28b5789b84facce Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 7 Mar 2022 23:57:54 -0800 Subject: [PATCH 1056/1451] upgrade: bump to v6.0.22 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 9931e7d2..d0b095f0 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.21" +AURWEB_VERSION = "v6.0.22" _parser = None diff --git a/pyproject.toml b/pyproject.toml index ce081ce6..f2401b88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.21" +version = "v6.0.22" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 13217be939278a483e77e46fd1e1dd5081d7a829 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 17:49:21 -0800 Subject: [PATCH 1057/1451] fix: don't check suspension for ownership changes People can change comaintainer ownership to suspended users if they want to. Suspended users cannot login, so there is no breach of security here. It does make sense to allow ownership to be changed, imo. Closes #339 Signed-off-by: Kevin Morris --- aurweb/scripts/notify.py | 5 +---- aurweb/testing/email.py | 9 +++++++++ test/test_notify.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index c823b09e..dbef3aa5 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -399,10 +399,7 @@ class ComaintainershipEventNotification(Notification): self._pkgbase = db.query(PackageBase.Name).filter( PackageBase.ID == pkgbase_id).first().Name - user = db.query(User).filter( - and_(User.ID == uid, - User.Suspended == 0) - ).with_entities( + user = db.query(User).filter(User.ID == uid).with_entities( User.Email, User.LangPreference ).first() diff --git a/aurweb/testing/email.py b/aurweb/testing/email.py index c0be2797..b3e3990b 100644 --- a/aurweb/testing/email.py +++ b/aurweb/testing/email.py @@ -37,6 +37,15 @@ class Email: if autoparse: self._parse() + @staticmethod + def reset() -> None: + # Cleanup all email files for this test suite. + prefix = Email.email_prefix(suite=True) + files = os.listdir(Email.TEST_DIR) + for file in files: + if file.startswith(prefix): + os.remove(os.path.join(Email.TEST_DIR, file)) + @staticmethod def email_prefix(suite: bool = False) -> str: """ diff --git a/test/test_notify.py b/test/test_notify.py index a8e994c5..2009e3a8 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -299,6 +299,21 @@ You were removed from the co-maintainer list of {pkgbase.Name} [1]. assert email.body == expected +def test_suspended_ownership_change(user: User, pkgbases: List[PackageBase]): + with db.begin(): + user.Suspended = 1 + + pkgbase = pkgbases[0] + notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + Email.reset() # Clear the Email pool + notif = notify.ComaintainerRemoveNotification(user.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + def test_delete(user: User, user2: User, pkgbases: List[PackageBase]): pkgbase = pkgbases[0] notif = notify.DeleteNotification(user2.ID, pkgbase.ID) From e00cf5f1249b522e58fb0651ae8b00e7b74c6ab2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 17:51:44 -0800 Subject: [PATCH 1058/1451] test: use smtplib.SMTP[_SSL] timeout = notifications.smtp-timeout A new option has been added for configuration of SMTP timeout: - notifications.smtp-timeout During tests, we can change this timeout to be small, so we aren't depending on hardware-based RNG to pass the timeout. Without a timeout, users can run into a long-running test for no particular reason. Signed-off-by: Kevin Morris --- aurweb/scripts/notify.py | 5 +++- aurweb/testing/smtp.py | 3 +++ conf/config.defaults | 1 + test/test_notify.py | 49 ++++++++++++++++++++++++---------------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index dbef3aa5..6afa65ae 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -104,7 +104,10 @@ class Notification: False: smtplib.SMTP, True: smtplib.SMTP_SSL, } - server = classes[use_ssl](server_addr, server_port) + smtp_timeout = aurweb.config.getint("notifications", + "smtp-timeout") + server = classes[use_ssl](server_addr, server_port, + timeout=smtp_timeout) if use_starttls: server.ehlo() diff --git a/aurweb/testing/smtp.py b/aurweb/testing/smtp.py index da64c93f..e5d67991 100644 --- a/aurweb/testing/smtp.py +++ b/aurweb/testing/smtp.py @@ -36,6 +36,9 @@ class FakeSMTP: def quit(self) -> None: self.quit_count += 1 + def __call__(self, *args, **kwargs) -> "FakeSMTP": + return self + class FakeSMTP_SSL(FakeSMTP): """ A fake version of smtplib.SMTP_SSL used for testing. """ diff --git a/conf/config.defaults b/conf/config.defaults index 371c99b2..722802cc 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -65,6 +65,7 @@ smtp-use-ssl = 0 smtp-use-starttls = 0 smtp-user = smtp-password = +smtp-timeout = 60 sender = notify@aur.archlinux.org reply-to = noreply@aur.archlinux.org diff --git a/test/test_notify.py b/test/test_notify.py index 2009e3a8..fdec5ed7 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -547,18 +547,18 @@ def test_smtp(user: User): with db.begin(): user.ResetKey = "12345678901234567890123456789012" - SMTP = FakeSMTP() + smtp = FakeSMTP() get = "aurweb.config.get" getboolean = "aurweb.config.getboolean" with mock.patch(get, side_effect=mock_smtp_config(str)): with mock.patch(getboolean, side_effect=mock_smtp_config(bool)): - with mock.patch("smtplib.SMTP", side_effect=lambda a, b: SMTP): + with mock.patch("smtplib.SMTP", side_effect=smtp): config.rehash() notif = notify.WelcomeNotification(user.ID) notif.send() config.rehash() - assert len(SMTP.emails) == 1 + assert len(smtp.emails) == 1 def mock_smtp_starttls_config(cls): @@ -586,25 +586,25 @@ def test_smtp_starttls(user: User): user.ResetKey = "12345678901234567890123456789012" user.BackupEmail = "backup@example.org" - SMTP = FakeSMTP() + smtp = FakeSMTP() get = "aurweb.config.get" getboolean = "aurweb.config.getboolean" with mock.patch(get, side_effect=mock_smtp_starttls_config(str)): with mock.patch( getboolean, side_effect=mock_smtp_starttls_config(bool)): - with mock.patch("smtplib.SMTP", side_effect=lambda a, b: SMTP): + with mock.patch("smtplib.SMTP", side_effect=smtp): notif = notify.WelcomeNotification(user.ID) notif.send() - assert SMTP.starttls_enabled - assert SMTP.user - assert SMTP.passwd + assert smtp.starttls_enabled + assert smtp.user + assert smtp.passwd - assert len(SMTP.emails) == 2 - to = SMTP.emails[0][1] + assert len(smtp.emails) == 2 + to = smtp.emails[0][1] assert to == [user.Email] - to = SMTP.emails[1][1] + to = smtp.emails[1][1] assert to == [user.BackupEmail] @@ -629,19 +629,19 @@ def test_smtp_ssl(user: User): with db.begin(): user.ResetKey = "12345678901234567890123456789012" - SMTP = FakeSMTP_SSL() + smtp = FakeSMTP_SSL() get = "aurweb.config.get" getboolean = "aurweb.config.getboolean" with mock.patch(get, side_effect=mock_smtp_ssl_config(str)): with mock.patch(getboolean, side_effect=mock_smtp_ssl_config(bool)): - with mock.patch("smtplib.SMTP_SSL", side_effect=lambda a, b: SMTP): + with mock.patch("smtplib.SMTP_SSL", side_effect=smtp): notif = notify.WelcomeNotification(user.ID) notif.send() - assert len(SMTP.emails) == 1 - assert SMTP.use_ssl - assert SMTP.user - assert SMTP.passwd + assert len(smtp.emails) == 1 + assert smtp.use_ssl + assert smtp.user + assert smtp.passwd def test_notification_defaults(): @@ -655,6 +655,7 @@ def test_notification_oserror(user: User, caplog: pytest.LogCaptureFixture): """ Try sending a notification with a bad SMTP configuration. """ caplog.set_level(ERROR) config_get = config.get + config_getint = config.getint mocked_options = { "sendmail": str(), @@ -662,8 +663,9 @@ def test_notification_oserror(user: User, caplog: pytest.LogCaptureFixture): "smtp-port": "587", "smtp-user": "notify@server.xyz", "smtp-password": "notify_server_xyz", + "smtp-timeout": 1, "sender": "notify@server.xyz", - "reply-to": "no-reply@server.xyz" + "reply-to": "no-reply@server.xyz", } def mock_config_get(section: str, key: str) -> str: @@ -672,9 +674,16 @@ def test_notification_oserror(user: User, caplog: pytest.LogCaptureFixture): return mocked_options.get(key) return config_get(section, key) + def mock_config_getint(section: str, key: str) -> str: + if section == "notifications": + if key in mocked_options: + return mocked_options.get(key) + return config_getint(section, key) + notif = notify.WelcomeNotification(user.ID) - with mock.patch("aurweb.config.get", side_effect=mock_config_get): - notif.send() + with mock.patch("aurweb.config.getint", side_effect=mock_config_getint): + with mock.patch("aurweb.config.get", side_effect=mock_config_get): + notif.send() expected = "Unable to emit notification due to an OSError" assert expected in caplog.text From 2a393f95faa8a4952faf22b1cbcb4e0c8e8318ae Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 17:59:00 -0800 Subject: [PATCH 1059/1451] upgrade: bump to v6.0.23 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index d0b095f0..637024de 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.22" +AURWEB_VERSION = "v6.0.23" _parser = None diff --git a/pyproject.toml b/pyproject.toml index f2401b88..e930a331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.22" +version = "v6.0.23" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From a1a88ea8729f4eafee396197d40fa8a290716bfa Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 19:00:19 -0800 Subject: [PATCH 1060/1451] fix(rpc): suggestions should only suggest based on % Previously, Python code was looking for suggestions based on `%%`. This was inconsistent with PHP's suggestion implementation and cause more records to be bundled with a suggestion, along with supplying misleading suggestions. Closes #343 Signed-off-by: Kevin Morris --- aurweb/rpc.py | 5 +++-- test/test_rpc.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 70d8c2fd..5bc6b80d 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -332,7 +332,7 @@ class RPC: models.PackageBase ).filter( and_(models.PackageBase.PackagerUID.isnot(None), - models.Package.Name.like(f"%{arg}%")) + models.Package.Name.like(f"{arg}%")) ).order_by(models.Package.Name.asc()).limit(20) return [pkg.Name for pkg in packages] @@ -341,9 +341,10 @@ class RPC: if not args: return [] + arg = args[0] packages = db.query(models.PackageBase.Name).filter( and_(models.PackageBase.PackagerUID.isnot(None), - models.PackageBase.Name.like(f"%{args[0]}%")) + models.PackageBase.Name.like(f"{arg}%")) ).order_by(models.PackageBase.Name.asc()).limit(20) return [pkg.Name for pkg in packages] diff --git a/test/test_rpc.py b/test/test_rpc.py index 0d6b2931..2f7f7860 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -551,6 +551,14 @@ def test_rpc_suggest_pkgbase(client: TestClient, packages: List[Package]): data = response.json() assert data == [] + # Test that suggestions are only given based on the beginning + # of the keyword string. + params["arg"] = "ther-pkg" + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data == [] + def test_rpc_suggest(client: TestClient, packages: List[Package]): params = {"v": 5, "type": "suggest", "arg": "other"} @@ -573,6 +581,14 @@ def test_rpc_suggest(client: TestClient, packages: List[Package]): data = response.json() assert data == [] + # Test that suggestions are only given based on the beginning + # of the keyword string. + params["arg"] = "ther-pkg" + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data == [] + def mock_config_getint(section: str, key: str): if key == "request_limit": From 0afa07ed3b895efd84adfba8e54a342065c54f78 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 19:16:02 -0800 Subject: [PATCH 1061/1451] upgrade: bump to v6.0.24 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 637024de..287152d4 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.23" +AURWEB_VERSION = "v6.0.24" _parser = None diff --git a/pyproject.toml b/pyproject.toml index e930a331..7a2f6ca3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.23" +version = "v6.0.24" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 49c5a3facf096e9b0a1905e5ee38fe8750a5bb63 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 20:28:09 -0800 Subject: [PATCH 1062/1451] feat: display stats about total & active TUs on proposals This patch brings in two new features: - when viewing proposal listings, there is a new Statistics section, containing the total and active number of Trusted Users found in the database. - when viewing a proposal directly, the number of active trusted users assigned when the proposal was added is now displayed in the details section. Closes #323 Signed-off-by: Kevin Morris --- aurweb/routers/trusted_user.py | 20 +++++++++ po/aurweb.pot | 4 ++ templates/partials/tu/proposal/details.html | 5 +++ templates/tu/index.html | 16 +++++++ test/test_trusted_user_routes.py | 49 +++++++++++++++++++++ web/html/css/aurweb.css | 13 ++++++ 6 files changed, 107 insertions(+) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index cbe3e47d..3f0eb836 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -2,6 +2,7 @@ import html import typing from http import HTTPStatus +from typing import Any, Dict from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import RedirectResponse, Response @@ -33,6 +34,21 @@ ADDVOTE_SPECIFICS = { } +def populate_trusted_user_counts(context: Dict[str, Any]) -> None: + tu_query = db.query(User).filter( + or_(User.AccountTypeID == TRUSTED_USER_ID, + User.AccountTypeID == TRUSTED_USER_AND_DEV_ID) + ) + context["trusted_user_count"] = tu_query.count() + + # In case any records have a None InactivityTS. + active_tu_query = tu_query.filter( + or_(User.InactivityTS.is_(None), + User.InactivityTS == 0) + ) + context["active_trusted_user_count"] = active_tu_query.count() + + @router.get("/tu") @requires_auth async def trusted_user(request: Request, @@ -40,6 +56,8 @@ async def trusted_user(request: Request, cby: str = "desc", # current by poff: int = 0, # past offset pby: str = "desc"): # past by + """ Proposal listings. """ + if not request.user.has_credential(creds.TU_LIST_VOTES): return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) @@ -102,6 +120,8 @@ async def trusted_user(request: Request, context["current_by_next"] = "asc" if current_by == "desc" else "desc" context["past_by_next"] = "asc" if past_by == "desc" else "desc" + populate_trusted_user_counts(context) + context["q"] = { "coff": current_off, "cby": current_by, diff --git a/po/aurweb.pot b/po/aurweb.pot index bec1b672..e7c632e3 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -2334,3 +2334,7 @@ msgid "This action will close any pending package requests " "related to it. If %sComments%s are omitted, a closure " "comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" diff --git a/templates/partials/tu/proposal/details.html b/templates/partials/tu/proposal/details.html index f7a55148..4cbee9ad 100644 --- a/templates/partials/tu/proposal/details.html +++ b/templates/partials/tu/proposal/details.html @@ -21,6 +21,11 @@
      +
      + {{ "Active" | tr }} {{ "Trusted Users" | tr }} {{ "assigned" | tr }}: + {{ voteinfo.ActiveTUs }} +
      + {% set submitter = voteinfo.Submitter.Username %} {% set submitter_uri = "/account/%s" | format(submitter) %} {% set submitter = '%s' | format(submitter_uri, submitter) %} diff --git a/templates/tu/index.html b/templates/tu/index.html index 5060e1f7..4c7a3c35 100644 --- a/templates/tu/index.html +++ b/templates/tu/index.html @@ -1,6 +1,22 @@ {% extends "partials/layout.html" %} {% block pageContent %} +
      +

      {{ "Statistics" | tr }}

      + + + + + + + + + + + +
      {{ "Total" | tr }} {{ "Trusted Users" | tr }}:{{ trusted_user_count }}
      {{ "Active" | tr }} {{ "Trusted Users" | tr }}:{{ active_trusted_user_count }}
      +
      + {% with table_class = "current-votes", total_votes = current_votes_count, diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index a5c4c5e8..2e7dc193 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -267,6 +267,48 @@ def test_tu_index(client, tu_user): assert int(vote_id.text.strip()) == vote_records[1].ID +def test_tu_stats(client: TestClient, tu_user: User): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == HTTPStatus.OK + + root = parse_root(response.text) + stats = root.xpath('//table[@class="no-width"]')[0] + rows = stats.xpath("./tbody/tr") + + # We have one trusted user. + total = rows[0] + label, count = total.xpath("./td") + assert int(count.text.strip()) == 1 + + # And we have one active TU. + active = rows[1] + label, count = active.xpath("./td") + assert int(count.text.strip()) == 1 + + with db.begin(): + tu_user.InactivityTS = time.utcnow() + + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == HTTPStatus.OK + + root = parse_root(response.text) + stats = root.xpath('//table[@class="no-width"]')[0] + rows = stats.xpath("./tbody/tr") + + # We have one trusted user. + total = rows[0] + label, count = total.xpath("./td") + assert int(count.text.strip()) == 1 + + # But we have no more active TUs. + active = rows[1] + label, count = active.xpath("./td") + assert int(count.text.strip()) == 0 + + def test_tu_index_table_paging(client, tu_user): ts = time.utcnow() @@ -515,6 +557,8 @@ def test_tu_proposal_unauthorized(client: TestClient, user: User, def test_tu_running_proposal(client: TestClient, proposal: Tuple[User, User, TUVoteInfo]): tu_user, user, voteinfo = proposal + with db.begin(): + voteinfo.ActiveTUs = 1 # Initiate an authenticated GET request to /tu/{proposal_id}. proposal_id = voteinfo.ID @@ -536,6 +580,11 @@ def test_tu_running_proposal(client: TestClient, './div[contains(@class, "user")]/strong/a/text()')[0] assert username.strip() == user.Username + active = details.xpath('./div[contains(@class, "field")]')[1] + content = active.text.strip() + assert "Active Trusted Users assigned:" in content + assert "1" in content + submitted = details.xpath( './div[contains(@class, "submitted")]/text()')[0] assert re.match(r'^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$', diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 22b5ac65..59ae7216 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -282,3 +282,16 @@ pre.traceback { white-space: -o-pre-wrap; word-wrap: break-all; } + +/* A text aligning alias. */ +.text-right { + text-align: right; +} + +/* By default, tables use 100% width, which we do not always want. */ +table.no-width { + width: auto; +} +table.no-width > tbody > tr > td { + padding-right: 2px; +} From d7cb04b93dcdad64b6ea8ad081f6dad6387545d0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 8 Mar 2022 20:35:21 -0800 Subject: [PATCH 1063/1451] upgrade: bump to v6.0.25 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 287152d4..9565b70c 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.24" +AURWEB_VERSION = "v6.0.25" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 7a2f6ca3..8b7a2e93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.24" +version = "v6.0.25" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 7ddce6bb2d8a18fd9b63a23e7a022197226ef672 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 14 Mar 2022 05:55:19 -0700 Subject: [PATCH 1064/1451] doc: update CONTRIBUTING.md Signed-off-by: Kevin Morris --- CONTRIBUTING.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bb840f5..3d99d887 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ You can add a git hook to do this by installing `python-pre-commit` and running `pre-commit install`. [1]: https://lists.archlinux.org/listinfo/aur-dev -[2]: https://gitlab.archlinunx.org/archlinux/aurweb +[2]: https://gitlab.archlinux.org/archlinux/aurweb ### Coding Guidelines @@ -23,6 +23,76 @@ development. 3. Use four space indentation 4. Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 5. DRY: Don't Repeat Yourself -6. All code should be tested for good _and_ bad cases +6. All code should be tested for good _and_ bad cases (see [test/README.md][3]) + +[3]: https://gitlab.archlinux.org/archlinux/aurweb/-/blob/master/test/README.md Test patches that increase coverage in the codebase are always welcome. + +### Coding Style + +We use the `flake8` and `isort` tools to manage PEP-8 coherenace and +import ordering in this project. + +There are plugins for editors or IDEs which automate this process. Some +example plugins: + +- [tell-k/vim-autopep8](https://github.com/tell-k/vim-autopep8) +- [fisadev/vim-isort](https://github.com/fisadev/vim-isort) +- [prabirshrestha/vim-lsp](https://github.com/prabirshrestha/vim-lsp) + +See `setup.cfg` for flake8 and isort specific rules. + +Note: We are planning on switching to [psf/black](https://github.com/psf/black). +For now, developers should ensure that flake8 and isort passes when submitting +merge requests or patch sets. + +### Development Environment + +To get started with local development, an instance of aurweb must be +brought up. This can be done using the following sections: + +- [Using Docker](#using-docker) +- [Using INSTALL](#using-install) + +There are a number of services aurweb employs to run the application +in its entirety: + +- ssh +- cron jobs +- starlette/fastapi asgi server + +Project structure: + +- `./aurweb`: `aurweb` Python package +- `./templates`: Jinja2 templates +- `./docker`: Docker scripts and configuration files + +#### Using Docker + +Using Docker, we can run the entire infrastructure in two steps: + + # Build the aurweb:latest image + $ docker-compose build + + # Start all services in the background + $ docker-compose up -d nginx + +`docker-compose` services will generate a locally signed root certificate +at `./data/root_ca.crt`. Users can import this into ca-certificates or their +browser if desired. + +Accessible services (on the host): + +- https://localhost:8444 (python via nginx) +- https://localhost:8443 (php via nginx) +- localhost:13306 (mariadb) +- localhost:16379 (redis) + +Docker services, by default, are setup to be hot reloaded when source code +is changed. + +#### Using INSTALL + +The [INSTALL](INSTALL) file describes steps to install the application on +bare-metal systems. From 790ca4194a6360e9f47f56fe9d39aae4cbe14c25 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 14 Mar 2022 05:57:06 -0700 Subject: [PATCH 1065/1451] fix: coherenace -> coherence Signed-off-by: Kevin Morris --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d99d887..52e182c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ Test patches that increase coverage in the codebase are always welcome. ### Coding Style -We use the `flake8` and `isort` tools to manage PEP-8 coherenace and +We use the `flake8` and `isort` tools to manage PEP-8 coherence and import ordering in this project. There are plugins for editors or IDEs which automate this process. Some From afd25c248fcee508da6724398f6c37c47bf4be5e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 14 Mar 2022 06:24:15 -0700 Subject: [PATCH 1066/1451] fix: remove HEAD and OPTIONS handling from metrics Signed-off-by: Kevin Morris --- aurweb/prometheus.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index 73be3ef6..272ee023 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -60,6 +60,9 @@ def http_requests_total() -> Callable[[Info], None]: labelnames=("method", "path", "status")) def instrumentation(info: Info) -> None: + if info.request.method.lower() in ("head", "options"): # pragma: no cover + return + scope = info.request.scope # Taken from https://github.com/stephenhillier/starlette_exporter @@ -70,8 +73,8 @@ def http_requests_total() -> Callable[[Info], None]: if not (scope.get("endpoint", None) and scope.get("router", None)): return None - root_path = scope.get("root_path", "") - app = scope.get("app", {}) + root_path = scope.get("root_path", str()) + app = scope.get("app", dict()) if hasattr(app, "root_path"): app_root_path = getattr(app, "root_path") @@ -102,6 +105,9 @@ def http_api_requests_total() -> Callable[[Info], None]: labelnames=("type", "status")) def instrumentation(info: Info) -> None: + if info.request.method.lower() in ("head", "options"): # pragma: no cover + return + if info.request.url.path.rstrip("/") == "/rpc": type = info.request.query_params.get("type", "None") if info.response: From d8564e446b744bbc7b6bd8fea22ab6b614acc5ab Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 30 Mar 2022 12:30:21 -0700 Subject: [PATCH 1067/1451] upgrade: bump to v6.0.26 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 9565b70c..53942b75 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.25" +AURWEB_VERSION = "v6.0.26" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 8b7a2e93..b15af272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.25" +version = "v6.0.26" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From ed41a4fe1933e13d19a4e648d63011b3d2a67cc5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 30 Mar 2022 16:16:47 -0700 Subject: [PATCH 1068/1451] feat: add paging to package depends & required by This patch does not include a javascript implementating, but provides a pure HTML/HTTP method of paging through these lists. Also fixes erroneous limiting. We now use a hardcoded limit of 20 by default. Signed-off-by: Kevin Morris --- aurweb/packages/util.py | 6 +-- aurweb/pkgbase/util.py | 13 ++++- aurweb/routers/packages.py | 51 +++++++++++++++---- aurweb/templates.py | 2 + po/aurweb.pot | 8 +++ .../partials/packages/package_metadata.html | 16 +++++- test/test_packages_routes.py | 45 ++++++++++++++++ 7 files changed, 125 insertions(+), 16 deletions(-) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index e8569f29..5085ddf4 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -214,7 +214,7 @@ def query_notified(query: List[models.Package], return output -def pkg_required(pkgname: str, provides: List[str], limit: int) \ +def pkg_required(pkgname: str, provides: List[str]) \ -> List[PackageDependency]: """ Get dependencies that match a string in `[pkgname] + provides`. @@ -227,8 +227,8 @@ def pkg_required(pkgname: str, provides: List[str], limit: int) \ targets = set([pkgname] + provides) query = db.query(PackageDependency).join(Package).filter( PackageDependency.DepName.in_(targets) - ).order_by(Package.Name.asc()).limit(limit) - return query.all() + ).order_by(Package.Name.asc()) + return query @register_filter("source_uri") diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 18af3df0..ea952dce 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -11,16 +11,25 @@ from aurweb.models.package_request import PENDING_ID, PackageRequest from aurweb.models.package_vote import PackageVote from aurweb.scripts import notify from aurweb.templates import make_context as _make_context +from aurweb.templates import make_variable_context as _make_variable_context -def make_context(request: Request, pkgbase: PackageBase) -> Dict[str, Any]: +async def make_variable_context(request: Request, pkgbase: PackageBase) \ + -> Dict[str, Any]: + ctx = await _make_variable_context(request, pkgbase.Name) + return make_context(request, pkgbase, ctx) + + +def make_context(request: Request, pkgbase: PackageBase, + context: Dict[str, Any] = None) -> Dict[str, Any]: """ Make a basic context for package or pkgbase. :param request: FastAPI request :param pkgbase: PackageBase instance :return: A pkgbase context without specific differences """ - context = _make_context(request, pkgbase.Name) + if not context: + context = _make_context(request, pkgbase.Name) context["git_clone_uri_anon"] = config.get("options", "git_clone_uri_anon") context["git_clone_uri_priv"] = config.get("options", "git_clone_uri_priv") diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index bc12455d..f14b0ad8 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -2,7 +2,7 @@ from collections import defaultdict from http import HTTPStatus from typing import Any, Dict, List -from fastapi import APIRouter, Form, Request, Response +from fastapi import APIRouter, Form, Query, Request, Response import aurweb.filters # noqa: F401 @@ -33,7 +33,7 @@ async def packages_get(request: Request, context: Dict[str, Any], context["O"] = offset # Limit PP to options.max_search_results - max_search_results = aurweb.config.getint("options", "max_search_results") + max_search_results = config.getint("options", "max_search_results") context["PP"] = per_page = min(per_page, max_search_results) # Query search by. @@ -123,7 +123,22 @@ async def packages(request: Request) -> Response: @router.get("/packages/{name}") -async def package(request: Request, name: str) -> Response: +async def package(request: Request, name: str, + all_deps: bool = Query(default=False), + all_reqs: bool = Query(default=False)) -> Response: + """ + Get a package by name. + + By default, we limit the number of depends and requires results + to 20. To bypass this and load all of them, which should be triggered + via a "Show more" link near the limited listing. + + :param name: Package.Name + :param all_deps: Boolean indicating whether we should load all depends + :param all_reqs: Boolean indicating whether we should load all requires + :return: FastAPI Response + """ + # Get the Package. pkg = get_pkg_or_base(name, models.Package) pkgbase = pkg.PackageBase @@ -139,23 +154,41 @@ async def package(request: Request, name: str) -> Response: rels_data["r"].append(rel) # Add our base information. - context = pkgbaseutil.make_context(request, pkgbase) + context = await pkgbaseutil.make_variable_context(request, pkgbase) + + context.update( + { + "all_deps": all_deps, + "all_reqs": all_reqs + } + ) + context["package"] = pkg # Package sources. context["sources"] = pkg.package_sources.order_by( models.PackageSource.Source.asc()).all() + # Listing metadata. + context["max_listing"] = max_listing = 20 + # Package dependencies. - max_depends = config.getint("options", "max_depends") - context["dependencies"] = pkg.package_dependencies.order_by( + deps = pkg.package_dependencies.order_by( models.PackageDependency.DepTypeID.asc(), models.PackageDependency.DepName.asc() - ).limit(max_depends).all() + ) + context["depends_count"] = deps.count() + if not all_deps: + deps = deps.limit(max_listing) + context["dependencies"] = deps.all() # Package requirements (other packages depend on this one). - context["required_by"] = pkgutil.pkg_required( - pkg.Name, [p.RelName for p in rels_data.get("p", [])], max_depends) + reqs = pkgutil.pkg_required( + pkg.Name, [p.RelName for p in rels_data.get("p", [])]) + context["reqs_count"] = reqs.count() + if not all_reqs: + reqs = reqs.limit(max_listing) + context["required_by"] = reqs.all() context["licenses"] = pkg.package_licenses diff --git a/aurweb/templates.py b/aurweb/templates.py index ccadb16d..6520bedf 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -100,6 +100,8 @@ async def make_variable_context(request: Request, title: str, next: str = None): for k, v in to_copy.items(): context[k] = v + context["q"] = dict(request.query_params) + return context diff --git a/po/aurweb.pot b/po/aurweb.pot index e7c632e3..bc4bab84 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -2338,3 +2338,11 @@ msgstr "" #: templates/partials/tu/proposal/details.html msgid "assigned" msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html index 6f58c2be..123b994d 100644 --- a/templates/partials/packages/package_metadata.html +++ b/templates/partials/packages/package_metadata.html @@ -1,5 +1,5 @@
      -

      {{ "Dependencies" | tr }} ({{ dependencies | length }})

      +

      {{ "Dependencies" | tr }} ({{ depends_count }})

      -

      {{ "Required by" | tr }} ({{ required_by | length }})

      +

      {{ "Required by" | tr }} ({{ reqs_count }})

      diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index ee837912..e4c992af 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -276,6 +276,51 @@ def test_package(client: TestClient, package: Package): assert conflicts[0].text.strip() == ", ".join(expected) +def paged_depends_required(client: TestClient, package: Package): + maint = package.PackageBase.Maintainer + new_pkgs = [] + + with db.begin(): + # Create 25 new packages that'll be used to depend on our package. + for i in range(26): + base = db.create(PackageBase, Name=f"new_pkg{i}", Maintainer=maint) + new_pkgs.append(db.create(Package, Name=base.Name)) + + # Create 25 deps. + for i in range(25): + create_package_dep(package, f"dep_{i}") + + with db.begin(): + # Create depends on this package so we get some required by listings. + for new_pkg in new_pkgs: + create_package_dep(new_pkg, package.Name) + + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + # Test depends show link. + assert "Show 5 more" in resp.text + + # Test required by show more link, we added 26 packages. + assert "Show 6 more" in resp.text + + # Follow both links at the same time. + with client as request: + resp = request.get( + package_endpoint(package), + params={ + "all_deps": True, + "all_reqs": True, + } + ) + assert resp.status_code == int(HTTPStatus.OK) + + # We're should see everything and have no link. + assert "Show 5 more" not in resp.text + assert "Show 6 more" not in resp.text + + def test_package_comments(client: TestClient, user: User, package: Package): now = (time.utcnow()) with db.begin(): From cf4295a13e43dcc0daea9a9bb7cb54452b4c8b34 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 31 Mar 2022 17:45:39 -0700 Subject: [PATCH 1069/1451] upgrade: bump to v6.0.27 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 53942b75..69d9b31f 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.26" +AURWEB_VERSION = "v6.0.27" _parser = None diff --git a/pyproject.toml b/pyproject.toml index b15af272..c50af62b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.26" +version = "v6.0.27" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From a553d5d95adb9339ca1ba62fcb375ab34e02d013 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 31 Mar 2022 20:45:59 -0700 Subject: [PATCH 1070/1451] fix: replace distutils.util.strtobool with our own Reference from github.com/PostHog/posthog/pull/4631/commits/341c28da0f6d33d6fb12fe443766a2d822ff0097 This fixes a deprecation warning regarding distutil's strtobool. Signed-off-by: Kevin Morris --- aurweb/util.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aurweb/util.py b/aurweb/util.py index 6759794f..5138f7da 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -4,7 +4,6 @@ import secrets import string from datetime import datetime -from distutils.util import strtobool as _strtobool from http import HTTPStatus from subprocess import PIPE, Popen from typing import Callable, Iterable, List, Tuple, Union @@ -114,9 +113,9 @@ def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: def strtobool(value: Union[str, bool]) -> bool: - if isinstance(value, str): - return _strtobool(value or "False") - return value + if not value: + return False + return str(value).lower() in ("y", "yes", "t", "true", "on", "1") def file_hash(filepath: str, hash_function: Callable) -> str: From 7a525d769363a78c080e91b6cfee0b2e0b6df10b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 31 Mar 2022 20:47:34 -0700 Subject: [PATCH 1071/1451] change: remove poetry-dynamic-versioning We've not been using this as it is and its now warning us about strtobool deprecation changes. Removing it for now. Signed-off-by: Kevin Morris --- poetry.lock | 69 ++++++++++++++++++-------------------------------- pyproject.toml | 1 - 2 files changed, 24 insertions(+), 46 deletions(-) diff --git a/poetry.lock b/poetry.lock index c9d0b38a..7744606e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -212,17 +212,6 @@ idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.20)"] wmi = ["wmi (>=1.5.1,<2.0.0)"] -[[package]] -name = "dunamai" -version = "1.8.0" -description = "Dynamic version generation" -category = "main" -optional = false -python-versions = ">=3.5,<4.0" - -[package.dependencies] -packaging = ">=20.9" - [[package]] name = "email-validator" version = "1.1.3" @@ -627,19 +616,6 @@ python-versions = ">=3.6" dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "poetry-dynamic-versioning" -version = "0.13.1" -description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" -category = "main" -optional = false -python-versions = ">=3.5,<4.0" - -[package.dependencies] -dunamai = ">=1.5,<2.0" -jinja2 = {version = ">=2.11.1,<4", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -tomlkit = ">=0.4" - [[package]] name = "posix-ipc" version = "1.0.5" @@ -1029,14 +1005,6 @@ category = "dev" optional = false python-versions = ">=3.7" -[[package]] -name = "tomlkit" -version = "0.9.0" -description = "Style preserving TOML library" -category = "main" -optional = false -python-versions = ">=3.6,<4.0" - [[package]] name = "typing-extensions" version = "4.0.1" @@ -1119,7 +1087,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "1f6a0dd3780d8857ba0d5123814f299a8178a80e79c2235805623f43b8e0381f" +content-hash = "ffe7ab6733020584382d2d01950153072a46d0738f6d2fe52ac84653d0b16086" [metadata.files] aiofiles = [ @@ -1151,10 +1119,13 @@ authlib = [ {file = "Authlib-0.15.5.tar.gz", hash = "sha256:b83cf6360c8e92b0e9df0d1f32d675790bcc4e3c03977499b1eed24dcdef4252"}, ] bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd"}, {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7"}, + {file = "bcrypt-3.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d"}, {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, @@ -1300,10 +1271,6 @@ dnspython = [ {file = "dnspython-2.2.0-py3-none-any.whl", hash = "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44"}, {file = "dnspython-2.2.0.tar.gz", hash = "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6"}, ] -dunamai = [ - {file = "dunamai-1.8.0-py3-none-any.whl", hash = "sha256:846855e45d5969f6d11835d486bbf4d6ca175d4169a0ab11f619a5135cc86bdf"}, - {file = "dunamai-1.8.0.tar.gz", hash = "sha256:ff1f958af3575ec612e72c84bf96367469f418d31b9685f8311a5de2eb754a85"}, -] email-validator = [ {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, @@ -1343,6 +1310,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, @@ -1355,6 +1323,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, @@ -1363,6 +1332,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, @@ -1371,6 +1341,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, @@ -1379,6 +1350,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, @@ -1515,6 +1487,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, @@ -1526,6 +1501,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1537,6 +1515,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, @@ -1549,6 +1530,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1561,6 +1545,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -1619,10 +1606,6 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -poetry-dynamic-versioning = [ - {file = "poetry-dynamic-versioning-0.13.1.tar.gz", hash = "sha256:5c0e7b22560db76812057ef95dadad662ecc63eb270145787eabe73da7c222f9"}, - {file = "poetry_dynamic_versioning-0.13.1-py3-none-any.whl", hash = "sha256:6d79f76436c624653fc06eb9bb54fb4f39b1d54362bc366ad2496855711d3a78"}, -] posix-ipc = [ {file = "posix_ipc-1.0.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ccb36ba90efec56a1796f1566eee9561f355a4f45babbc4d18ac46fb2d0b246b"}, {file = "posix_ipc-1.0.5-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:613bf1afe90e84c06255ec1a6f52c9b24062492de66e5f0dbe068adf67fc3454"}, @@ -1876,10 +1859,6 @@ tomli = [ {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, ] -tomlkit = [ - {file = "tomlkit-0.9.0-py3-none-any.whl", hash = "sha256:c1b0fc73abd4f1e77c29ea4061ca0f2e11cbfb77342e17df3d3fdd496fc3f899"}, - {file = "tomlkit-0.9.0.tar.gz", hash = "sha256:5a83672c565f78f5fc8f1e44e5f2726446cc6b765113efd21d03e9331747d9ab"}, -] typing-extensions = [ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, diff --git a/pyproject.toml b/pyproject.toml index c50af62b..001e0287 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ python = ">=3.9,<3.11" # poetry-dynamic-versioning is used to produce tool.poetry.version # based on git tags. -poetry-dynamic-versioning = "^0.13.1" # General aiofiles = "^0.7.0" From 02d114d575a72c0ec5038c762cddd8f1424e2c12 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Fri, 6 May 2022 18:30:29 +0100 Subject: [PATCH 1072/1451] fix: hide email when account's email hidden is set Fixes: 362 Signed-off-by: Leonidas Spyropoulos --- templates/account/show.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/account/show.html b/templates/account/show.html index a9bb3c30..a57efb77 100644 --- a/templates/account/show.html +++ b/templates/account/show.html @@ -25,7 +25,11 @@ {% trans %}Email Address{% endtrans %}: + {% if not user.HideEmail %} {{ user.Email }} + {% else %} + <{% trans %}hidden{% endtrans %}> + {% endif %} From 0b544885636fead56551b9f229400e7abacd0d73 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 12 May 2022 23:26:57 +0100 Subject: [PATCH 1073/1451] fix(poetry): remove mysql-connector dependency Reverting a8287921 Signed-off-by: Leonidas Spyropoulos --- poetry.lock | 12 ------------ pyproject.toml | 1 - 2 files changed, 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7744606e..fe1575a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -553,14 +553,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "mysql-connector" -version = "2.2.9" -description = "MySQL driver written in Python" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "mysqlclient" version = "2.1.0" @@ -943,7 +935,6 @@ mssql_pymssql = ["pymssql"] mssql_pyodbc = ["pyodbc"] mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] -mysql_connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] postgresql = ["psycopg2 (>=2.7)"] postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] @@ -1556,9 +1547,6 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -mysql-connector = [ - {file = "mysql-connector-2.2.9.tar.gz", hash = "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"}, -] mysqlclient = [ {file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"}, {file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"}, diff --git a/pyproject.toml b/pyproject.toml index 001e0287..9ba73c2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,6 @@ SQLAlchemy = "^1.4.26" uvicorn = "^0.15.0" gunicorn = "^20.1.0" Hypercorn = "^0.11.2" -mysql-connector = "^2.2.9" prometheus-fastapi-instrumentator = "^5.7.1" pytest-xdist = "^2.4.0" filelock = "^3.3.2" From 4ddd1dec9c1d19481593f4095ba30de7b6d22cde Mon Sep 17 00:00:00 2001 From: Kristian Klausen Date: Fri, 13 May 2022 00:37:34 +0200 Subject: [PATCH 1074/1451] upgrade: bump to v6.0.28 --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 69d9b31f..6069910f 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -6,7 +6,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.27" +AURWEB_VERSION = "v6.0.28" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 9ba73c2d..41d8301f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.27" +version = "v6.0.28" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 8598ea6f748405de5678e4f6c17b95afaa9df886 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 27 Jun 2022 20:52:43 +0200 Subject: [PATCH 1075/1451] fix(gitlab-ci): update coverage reporting in CI Gitlab 14.10 introduced a coverage_report key which obsoletes the old way of reporting coverage data. --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c5554e92..98f99ae3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -53,7 +53,9 @@ test: coverage: '/TOTAL.*\s+(\d+\%)/' artifacts: reports: - cobertura: coverage.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml deploy: stage: deploy From 98f55879d37be1ffbdcf9861ef70410317f90af2 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Tue, 28 Jun 2022 22:07:00 +0200 Subject: [PATCH 1076/1451] fix(docker): don't run redis with protected mode For our development setup we run a redis container without a username/password. Redis recently set protected mode by default which disallows this, turn it off as it has no security implication. --- docker/redis-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/redis-entrypoint.sh b/docker/redis-entrypoint.sh index e92be6c5..669716d7 100755 --- a/docker/redis-entrypoint.sh +++ b/docker/redis-entrypoint.sh @@ -2,5 +2,6 @@ set -eou pipefail sed -ri 's/^bind .*$/bind 0.0.0.0 -::1/g' /etc/redis/redis.conf +sed -ri 's/protected-mode yes/protected-mode no/g' /etc/redis/redis.conf exec "$@" From ade624c215989532c9536cebc4a17000999974f3 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 27 Jun 2022 20:48:18 +0200 Subject: [PATCH 1077/1451] doc(README): update contributing guidelines --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f156455..2741efa2 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Links ----- * The repository is hosted at https://gitlab.archlinux.org/archlinux/aurweb - -- see doc/CodingGuidelines for information on the patch submission process. + -- see [CONTRIBUTING.md](./CONTRIBUTING.md) for information on the patch submission process. * Bugs can (and should) be submitted to the aurweb bug tracker: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/new?issuable_template=Bug From edef6cc6ac01b68d18fe8d7e7c948fc3be13b36b Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Thu, 30 Jun 2022 21:57:52 +0200 Subject: [PATCH 1078/1451] chore(css): drop old vendor prefixes All of these vendor prefixes are already supported by all browsers for quite a while. --- web/html/css/aurweb.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 59ae7216..281b8f59 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -125,13 +125,11 @@ } .rss-icon, .delete-comment, .undelete-comment, .edit-comment, .pin-comment { - -webkit-filter: grayscale(100%); filter: grayscale(100%); opacity: 0.6; } .rss-icon:hover, .delete-comment:hover, .undelete-comment:hover, .edit-comment:hover, .pin-comment:hover { - -webkit-filter: none; filter: none; opacity: 1; } @@ -277,9 +275,6 @@ div.box form.link button { pre.traceback { /* https://css-tricks.com/snippets/css/make-pre-text-wrap/ */ white-space: pre-wrap; - white-space: -moz-pre-wrap; - white-space: -pre-wrap; - white-space: -o-pre-wrap; word-wrap: break-all; } From 4a58e1349cb34844c8f706cdedf902ef66adb8d8 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 4 Jul 2022 21:35:06 +0200 Subject: [PATCH 1079/1451] fix(docker): fix typo scheme -> schema --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 89dbb739..81628a39 100644 --- a/docker/README.md +++ b/docker/README.md @@ -46,7 +46,7 @@ container running the FastAPI. Then: ```sh docker exec -it /bin/bash -./scheme/gendummydata.py dummy.sql +./schema/gendummydata.py dummy.sql mysql aurweb < dummy.sql ``` From 0b03a6871e288c837e273cf5577e8d81dc7b44fd Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Mon, 4 Jul 2022 21:35:41 +0200 Subject: [PATCH 1080/1451] fix(docker): document runtime deps --- docker/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/README.md b/docker/README.md index 81628a39..88fb763e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -46,6 +46,7 @@ container running the FastAPI. Then: ```sh docker exec -it /bin/bash +pacman -S words fortune-mod ./schema/gendummydata.py dummy.sql mysql aurweb < dummy.sql ``` From 034e47bc282a43b661b6d5db2759b4c8ca723a3f Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 19 May 2022 13:13:36 +0100 Subject: [PATCH 1081/1451] fix: hide Unflag package from non-maintainers Closes: #364 Signed-off-by: Leonidas Spyropoulos --- aurweb/pkgbase/actions.py | 4 ++-- aurweb/pkgbase/util.py | 3 +++ templates/partials/packages/actions.html | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 229d52b9..6fd55497 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -38,8 +38,8 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: def pkgbase_unflag_instance(request: Request, pkgbase: PackageBase) -> None: - has_cred = request.user.has_credential( - creds.PKGBASE_UNFLAG, approved=[pkgbase.Flagger, pkgbase.Maintainer]) + has_cred = request.user.has_credential(creds.PKGBASE_UNFLAG, approved=[ + pkgbase.Flagger, pkgbase.Maintainer] + [c.User for c in pkgbase.comaintainers]) if has_cred: with db.begin(): pkgbase.OutOfDateTS = None diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index ea952dce..55dbb022 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -39,6 +39,9 @@ def make_context(request: Request, pkgbase: PackageBase, PackageComaintainer.Priority.asc() ).all() ] + context["unflaggers"] = context["comaintainers"].copy() + context["unflaggers"].append(pkgbase.Maintainer) + context["packages_count"] = pkgbase.packages.count() context["keywords"] = pkgbase.keywords context["comments"] = pkgbase.comments.order_by( diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 88420222..2144b07a 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -41,6 +41,7 @@ + {% if request.user.has_credential(creds.PKGBASE_UNFLAG, approved=unflaggers) %}
    • + {% endif %} {% endif %}
    • {% if not voted %} From 28970ccc9179d210781fedabcf42cc04332cd1ec Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sun, 17 Jul 2022 12:19:18 +0100 Subject: [PATCH 1082/1451] fix: align text on left Closes: #368 Signed-off-by: Leonidas Spyropoulos --- templates/tu/index.html | 4 ++-- web/html/css/aurweb.css | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/templates/tu/index.html b/templates/tu/index.html index 4c7a3c35..9f5bfd50 100644 --- a/templates/tu/index.html +++ b/templates/tu/index.html @@ -6,11 +6,11 @@ - + - + diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 281b8f59..59f7ed1e 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -278,11 +278,6 @@ pre.traceback { word-wrap: break-all; } -/* A text aligning alias. */ -.text-right { - text-align: right; -} - /* By default, tables use 100% width, which we do not always want. */ table.no-width { width: auto; From d6fa4ec5a8d76b6f791bb6d855eb267661baa012 Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Tue, 19 Jul 2022 18:29:26 +0200 Subject: [PATCH 1083/1451] Explain how to populate dummy data for TESTING Signed-off-by: Hugo Osvaldo Barrera --- TESTING | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/TESTING b/TESTING index 776be2f4..cb34c0e9 100644 --- a/TESTING +++ b/TESTING @@ -31,6 +31,16 @@ docker-compose Python: https://localhost:8444/ PHP: https://localhost:8443/ +5) [Optionally] populate the database with dummy data: + + $ docker-compose up mariadb + $ docker-compose exec mariadb /bin/sh + # pacman -S --noconfirm words fortune-mod + # poetry run schema/gendummydata.py dummy_data.sql + # mysql -uaur -paur aurweb < dummy_data.sql + +Inspect `dummy_data.sql` for test credentials. Passwords match usernames. + Bare Metal ---------- From a509e4047483763316d3b06a4dfe3c2004455aff Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sun, 31 Jul 2022 20:58:39 +0200 Subject: [PATCH 1084/1451] fix(python): use standard dict/list type annotation Since Python 3.9 list/dict can be used as type hint. --- aurweb/filters.py | 20 +++++----- aurweb/models/package_dependency.py | 4 +- aurweb/models/user.py | 4 +- aurweb/packages/requests.py | 10 ++--- aurweb/packages/util.py | 18 ++++----- aurweb/pkgbase/actions.py | 4 +- aurweb/pkgbase/util.py | 10 ++--- aurweb/pkgbase/validate.py | 4 +- aurweb/prometheus.py | 4 +- aurweb/routers/accounts.py | 4 +- aurweb/routers/packages.py | 24 ++++++------ aurweb/routers/rpc.py | 8 ++-- aurweb/routers/trusted_user.py | 4 +- aurweb/rpc.py | 44 ++++++++++----------- aurweb/scripts/mkpkglists.py | 4 +- aurweb/scripts/popupdate.py | 4 +- aurweb/spawn.py | 10 ++--- aurweb/testing/alpm.py | 4 +- aurweb/testing/html.py | 5 +-- aurweb/testing/requests.py | 6 +-- aurweb/users/update.py | 8 ++-- aurweb/util.py | 4 +- test/test_adduser.py | 3 +- test/test_mkpkglists.py | 7 ++-- test/test_notify.py | 35 +++++++++-------- test/test_packages_routes.py | 31 ++++++++------- test/test_pkgbase_routes.py | 5 +-- test/test_pkgmaint.py | 8 ++-- test/test_requests.py | 11 +++--- test/test_rpc.py | 59 ++++++++++++++--------------- test/test_templates.py | 4 +- 31 files changed, 175 insertions(+), 195 deletions(-) diff --git a/aurweb/filters.py b/aurweb/filters.py index 45cb6d83..22f65024 100644 --- a/aurweb/filters.py +++ b/aurweb/filters.py @@ -2,7 +2,7 @@ import copy import math from datetime import datetime -from typing import Any, Dict, Union +from typing import Any, Union from urllib.parse import quote_plus, urlencode from zoneinfo import ZoneInfo @@ -19,7 +19,7 @@ from aurweb.templates import register_filter, register_function @register_filter("pager_nav") @pass_context -def pager_nav(context: Dict[str, Any], +def pager_nav(context: dict[str, Any], page: int, total: int, prefix: str) -> str: page = int(page) # Make sure this is an int. @@ -71,7 +71,7 @@ def do_round(f: float) -> int: @register_filter("tr") @pass_context -def tr(context: Dict[str, Any], value: str): +def tr(context: dict[str, Any], value: str): """ A translation filter; example: {{ "Hello" | tr("de") }}. """ _ = l10n.get_translator_for_request(context.get("request")) return _(value) @@ -79,7 +79,7 @@ def tr(context: Dict[str, Any], value: str): @register_filter("tn") @pass_context -def tn(context: Dict[str, Any], count: int, +def tn(context: dict[str, Any], count: int, singular: str, plural: str) -> str: """ A singular and plural translation filter. @@ -107,7 +107,7 @@ def as_timezone(dt: datetime, timezone: str): @register_filter("extend_query") -def extend_query(query: Dict[str, Any], *additions) -> Dict[str, Any]: +def extend_query(query: dict[str, Any], *additions) -> dict[str, Any]: """ Add additional key value pairs to query. """ q = copy.copy(query) for k, v in list(additions): @@ -116,7 +116,7 @@ def extend_query(query: Dict[str, Any], *additions) -> Dict[str, Any]: @register_filter("urlencode") -def to_qs(query: Dict[str, Any]) -> str: +def to_qs(query: dict[str, Any]) -> str: return urlencode(query, doseq=True) @@ -134,7 +134,7 @@ def number_format(value: float, places: int): @register_filter("account_url") @pass_context -def account_url(context: Dict[str, Any], +def account_url(context: dict[str, Any], user: "aurweb.models.user.User") -> str: base = aurweb.config.get("options", "aur_location") return f"{base}/account/{user.Username}" @@ -152,7 +152,7 @@ def ceil(*args, **kwargs) -> int: @register_function("date_strftime") @pass_context -def date_strftime(context: Dict[str, Any], dt: Union[int, datetime], fmt: str) \ +def date_strftime(context: dict[str, Any], dt: Union[int, datetime], fmt: str) \ -> str: if isinstance(dt, int): dt = timestamp_to_datetime(dt) @@ -162,11 +162,11 @@ def date_strftime(context: Dict[str, Any], dt: Union[int, datetime], fmt: str) \ @register_function("date_display") @pass_context -def date_display(context: Dict[str, Any], dt: Union[int, datetime]) -> str: +def date_display(context: dict[str, Any], dt: Union[int, datetime]) -> str: return date_strftime(context, dt, "%Y-%m-%d (%Z)") @register_function("datetime_display") @pass_context -def datetime_display(context: Dict[str, Any], dt: Union[int, datetime]) -> str: +def datetime_display(context: dict[str, Any], dt: Union[int, datetime]) -> str: return date_strftime(context, dt, "%Y-%m-%d %H:%M (%Z)") diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index 2fd87f2a..67a7717f 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -1,5 +1,3 @@ -from typing import List - from sqlalchemy import and_, literal from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship @@ -60,7 +58,7 @@ class PackageDependency(Base): _OfficialProvider.Name == self.DepName).exists() return db.query(pkg).scalar() or db.query(official).scalar() - def provides(self) -> List[PackageRelation]: + def provides(self) -> list[PackageRelation]: from aurweb.models.relation_type import PROVIDES_ID rels = db.query(PackageRelation).join(_Package).filter( diff --git a/aurweb/models/user.py b/aurweb/models/user.py index c375fcbc..3fa72a85 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -1,6 +1,6 @@ import hashlib -from typing import List, Set +from typing import Set import bcrypt @@ -149,7 +149,7 @@ class User(Base): return self.session.SessionID def has_credential(self, credential: Set[int], - approved: List["User"] = list()): + approved: list["User"] = list()): from aurweb.auth.creds import has_credential return has_credential(self, credential, approved) diff --git a/aurweb/packages/requests.py b/aurweb/packages/requests.py index 6aaa59ab..42026a33 100644 --- a/aurweb/packages/requests.py +++ b/aurweb/packages/requests.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Set +from typing import Optional, Set from fastapi import Request from sqlalchemy import and_, orm @@ -139,7 +139,7 @@ def close_pkgreq(pkgreq: PackageRequest, closer: User, def handle_request(request: Request, reqtype_id: int, pkgbase: PackageBase, - target: PackageBase = None) -> List[notify.Notification]: + target: PackageBase = None) -> list[notify.Notification]: """ Handle package requests before performing an action. @@ -158,7 +158,7 @@ def handle_request(request: Request, reqtype_id: int, :param pkgbase: PackageBase which the request is about :param target: Optional target to merge into """ - notifs: List[notify.Notification] = [] + notifs: list[notify.Notification] = [] # If it's an orphan request, perform further verification # regarding existing requests. @@ -187,13 +187,13 @@ def handle_request(request: Request, reqtype_id: int, PackageRequest.MergeBaseName == target.Name) # Build an accept list out of `accept_query`. - to_accept: List[PackageRequest] = accept_query.all() + to_accept: list[PackageRequest] = accept_query.all() accepted_ids: Set[int] = set(p.ID for p in to_accept) # Build a reject list out of `query` filtered by IDs not found # in `to_accept`. That is, unmatched records of the same base # query properties. - to_reject: List[PackageRequest] = query.filter( + to_reject: list[PackageRequest] = query.filter( ~PackageRequest.ID.in_(accepted_ids) ).all() diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 5085ddf4..bd173065 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -1,6 +1,6 @@ from collections import defaultdict from http import HTTPStatus -from typing import Dict, List, Tuple, Union +from typing import Tuple, Union import orjson @@ -15,7 +15,7 @@ from aurweb.models.package_relation import PackageRelation from aurweb.redis import redis_connection from aurweb.templates import register_filter -Providers = List[Union[PackageRelation, OfficialProvider]] +Providers = list[Union[PackageRelation, OfficialProvider]] def dep_extra_with_arch(dep: models.PackageDependency, annotation: str) -> str: @@ -123,7 +123,7 @@ def out_of_date(packages: orm.Query) -> orm.Query: def updated_packages(limit: int = 0, - cache_ttl: int = 600) -> List[models.Package]: + cache_ttl: int = 600) -> list[models.Package]: """ Return a list of valid Package objects ordered by their ModifiedTS column in descending order from cache, after setting the cache when no key yet exists. @@ -168,8 +168,8 @@ def updated_packages(limit: int = 0, return packages -def query_voted(query: List[models.Package], - user: models.User) -> Dict[int, bool]: +def query_voted(query: list[models.Package], + user: models.User) -> dict[int, bool]: """ Produce a dictionary of package base ID keys to boolean values, which indicate whether or not the package base has a vote record related to user. @@ -191,8 +191,8 @@ def query_voted(query: List[models.Package], return output -def query_notified(query: List[models.Package], - user: models.User) -> Dict[int, bool]: +def query_notified(query: list[models.Package], + user: models.User) -> dict[int, bool]: """ Produce a dictionary of package base ID keys to boolean values, which indicate whether or not the package base has a notification record related to user. @@ -214,8 +214,8 @@ def query_notified(query: List[models.Package], return output -def pkg_required(pkgname: str, provides: List[str]) \ - -> List[PackageDependency]: +def pkg_required(pkgname: str, provides: list[str]) \ + -> list[PackageDependency]: """ Get dependencies that match a string in `[pkgname] + provides`. diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 6fd55497..46609f89 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -1,5 +1,3 @@ -from typing import List - from fastapi import Request from aurweb import db, logging, util @@ -86,7 +84,7 @@ def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: def pkgbase_delete_instance(request: Request, pkgbase: PackageBase, comments: str = str()) \ - -> List[notify.Notification]: + -> list[notify.Notification]: notifs = handle_request(request, DELETION_ID, pkgbase) + [ notify.DeleteNotification(request.user.ID, pkgbase.ID) ] diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 55dbb022..5a7d952a 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any from fastapi import Request from sqlalchemy import and_ @@ -15,13 +15,13 @@ from aurweb.templates import make_variable_context as _make_variable_context async def make_variable_context(request: Request, pkgbase: PackageBase) \ - -> Dict[str, Any]: + -> dict[str, Any]: ctx = await _make_variable_context(request, pkgbase.Name) return make_context(request, pkgbase, ctx) def make_context(request: Request, pkgbase: PackageBase, - context: Dict[str, Any] = None) -> Dict[str, Any]: + context: dict[str, Any] = None) -> dict[str, Any]: """ Make a basic context for package or pkgbase. :param request: FastAPI request @@ -89,7 +89,7 @@ def remove_comaintainer(comaint: PackageComaintainer) \ return notif -def remove_comaintainers(pkgbase: PackageBase, usernames: List[str]) -> None: +def remove_comaintainers(pkgbase: PackageBase, usernames: list[str]) -> None: """ Remove comaintainers from `pkgbase`. @@ -163,7 +163,7 @@ def add_comaintainer(pkgbase: PackageBase, comaintainer: User) \ def add_comaintainers(request: Request, pkgbase: PackageBase, - usernames: List[str]) -> None: + usernames: list[str]) -> None: """ Add comaintainers to `pkgbase`. diff --git a/aurweb/pkgbase/validate.py b/aurweb/pkgbase/validate.py index 8d05a3d7..baefc415 100644 --- a/aurweb/pkgbase/validate.py +++ b/aurweb/pkgbase/validate.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any from aurweb import db from aurweb.exceptions import ValidationError @@ -7,7 +7,7 @@ from aurweb.models import PackageBase def request(pkgbase: PackageBase, type: str, comments: str, merge_into: str, - context: Dict[str, Any]) -> None: + context: dict[str, Any]) -> None: if not comments: raise ValidationError(["The comment field must not be empty."]) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index 272ee023..227d46ed 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Optional from prometheus_client import Counter from prometheus_fastapi_instrumentator import Instrumentator @@ -19,7 +19,7 @@ def instrumentator(): # Their license is included in LICENSES/starlette_exporter. # The code has been modified to remove child route checks # (since we don't have any) and to stay within an 80-width limit. -def get_matching_route_path(scope: Dict[Any, Any], routes: List[Route], +def get_matching_route_path(scope: dict[Any, Any], routes: list[Route], route_name: Optional[str] = None) -> str: """ Find a matching route and return its original path string diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index b603d22a..dcac72b0 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -2,7 +2,7 @@ import copy import typing from http import HTTPStatus -from typing import Any, Dict +from typing import Any from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse @@ -108,7 +108,7 @@ async def passreset_post(request: Request, def process_account_form(request: Request, user: models.User, - args: Dict[str, Any]): + args: dict[str, Any]): """ Process an account form. All fields are optional and only checks requirements in the case they are present. diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index f14b0ad8..7bf4e3d4 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -1,6 +1,6 @@ from collections import defaultdict from http import HTTPStatus -from typing import Any, Dict, List +from typing import Any from fastapi import APIRouter, Form, Query, Request, Response @@ -21,7 +21,7 @@ logger = logging.get_logger(__name__) router = APIRouter() -async def packages_get(request: Request, context: Dict[str, Any], +async def packages_get(request: Request, context: dict[str, Any], status_code: HTTPStatus = HTTPStatus.OK): # Query parameters used in this request. context["q"] = dict(request.query_params) @@ -210,7 +210,7 @@ async def package(request: Request, name: str, return render_template(request, "packages/show.html", context) -async def packages_unflag(request: Request, package_ids: List[int] = [], +async def packages_unflag(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: return (False, ["You did not select any packages to unflag."]) @@ -236,7 +236,7 @@ async def packages_unflag(request: Request, package_ids: List[int] = [], return (True, ["The selected packages have been unflagged."]) -async def packages_notify(request: Request, package_ids: List[int] = [], +async def packages_notify(request: Request, package_ids: list[int] = [], **kwargs): # In cases where we encounter errors with the request, we'll # use this error tuple as a return value. @@ -275,7 +275,7 @@ async def packages_notify(request: Request, package_ids: List[int] = [], return (True, ["The selected packages' notifications have been enabled."]) -async def packages_unnotify(request: Request, package_ids: List[int] = [], +async def packages_unnotify(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: # TODO: This error does not yet have a translation. @@ -312,7 +312,7 @@ async def packages_unnotify(request: Request, package_ids: List[int] = [], return (True, ["The selected packages' notifications have been removed."]) -async def packages_adopt(request: Request, package_ids: List[int] = [], +async def packages_adopt(request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs): if not package_ids: return (False, ["You did not select any packages to adopt."]) @@ -345,8 +345,8 @@ async def packages_adopt(request: Request, package_ids: List[int] = [], return (True, ["The selected packages have been adopted."]) -def disown_all(request: Request, pkgbases: List[models.PackageBase]) \ - -> List[str]: +def disown_all(request: Request, pkgbases: list[models.PackageBase]) \ + -> list[str]: errors = [] for pkgbase in pkgbases: try: @@ -356,7 +356,7 @@ def disown_all(request: Request, pkgbases: List[models.PackageBase]) \ return errors -async def packages_disown(request: Request, package_ids: List[int] = [], +async def packages_disown(request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs): if not package_ids: return (False, ["You did not select any packages to disown."]) @@ -390,7 +390,7 @@ async def packages_disown(request: Request, package_ids: List[int] = [], return (True, ["The selected packages have been disowned."]) -async def packages_delete(request: Request, package_ids: List[int] = [], +async def packages_delete(request: Request, package_ids: list[int] = [], confirm: bool = False, merge_into: str = str(), **kwargs): if not package_ids: @@ -430,7 +430,7 @@ async def packages_delete(request: Request, package_ids: List[int] = [], # A mapping of action string -> callback functions used within the # `packages_post` route below. We expect any action callback to -# return a tuple in the format: (succeeded: bool, message: List[str]). +# return a tuple in the format: (succeeded: bool, message: list[str]). PACKAGE_ACTIONS = { "unflag": packages_unflag, "notify": packages_notify, @@ -445,7 +445,7 @@ PACKAGE_ACTIONS = { @handle_form_exceptions @requires_auth async def packages_post(request: Request, - IDs: List[int] = Form(default=[]), + IDs: list[int] = Form(default=[]), action: str = Form(default=str()), confirm: bool = Form(default=False)): diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 49e98f8c..ff58063f 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -2,7 +2,7 @@ import hashlib import re from http import HTTPStatus -from typing import List, Optional +from typing import Optional from urllib.parse import unquote import orjson @@ -71,7 +71,7 @@ async def rpc_request(request: Request, type: Optional[str] = None, by: Optional[str] = defaults.RPC_SEARCH_BY, arg: Optional[str] = None, - args: Optional[List[str]] = [], + args: Optional[list[str]] = [], callback: Optional[str] = None): # Create a handle to our RPC class. @@ -140,7 +140,7 @@ async def rpc(request: Request, type: Optional[str] = Query(default=None), by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), arg: Optional[str] = Query(default=None), - args: Optional[List[str]] = Query(default=[], alias="arg[]"), + args: Optional[list[str]] = Query(default=[], alias="arg[]"), callback: Optional[str] = Query(default=None)): if not request.url.query: return documentation() @@ -157,6 +157,6 @@ async def rpc_post(request: Request, type: Optional[str] = Form(default=None), by: Optional[str] = Form(default=defaults.RPC_SEARCH_BY), arg: Optional[str] = Form(default=None), - args: Optional[List[str]] = Form(default=[], alias="arg[]"), + args: Optional[list[str]] = Form(default=[], alias="arg[]"), callback: Optional[str] = Form(default=None)): return await rpc_request(request, v, type, by, arg, args, callback) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 3f0eb836..e1267409 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -2,7 +2,7 @@ import html import typing from http import HTTPStatus -from typing import Any, Dict +from typing import Any from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import RedirectResponse, Response @@ -34,7 +34,7 @@ ADDVOTE_SPECIFICS = { } -def populate_trusted_user_counts(context: Dict[str, Any]) -> None: +def populate_trusted_user_counts(context: dict[str, Any]) -> None: tu_query = db.query(User).filter( or_(User.AccountTypeID == TRUSTED_USER_ID, User.AccountTypeID == TRUSTED_USER_AND_DEV_ID) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 5bc6b80d..f04de7d6 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -1,7 +1,7 @@ import os from collections import defaultdict -from typing import Any, Callable, Dict, List, NewType, Union +from typing import Any, Callable, NewType, Union from fastapi.responses import HTMLResponse from sqlalchemy import and_, literal, orm @@ -24,7 +24,7 @@ TYPE_MAPPING = { } DataGenerator = NewType("DataGenerator", - Callable[[models.Package], Dict[str, Any]]) + Callable[[models.Package], dict[str, Any]]) def documentation(): @@ -86,7 +86,7 @@ class RPC: self.version = version self.type = RPC.TYPE_ALIASES.get(type, type) - def error(self, message: str) -> Dict[str, Any]: + def error(self, message: str) -> dict[str, Any]: return { "version": self.version, "results": [], @@ -95,7 +95,7 @@ class RPC: "error": message } - def _verify_inputs(self, by: str = [], args: List[str] = []) -> None: + def _verify_inputs(self, by: str = [], args: list[str] = []) -> None: if self.version is None: raise RPCError("Please specify an API version.") @@ -111,11 +111,11 @@ class RPC: if self.type not in RPC.EXPOSED_TYPES: raise RPCError("Incorrect request type specified.") - def _enforce_args(self, args: List[str]) -> None: + def _enforce_args(self, args: list[str]) -> None: if not args: raise RPCError("No request type/data specified.") - def _get_json_data(self, package: models.Package) -> Dict[str, Any]: + def _get_json_data(self, package: models.Package) -> dict[str, Any]: """ Produce dictionary data of one Package that can be JSON-serialized. :param package: Package instance @@ -146,7 +146,7 @@ class RPC: "LastModified": package.ModifiedTS } - def _get_info_json_data(self, package: models.Package) -> Dict[str, Any]: + def _get_info_json_data(self, package: models.Package) -> dict[str, Any]: data = self._get_json_data(package) # All info results have _at least_ an empty list of @@ -163,9 +163,9 @@ class RPC: return data - def _assemble_json_data(self, packages: List[models.Package], + def _assemble_json_data(self, packages: list[models.Package], data_generator: DataGenerator) \ - -> List[Dict[str, Any]]: + -> list[dict[str, Any]]: """ Assemble JSON data out of a list of packages. @@ -192,8 +192,8 @@ class RPC: models.User.Username.label("Maintainer"), ).group_by(models.Package.ID) - def _handle_multiinfo_type(self, args: List[str] = [], **kwargs) \ - -> List[Dict[str, Any]]: + def _handle_multiinfo_type(self, args: list[str] = [], **kwargs) \ + -> list[dict[str, Any]]: self._enforce_args(args) args = set(args) @@ -296,7 +296,7 @@ class RPC: return self._assemble_json_data(packages, self._get_info_json_data) def _handle_search_type(self, by: str = defaults.RPC_SEARCH_BY, - args: List[str] = []) -> List[Dict[str, Any]]: + args: list[str] = []) -> list[dict[str, Any]]: # If `by` isn't maintainer and we don't have any args, raise an error. # In maintainer's case, return all orphans if there are no args, # so we need args to pass through to the handler without errors. @@ -318,12 +318,12 @@ class RPC: return self._assemble_json_data(results, self._get_json_data) - def _handle_msearch_type(self, args: List[str] = [], **kwargs)\ - -> List[Dict[str, Any]]: + def _handle_msearch_type(self, args: list[str] = [], **kwargs)\ + -> list[dict[str, Any]]: return self._handle_search_type(by="m", args=args) - def _handle_suggest_type(self, args: List[str] = [], **kwargs)\ - -> List[str]: + def _handle_suggest_type(self, args: list[str] = [], **kwargs)\ + -> list[str]: if not args: return [] @@ -336,8 +336,8 @@ class RPC: ).order_by(models.Package.Name.asc()).limit(20) return [pkg.Name for pkg in packages] - def _handle_suggest_pkgbase_type(self, args: List[str] = [], **kwargs)\ - -> List[str]: + def _handle_suggest_pkgbase_type(self, args: list[str] = [], **kwargs)\ + -> list[str]: if not args: return [] @@ -351,16 +351,16 @@ class RPC: def _is_suggestion(self) -> bool: return self.type.startswith("suggest") - def _handle_callback(self, by: str, args: List[str])\ - -> Union[List[Dict[str, Any]], List[str]]: + def _handle_callback(self, by: str, args: list[str])\ + -> Union[list[dict[str, Any]], list[str]]: # Get a handle to our callback and trap an RPCError with # an empty list of results based on callback's execution. callback = getattr(self, f"_handle_{self.type.replace('-', '_')}_type") results = callback(by=by, args=args) return results - def handle(self, by: str = defaults.RPC_SEARCH_BY, args: List[str] = [])\ - -> Union[List[Dict[str, Any]], Dict[str, Any]]: + def handle(self, by: str = defaults.RPC_SEARCH_BY, args: list[str] = [])\ + -> Union[list[dict[str, Any]], dict[str, Any]]: """ Request entrypoint. A router should pass v, type and args to this function and expect an output dictionary to be returned. diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 00096d74..888e346c 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -27,7 +27,7 @@ import sys import tempfile from collections import defaultdict -from typing import Any, Dict +from typing import Any import orjson @@ -151,7 +151,7 @@ EXTENDED_FIELD_HANDLERS = { } -def as_dict(package: Package) -> Dict[str, Any]: +def as_dict(package: Package) -> dict[str, Any]: return { "ID": package.ID, "Name": package.Name, diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index a2a796fd..637173eb 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -from typing import List - from sqlalchemy import and_, func from sqlalchemy.sql.functions import coalesce from sqlalchemy.sql.functions import sum as _sum @@ -10,7 +8,7 @@ from aurweb import db, time from aurweb.models import PackageBase, PackageVote -def run_variable(pkgbases: List[PackageBase] = []) -> None: +def run_variable(pkgbases: list[PackageBase] = []) -> None: """ Update popularity on a list of PackageBases. diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 46f2f021..c7d54c4e 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -17,7 +17,7 @@ import sys import tempfile import time -from typing import Iterable, List +from typing import Iterable import aurweb.config import aurweb.schema @@ -204,8 +204,8 @@ def start(): """) -def _kill_children(children: Iterable, exceptions: List[Exception] = []) \ - -> List[Exception]: +def _kill_children(children: Iterable, exceptions: list[Exception] = []) \ + -> list[Exception]: """ Kill each process found in `children`. @@ -223,8 +223,8 @@ def _kill_children(children: Iterable, exceptions: List[Exception] = []) \ return exceptions -def _wait_for_children(children: Iterable, exceptions: List[Exception] = []) \ - -> List[Exception]: +def _wait_for_children(children: Iterable, exceptions: list[Exception] = []) \ + -> list[Exception]: """ Wait for each process to end found in `children`. diff --git a/aurweb/testing/alpm.py b/aurweb/testing/alpm.py index 6015d859..ce30d042 100644 --- a/aurweb/testing/alpm.py +++ b/aurweb/testing/alpm.py @@ -4,8 +4,6 @@ import re import shutil import subprocess -from typing import List - from aurweb import logging, util from aurweb.templates import base_template @@ -38,7 +36,7 @@ class AlpmDatabase: return pkgdir def add(self, pkgname: str, pkgver: str, arch: str, - provides: List[str] = []) -> None: + provides: list[str] = []) -> None: context = { "pkgname": pkgname, "pkgver": pkgver, diff --git a/aurweb/testing/html.py b/aurweb/testing/html.py index f01aaf3d..8c923438 100644 --- a/aurweb/testing/html.py +++ b/aurweb/testing/html.py @@ -1,5 +1,4 @@ from io import StringIO -from typing import List from lxml import etree @@ -15,11 +14,11 @@ def parse_root(html: str) -> etree.Element: return etree.parse(StringIO(html), parser) -def get_errors(content: str) -> List[etree._Element]: +def get_errors(content: str) -> list[etree._Element]: root = parse_root(content) return root.xpath('//ul[@class="errorlist"]/li') -def get_successes(content: str) -> List[etree._Element]: +def get_successes(content: str) -> list[etree._Element]: root = parse_root(content) return root.xpath('//ul[@class="success"]/li') diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py index be13ab77..c97d1532 100644 --- a/aurweb/testing/requests.py +++ b/aurweb/testing/requests.py @@ -1,5 +1,3 @@ -from typing import Dict - import aurweb.config @@ -35,8 +33,8 @@ class Request: user: User = User(), authenticated: bool = False, method: str = "GET", - headers: Dict[str, str] = dict(), - cookies: Dict[str, str] = dict()) -> "Request": + headers: dict[str, str] = dict(), + cookies: dict[str, str] = dict()) -> "Request": self.user = user self.user.authenticated = authenticated diff --git a/aurweb/users/update.py b/aurweb/users/update.py index 5a32fd01..ffea1f2f 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any from fastapi import Request @@ -34,7 +34,7 @@ def simple(U: str = str(), E: str = str(), H: bool = False, def language(L: str = str(), request: Request = None, user: models.User = None, - context: Dict[str, Any] = {}, + context: dict[str, Any] = {}, **kwargs) -> None: if L and L != user.LangPreference: with db.begin(): @@ -45,7 +45,7 @@ def language(L: str = str(), def timezone(TZ: str = str(), request: Request = None, user: models.User = None, - context: Dict[str, Any] = {}, + context: dict[str, Any] = {}, **kwargs) -> None: if TZ and TZ != user.Timezone: with db.begin(): @@ -95,7 +95,7 @@ def account_type(T: int = None, def password(P: str = str(), request: Request = None, user: models.User = None, - context: Dict[str, Any] = {}, + context: dict[str, Any] = {}, **kwargs) -> None: if P and not user.valid_password(P): # Remove the fields we consumed for passwords. diff --git a/aurweb/util.py b/aurweb/util.py index 5138f7da..8291b578 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -6,7 +6,7 @@ import string from datetime import datetime from http import HTTPStatus from subprocess import PIPE, Popen -from typing import Callable, Iterable, List, Tuple, Union +from typing import Callable, Iterable, Tuple, Union from urllib.parse import urlparse import fastapi @@ -194,6 +194,6 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: return (prefix, key) -def parse_ssh_keys(string: str) -> List[Tuple[str, str]]: +def parse_ssh_keys(string: str) -> list[Tuple[str, str]]: """ Parse a list of SSH public keys. """ return [parse_ssh_key(e) for e in string.splitlines()] diff --git a/test/test_adduser.py b/test/test_adduser.py index c6210e74..65968d40 100644 --- a/test/test_adduser.py +++ b/test/test_adduser.py @@ -1,4 +1,3 @@ -from typing import List from unittest import mock import pytest @@ -21,7 +20,7 @@ def setup(db_test): return -def run_main(args: List[str] = []): +def run_main(args: list[str] = []): with mock.patch("sys.argv", ["aurweb-adduser"] + args): adduser.main() diff --git a/test/test_mkpkglists.py b/test/test_mkpkglists.py index 7b538e02..9bc1073b 100644 --- a/test/test_mkpkglists.py +++ b/test/test_mkpkglists.py @@ -2,7 +2,6 @@ import gzip import json import os -from typing import List from unittest import mock import py @@ -47,7 +46,7 @@ def user() -> User: @pytest.fixture -def packages(user: User) -> List[Package]: +def packages(user: User) -> list[Package]: output = [] with db.begin(): lic = db.create(License, Name="GPL") @@ -89,7 +88,7 @@ def config_mock(tmpdir: py.path.local) -> None: config.rehash() -def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packages: List[Package]): +def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packages: list[Package]): from aurweb.scripts import mkpkglists mkpkglists.main() @@ -168,7 +167,7 @@ def test_mkpkglists_extended_empty(config_mock: None): @mock.patch("sys.argv", ["mkpkglists", "--extended"]) def test_mkpkglists_extended(config_mock: None, user: User, - packages: List[Package]): + packages: list[Package]): from aurweb.scripts import mkpkglists mkpkglists.main() diff --git a/test/test_notify.py b/test/test_notify.py index fdec5ed7..bbcc6b5a 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -1,5 +1,4 @@ from logging import ERROR -from typing import List from unittest import mock import pytest @@ -46,7 +45,7 @@ def user2() -> User: @pytest.fixture -def pkgbases(user: User) -> List[PackageBase]: +def pkgbases(user: User) -> list[PackageBase]: now = time.utcnow() output = [] @@ -62,7 +61,7 @@ def pkgbases(user: User) -> List[PackageBase]: @pytest.fixture -def pkgreq(user2: User, pkgbases: List[PackageBase]): +def pkgreq(user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): pkgreq_ = db.create(PackageRequest, PackageBase=pkgbase, @@ -74,7 +73,7 @@ def pkgreq(user2: User, pkgbases: List[PackageBase]): @pytest.fixture -def packages(pkgbases: List[PackageBase]) -> List[Package]: +def packages(pkgbases: list[PackageBase]) -> list[Package]: output = [] with db.begin(): for i, pkgbase in enumerate(pkgbases): @@ -85,7 +84,7 @@ def packages(pkgbases: List[PackageBase]) -> List[Package]: def test_out_of_date(user: User, user1: User, user2: User, - pkgbases: List[PackageBase]): + pkgbases: list[PackageBase]): pkgbase = pkgbases[0] # Create two comaintainers. We'll pass the maintainer uid to # FlagNotification, so we should expect to get two emails. @@ -162,7 +161,7 @@ link does not work, try copying and pasting it into your browser. assert email.body == expected -def test_comment(user: User, user2: User, pkgbases: List[PackageBase]): +def test_comment(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): @@ -194,7 +193,7 @@ please go to the package page [2] and select "Disable notifications". assert expected == email.body -def test_update(user: User, user2: User, pkgbases: List[PackageBase]): +def test_update(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): user.UpdateNotify = 1 @@ -221,7 +220,7 @@ please go to the package page [2] and select "Disable notifications". assert expected == email.body -def test_adopt(user: User, user2: User, pkgbases: List[PackageBase]): +def test_adopt(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.AdoptNotification(user2.ID, pkgbase.ID) notif.send() @@ -241,7 +240,7 @@ The package {pkgbase.Name} [1] was adopted by {user2.Username} [2]. assert email.body == expected -def test_disown(user: User, user2: User, pkgbases: List[PackageBase]): +def test_disown(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.DisownNotification(user2.ID, pkgbase.ID) notif.send() @@ -261,7 +260,7 @@ The package {pkgbase.Name} [1] was disowned by {user2.Username} [2]. assert email.body == expected -def test_comaintainer_addition(user: User, pkgbases: List[PackageBase]): +def test_comaintainer_addition(user: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID) notif.send() @@ -280,7 +279,7 @@ You were added to the co-maintainer list of {pkgbase.Name} [1]. assert email.body == expected -def test_comaintainer_removal(user: User, pkgbases: List[PackageBase]): +def test_comaintainer_removal(user: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.ComaintainerRemoveNotification(user.ID, pkgbase.ID) notif.send() @@ -299,7 +298,7 @@ You were removed from the co-maintainer list of {pkgbase.Name} [1]. assert email.body == expected -def test_suspended_ownership_change(user: User, pkgbases: List[PackageBase]): +def test_suspended_ownership_change(user: User, pkgbases: list[PackageBase]): with db.begin(): user.Suspended = 1 @@ -314,7 +313,7 @@ def test_suspended_ownership_change(user: User, pkgbases: List[PackageBase]): assert Email.count() == 1 -def test_delete(user: User, user2: User, pkgbases: List[PackageBase]): +def test_delete(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.DeleteNotification(user2.ID, pkgbase.ID) notif.send() @@ -336,7 +335,7 @@ You will no longer receive notifications about this package. assert email.body == expected -def test_merge(user: User, user2: User, pkgbases: List[PackageBase]): +def test_merge(user: User, user2: User, pkgbases: list[PackageBase]): source, target = pkgbases[:2] notif = notify.DeleteNotification(user2.ID, source.ID, target.ID) notif.send() @@ -361,7 +360,7 @@ please go to [3] and click "Disable notifications". assert email.body == expected -def set_tu(users: List[User]) -> User: +def set_tu(users: list[User]) -> User: with db.begin(): for user in users: user.AccountTypeID = TRUSTED_USER_ID @@ -369,7 +368,7 @@ def set_tu(users: List[User]) -> User: def test_open_close_request(user: User, user2: User, pkgreq: PackageRequest, - pkgbases: List[PackageBase]): + pkgbases: list[PackageBase]): set_tu([user]) pkgbase = pkgbases[0] @@ -432,7 +431,7 @@ Request #{pkgreq.ID} has been rejected by {user2.Username} [1]. def test_close_request_comaintainer_cc(user: User, user2: User, pkgreq: PackageRequest, - pkgbases: List[PackageBase]): + pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): db.create(models.PackageComaintainer, PackageBase=pkgbase, @@ -449,7 +448,7 @@ def test_close_request_comaintainer_cc(user: User, user2: User, def test_close_request_closure_comment(user: User, user2: User, pkgreq: PackageRequest, - pkgbases: List[PackageBase]): + pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): pkgreq.ClosureComment = "This is a test closure comment." diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index e4c992af..62f89e23 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1,7 +1,6 @@ import re from http import HTTPStatus -from typing import List from unittest import mock import pytest @@ -177,7 +176,7 @@ def comment(user: User, package: Package) -> PackageComment: @pytest.fixture -def packages(maintainer: User) -> List[Package]: +def packages(maintainer: User) -> list[Package]: """ Yield 55 packages named pkg_0 .. pkg_54. """ packages_ = [] now = time.utcnow() @@ -521,7 +520,7 @@ def test_package_dependencies(client: TestClient, maintainer: User, assert broken_node.text.strip() == broken_dep.DepName -def test_packages(client: TestClient, packages: List[Package]): +def test_packages(client: TestClient, packages: list[Package]): with client as request: response = request.get("/packages", params={ "SeB": "X", # "X" isn't valid, defaults to "nd" @@ -550,7 +549,7 @@ def test_packages_empty(client: TestClient): assert results[0].text.strip() == expected -def test_packages_search_by_name(client: TestClient, packages: List[Package]): +def test_packages_search_by_name(client: TestClient, packages: list[Package]): with client as request: response = request.get("/packages", params={ "SeB": "n", @@ -565,7 +564,7 @@ def test_packages_search_by_name(client: TestClient, packages: List[Package]): def test_packages_search_by_exact_name(client: TestClient, - packages: List[Package]): + packages: list[Package]): with client as request: response = request.get("/packages", params={ "SeB": "N", @@ -594,7 +593,7 @@ def test_packages_search_by_exact_name(client: TestClient, def test_packages_search_by_pkgbase(client: TestClient, - packages: List[Package]): + packages: list[Package]): with client as request: response = request.get("/packages", params={ "SeB": "b", @@ -609,7 +608,7 @@ def test_packages_search_by_pkgbase(client: TestClient, def test_packages_search_by_exact_pkgbase(client: TestClient, - packages: List[Package]): + packages: list[Package]): with client as request: response = request.get("/packages", params={ "SeB": "B", @@ -634,7 +633,7 @@ def test_packages_search_by_exact_pkgbase(client: TestClient, def test_packages_search_by_keywords(client: TestClient, - packages: List[Package]): + packages: list[Package]): # None of our packages have keywords, so this query should return nothing. with client as request: response = request.get("/packages", params={ @@ -791,7 +790,7 @@ def test_packages_search_by_submitter(client: TestClient, assert len(rows) == 1 -def test_packages_sort_by_name(client: TestClient, packages: List[Package]): +def test_packages_sort_by_name(client: TestClient, packages: list[Package]): with client as request: response = request.get("/packages", params={ "SB": "n", # Name @@ -820,7 +819,7 @@ def test_packages_sort_by_name(client: TestClient, packages: List[Package]): def test_packages_sort_by_votes(client: TestClient, maintainer: User, - packages: List[Package]): + packages: list[Package]): # Set the first package's NumVotes to 1. with db.begin(): packages[0].PackageBase.NumVotes = 1 @@ -855,7 +854,7 @@ def test_packages_sort_by_votes(client: TestClient, def test_packages_sort_by_popularity(client: TestClient, maintainer: User, - packages: List[Package]): + packages: list[Package]): # Set the first package's Popularity to 0.50. with db.begin(): packages[0].PackageBase.Popularity = "0.50" @@ -875,7 +874,7 @@ def test_packages_sort_by_popularity(client: TestClient, def test_packages_sort_by_voted(client: TestClient, maintainer: User, - packages: List[Package]): + packages: list[Package]): now = time.utcnow() with db.begin(): db.create(PackageVote, PackageBase=packages[0].PackageBase, @@ -902,7 +901,7 @@ def test_packages_sort_by_voted(client: TestClient, def test_packages_sort_by_notify(client: TestClient, maintainer: User, - packages: List[Package]): + packages: list[Package]): db.create(PackageNotification, PackageBase=packages[0].PackageBase, User=maintainer) @@ -970,7 +969,7 @@ def test_packages_sort_by_maintainer(client: TestClient, def test_packages_sort_by_last_modified(client: TestClient, - packages: List[Package]): + packages: list[Package]): now = time.utcnow() # Set the first package's ModifiedTS to be 1000 seconds before now. package = packages[0] @@ -996,7 +995,7 @@ def test_packages_sort_by_last_modified(client: TestClient, def test_packages_flagged(client: TestClient, maintainer: User, - packages: List[Package]): + packages: list[Package]): package = packages[0] now = time.utcnow() @@ -1029,7 +1028,7 @@ def test_packages_flagged(client: TestClient, maintainer: User, assert len(rows) == 50 -def test_packages_orphans(client: TestClient, packages: List[Package]): +def test_packages_orphans(client: TestClient, packages: list[Package]): package = packages[0] with db.begin(): package.PackageBase.Maintainer = None diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index 5edae592..3468656e 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -1,7 +1,6 @@ import re from http import HTTPStatus -from typing import List from unittest import mock import pytest @@ -176,7 +175,7 @@ def comment(user: User, package: Package) -> PackageComment: @pytest.fixture -def packages(maintainer: User) -> List[Package]: +def packages(maintainer: User) -> list[Package]: """ Yield 55 packages named pkg_0 .. pkg_54. """ packages_ = [] now = time.utcnow() @@ -197,7 +196,7 @@ def packages(maintainer: User) -> List[Package]: @pytest.fixture -def requests(user: User, packages: List[Package]) -> List[PackageRequest]: +def requests(user: User, packages: list[Package]) -> list[PackageRequest]: pkgreqs = [] deletion_type = db.query(RequestType).filter( RequestType.ID == DELETION_ID diff --git a/test/test_pkgmaint.py b/test/test_pkgmaint.py index 5d6a56de..da758c22 100644 --- a/test/test_pkgmaint.py +++ b/test/test_pkgmaint.py @@ -1,5 +1,3 @@ -from typing import List - import pytest from aurweb import db, time @@ -22,7 +20,7 @@ def user() -> User: @pytest.fixture -def packages(user: User) -> List[Package]: +def packages(user: User) -> list[Package]: output = [] now = time.utcnow() @@ -37,14 +35,14 @@ def packages(user: User) -> List[Package]: yield output -def test_pkgmaint_noop(packages: List[Package]): +def test_pkgmaint_noop(packages: list[Package]): assert len(packages) == 5 pkgmaint.main() packages = db.query(Package).all() assert len(packages) == 5 -def test_pkgmaint(packages: List[Package]): +def test_pkgmaint(packages: list[Package]): assert len(packages) == 5 # Modify the first package so it's out of date and gets deleted. diff --git a/test/test_requests.py b/test/test_requests.py index 5ac558e0..b7ab3835 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -2,7 +2,6 @@ import re from http import HTTPStatus from logging import DEBUG -from typing import List import pytest @@ -91,7 +90,7 @@ def maintainer() -> User: @pytest.fixture -def packages(maintainer: User) -> List[Package]: +def packages(maintainer: User) -> list[Package]: """ Yield 55 packages named pkg_0 .. pkg_54. """ packages_ = [] now = time.utcnow() @@ -112,7 +111,7 @@ def packages(maintainer: User) -> List[Package]: @pytest.fixture -def requests(user: User, packages: List[Package]) -> List[PackageRequest]: +def requests(user: User, packages: list[Package]) -> list[PackageRequest]: pkgreqs = [] with db.begin(): for i in range(55): @@ -660,8 +659,8 @@ def test_requests_unauthorized(client: TestClient): def test_requests(client: TestClient, tu_user: User, - packages: List[Package], - requests: List[PackageRequest]): + packages: list[Package], + requests: list[PackageRequest]): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: resp = request.get("/requests", params={ @@ -697,7 +696,7 @@ def test_requests(client: TestClient, def test_requests_selfmade(client: TestClient, user: User, - requests: List[PackageRequest]): + requests: list[PackageRequest]): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: resp = request.get("/requests", cookies=cookies) diff --git a/test/test_rpc.py b/test/test_rpc.py index 2f7f7860..0e24467a 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1,7 +1,6 @@ import re from http import HTTPStatus -from typing import List from unittest import mock import orjson @@ -62,7 +61,7 @@ def user3() -> User: @pytest.fixture -def packages(user: User, user2: User, user3: User) -> List[Package]: +def packages(user: User, user2: User, user3: User) -> list[Package]: output = [] # Create package records used in our tests. @@ -123,7 +122,7 @@ def packages(user: User, user2: User, user3: User) -> List[Package]: @pytest.fixture -def depends(packages: List[Package]) -> List[PackageDependency]: +def depends(packages: list[Package]) -> list[PackageDependency]: output = [] with db.begin(): @@ -162,7 +161,7 @@ def depends(packages: List[Package]) -> List[PackageDependency]: @pytest.fixture -def relations(user: User, packages: List[Package]) -> List[PackageRelation]: +def relations(user: User, packages: list[Package]) -> list[PackageRelation]: output = [] with db.begin(): @@ -241,9 +240,9 @@ def test_rpc_documentation_missing(): def test_rpc_singular_info(client: TestClient, user: User, - packages: List[Package], - depends: List[PackageDependency], - relations: List[PackageRelation]): + packages: list[Package], + depends: list[PackageDependency], + relations: list[PackageRelation]): # Define expected response. pkg = packages[0] expected_data = { @@ -310,7 +309,7 @@ def test_rpc_nonexistent_package(client: TestClient): assert response_data["resultcount"] == 0 -def test_rpc_multiinfo(client: TestClient, packages: List[Package]): +def test_rpc_multiinfo(client: TestClient, packages: list[Package]): # Make dummy request. request_packages = ["big-chungus", "chungy-chungus"] with client as request: @@ -328,7 +327,7 @@ def test_rpc_multiinfo(client: TestClient, packages: List[Package]): assert request_packages == [] -def test_rpc_mixedargs(client: TestClient, packages: List[Package]): +def test_rpc_mixedargs(client: TestClient, packages: list[Package]): # Make dummy request. response1_packages = ["gluggly-chungus"] response2_packages = ["gluggly-chungus", "chungy-chungus"] @@ -361,9 +360,9 @@ def test_rpc_mixedargs(client: TestClient, packages: List[Package]): def test_rpc_no_dependencies_omits_key(client: TestClient, user: User, - packages: List[Package], - depends: List[PackageDependency], - relations: List[PackageRelation]): + packages: list[Package], + depends: list[PackageDependency], + relations: list[PackageRelation]): """ This makes sure things like 'MakeDepends' get removed from JSON strings when they don't have set values. @@ -517,7 +516,7 @@ def test_rpc_no_args(client: TestClient): assert expected_data == response_data -def test_rpc_no_maintainer(client: TestClient, packages: List[Package]): +def test_rpc_no_maintainer(client: TestClient, packages: list[Package]): # Make dummy request. with client as request: response = request.get("/rpc", params={ @@ -531,7 +530,7 @@ def test_rpc_no_maintainer(client: TestClient, packages: List[Package]): assert response_data["results"][0]["Maintainer"] is None -def test_rpc_suggest_pkgbase(client: TestClient, packages: List[Package]): +def test_rpc_suggest_pkgbase(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} with client as request: response = request.get("/rpc", params=params) @@ -560,7 +559,7 @@ def test_rpc_suggest_pkgbase(client: TestClient, packages: List[Package]): assert data == [] -def test_rpc_suggest(client: TestClient, packages: List[Package]): +def test_rpc_suggest(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "suggest", "arg": "other"} with client as request: response = request.get("/rpc", params=params) @@ -600,7 +599,7 @@ def mock_config_getint(section: str, key: str): @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, - pipeline: Pipeline, packages: List[Package]): + pipeline: Pipeline, packages: list[Package]): params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} for i in range(4): @@ -626,7 +625,7 @@ def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, assert response.status_code == int(HTTPStatus.OK) -def test_rpc_etag(client: TestClient, packages: List[Package]): +def test_rpc_etag(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} with client as request: @@ -647,7 +646,7 @@ def test_rpc_search_arg_too_small(client: TestClient): assert response.json().get("error") == "Query arg too small." -def test_rpc_search(client: TestClient, packages: List[Package]): +def test_rpc_search(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "search", "arg": "big"} with client as request: response = request.get("/rpc", params=params) @@ -673,7 +672,7 @@ def test_rpc_search(client: TestClient, packages: List[Package]): assert response.json().get("error") == "No request type/data specified." -def test_rpc_msearch(client: TestClient, user: User, packages: List[Package]): +def test_rpc_msearch(client: TestClient, user: User, packages: list[Package]): params = {"v": 5, "type": "msearch", "arg": user.Username} with client as request: response = request.get("/rpc", params=params) @@ -709,8 +708,8 @@ def test_rpc_msearch(client: TestClient, user: User, packages: List[Package]): assert result.get("Name") == "big-chungus" -def test_rpc_search_depends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): +def test_rpc_search_depends(client: TestClient, packages: list[Package], + depends: list[PackageDependency]): params = { "v": 5, "type": "search", "by": "depends", "arg": "chungus-depends" } @@ -722,8 +721,8 @@ def test_rpc_search_depends(client: TestClient, packages: List[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_makedepends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): +def test_rpc_search_makedepends(client: TestClient, packages: list[Package], + depends: list[PackageDependency]): params = { "v": 5, "type": "search", @@ -738,8 +737,8 @@ def test_rpc_search_makedepends(client: TestClient, packages: List[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_optdepends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): +def test_rpc_search_optdepends(client: TestClient, packages: list[Package], + depends: list[PackageDependency]): params = { "v": 5, "type": "search", @@ -754,8 +753,8 @@ def test_rpc_search_optdepends(client: TestClient, packages: List[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_checkdepends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): +def test_rpc_search_checkdepends(client: TestClient, packages: list[Package], + depends: list[PackageDependency]): params = { "v": 5, "type": "search", @@ -802,7 +801,7 @@ def test_rpc_jsonp_callback(client: TestClient): assert response.json().get("error") == "Invalid callback name." -def test_rpc_post(client: TestClient, packages: List[Package]): +def test_rpc_post(client: TestClient, packages: list[Package]): data = { "v": 5, "type": "info", @@ -816,7 +815,7 @@ def test_rpc_post(client: TestClient, packages: List[Package]): def test_rpc_too_many_search_results(client: TestClient, - packages: List[Package]): + packages: list[Package]): config_getint = config.getint def mock_config(section: str, key: str): @@ -831,7 +830,7 @@ def test_rpc_too_many_search_results(client: TestClient, assert resp.json().get("error") == "Too many package results." -def test_rpc_too_many_info_results(client: TestClient, packages: List[Package]): +def test_rpc_too_many_info_results(client: TestClient, packages: list[Package]): # Make many of these packages depend and rely on each other. # This way, we can test to see that the exceeded limit stays true # regardless of the number of related records. diff --git a/test/test_templates.py b/test/test_templates.py index 7d6b585c..e4888127 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -1,6 +1,6 @@ import re -from typing import Any, Dict +from typing import Any import pytest @@ -126,7 +126,7 @@ def test_commit_hash(): assert commit_hash not in render -def pager_context(num_packages: int) -> Dict[str, Any]: +def pager_context(num_packages: int) -> dict[str, Any]: return { "request": Request(), "singular": "%d package found.", From 1d6335363c028591d72eac40c85109d435e469cb Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 1 Aug 2022 19:02:17 +0300 Subject: [PATCH 1085/1451] fix: strip whitespace when parsing package keywords Remove all extra whitespace when parsing Keywords to ensure we don't add empty keywords in the DB. Closes: #332 Signed-off-by: Leonidas Spyropoulos --- aurweb/routers/pkgbase.py | 2 +- test/test_pkgbase_routes.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 2cef5436..6cd4199d 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -98,7 +98,7 @@ async def pkgbase_keywords(request: Request, name: str, # Lowercase all keywords. Our database table is case insensitive, # and providing CI duplicates of keywords is erroneous. - keywords = set(k.lower() for k in keywords.split(" ")) + keywords = set(k.lower() for k in keywords.split()) # Delete all keywords which are not supplied by the user. with db.begin(): diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index 3468656e..a152c590 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -1396,3 +1396,33 @@ def test_pkgbase_keywords(client: TestClient, user: User, package: Package): expected = ["abc", "test"] for i, keyword in enumerate(keywords): assert keyword.text.strip() == expected[i] + + +def test_pkgbase_empty_keywords(client: TestClient, user: User, package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}" + with client as request: + resp = request.get(endpoint) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + keywords = root.xpath('//a[@class="keyword"]') + assert len(keywords) == 0 + + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_endpoint = f"{endpoint}/keywords" + with client as request: + resp = request.post(post_endpoint, data={ + "keywords": "abc test foo bar " + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + with client as request: + resp = request.get(resp.headers.get("location")) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + keywords = root.xpath('//a[@class="keyword"]') + assert len(keywords) == 4 + expected = ["abc", "bar", "foo", "test"] + for i, keyword in enumerate(keywords): + assert keyword.text.strip() == expected[i] From 2c080b2ea9a91668e6009c690e8e46826e4d05cb Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 2 Aug 2022 20:27:47 +0300 Subject: [PATCH 1086/1451] feature: add pagination on comments Fixes: #354 Signed-off-by: Leonidas Spyropoulos --- aurweb/defaults.py | 3 +++ aurweb/pkgbase/util.py | 13 +++++++++++-- templates/partials/packages/comments.html | 8 ++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/aurweb/defaults.py b/aurweb/defaults.py index 51072e8f..91ba367a 100644 --- a/aurweb/defaults.py +++ b/aurweb/defaults.py @@ -6,6 +6,9 @@ O = 0 # Default [P]er [P]age PP = 50 +# Default Comments Per Page +COMMENTS_PER_PAGE = 10 + # A whitelist of valid PP values PP_WHITELIST = {50, 100, 250} diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 5a7d952a..5ffe490e 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -3,7 +3,7 @@ from typing import Any from fastapi import Request from sqlalchemy import and_ -from aurweb import config, db, l10n, util +from aurweb import config, db, defaults, l10n, util from aurweb.models import PackageBase, User from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comment import PackageComment @@ -31,6 +31,12 @@ def make_context(request: Request, pkgbase: PackageBase, if not context: context = _make_context(request, pkgbase.Name) + # Per page and offset. + offset, per_page = util.sanitize_params( + request.query_params.get("O", defaults.O), + request.query_params.get("PP", defaults.COMMENTS_PER_PAGE)) + context["O"] = offset + context["PP"] = per_page context["git_clone_uri_anon"] = config.get("options", "git_clone_uri_anon") context["git_clone_uri_priv"] = config.get("options", "git_clone_uri_priv") context["pkgbase"] = pkgbase @@ -44,9 +50,12 @@ def make_context(request: Request, pkgbase: PackageBase, context["packages_count"] = pkgbase.packages.count() context["keywords"] = pkgbase.keywords + context["comments_total"] = pkgbase.comments.order_by( + PackageComment.CommentTS.desc() + ).count() context["comments"] = pkgbase.comments.order_by( PackageComment.CommentTS.desc() - ) + ).limit(per_page).offset(offset) context["pinned_comments"] = pkgbase.comments.filter( PackageComment.PinnedTS != 0 ).order_by(PackageComment.CommentTS.desc()) diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index 6e6b9a47..9d49bc86 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -33,6 +33,14 @@ {{ "Latest Comments" | tr }} + {% set page = ((O / PP) | int) %} + {% set pages = ((comments_total / PP) | ceil) %} + + {% if pages > 1 %} +

      + {{ page | pager_nav(comments_total, prefix) | safe }} +

      + {% endif %} {% for comment in comments.all() %} {% include "partials/packages/comment.html" %} From 9648628a2c29397216a609b75287a3e6643e67b2 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 9 Aug 2022 16:43:27 -0700 Subject: [PATCH 1087/1451] update: requests dependency Signed-off-by: Kevin Morris --- poetry.lock | 794 +++++++++++-------------------------------------- pyproject.toml | 2 +- 2 files changed, 175 insertions(+), 621 deletions(-) diff --git a/poetry.lock b/poetry.lock index fe1575a6..72b66638 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,11 +8,11 @@ python-versions = ">=3.6,<4.0" [[package]] name = "alembic" -version = "1.7.6" +version = "1.8.1" description = "A database migration tool for SQLAlchemy." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] Mako = "*" @@ -23,7 +23,7 @@ tz = ["python-dateutil"] [[package]] name = "anyio" -version = "3.5.0" +version = "3.6.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false @@ -35,12 +35,12 @@ sniffio = ">=1.1" [package.extras] doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] [[package]] name = "asgiref" -version = "3.5.0" +version = "3.5.2" description = "ASGI specs, helper code, and adapters" category = "main" optional = false @@ -51,7 +51,7 @@ tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "main" optional = false @@ -59,17 +59,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "authlib" @@ -87,7 +87,7 @@ client = ["requests"] [[package]] name = "bcrypt" -version = "3.2.0" +version = "3.2.2" description = "Modern password hashing for your software and your servers" category = "main" optional = false @@ -95,7 +95,6 @@ python-versions = ">=3.6" [package.dependencies] cffi = ">=1.1" -six = ">=1.4.1" [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] @@ -116,15 +115,15 @@ webencodings = "*" [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "cffi" -version = "1.15.0" +version = "1.15.1" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -135,29 +134,29 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.11" +version = "2.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.3" +version = "8.1.3" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -165,21 +164,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.3.1" +version = "6.4.3" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -tomli = {version = "*", optional = true, markers = "extra == \"toml\""} +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.1" +version = "37.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -194,11 +193,11 @@ docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "dnspython" -version = "2.2.0" +version = "2.2.1" description = "DNS toolkit" category = "main" optional = false @@ -237,21 +236,20 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.7.0" +version = "1.9.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7,<4.0" [package.dependencies] -packaging = "*" -redis = "<4.1.0" -six = ">=1.12" -sortedcontainers = "*" +redis = "<4.4" +six = ">=1.16.0,<2.0.0" +sortedcontainers = ">=2.4.0,<3.0.0" [package.extras] -aioredis = ["aioredis"] -lua = ["lupa"] +aioredis = ["aioredis (>=2.0.1,<3.0.0)"] +lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "fastapi" @@ -285,7 +283,7 @@ python-dateutil = "*" [[package]] name = "filelock" -version = "3.4.2" +version = "3.7.1" description = "A platform independent file lock." category = "main" optional = false @@ -436,7 +434,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.10.1" +version = "4.12.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -446,9 +444,9 @@ python-versions = ">=3.7" zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -474,19 +472,19 @@ plugins = ["setuptools"] [[package]] name = "itsdangerous" -version = "2.0.1" +version = "2.1.2" description = "Safely pass data to untrusted environments and back." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.2" description = "A very fast and expressive template engine." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -496,7 +494,7 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lxml" -version = "4.7.1" +version = "4.9.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -510,11 +508,11 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "mako" -version = "1.1.6" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +version = "1.2.1" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=0.9.2" @@ -522,14 +520,15 @@ MarkupSafe = ">=0.9.2" [package.extras] babel = ["babel"] lingua = ["lingua"] +testing = ["pytest"] [[package]] name = "markdown" -version = "3.3.6" +version = "3.4.1" description = "Python implementation of Markdown." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} @@ -539,11 +538,11 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mccabe" @@ -555,7 +554,7 @@ python-versions = "*" [[package]] name = "mysqlclient" -version = "2.1.0" +version = "2.1.1" description = "Python interface to MySQL" category = "main" optional = false @@ -563,7 +562,7 @@ python-versions = ">=3.5" [[package]] name = "orjson" -version = "3.6.6" +version = "3.7.11" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false @@ -626,7 +625,7 @@ python-versions = ">=3.6.1" [[package]] name = "prometheus-client" -version = "0.13.1" +version = "0.14.1" description = "Python client for the Prometheus monitoring system." category = "main" optional = false @@ -637,11 +636,11 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "5.7.1" +version = "5.8.2" description = "Instrument your FastAPI with Prometheus metrics" category = "main" optional = false -python-versions = ">=3.6.0,<4.0.0" +python-versions = ">=3.7.0,<4.0.0" [package.dependencies] fastapi = ">=0.38.1,<1.0.0" @@ -649,11 +648,11 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "3.19.4" +version = "3.20.1" description = "Protocol Buffers" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [[package]] name = "py" @@ -689,8 +688,8 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" -version = "1.9.0" -description = "Data validation and settings management using python 3.6 type hinting" +version = "1.9.1" +description = "Data validation and settings management using python type hints" category = "main" optional = false python-versions = ">=3.6.1" @@ -712,25 +711,25 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygit2" -version = "1.7.2" +version = "1.10.0" description = "Python bindings for libgit2." category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -cffi = ">=1.4.0" +cffi = ">=1.9.1" [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pytest" @@ -859,21 +858,21 @@ hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "requests" -version = "2.27.1" +version = "2.28.1" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rfc3986" @@ -915,7 +914,7 @@ python-versions = "*" [[package]] name = "sqlalchemy" -version = "1.4.31" +version = "1.4.40" description = "Database Abstraction Library" category = "main" optional = false @@ -928,17 +927,18 @@ greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platfo aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3)"] -mariadb_connector = ["mariadb (>=1.0.1)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] +mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] mssql_pymssql = ["pymssql"] mssql_pyodbc = ["pyodbc"] mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] postgresql = ["psycopg2 (>=2.7)"] postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] -postgresql_pg8000 = ["pg8000 (>=1.16.6)"] +postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] postgresql_psycopg2binary = ["psycopg2-binary"] postgresql_psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql (<1)", "pymysql"] @@ -990,7 +990,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "2.0.0" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false @@ -998,22 +998,22 @@ python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.8" +version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] @@ -1043,225 +1043,76 @@ python-versions = "*" [[package]] name = "werkzeug" -version = "2.0.2" +version = "2.2.2" description = "The comprehensive WSGI web application library." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog"] [[package]] name = "wsproto" -version = "1.0.0" +version = "1.1.0" description = "WebSockets state-machine based protocol implementation" category = "main" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7.0" [package.dependencies] h11 = ">=0.9.0,<1" [[package]] name = "zipp" -version = "3.7.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "ffe7ab6733020584382d2d01950153072a46d0738f6d2fe52ac84653d0b16086" +content-hash = "7f939b59288f41a063f4a6634a61d3744b1f73e4c3bce76e97dc766b7919ffe7" [metadata.files] aiofiles = [ {file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"}, {file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"}, ] -alembic = [ - {file = "alembic-1.7.6-py3-none-any.whl", hash = "sha256:ad842f2c3ab5c5d4861232730779c05e33db4ba880a08b85eb505e87c01095bc"}, - {file = "alembic-1.7.6.tar.gz", hash = "sha256:6c0c05e9768a896d804387e20b299880fe01bc56484246b0dffe8075d6d3d847"}, -] -anyio = [ - {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, - {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, -] -asgiref = [ - {file = "asgiref-3.5.0-py3-none-any.whl", hash = "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"}, - {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] +alembic = [] +anyio = [] +asgiref = [] +atomicwrites = [] +attrs = [] authlib = [ {file = "Authlib-0.15.5-py2.py3-none-any.whl", hash = "sha256:ecf4a7a9f2508c0bb07e93a752dd3c495cfaffc20e864ef0ffc95e3f40d2abaf"}, {file = "Authlib-0.15.5.tar.gz", hash = "sha256:b83cf6360c8e92b0e9df0d1f32d675790bcc4e3c03977499b1eed24dcdef4252"}, ] -bcrypt = [ - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd"}, - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7"}, - {file = "bcrypt-3.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d"}, - {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, - {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, - {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, -] +bcrypt = [] bleach = [ {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, ] -certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, -] -cffi = [ - {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, - {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, - {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, - {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, - {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, - {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, - {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, - {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, - {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, - {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, - {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, - {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, - {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, - {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, - {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, - {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, - {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, - {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, -] -click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, -] +certifi = [] +cffi = [] +charset-normalizer = [] +click = [] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, - {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, - {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, - {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, - {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, - {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, - {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, - {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, - {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, - {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, - {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, - {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, - {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, -] -cryptography = [ - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, - {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, - {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, - {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, -] -dnspython = [ - {file = "dnspython-2.2.0-py3-none-any.whl", hash = "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44"}, - {file = "dnspython-2.2.0.tar.gz", hash = "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] +coverage = [] +cryptography = [] +dnspython = [] email-validator = [ {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, @@ -1270,10 +1121,7 @@ execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] -fakeredis = [ - {file = "fakeredis-1.7.0-py3-none-any.whl", hash = "sha256:6f1e04f64557ad3b6835bdc6e5a8d022cbace4bdc24a47ad58f6a72e0fbff760"}, - {file = "fakeredis-1.7.0.tar.gz", hash = "sha256:c9bd12e430336cbd3e189fae0e91eb99997b93e76dbfdd6ed67fa352dc684c71"}, -] +fakeredis = [] fastapi = [ {file = "fastapi-0.71.0-py3-none-any.whl", hash = "sha256:a78eca6b084de9667f2d5f37e2ae297270e5a119cd01c2f04815795da92fc87f"}, {file = "fastapi-0.71.0.tar.gz", hash = "sha256:2b5ac0ae89c80b40d1dd4b2ea0bb1f78d7c4affd3644d080bf050f084759fff2"}, @@ -1281,10 +1129,7 @@ fastapi = [ feedgen = [ {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, ] -filelock = [ - {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, - {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, -] +filelock = [] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, @@ -1382,10 +1227,7 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -importlib-metadata = [ - {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, - {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, -] +importlib-metadata = [] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1394,192 +1236,62 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -itsdangerous = [ - {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, - {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, -] +itsdangerous = [] jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, -] -lxml = [ - {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, - {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, - {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, - {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, - {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, - {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, - {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, - {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, - {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, - {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, - {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, - {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, - {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, - {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, - {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, - {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, - {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, - {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, - {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, - {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, - {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, - {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, - {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, - {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, - {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, - {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, -] -mako = [ - {file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"}, - {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, -] -markdown = [ - {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, - {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] +lxml = [] +mako = [] +markdown = [] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -mysqlclient = [ - {file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"}, - {file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"}, - {file = "mysqlclient-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2c8410f54492a3d2488a6a53e2d85b7e016751a1e7d116e7aea9c763f59f5e8c"}, - {file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"}, - {file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"}, -] -orjson = [ - {file = "orjson-3.6.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:e4a7cad6c63306318453980d302c7c0b74c0cc290dd1f433bbd7d31a5af90cf1"}, - {file = "orjson-3.6.6-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e533941dca4a0530a876de32e54bf2fd3269cdec3751aebde7bfb5b5eba98e74"}, - {file = "orjson-3.6.6-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:9adf63be386eaa34278967512b83ff8fc4bed036a246391ae236f68d23c47452"}, - {file = "orjson-3.6.6-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:3b636753ae34d4619b11ea7d664a2f1e87e55e9738e5123e12bcce22acae9d13"}, - {file = "orjson-3.6.6-cp310-none-win_amd64.whl", hash = "sha256:78a10295ed048fd916c6584d6d27c232eae805a43e7c14be56e3745f784f0eb6"}, - {file = "orjson-3.6.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:82b4f9fb2af7799b52932a62eac484083f930d5519560d6f64b24d66a368d03f"}, - {file = "orjson-3.6.6-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a0033d07309cc7d8b8c4bc5d42f0dd4422b53ceb91dee9f4086bb2afa70b7772"}, - {file = "orjson-3.6.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b321f99473116ab7c7c028377372f7b4adba4029aaca19cd567e83898f55579"}, - {file = "orjson-3.6.6-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:b9c98ed94f1688cc11b5c61b8eea39d854a1a2f09f71d8a5af005461b14994ed"}, - {file = "orjson-3.6.6-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:00b333a41392bd07a8603c42670547dbedf9b291485d773f90c6470eff435608"}, - {file = "orjson-3.6.6-cp37-none-win_amd64.whl", hash = "sha256:8d4fd3bdee65a81f2b79c50937d4b3c054e1e6bfa3fc72ed018a97c0c7c3d521"}, - {file = "orjson-3.6.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:954c9f8547247cd7a8c91094ff39c9fe314b5eaeaec90b7bfb7384a4108f416f"}, - {file = "orjson-3.6.6-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:74e5aed657ed0b91ef05d44d6a26d3e3e12ce4d2d71f75df41a477b05878c4a9"}, - {file = "orjson-3.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4008a5130e6e9c33abaa95e939e0e755175da10745740aa6968461b2f16830e2"}, - {file = "orjson-3.6.6-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:012761d5f3d186deb4f6238f15e9ea7c1aac6deebc8f5b741ba3b4fafe017460"}, - {file = "orjson-3.6.6-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:b464546718a940b48d095a98df4c04808bfa6c8706fe751fc3f9390bc2f82643"}, - {file = "orjson-3.6.6-cp38-none-win_amd64.whl", hash = "sha256:f10a800f4e5a4aab52076d4628e9e4dab9370bdd9d8ea254ebfde846b653ab25"}, - {file = "orjson-3.6.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:8010d2610cfab721725ef14d578c7071e946bbdae63322d8f7b49061cf3fde8d"}, - {file = "orjson-3.6.6-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8dca67a4855e1e0f9a2ea0386e8db892708522e1171dc0ddf456932288fbae63"}, - {file = "orjson-3.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af065d60523139b99bd35b839c7a2d8c5da55df8a8c4402d2eb6cdc07fa7a624"}, - {file = "orjson-3.6.6-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:fa1f389cc9f766ae0cf7ba3533d5089836b01a5ccb3f8d904297f1fcf3d9dc34"}, - {file = "orjson-3.6.6-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:ec1221ad78f94d27b162a1d35672b62ef86f27f0e4c2b65051edb480cc86b286"}, - {file = "orjson-3.6.6-cp39-none-win_amd64.whl", hash = "sha256:afed2af55eeda1de6b3f1cbc93431981b19d380fcc04f6ed86e74c1913070304"}, - {file = "orjson-3.6.6.tar.gz", hash = "sha256:55dd988400fa7fbe0e31407c683f5aaab013b5bd967167b8fe058186773c4d6c"}, -] +mysqlclient = [] +orjson = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1606,42 +1318,9 @@ priority = [ {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, ] -prometheus-client = [ - {file = "prometheus_client-0.13.1-py3-none-any.whl", hash = "sha256:357a447fd2359b0a1d2e9b311a0c5778c330cfbe186d880ad5a6b39884652316"}, - {file = "prometheus_client-0.13.1.tar.gz", hash = "sha256:ada41b891b79fca5638bd5cfe149efa86512eaa55987893becd2c6d8d0a5dfc5"}, -] -prometheus-fastapi-instrumentator = [ - {file = "prometheus-fastapi-instrumentator-5.7.1.tar.gz", hash = "sha256:5371f1b494e2b00017a02898d854119b4929025d1a203670b07b3f42dd0b5526"}, - {file = "prometheus_fastapi_instrumentator-5.7.1-py3-none-any.whl", hash = "sha256:da40ea0df14b0e95d584769747fba777522a8df6a8c47cec2edf798f1fff49b5"}, -] -protobuf = [ - {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, - {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, - {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, - {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, - {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, - {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, - {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, - {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, - {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, - {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, - {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, - {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, - {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, - {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, - {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, - {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, - {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, - {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, -] +prometheus-client = [] +prometheus-fastapi-instrumentator = [] +protobuf = [] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1657,82 +1336,15 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -pydantic = [ - {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, - {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, - {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, - {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, - {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, - {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, - {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, - {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, - {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, - {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, - {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, -] +pydantic = [] pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] -pygit2 = [ - {file = "pygit2-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4a9a031bb0d2c5cf964da1f6d7a193416a97664655ec43ec349d3609bbde154"}, - {file = "pygit2-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:afcfb8ba97cfedcb8f890ff1e74c4d63755234cca1ca22c2a969e91b8059ae3e"}, - {file = "pygit2-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f87498ce717302a7525dad1ee604badc85bdff7bd453473d601077ac58e7cae"}, - {file = "pygit2-1.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2355cf24719a35542a88075988c8e11cd155aa375a1f12d62b980986da992eb4"}, - {file = "pygit2-1.7.2-cp310-cp310-win32.whl", hash = "sha256:0d72bd05dd3cf514ea2e2df32a2d361f6f29da7d5f02cf0980ea149f49cdfb37"}, - {file = "pygit2-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:1b7ff5b656db280ca5d31ecdb17709ed7eaf4e9f419b2fa66f7ff92d8234f621"}, - {file = "pygit2-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6aa018c101056c2a8e1fb6467c10281afa088b3b7bc7c17defb404f66039669a"}, - {file = "pygit2-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a27f8cab6dbef912ccdd690b97948dbf978cffc2ef96ee01b1a8944bfb713f0b"}, - {file = "pygit2-1.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c538a0234baa091a02342895d31e5b7c29d85ada44a0b9b4a5fdf78b5607cd48"}, - {file = "pygit2-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:b15579b69381ba41199f5eb7fc85f153105d535c91b8da0321aaa14fec19f09c"}, - {file = "pygit2-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6c2ee00048862e193b2b88267f880632735f53db0f2c7f9ebebb21a43d22e58b"}, - {file = "pygit2-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c24f3413522c970ae46e79b645ac0978a5be98863a6c6619e8f710bb137e1cb"}, - {file = "pygit2-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d42a7cc4b53cc369b82266c7257fe1808ec0e30c34f1796a0b0fa12a0db9ebe"}, - {file = "pygit2-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b1694ad8b4702e9e83a79a97bf3f1b44674057ae9d40bc7eb92e4b4baf79d94"}, - {file = "pygit2-1.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a382db82ad4ba3109e74c7b82d6c6c1e451200ee379bad8a17936027c65ea98"}, - {file = "pygit2-1.7.2-cp38-cp38-win32.whl", hash = "sha256:6c168efd7e3bdaeeccfa5ccbe2718107a1fe65cda959586f88a73228217a8783"}, - {file = "pygit2-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:041e34f7efd96c7edbea2f478704756fc189082561611c88bc95cf2d085923b5"}, - {file = "pygit2-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ef34b881da55b6702087575ea48a90a08e1077a7f64faa909d9840e16f36c63b"}, - {file = "pygit2-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e6368a96058cf619ad574de2b4575f58d363f3f6d4de8e172e1e5d10e1fad36"}, - {file = "pygit2-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0748b413966da9b3d3ca8a0a79c63f6581a89b883d2ba64355bbfdb250f2e066"}, - {file = "pygit2-1.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d34954c21f109f176d8104b253fc8ce2ca17efb43cfe228d6530c200f362b83"}, - {file = "pygit2-1.7.2-cp39-cp39-win32.whl", hash = "sha256:32979cb98ffd183ed0458c519e6615deeb6a8cc1252223396eee8f526f09989f"}, - {file = "pygit2-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:56d55452dc3eca844d92503d755c8e11699b7ab3b845b81cf365f85d6385d7e0"}, - {file = "pygit2-1.7.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:409c76dea47c2c678295c42f55798da7a0a9adcc6394fe75c061864254bafeef"}, - {file = "pygit2-1.7.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be038fecd27a9a7046cd45b4a6e847955dab2d6e2352ff41ab3b55f700aa0f3d"}, - {file = "pygit2-1.7.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3e91afd629b90b528b756ca2a0fbd5bf8df2cdc08ccd5ab144fbfe69bfc587d"}, - {file = "pygit2-1.7.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:17b06a1ecc16b90fa652cf5cf9698dfb16a87501b76f7001e1d4934a38a49737"}, - {file = "pygit2-1.7.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fe7cdd56d0e5a89ed7754d1aedc6516349f16072225ccfc7b9349ab6448a052"}, - {file = "pygit2-1.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2a766b5a988ab373a040d1769e0e1df4618a9f8f33464746b9b2a3c92576df4"}, - {file = "pygit2-1.7.2.tar.gz", hash = "sha256:70a4536a35452c31f823b59b6fdb665aa3778a43b73ccda3a4f79fa9962ad2bb"}, -] +pygit2 = [] pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -1769,10 +1381,7 @@ redis = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] -requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] +requests = [] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, @@ -1789,44 +1398,7 @@ sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] -sqlalchemy = [ - {file = "SQLAlchemy-1.4.31-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c3abc34fed19fdeaead0ced8cf56dd121f08198008c033596aa6aae7cc58f59f"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8d0949b11681380b4a50ac3cd075e4816afe9fa4a8c8ae006c1ca26f0fa40ad8"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-win32.whl", hash = "sha256:f3b7ec97e68b68cb1f9ddb82eda17b418f19a034fa8380a0ac04e8fe01532875"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-win_amd64.whl", hash = "sha256:81f2dd355b57770fdf292b54f3e0a9823ec27a543f947fa2eb4ec0df44f35f0d"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ad31cec8b49fd718470328ad9711f4dc703507d434fd45461096da0a7135ee0"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:05fa14f279d43df68964ad066f653193187909950aa0163320b728edfc400167"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dccff41478050e823271642837b904d5f9bda3f5cf7d371ce163f00a694118d6"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57205844f246bab9b666a32f59b046add8995c665d9ecb2b7b837b087df90639"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8210090a816d48a4291a47462bac750e3bc5c2442e6d64f7b8137a7c3f9ac5"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-win32.whl", hash = "sha256:2e216c13ecc7fcdcbb86bb3225425b3ed338e43a8810c7089ddb472676124b9b"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-win_amd64.whl", hash = "sha256:e3a86b59b6227ef72ffc10d4b23f0fe994bef64d4667eab4fb8cd43de4223bec"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2fd4d3ca64c41dae31228b80556ab55b6489275fb204827f6560b65f95692cf3"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f22c040d196f841168b1456e77c30a18a3dc16b336ddbc5a24ce01ab4e95ae0"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0c7171aa5a57e522a04a31b84798b6c926234cb559c0939840c3235cf068813"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d046a9aeba9bc53e88a41e58beb72b6205abb9a20f6c136161adf9128e589db5"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-win32.whl", hash = "sha256:d86132922531f0dc5a4f424c7580a472a924dd737602638e704841c9cb24aea2"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-win_amd64.whl", hash = "sha256:ca68c52e3cae491ace2bf39b35fef4ce26c192fd70b4cd90f040d419f70893b5"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:cf2cd387409b12d0a8b801610d6336ee7d24043b6dd965950eaec09b73e7262f"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4b15fb1f0aafa65cbdc62d3c2078bea1ceecbfccc9a1f23a2113c9ac1191fa"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c317ddd7c586af350a6aef22b891e84b16bff1a27886ed5b30f15c1ed59caeaa"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c7ed6c69debaf6198fadb1c16ae1253a29a7670bbf0646f92582eb465a0b999"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-win32.whl", hash = "sha256:6a01ec49ca54ce03bc14e10de55dfc64187a2194b3b0e5ac0fdbe9b24767e79e"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-win_amd64.whl", hash = "sha256:330eb45395874cc7787214fdd4489e2afb931bc49e0a7a8f9cd56d6e9c5b1639"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5e9c7b3567edbc2183607f7d9f3e7e89355b8f8984eec4d2cd1e1513c8f7b43f"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de85c26a5a1c72e695ab0454e92f60213b4459b8d7c502e0be7a6369690eeb1a"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:975f5c0793892c634c4920057da0de3a48bbbbd0a5c86f5fcf2f2fedf41b76da"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5c20c8415173b119762b6110af64448adccd4d11f273fb9f718a9865b88a99c"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-win32.whl", hash = "sha256:b35dca159c1c9fa8a5f9005e42133eed82705bf8e243da371a5e5826440e65ca"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-win_amd64.whl", hash = "sha256:b7b20c88873675903d6438d8b33fba027997193e274b9367421e610d9da76c08"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:85e4c244e1de056d48dae466e9baf9437980c19fcde493e0db1a0a986e6d75b4"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79e73d5ee24196d3057340e356e6254af4d10e1fc22d3207ea8342fc5ffb977"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:15a03261aa1e68f208e71ae3cd845b00063d242cbf8c87348a0c2c0fc6e1f2ac"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ddc5e5ccc0160e7ad190e5c61eb57560f38559e22586955f205e537cda26034"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-win32.whl", hash = "sha256:289465162b1fa1e7a982f8abe59d26a8331211cad4942e8031d2b7db1f75e649"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-win_amd64.whl", hash = "sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f"}, - {file = "SQLAlchemy-1.4.31.tar.gz", hash = "sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418"}, -] +sqlalchemy = [] srcinfo = [ {file = "srcinfo-0.0.8-py3-none-any.whl", hash = "sha256:0922ee4302b927d7ddea74c47e539b226a0a7738dc89f95b66404a28d07f3f6b"}, {file = "srcinfo-0.0.8.tar.gz", hash = "sha256:5ac610cf8b15d4b0a0374bd1f7ad301675c2938f0414addf3ef7d7e3fcaf5c65"}, @@ -1843,18 +1415,9 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -tomli = [ - {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, - {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, -] -typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, -] -urllib3 = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, -] +tomli = [] +typing-extensions = [] +urllib3 = [] uvicorn = [ {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, @@ -1863,15 +1426,6 @@ webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -werkzeug = [ - {file = "Werkzeug-2.0.2-py3-none-any.whl", hash = "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f"}, - {file = "Werkzeug-2.0.2.tar.gz", hash = "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a"}, -] -wsproto = [ - {file = "wsproto-1.0.0-py3-none-any.whl", hash = "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"}, - {file = "wsproto-1.0.0.tar.gz", hash = "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38"}, -] -zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, -] +werkzeug = [] +wsproto = [] +zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 41d8301f..656a854b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ protobuf = "^3.19.0" pygit2 = "^1.7.0" python-multipart = "^0.0.5" redis = "^3.5.3" -requests = "^2.26.0" +requests = "^2.28.1" paginate = "^0.5.6" # SQL From 0e82916b0a149e81b0936eb5edcfd387798e9481 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Tue, 2 Aug 2022 16:30:45 +0200 Subject: [PATCH 1088/1451] fix(python): don't show maintainer link for non logged in users Show a plain maintainer text for non logged in users like the submitted, last packager. Closes #373 --- templates/partials/packages/details.html | 5 ++++- test/test_pkgbase_routes.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index e0eda54c..771b311d 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -108,7 +108,7 @@

      diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index a152c590..dae43e37 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -272,9 +272,9 @@ def test_pkgbase_maintainer(client: TestClient, user: User, maintainer: User, root = parse_root(resp.text) maint = root.xpath('//table[@id="pkginfo"]/tr[@class="pkgmaint"]/td')[0] - maint, comaint = maint.xpath('./a') - assert maint.text.strip() == maintainer.Username - assert comaint.text.strip() == user.Username + maint, comaint = maint.text.strip().split() + assert maint == maintainer.Username + assert comaint == f'({user.Username})' def test_pkgbase_voters(client: TestClient, tu_user: User, package: Package): From 913ce8a4f0cac79fcd089cb8a66a6aa1b02be601 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 12 Aug 2022 19:58:55 -0700 Subject: [PATCH 1089/1451] fix(performance): lazily load expensive modules within aurweb.db Closes #374 Signed-off-by: Kevin Morris --- aurweb/db.py | 81 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/aurweb/db.py b/aurweb/db.py index 4c53730a..94514d35 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,34 +1,15 @@ -import functools -import hashlib -import math -import os -import re - -from typing import Iterable, NewType - -import sqlalchemy - -from sqlalchemy import create_engine, event -from sqlalchemy.engine.base import Engine -from sqlalchemy.engine.url import URL -from sqlalchemy.orm import Query, Session, SessionTransaction, scoped_session, sessionmaker - -import aurweb.config -import aurweb.util - +# Supported database drivers. DRIVERS = { "mysql": "mysql+mysqldb" } -# Some types we don't get access to in this module. -Base = NewType("Base", "aurweb.models.declarative_base.Base") - def make_random_value(table: str, column: str, length: int): """ Generate a unique, random value for a string column in a table. :return: A unique string that is not in the database """ + import aurweb.util string = aurweb.util.make_random_string(length) while query(table).filter(column == string).first(): string = aurweb.util.make_random_string(length) @@ -52,6 +33,10 @@ def test_name() -> str: :return: Unhashed database name """ + import os + + import aurweb.config + db = os.environ.get("PYTEST_CURRENT_TEST", aurweb.config.get("database", "name")) return db.split(":")[0] @@ -70,7 +55,10 @@ def name() -> str: dbname = test_name() if not dbname.startswith("test/"): return dbname + + import hashlib sha1 = hashlib.sha1(dbname.encode()).hexdigest() + return "db" + sha1 @@ -78,12 +66,13 @@ def name() -> str: _sessions = dict() -def get_session(engine: Engine = None) -> Session: +def get_session(engine=None): """ Return aurweb.db's global session. """ dbname = name() global _sessions if dbname not in _sessions: + from sqlalchemy.orm import scoped_session, sessionmaker if not engine: # pragma: no cover engine = get_engine() @@ -106,13 +95,17 @@ def pop_session(dbname: str) -> None: _sessions.pop(dbname) -def refresh(model: Base) -> Base: - """ Refresh the session's knowledge of `model`. """ +def refresh(model): + """ + Refresh the session's knowledge of `model`. + + :returns: Passed in `model` + """ get_session().refresh(model) return model -def query(Model: Base, *args, **kwargs) -> Query: +def query(Model, *args, **kwargs): """ Perform an ORM query against the database session. @@ -124,7 +117,7 @@ def query(Model: Base, *args, **kwargs) -> Query: return get_session().query(Model).filter(*args, **kwargs) -def create(Model: Base, *args, **kwargs) -> Base: +def create(Model, *args, **kwargs): """ Create a record and add() it to the database session. @@ -135,7 +128,7 @@ def create(Model: Base, *args, **kwargs) -> Base: return add(instance) -def delete(model: Base) -> None: +def delete(model) -> None: """ Delete a set of records found by Query.filter(*args, **kwargs). @@ -144,8 +137,9 @@ def delete(model: Base) -> None: get_session().delete(model) -def delete_all(iterable: Iterable) -> None: +def delete_all(iterable) -> None: """ Delete each instance found in `iterable`. """ + import aurweb.util session_ = get_session() aurweb.util.apply_all(iterable, session_.delete) @@ -155,23 +149,29 @@ def rollback() -> None: get_session().rollback() -def add(model: Base) -> Base: +def add(model): """ Add `model` to the database session. """ get_session().add(model) return model -def begin() -> SessionTransaction: +def begin(): """ Begin an SQLAlchemy SessionTransaction. """ return get_session().begin() -def get_sqlalchemy_url() -> URL: +def get_sqlalchemy_url(): """ Build an SQLAlchemy URL for use with create_engine. :return: sqlalchemy.engine.url.URL """ + import sqlalchemy + + from sqlalchemy.engine.url import URL + + import aurweb.config + constructor = URL parts = sqlalchemy.__version__.split('.') @@ -209,13 +209,17 @@ def get_sqlalchemy_url() -> URL: def sqlite_regexp(regex, item) -> bool: # pragma: no cover """ Method which mimics SQL's REGEXP for SQLite. """ + import re return bool(re.search(regex, str(item))) -def setup_sqlite(engine: Engine) -> None: # pragma: no cover +def setup_sqlite(engine) -> None: # pragma: no cover """ Perform setup for an SQLite engine. """ + from sqlalchemy import event + @event.listens_for(engine, "connect") def do_begin(conn, record): + import functools create_deterministic_function = functools.partial( conn.create_function, deterministic=True @@ -227,7 +231,7 @@ def setup_sqlite(engine: Engine) -> None: # pragma: no cover _engines = dict() -def get_engine(dbname: str = None, echo: bool = False) -> Engine: +def get_engine(dbname: str = None, echo: bool = False): """ Return the SQLAlchemy engine for `dbname`. @@ -238,6 +242,8 @@ def get_engine(dbname: str = None, echo: bool = False) -> Engine: :param echo: Flag passed through to sqlalchemy.create_engine :return: SQLAlchemy Engine instance """ + import aurweb.config + if not dbname: dbname = name() @@ -254,6 +260,7 @@ def get_engine(dbname: str = None, echo: bool = False) -> Engine: "echo": echo, "connect_args": connect_args } + from sqlalchemy import create_engine _engines[dbname] = create_engine(get_sqlalchemy_url(), **kwargs) if is_sqlite: # pragma: no cover @@ -301,7 +308,10 @@ class ConnectionExecutor: _conn = None _paramstyle = None - def __init__(self, conn, backend=aurweb.config.get("database", "backend")): + def __init__(self, conn, backend=None): + import aurweb.config + + backend = backend or aurweb.config.get("database", "backend") self._conn = conn if backend == "mysql": self._paramstyle = "format" @@ -339,6 +349,7 @@ class Connection: _conn = None def __init__(self): + import aurweb.config aur_db_backend = aurweb.config.get('database', 'backend') if aur_db_backend == 'mysql': @@ -357,7 +368,9 @@ class Connection: elif aur_db_backend == 'sqlite': # pragma: no cover # TODO: SQLite support has been removed in FastAPI. It remains # here to fund its support for PHP until it is removed. + import math import sqlite3 + aur_db_name = aurweb.config.get('database', 'name') self._conn = sqlite3.connect(aur_db_name) self._conn.create_function("POWER", 2, math.pow) From 1a7f6e1fa9f500fead3650ef1e4ec9521884e1d8 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 12 Aug 2022 21:37:34 -0700 Subject: [PATCH 1090/1451] feat(db): add an index for SSHPubKeys.PubKey Speeds up SSHPubKeys.PubKey searches in a larger database. Signed-off-by: Kevin Morris --- aurweb/schema.py | 1 + ...d70103d2e82_add_sshpubkeys_pubkey_index.py | 28 +++++++++++++++++++ test/test_migration.py | 23 +++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py create mode 100644 test/test_migration.py diff --git a/aurweb/schema.py b/aurweb/schema.py index d2644541..e1373bf4 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -87,6 +87,7 @@ SSHPubKeys = Table( Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), Column('Fingerprint', String(44), primary_key=True), Column('PubKey', String(4096), nullable=False), + Index('SSHPubKeysPubKey', 'PubKey'), mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', ) diff --git a/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py b/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py new file mode 100644 index 00000000..61e4dc79 --- /dev/null +++ b/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py @@ -0,0 +1,28 @@ +"""add SSHPubKeys.PubKey index + +Revision ID: dd70103d2e82 +Revises: d64e5571bc8d +Create Date: 2022-08-12 21:30:26.155465 + +""" +import traceback + +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'dd70103d2e82' +down_revision = 'd64e5571bc8d' +branch_labels = None +depends_on = None + + +def upgrade(): + try: + op.create_index("SSHPubKeysPubKey", "SSHPubKeys", ["PubKey"]) + except Exception: + traceback.print_exc() + print("failing silently...") + + +def downgrade(): + op.drop_index("SSHPubKeysPubKey", "SSHPubKeys") diff --git a/test/test_migration.py b/test/test_migration.py new file mode 100644 index 00000000..cf8702fa --- /dev/null +++ b/test/test_migration.py @@ -0,0 +1,23 @@ +import pytest + +from sqlalchemy import inspect + +from aurweb.db import get_engine +from aurweb.models.ssh_pub_key import SSHPubKey + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_sshpubkeys_pubkey_index(): + insp = inspect(get_engine()) + indexes = insp.get_indexes(SSHPubKey.__tablename__) + + found_pk = False + for idx in indexes: + if idx.get("name") == "SSHPubKeysPubKey": + assert idx.get("column_names") == ["PubKey"] + found_pk = True + assert found_pk From 5abd5db313c871678bcf54e7a2c2a0fc056401b0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 13 Aug 2022 19:23:19 -0700 Subject: [PATCH 1091/1451] Revert "feat(db): add an index for SSHPubKeys.PubKey" This reverts commit 1a7f6e1fa9f500fead3650ef1e4ec9521884e1d8. This commit broke account creation in some way. We'd still like to do this, but we need to ensure it does not intrude on other facets. Extra: We should really work out how this even passed tests; it should not have. --- aurweb/schema.py | 1 - ...d70103d2e82_add_sshpubkeys_pubkey_index.py | 28 ------------------- test/test_migration.py | 23 --------------- 3 files changed, 52 deletions(-) delete mode 100644 migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py delete mode 100644 test/test_migration.py diff --git a/aurweb/schema.py b/aurweb/schema.py index e1373bf4..d2644541 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -87,7 +87,6 @@ SSHPubKeys = Table( Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), Column('Fingerprint', String(44), primary_key=True), Column('PubKey', String(4096), nullable=False), - Index('SSHPubKeysPubKey', 'PubKey'), mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', ) diff --git a/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py b/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py deleted file mode 100644 index 61e4dc79..00000000 --- a/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py +++ /dev/null @@ -1,28 +0,0 @@ -"""add SSHPubKeys.PubKey index - -Revision ID: dd70103d2e82 -Revises: d64e5571bc8d -Create Date: 2022-08-12 21:30:26.155465 - -""" -import traceback - -from alembic import op - -# revision identifiers, used by Alembic. -revision = 'dd70103d2e82' -down_revision = 'd64e5571bc8d' -branch_labels = None -depends_on = None - - -def upgrade(): - try: - op.create_index("SSHPubKeysPubKey", "SSHPubKeys", ["PubKey"]) - except Exception: - traceback.print_exc() - print("failing silently...") - - -def downgrade(): - op.drop_index("SSHPubKeysPubKey", "SSHPubKeys") diff --git a/test/test_migration.py b/test/test_migration.py deleted file mode 100644 index cf8702fa..00000000 --- a/test/test_migration.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from sqlalchemy import inspect - -from aurweb.db import get_engine -from aurweb.models.ssh_pub_key import SSHPubKey - - -@pytest.fixture(autouse=True) -def setup(db_test): - return - - -def test_sshpubkeys_pubkey_index(): - insp = inspect(get_engine()) - indexes = insp.get_indexes(SSHPubKey.__tablename__) - - found_pk = False - for idx in indexes: - if idx.get("name") == "SSHPubKeysPubKey": - assert idx.get("column_names") == ["PubKey"] - found_pk = True - assert found_pk From 6c7e2749688100a10ac7de1d422b8c4cee98f393 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 13 Aug 2022 19:52:50 -0700 Subject: [PATCH 1092/1451] feat(db): add an index for SSHPubKeys.PubKey (#2) Speeds up SSHPubKeys.PubKey searches in a larger database. Fixed form of the original commit which was reverted, 1a7f6e1fa9f500fead3650ef1e4ec9521884e1d8 Signed-off-by: Kevin Morris --- aurweb/schema.py | 2 ++ ...d70103d2e82_add_sshpubkeys_pubkey_index.py | 35 +++++++++++++++++++ test/test_migration.py | 23 ++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py create mode 100644 test/test_migration.py diff --git a/aurweb/schema.py b/aurweb/schema.py index d2644541..3d8369c9 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -87,6 +87,8 @@ SSHPubKeys = Table( Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), Column('Fingerprint', String(44), primary_key=True), Column('PubKey', String(4096), nullable=False), + Index('SSHPubKeysUserID', 'UserID'), + Index('SSHPubKeysPubKey', 'PubKey'), mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', ) diff --git a/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py b/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py new file mode 100644 index 00000000..7d3f4b59 --- /dev/null +++ b/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py @@ -0,0 +1,35 @@ +"""add SSHPubKeys.PubKey index + +Revision ID: dd70103d2e82 +Revises: d64e5571bc8d +Create Date: 2022-08-12 21:30:26.155465 + +""" +import traceback + +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'dd70103d2e82' +down_revision = 'd64e5571bc8d' +branch_labels = None +depends_on = None + + +def upgrade(): + try: + op.create_index("SSHPubKeysUserID", "SSHPubKeys", ["UserID"]) + except Exception: + traceback.print_exc() + print("failing silently...") + + try: + op.create_index("SSHPubKeysPubKey", "SSHPubKeys", ["PubKey"]) + except Exception: + traceback.print_exc() + print("failing silently...") + + +def downgrade(): + op.drop_index("SSHPubKeysPubKey", "SSHPubKeys") + op.drop_index("SSHPubKeysUserID", "SSHPubKeys") diff --git a/test/test_migration.py b/test/test_migration.py new file mode 100644 index 00000000..cf8702fa --- /dev/null +++ b/test/test_migration.py @@ -0,0 +1,23 @@ +import pytest + +from sqlalchemy import inspect + +from aurweb.db import get_engine +from aurweb.models.ssh_pub_key import SSHPubKey + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_sshpubkeys_pubkey_index(): + insp = inspect(get_engine()) + indexes = insp.get_indexes(SSHPubKey.__tablename__) + + found_pk = False + for idx in indexes: + if idx.get("name") == "SSHPubKeysPubKey": + assert idx.get("column_names") == ["PubKey"] + found_pk = True + assert found_pk From 952c24783baa6c5924c5aaf2b9d9003866284657 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 13 Aug 2022 20:12:58 -0700 Subject: [PATCH 1093/1451] fix(docker): apply chown each time sshd is started Signed-off-by: Kevin Morris --- docker/scripts/run-sshd.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker/scripts/run-sshd.sh b/docker/scripts/run-sshd.sh index d488e80d..45bd0e08 100755 --- a/docker/scripts/run-sshd.sh +++ b/docker/scripts/run-sshd.sh @@ -1,2 +1,7 @@ #!/bin/bash + +# Update this every time. +chown -R aur:aur /aurweb/aur.git + +# Start up sshd exec /usr/sbin/sshd -e -p 2222 -D From 829a8b4b813c23e2e85dad4e7aca00ffba86601d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 13 Aug 2022 20:56:43 -0700 Subject: [PATCH 1094/1451] Revert "fix(docker): apply chown each time sshd is started" This reverts commit 952c24783baa6c5924c5aaf2b9d9003866284657. The issue found was actually: - If `./aur.git` exists within the aurweb repository locally, it also ends up in the destination, stopping the aurweb_git_data volume from being mounted properly. --- docker/scripts/run-sshd.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docker/scripts/run-sshd.sh b/docker/scripts/run-sshd.sh index 45bd0e08..d488e80d 100755 --- a/docker/scripts/run-sshd.sh +++ b/docker/scripts/run-sshd.sh @@ -1,7 +1,2 @@ #!/bin/bash - -# Update this every time. -chown -R aur:aur /aurweb/aur.git - -# Start up sshd exec /usr/sbin/sshd -e -p 2222 -D From 6f7ac33166883c1c9c1b2b73c5735e46a49d3f6d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 13 Aug 2022 23:28:31 -0700 Subject: [PATCH 1095/1451] Revert "feat(db): add an index for SSHPubKeys.PubKey (#2)" This reverts commit 6c7e2749688100a10ac7de1d422b8c4cee98f393. Once again, this does actually cause issues with foreign keys. Removing it for now and will revisit this. --- aurweb/schema.py | 2 -- ...d70103d2e82_add_sshpubkeys_pubkey_index.py | 35 ------------------- test/test_migration.py | 23 ------------ 3 files changed, 60 deletions(-) delete mode 100644 migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py delete mode 100644 test/test_migration.py diff --git a/aurweb/schema.py b/aurweb/schema.py index 3d8369c9..d2644541 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -87,8 +87,6 @@ SSHPubKeys = Table( Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), Column('Fingerprint', String(44), primary_key=True), Column('PubKey', String(4096), nullable=False), - Index('SSHPubKeysUserID', 'UserID'), - Index('SSHPubKeysPubKey', 'PubKey'), mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', ) diff --git a/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py b/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py deleted file mode 100644 index 7d3f4b59..00000000 --- a/migrations/versions/dd70103d2e82_add_sshpubkeys_pubkey_index.py +++ /dev/null @@ -1,35 +0,0 @@ -"""add SSHPubKeys.PubKey index - -Revision ID: dd70103d2e82 -Revises: d64e5571bc8d -Create Date: 2022-08-12 21:30:26.155465 - -""" -import traceback - -from alembic import op - -# revision identifiers, used by Alembic. -revision = 'dd70103d2e82' -down_revision = 'd64e5571bc8d' -branch_labels = None -depends_on = None - - -def upgrade(): - try: - op.create_index("SSHPubKeysUserID", "SSHPubKeys", ["UserID"]) - except Exception: - traceback.print_exc() - print("failing silently...") - - try: - op.create_index("SSHPubKeysPubKey", "SSHPubKeys", ["PubKey"]) - except Exception: - traceback.print_exc() - print("failing silently...") - - -def downgrade(): - op.drop_index("SSHPubKeysPubKey", "SSHPubKeys") - op.drop_index("SSHPubKeysUserID", "SSHPubKeys") diff --git a/test/test_migration.py b/test/test_migration.py deleted file mode 100644 index cf8702fa..00000000 --- a/test/test_migration.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from sqlalchemy import inspect - -from aurweb.db import get_engine -from aurweb.models.ssh_pub_key import SSHPubKey - - -@pytest.fixture(autouse=True) -def setup(db_test): - return - - -def test_sshpubkeys_pubkey_index(): - insp = inspect(get_engine()) - indexes = insp.get_indexes(SSHPubKey.__tablename__) - - found_pk = False - for idx in indexes: - if idx.get("name") == "SSHPubKeysPubKey": - assert idx.get("column_names") == ["PubKey"] - found_pk = True - assert found_pk From d63615a9946c2d82af82750f09cceca441f7117c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 13 Aug 2022 23:17:53 -0700 Subject: [PATCH 1096/1451] fix(docker): fix ca entrypoint logic and healthcheck With this commit, it is advised to `rm ./data/root_ca.crt ./data/*.pem`, as new certificates and a root CA will be generated while utilizing the step volume. Closes #367 Signed-off-by: Kevin Morris --- docker-compose.yml | 30 ++++++++++++++++-------------- docker/ca-entrypoint.sh | 40 ++++++++++++++++------------------------ docker/health/ca.sh | 4 ++-- 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a56cbe72..9edffeeb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,8 +31,10 @@ services: entrypoint: /docker/ca-entrypoint.sh command: /docker/scripts/run-ca.sh healthcheck: - test: "bash /docker/health/run-ca.sh" - interval: 2s + test: "bash /docker/health/ca.sh" + interval: 3s + volumes: + - step:/root/.step memcached: image: aurweb:latest @@ -40,7 +42,7 @@ services: command: /docker/scripts/run-memcached.sh healthcheck: test: "bash /docker/health/memcached.sh" - interval: 2s + interval: 3s redis: image: aurweb:latest @@ -49,7 +51,7 @@ services: command: /docker/scripts/run-redis.sh healthcheck: test: "bash /docker/health/redis.sh" - interval: 2s + interval: 3s ports: - "127.0.0.1:16379:6379" @@ -67,7 +69,7 @@ services: - mariadb_data:/var/lib/mysql healthcheck: test: "bash /docker/health/mariadb.sh" - interval: 2s + interval: 3s mariadb_init: image: aurweb:latest @@ -98,7 +100,7 @@ services: - mariadb_test_run:/var/run/mysqld # Bind socket in this volume. healthcheck: test: "bash /docker/health/mariadb.sh" - interval: 2s + interval: 3s git: image: aurweb:latest @@ -113,7 +115,7 @@ services: - "2222:2222" healthcheck: test: "bash /docker/health/sshd.sh" - interval: 2s + interval: 3s depends_on: mariadb_init: condition: service_started @@ -129,7 +131,7 @@ services: command: /docker/scripts/run-smartgit.sh healthcheck: test: "bash /docker/health/smartgit.sh" - interval: 2s + interval: 3s cgit-php: image: aurweb:latest @@ -142,7 +144,7 @@ services: command: /docker/scripts/run-cgit.sh 3000 healthcheck: test: "bash /docker/health/cgit.sh 3000" - interval: 2s + interval: 3s depends_on: git: condition: service_healthy @@ -162,7 +164,7 @@ services: command: /docker/scripts/run-cgit.sh 3000 healthcheck: test: "bash /docker/health/cgit.sh 3000" - interval: 2s + interval: 3s depends_on: git: condition: service_healthy @@ -199,7 +201,7 @@ services: command: /docker/scripts/run-php.sh healthcheck: test: "bash /docker/health/php.sh" - interval: 2s + interval: 3s depends_on: git: condition: service_healthy @@ -228,7 +230,7 @@ services: command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" healthcheck: test: "bash /docker/health/fastapi.sh ${FASTAPI_BACKEND}" - interval: 2s + interval: 3s depends_on: git: condition: service_healthy @@ -254,10 +256,10 @@ services: - "127.0.0.1:8444:8444" # FastAPI healthcheck: test: "bash /docker/health/nginx.sh" - interval: 2s + interval: 3s depends_on: ca: - condition: service_started + condition: service_healthy cgit-php: condition: service_healthy cgit-fastapi: diff --git a/docker/ca-entrypoint.sh b/docker/ca-entrypoint.sh index d03efbbc..55c7cd75 100755 --- a/docker/ca-entrypoint.sh +++ b/docker/ca-entrypoint.sh @@ -89,34 +89,26 @@ step_cert_request() { chmod 666 /data/${1}.*.pem } -if [ ! -f $DATA_ROOT_CA ]; then +if [ ! -d /root/.step/config ]; then + # Remove existing certs. + rm -vf /data/localhost.{cert,key}.pem /data/root_ca.crt + setup_step_ca install_step_ca + + start_step_ca + for host in $DATA_CERT_HOSTS; do + step_cert_request $host /data/${host}.cert.pem /data/${host}.key.pem + done + kill_step_ca + + echo -n "WARN: Your certificates are being regenerated to resolve " + echo -n "an inconsistent step-ca state. You will need to re-import " + echo "the root CA certificate into your browser." +else + exec "$@" fi -# For all hosts separated by spaces in $DATA_CERT_HOSTS, perform a check -# for their existence in /data and react accordingly. -for host in $DATA_CERT_HOSTS; do - if [ -f /data/${host}.cert.pem ] && [ -f /data/${host}.key.pem ]; then - # Found an override. Move on to running the service after - # printing a notification to the user. - echo "Found '${host}.{cert,key}.pem' override, skipping..." - echo -n "Note: If you need to regenerate certificates, run " - echo '`rm -f data/*.{cert,key}.pem` before starting this service.' - exec "$@" - else - # Otherwise, we had a missing cert or key, so remove both. - rm -f /data/${host}.cert.pem - rm -f /data/${host}.key.pem - fi -done - -start_step_ca -for host in $DATA_CERT_HOSTS; do - step_cert_request $host /data/${host}.cert.pem /data/${host}.key.pem -done -kill_step_ca - # Set permissions to /data to rwx for everybody. chmod 777 /data diff --git a/docker/health/ca.sh b/docker/health/ca.sh index 3e4bbe8e..6bf8360e 100755 --- a/docker/health/ca.sh +++ b/docker/health/ca.sh @@ -1,2 +1,2 @@ - -exec printf "" 2>>/dev/null >>/dev/tcp/127.0.0.1/8443 +#!/bin/bash +exec curl -qkiI 'https://localhost:8443/' From a82d552e1bf0892e93b2c560e0924ada67f4d89d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 12 Aug 2022 17:18:10 -0700 Subject: [PATCH 1097/1451] update: migrate new transifex client configuration Signed-off-by: Kevin Morris --- .tx/config | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.tx/config b/.tx/config index e986f81c..7f53b684 100644 --- a/.tx/config +++ b/.tx/config @@ -1,7 +1,8 @@ [main] host = https://www.transifex.com -[aurweb.aurwebpot] +[o:lfleischer:p:aurweb:r:aurwebpot] file_filter = po/.po source_file = po/aurweb.pot source_lang = en + From 4565aa38cf3cc227bf34a9f8fa40ca698b12ded5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 12 Aug 2022 17:18:54 -0700 Subject: [PATCH 1098/1451] update: Swedish translations Pulled from Transifex on 08/12/2022 - 08/13/2022. Signed-off-by: Kevin Morris --- po/sv_SE.po | 109 ++++++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/po/sv_SE.po b/po/sv_SE.po index 6d09e207..6abb8452 100644 --- a/po/sv_SE.po +++ b/po/sv_SE.po @@ -4,17 +4,18 @@ # # Translators: # Johannes Löthberg , 2015-2016 +# Kevin Morris , 2022 # Kim Svensson , 2011 # Kim Svensson , 2012 -# Luna Jernberg , 2021 +# Luna Jernberg , 2021-2022 # Robin Björnsvik , 2014-2015 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Kevin Morris , 2022\n" "Language-Team: Swedish (Sweden) (http://www.transifex.com/lfleischer/aurweb/language/sv_SE/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -36,17 +37,17 @@ msgstr "Notera" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "" +msgstr "git clone URLs är inte avsedda att öppnas i en webbläsare." #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "" +msgstr "För att klona Git-förrådet för %s, kör %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "" +msgstr "Klicka %shär%s för att återgå till %sdetaljsidan." #: html/503.php msgid "Service Unavailable" @@ -169,7 +170,7 @@ msgstr "Redigera kommentar" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Informationspanel" #: html/home.php template/header.php msgid "Home" @@ -197,7 +198,7 @@ msgstr "Sam-ansvariga paket" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Sök efter paket jag är sam-ansvarig för" #: html/home.php #, php-format @@ -459,7 +460,7 @@ msgstr "Fortsätt" msgid "" "If you have forgotten the user name and the primary e-mail address you used " "to register, please send a message to the %saur-general%s mailing list." -msgstr "" +msgstr "Om du har glömt användarnamnet och den primära e-postadress som du använde för att registrera dig, vänligen skicka ett meddelande till %saur-general%s e-postlistan." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" @@ -479,7 +480,7 @@ msgstr "De valda paketen har inte gjorts herrelösa, kryssa i konfirmationsrutan msgid "" "The selected packages have not been adopted, check the confirmation " "checkbox." -msgstr "" +msgstr "De valda paketen har inte adopteras, markera kryssrutan för bekräftelse." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -545,7 +546,7 @@ msgstr "Använd det här formuläret för att göra paket basen %s%s%s och de f msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "Genom att markera kryssrutan bekräftar du att du inte längre vill vara sam-ansvarig för paket." #: html/pkgdisown.php #, php-format @@ -585,7 +586,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "" +msgstr "Detta verkar vara ett VCS-paket. Flagga den %sinte%s som inaktuell om paketversionen i AUR inte matchar den mest senaste commit. Flaggning av detta paket bör endast göras om källkoden har flyttats eller ändringar i PKGBUILD krävs på grund av de senaste uppströmsändringarna." #: html/pkgflag.php #, php-format @@ -974,7 +975,7 @@ msgstr "Paket detaljer kunde inte hittas." #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "Dåligt referenshuvud." #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." @@ -982,19 +983,19 @@ msgstr "Du har inte valt några paket att notifieras om." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been enabled." -msgstr "" +msgstr "De valda paketens aviseringar har aktiverats." #: aurweb/routers/packages.py msgid "You did not select any packages for notification removal." -msgstr "" +msgstr "Du har inte valt några paket för notifieringsborttagning." #: aurweb/routers/packages.py msgid "A package you selected does not have notifications enabled." -msgstr "" +msgstr "Ett paket du valde har inga notifieringar aktiverade." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been removed." -msgstr "" +msgstr "De valda paketens notifieringar har blivit borttagna." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -1046,7 +1047,7 @@ msgstr "Du måste vara inloggad före du kan adoptera paket." #: aurweb/routers/package.py msgid "You are not allowed to adopt one of the packages you selected." -msgstr "" +msgstr "Du är inte tillåten att adoptera ett av paketen du valde." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." @@ -1054,7 +1055,7 @@ msgstr "Du måste vara inloggad före du kan göra paket härrelösa." #: aurweb/routers/packages.py msgid "You are not allowed to disown one of the packages you selected." -msgstr "" +msgstr "Du har inte tillåtelse att göra ett av paketen du valde herrelöst." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." @@ -1354,7 +1355,7 @@ msgid "" "If you do not hide your email address, it is visible to all registered AUR " "users. If you hide your email address, it is visible to members of the Arch " "Linux staff only." -msgstr "" +msgstr "Om du inte döljer din e-postadress är den synlig för alla registrerade AUR-användare. Om du döljer din e-postadress är den endast synlig för medlemmar av Arch Linux-personalen." #: template/account_edit_form.php msgid "Backup Email Address" @@ -1370,14 +1371,14 @@ msgstr "Ange eventuellt en sekundär e-postadress som kan användas för att åt msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Länkar för återställning av lösenord skickas alltid till både din primära och din reserv epostadress." #: template/account_edit_form.php #, php-format msgid "" "Your backup email address is always only visible to members of the Arch " "Linux staff, independent of the %s setting." -msgstr "" +msgstr "Din backup-e-postadress är alltid endast synlig för medlemmar av Arch Linux-personalen, oberoende av %s inställningen." #: template/account_edit_form.php msgid "Language" @@ -1435,7 +1436,7 @@ msgstr "Ditt nuvarande lösenord" msgid "" "To protect the AUR against automated account creation, we kindly ask you to " "provide the output of the following command:" -msgstr "" +msgstr "För att skydda AUR mot automatiskt kontoskapande ber vi dig att tillhandahålla utdata från följande kommando:" #: template/account_edit_form.php msgid "Answer" @@ -1654,7 +1655,7 @@ msgstr "Lägg till kommentar" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Git commit-identifierare som refererar till commits i AUR-paketförrådet och URL:er konverteras automatiskt till länkar." #: template/pkg_comment_form.php #, php-format @@ -1829,7 +1830,7 @@ msgid "" "By submitting a deletion request, you ask a Trusted User to delete the " "package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "" +msgstr "Genom att skicka en begäran om borttagning ber du en Trusted User att ta bort paketbasen. Denna typ av begäran bör användas för dubbletter, programvara som övergetts av uppström, såväl som olagliga och irreparabelt trasiga paket." #: template/pkgreq_form.php msgid "" @@ -1837,7 +1838,7 @@ msgid "" "base and transfer its votes and comments to another package base. Merging a " "package does not affect the corresponding Git repositories. Make sure you " "update the Git history of the target package yourself." -msgstr "" +msgstr "Genom att skicka en sammanslagningsförfrågan ber du en Trusted User att ta bort paketbasen och överföra dess röster och kommentarer till en annan paketbas. Att slå samman ett paket påverkar inte motsvarande Git-förråd. Se till att du själv uppdaterar Git-historiken för målpaketet." #: template/pkgreq_form.php msgid "" @@ -1845,7 +1846,7 @@ msgid "" "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "" +msgstr "Genom att skicka in en föräldralös begäran ber du en Trusted User att avfärda paketbasen. Vänligen gör endast detta om paketet behöver underhållsåtgärder, underhållaren är MIA och du redan försökt kontakta underhållaren tidigare." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1907,7 +1908,7 @@ msgstr "Stäng" #: template/pkgreq_results.php msgid "Pending" -msgstr "" +msgstr "Väntar på" #: template/pkgreq_results.php msgid "Closed" @@ -2026,7 +2027,7 @@ msgstr "Version" msgid "" "Popularity is calculated as the sum of all votes with each vote being " "weighted with a factor of %.2f per day since its creation." -msgstr "" +msgstr "Populäritet räknas ut som summan av alla röster, och alla röster har en vikt med en faktor av %.2f per dag sedan den skapades. " #: template/pkg_search_results.php template/tu_details.php #: template/tu_list.php @@ -2180,18 +2181,18 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "" +msgstr "En begäran om lösenordsåterställning skickades för kontot {user} som är kopplat till din e-postadress. Om du vill återställa ditt lösenord, följ länken [1] nedan, annars ignorera detta meddelande och ingenting kommer att hända." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "Välkommen till Arch User Repository" #: scripts/notify.py msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "" +msgstr "Välkommen till Arch User Repository! För att ställa in ett första lösenord för ditt nya konto, klicka på länken [1] nedan. Om länken inte fungerar, försök att kopiera och klistra in den i din webbläsare." #: scripts/notify.py #, python-brace-format @@ -2208,7 +2209,7 @@ msgstr "{user} [1] lade till följande kommentar till {pkgbase} [2]:" msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Om du inte längre vill få meddelanden om detta paket, gå till paketsidan [2] och välj \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2218,47 +2219,47 @@ msgstr "AUR paket uppdatering: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} [1] knuffade en ny commit till {pkgbase} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "AUR inaktuell avisering för {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "Ditt paket {pkgbase} [1] har flaggats som inaktuellt av {user} [2]:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "AUR-ägarskapsmeddelande för {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "Paketet {pkgbase} [1] adopterades av {user} [2]." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "Paketet {pkgbase} [1] gjordes herrelöst av {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "AUR sam-ansvarig meddelande för {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Du blev tillagd till sam-ansvarig listan för {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Du blev borttagen från sam-ansvarig listan för {pkgbase} [1]." #: scripts/notify.py #, python-brace-format @@ -2272,7 +2273,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] slog ihop {old} [2] till {new} [3].\n\n--\nOm du inte längre vill få meddelanden om det nya paketet, gå till [3] och klicka på \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2280,19 +2281,19 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] raderade {pkgbase} [2].\n\nDu kommer inte längre att få aviseringar om detta paket." #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "TU röstningspåminnelse: Förslag {id}" #: scripts/notify.py #, python-brace-format msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." -msgstr "" +msgstr "Kom ihåg att lägga din röst på förslaget {id} [1]. Omröstningsperioden slutar om mindre än 48 timmar." #: aurweb/routers/accounts.py msgid "Invalid account type provided." @@ -2308,33 +2309,33 @@ msgstr "Du har inte behörighet att ändra den här användarens kontotyp till % #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "Inga befintliga herrelösa begäranden att acceptera för %s." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Internt serverfel" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Ett allvarligt fel har inträffat." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Detaljer har loggats och kommer att granskas av postmästaren så snabbt som möjligt. Vi ber om ursäkt för eventuella besvär som detta kan ha orsakat." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR serverfel" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Relaterade paketförfrågningar stängningskommentar..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Denna åtgärd kommer att stänga alla väntande paketförfrågningar relaterade till det paketet. %sOm kommentarer%s utelämnas kommer en stängningskommentar att automatiskt genereras." From 9497f6e671dff3c742ce9a4ec7d8226bff102121 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sun, 14 Aug 2022 15:43:13 +0200 Subject: [PATCH 1099/1451] fix(aurweb): resolve exception in ratelimit Redis's get() method can return None which makes an RPC request error out: File "/srv/http/aurweb/aurweb/ratelimit.py", line 103, in check_ratelimit requests = int(requests.decode()) AttributeError: 'NoneType' object has no attribute 'decode' --- aurweb/ratelimit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/ratelimit.py b/aurweb/ratelimit.py index 659ab6b8..86063f5d 100644 --- a/aurweb/ratelimit.py +++ b/aurweb/ratelimit.py @@ -94,7 +94,7 @@ def check_ratelimit(request: Request): # valid cache value will be returned which must be converted # to an int. Otherwise, use the database record returned # by update_ratelimit. - if not config.getboolean("ratelimit", "cache"): + if not config.getboolean("ratelimit", "cache") or requests is None: # If we got nothing from pipeline.get, we did not use # the Redis path of logic: use the DB record's count. requests = record.Requests From b4e0aea2b73e2cde6968dac72fb1b2c9fcb5a17b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 14 Aug 2022 19:25:49 -0700 Subject: [PATCH 1100/1451] Merged bugfixes Brings in: 9497f6e671dff3c742ce9a4ec7d8226bff102121 Closes #512 Thanks, jelle! Signed-off-by: Kevin Morris From 801df832e53e56bc364f73fb88a1315eea91fb55 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 15 Aug 2022 10:06:44 -0700 Subject: [PATCH 1101/1451] fix(rpc): correct URLPath in package results This was incorrectly using the particular Package record's name to format options.snapshot_uri in order to produce URLPath. It should, instead, use the PackageBase record's name, which this commit resolves. Bug reported by thomy2000 Closes #382 Signed-off-by: Kevin Morris --- aurweb/rpc.py | 2 +- test/test_rpc.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index f04de7d6..3ea7e070 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -138,7 +138,7 @@ class RPC: "Version": package.Version, "Description": package.Description, "URL": package.URL, - "URLPath": snapshot_uri % package.Name, + "URLPath": snapshot_uri % package.PackageBaseName, "NumVotes": package.NumVotes, "Popularity": pop, "OutOfDate": package.OutOfDateTS, diff --git a/test/test_rpc.py b/test/test_rpc.py index 0e24467a..c0861d3d 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -297,6 +297,28 @@ def test_rpc_singular_info(client: TestClient, assert response_data == expected_data +def test_rpc_split_package_urlpath(client: TestClient, user: User): + with db.begin(): + pkgbase = db.create(PackageBase, Name="pkg", + Maintainer=user, Packager=user) + pkgs = [ + db.create(Package, PackageBase=pkgbase, Name="pkg_1"), + db.create(Package, PackageBase=pkgbase, Name="pkg_2"), + ] + + with client as request: + response = request.get("/rpc", params={ + "v": 5, + "type": "info", + "arg": [pkgs[0].Name], + }) + + data = orjson.loads(response.text) + snapshot_uri = config.get("options", "snapshot_uri") + urlpath = data.get("results")[0].get("URLPath") + assert urlpath == (snapshot_uri % pkgbase.Name) + + def test_rpc_nonexistent_package(client: TestClient): # Make dummy request. with client as request: From 7b047578fd5b64c0f8af47130aa02e7271690f6d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 15 Aug 2022 12:10:55 -0700 Subject: [PATCH 1102/1451] fix: correct kwarg name for approved users of creds.has_credential Signed-off-by: Kevin Morris --- aurweb/auth/creds.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/auth/creds.py b/aurweb/auth/creds.py index 100aad8c..05b30d5d 100644 --- a/aurweb/auth/creds.py +++ b/aurweb/auth/creds.py @@ -69,8 +69,8 @@ cred_filters = { def has_credential(user: User, credential: int, - approved_users: list = tuple()): + approved: list = tuple()): - if user in approved_users: + if user in approved: return True return user.AccountTypeID in cred_filters[credential] From 7a52da5587f3c9d751b3b88526889a3f818c9754 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 15 Aug 2022 13:57:32 -0700 Subject: [PATCH 1103/1451] fix: guard POST keywords & allow co-maintainers to see keyword form This addresses a severe security issue, which is omitted from this git message for obscurity purposes. Otherwise, it allows co-maintainers to see the keyword form when viewing a package they co-maintain. Closes #378 Signed-off-by: Kevin Morris --- aurweb/routers/pkgbase.py | 6 ++++++ templates/partials/packages/details.html | 4 ++-- test/test_pkgbase_routes.py | 19 +++++++++++++++++-- test/test_templates.py | 4 +++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 6cd4199d..1bca5ea3 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -96,6 +96,12 @@ async def pkgbase_keywords(request: Request, name: str, keywords: str = Form(default=str())): pkgbase = get_pkg_or_base(name, PackageBase) + approved = [pkgbase.Maintainer] + [c.User for c in pkgbase.comaintainers] + has_cred = creds.has_credential(request.user, creds.PKGBASE_SET_KEYWORDS, + approved=approved) + if not has_cred: + return Response(status_code=HTTPStatus.UNAUTHORIZED) + # Lowercase all keywords. Our database table is case insensitive, # and providing CI duplicates of keywords is erroneous. keywords = set(k.lower() for k in keywords.split()) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 771b311d..ca7159be 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -33,10 +33,10 @@ {% endif %} - {% if pkgbase.keywords.count() or request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} + {% if pkgbase.keywords.count() or request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer] + comaintainers) %} - {% if request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} + {% if request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer] + comaintainers) %}
      {{ "Total" | tr }} {{ "Trusted Users" | tr }}:{{ "Total" | tr }} {{ "Trusted Users" | tr }}: {{ trusted_user_count }}
      {{ "Active" | tr }} {{ "Trusted Users" | tr }}:{{ "Active" | tr }} {{ "Trusted Users" | tr }}: {{ active_trusted_user_count }}
      {{ "Maintainer" | tr }}: - {% if pkgbase.Maintainer %} + {% if request.user.is_authenticated() and pkgbase.Maintainer %} {{ pkgbase.Maintainer.Username }} @@ -118,6 +118,9 @@ {% endif %} {% else %} {{ pkgbase.Maintainer.Username | default("None" | tr) }} + {% if comaintainers %} + ({{ comaintainers|join(', ') }}) + {% endif %} {% endif %}
      {{ "Keywords" | tr }}:
      Date: Mon, 15 Aug 2022 14:49:34 -0700 Subject: [PATCH 1104/1451] fix: secure access to comment edits to user who owns the comment Found along with the previous commit to be a security hole in our implementation. This commit resolves an issue regarding comment editing. Signed-off-by: Kevin Morris --- aurweb/routers/pkgbase.py | 2 ++ test/test_pkgbase_routes.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 1bca5ea3..c735f474 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -286,6 +286,8 @@ async def pkgbase_comment_post( if not comment: raise HTTPException(status_code=HTTPStatus.BAD_REQUEST) + elif request.user.ID != db_comment.UsersID: + raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED) # If the provided comment is different than the record's version, # update the db record. diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index 5c44ea47..f6bcf5d7 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -467,6 +467,22 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, assert "form" in data +def test_pkgbase_comment_edit_unauthorized(client: TestClient, + user: User, + maintainer: User, + package: Package, + comment: PackageComment): + pkgbase = package.PackageBase + + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + endp = f"/pkgbase/{pkgbase.Name}/comments/{comment.ID}" + response = request.post(endp, data={ + "comment": "abcd im trying to change this comment." + }, cookies=cookies) + assert response.status_code == HTTPStatus.UNAUTHORIZED + + def test_pkgbase_comment_delete(client: TestClient, maintainer: User, user: User, From 33bf5df236166cbbbd8ef1b611145effc5813acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 12 Aug 2022 18:43:18 +0200 Subject: [PATCH 1105/1451] fix: show unflag link to flagger While the flagger is allowed to unflag a package, the link to do so is hidden from them. Fix by adding the flagger to the unflag list. Fix #380 --- aurweb/pkgbase/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 5ffe490e..63621d63 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -46,7 +46,7 @@ def make_context(request: Request, pkgbase: PackageBase, ).all() ] context["unflaggers"] = context["comaintainers"].copy() - context["unflaggers"].append(pkgbase.Maintainer) + context["unflaggers"].extend([pkgbase.Maintainer, pkgbase.Flagger]) context["packages_count"] = pkgbase.packages.count() context["keywords"] = pkgbase.keywords From fb1fb2ef3b6ff441ce30c5fd50781376e4cd02c4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Aug 2022 09:59:56 -0700 Subject: [PATCH 1106/1451] feat: documentation for web authentication (login, verification) Signed-off-by: Kevin Morris --- doc/web-auth.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 doc/web-auth.md diff --git a/doc/web-auth.md b/doc/web-auth.md new file mode 100644 index 00000000..5f6679d4 --- /dev/null +++ b/doc/web-auth.md @@ -0,0 +1,111 @@ +# aurweb Web Authentication + +aurweb uses an HTTP cookie to persist user sessions across requests. +This cookie **must** be delivered with a request in order to be considered +an authenticated user. + +See [HTTP Cookie](#http-cookie) for detailed information about the cookie. + +## HTTP Cookie + +aurweb utilizes an HTTP cookie by the name of `AURSID` to track +user authentication across requests. + +This cookie's requirements changes due to aurweb's configuration +in the following ways: + +- `options.disable_http_login: 0` + - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), Max-Age +- `options.disable_http_login: 1` + - [Secure, HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies), [Samesite=Strict](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), Max-Age + +### Max-Age + +The value used for the `AURSID` Max-Age attribute is decided based +off of the "Remember Me" checkbox on the login page. Both paths +use their own independent configuration for the number of seconds +that each type of session should stay alive. + +- "Remember Me" unchecked while logging in + - `options.login_timeout` is used +- "Remember Me" checked while logging in + - `options.persistent_cookie_timeout` is used + +Both `options.login_timeout` and `options.persistent_cookie_timeout` +indicate the number of seconds the session should live. + +### Notes + +At all times, aur.archlinux.org operates over HTTPS. Secure cookies will +only remain intact when subsequently requesting an aurweb route through +the HTTPS scheme at the same host as the cookie was obtained. + +## Login Process + +When a user logs in to aurweb, the following steps are taken: + +1. Was a Referer header delivered from an address starting with +`{aurweb_url}/login`? + 1. No, an HTTP 400 Bad Request response is returned + 2. Yes, move on to 2 +2. Does a Users database record exist for the given username/email? + 1. No, you are returned to the login page with `Bad username or password.` + error + 2. Yes, move on to 3 +3. Is the user suspended? + 1. Yes, you are returned to the login page with `Account Suspended` error + 2. No, move on to 4 +4. Can the user login with the given password? + 1. No, you are returned to the login page with `Bad username or password.` + error + 2. Yes, move on to 5 +5. Update the user's `LastLogin` and `LastLoginIPAddress` columns +6. Does the user have a related Sessions record? + 1. No, generate a new Sessions record with a new unique `SessionID` + 2. Yes, update the Sessions record's `SessionID` column with a new unique + string and update the Sessions record's `LastUpdateTS` column if it has + expired + 3. In both cases, set the user's `InactivityTS` column to `0` + 4. In both cases, return the new `SessionID` column value and move on to 7 +7. Return a redirect to the `next` GET variable with the +following cookies set: + 1. `AURSID` + - Unique session string matching the user's related + `Sessions.SessionID` column + 2. `AURTZ` + - User's timezone setting + 3. `AURLANG` + - User's language setting + 4. `AURREMEMBER` + - Boolean state of the "Remember Me" checkbox when login submitted + +## Auth Verification + +When a request is made toward aurweb, a middleware is responsible for +verifying the user's auth cookie. If no valid `AURSID` cookie could be +found for a user in the database, the request is considered unauthenticated. + +The following list of steps describes exactly how this verification works: +1. Was the `AURSID` cookie delivered? + 1. No, the algorithm ends, you are considered unauthenticated + 2. Yes, move on to 2 +2. Was the `AURREMEMBER` cookie delivered with a value of 1? + 1. No, set the expected session timeout **T** to `options.login_timeout` + 2. Yes, set the expected session timeout **T** to + `options.persistent_cookie_timeout` +3. Does a Sessions database record exist which matches the `AURSID`? + 1. No, the algorithm ends, you are considered unauthenticated + 2. Yes, move on to 4 +4. Does the Sessions record's LastUpdateTS column fit within `utcnow - T`? + 1. No, the Sessions record at hand is deleted, the algorithm ends, you + are considered unauthenticated + 2. Yes, move on to 5 +5. You are considered authenticated + +## aur.archlinux.org Auth-Related Configuration + +- Operates over HTTPS with a Let's Encrypt SSL certificate +- `options.disable_http_login: 1` +- `options.login_timeout: ` +- `options.persistent_cookie_timeout: ` + From f10732960cd17e680e1a8a7b420b0b15ff391099 Mon Sep 17 00:00:00 2001 From: Joakim Saario Date: Thu, 18 Aug 2022 18:35:25 +0200 Subject: [PATCH 1107/1451] fix: Use SameSite=Lax on cookies --- aurweb/cookies.py | 10 ++++---- doc/web-auth.md | 2 +- test/test_auth_routes.py | 52 ++++++++++++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/aurweb/cookies.py b/aurweb/cookies.py index 442a4c0a..58d14515 100644 --- a/aurweb/cookies.py +++ b/aurweb/cookies.py @@ -5,15 +5,13 @@ from aurweb import config def samesite() -> str: - """ Produce cookie SameSite value based on options.disable_http_login. + """ Produce cookie SameSite value. - When options.disable_http_login is True, "strict" is returned. Otherwise, - "lax" is returned. + Currently this is hard-coded to return "lax" - :returns "strict" if options.disable_http_login else "lax" + :returns "lax" """ - secure = config.getboolean("options", "disable_http_login") - return "strict" if secure else "lax" + return "lax" def timeout(extended: bool) -> int: diff --git a/doc/web-auth.md b/doc/web-auth.md index 5f6679d4..1161af6f 100644 --- a/doc/web-auth.md +++ b/doc/web-auth.md @@ -17,7 +17,7 @@ in the following ways: - `options.disable_http_login: 0` - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), Max-Age - `options.disable_http_login: 1` - - [Secure, HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies), [Samesite=Strict](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), Max-Age + - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), [Secure, HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) ### Max-Age diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 8467adea..5942edcf 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -109,14 +109,52 @@ def test_login_email(client: TestClient, user: user): assert "AURSID" in resp.cookies -def mock_getboolean(a, b): - if a == "options" and b == "disable_http_login": - return True - return bool(aurweb.config.get(a, b)) +def mock_getboolean(**overrided_configs): + mocked_config = { + tuple(config.split("__")): value + for config, value in overrided_configs.items() + } + + def side_effect(*args): + return mocked_config.get(args, bool(aurweb.config.get(*args))) + + return side_effect -@mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean) -def test_secure_login(getboolean: bool, client: TestClient, user: User): +@mock.patch( + "aurweb.config.getboolean", + side_effect=mock_getboolean(options__disable_http_login=False) +) +def test_insecure_login(getboolean: mock.Mock, client: TestClient, user: User): + post_data = { + "user": user.Username, + "passwd": "testPassword", + "next": "/" + } + + # Perform a login request with the data matching our user. + with client as request: + response = request.post("/login", data=post_data, + allow_redirects=False) + + # Make sure we got the expected status out of it. + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + # Let's check what we got in terms of cookies for AURSID. + # Make sure that a secure cookie got passed to us. + cookie = next(c for c in response.cookies if c.name == "AURSID") + assert cookie.secure is False + assert cookie.has_nonstandard_attr("HttpOnly") is False + assert cookie.has_nonstandard_attr("SameSite") is True + assert cookie.get_nonstandard_attr("SameSite") == "lax" + assert cookie.value is not None and len(cookie.value) > 0 + + +@mock.patch( + "aurweb.config.getboolean", + side_effect=mock_getboolean(options__disable_http_login=True) +) +def test_secure_login(getboolean: mock.Mock, client: TestClient, user: User): """ In this test, we check to verify the course of action taken by starlette when providing secure=True to a response cookie. This is achieved by mocking aurweb.config.getboolean to return @@ -154,7 +192,7 @@ def test_secure_login(getboolean: bool, client: TestClient, user: User): assert cookie.secure is True assert cookie.has_nonstandard_attr("HttpOnly") is True assert cookie.has_nonstandard_attr("SameSite") is True - assert cookie.get_nonstandard_attr("SameSite") == "strict" + assert cookie.get_nonstandard_attr("SameSite") == "lax" assert cookie.value is not None and len(cookie.value) > 0 # Let's make sure we actually have a session relationship From 4303086c0e59d510c2f5ad28083574889340eba6 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Aug 2022 14:47:24 -0700 Subject: [PATCH 1108/1451] Merged branch 'sameorigin-lax' Closes #351 Signed-off-by: Kevin Morris From 8e43932aa6497ccf024e957687fd87120f2125cf Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Aug 2022 14:57:42 -0700 Subject: [PATCH 1109/1451] fix(doc): re-add Max-Age to list of secure cookie attributes Signed-off-by: Kevin Morris --- doc/web-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/web-auth.md b/doc/web-auth.md index 1161af6f..17284889 100644 --- a/doc/web-auth.md +++ b/doc/web-auth.md @@ -17,7 +17,7 @@ in the following ways: - `options.disable_http_login: 0` - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), Max-Age - `options.disable_http_login: 1` - - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), [Secure, HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies) + - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), [Secure, HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies), Max-Age ### Max-Age From fd4aaed208fb862c2f66edbe122f4c4e5d52c765 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 17 Aug 2022 10:01:06 -0700 Subject: [PATCH 1110/1451] fix: use max-age for all cookie expirations in addition, remove cookie expiration for AURREMEMBER -- we don't really care about a session time for this cookie, it merely acts as a flag given out on login to remember what the user selected Signed-off-by: Kevin Morris --- aurweb/routers/auth.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 9f465388..50cec419 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -6,7 +6,7 @@ from sqlalchemy import or_ import aurweb.config -from aurweb import cookies, db, time +from aurweb import cookies, db from aurweb.auth import requires_auth, requires_guest from aurweb.exceptions import handle_form_exceptions from aurweb.l10n import get_translator_for_request @@ -65,15 +65,11 @@ async def login_post(request: Request, return await login_template(request, next, errors=["Bad username or password."]) - login_timeout = aurweb.config.getint("options", "login_timeout") - - expires_at = int(time.utcnow() + max(cookie_timeout, login_timeout)) - response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER) secure = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie("AURSID", sid, expires=expires_at, + response.set_cookie("AURSID", sid, max_age=cookie_timeout, secure=secure, httponly=secure, samesite=cookies.samesite()) response.set_cookie("AURTZ", user.Timezone, @@ -83,7 +79,6 @@ async def login_post(request: Request, secure=secure, httponly=secure, samesite=cookies.samesite()) response.set_cookie("AURREMEMBER", remember_me, - expires=expires_at, secure=secure, httponly=secure, samesite=cookies.samesite()) return response From ab2956eef79aed68e8da3c37b237950916f78c25 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 18 Aug 2022 16:02:03 -0700 Subject: [PATCH 1111/1451] feat: add pytest unit of independent user unflagging Signed-off-by: Kevin Morris --- test/test_pkgbase_routes.py | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index f6bcf5d7..8be08f83 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -1457,3 +1457,52 @@ def test_unauthorized_pkgbase_keywords(client: TestClient, package: Package): endp = f"/pkgbase/{pkgbase.Name}/keywords" response = request.post(endp, cookies=cookies) assert response.status_code == HTTPStatus.UNAUTHORIZED + + +def test_independent_user_unflag(client: TestClient, user: User, + package: Package): + with db.begin(): + flagger = db.create(User, Username="test_flagger", + Email="test_flagger@example.com", + Passwd="testPassword") + + pkgbase = package.PackageBase + cookies = {"AURSID": flagger.login(Request(), "testPassword")} + with client as request: + endp = f"/pkgbase/{pkgbase.Name}/flag" + response = request.post(endp, data={ + "comments": "This thing needs a flag!" + }, cookies=cookies, allow_redirects=True) + assert response.status_code == HTTPStatus.OK + + # At this point, we've flagged it as `flagger`. + # Now, we should be able to view the "Unflag package" link on the package + # page when browsing as that `flagger` user. + with client as request: + endp = f"/pkgbase/{pkgbase.Name}" + response = request.get(endp, cookies=cookies, allow_redirects=True) + assert response.status_code == HTTPStatus.OK + + # Assert that the "Unflag package" link appears in the DOM. + root = parse_root(response.text) + elems = root.xpath('//input[@name="do_UnFlag"]') + assert len(elems) == 1 + + # Now, unflag the package by "clicking" the "Unflag package" link. + with client as request: + endp = f"/pkgbase/{pkgbase.Name}/unflag" + response = request.post(endp, cookies=cookies, allow_redirects=True) + assert response.status_code == HTTPStatus.OK + + # For the last time, let's check the GET response. The package should + # not show as flagged anymore, and thus the "Unflag package" link + # should be missing. + with client as request: + endp = f"/pkgbase/{pkgbase.Name}" + response = request.get(endp, cookies=cookies, allow_redirects=True) + assert response.status_code == HTTPStatus.OK + + # Assert that the "Unflag package" link does not appear in the DOM. + root = parse_root(response.text) + elems = root.xpath('//input[@name="do_UnFlag"]') + assert len(elems) == 0 From 08d485206cc821f4b041c7ace163ac54821984ce Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 3 Aug 2022 16:50:52 +0300 Subject: [PATCH 1112/1451] feature: allow co-maintainers to disown their pkg Derived off of original work done by Leonidas Spyropoulos at https://gitlab.archlinux.org/archlinux/aurweb/-/merge_requests/503 This revision of that original work finishes off the inconsistencies mentioned in the original MR and adds a small bit of testing for more regression checks. Fixes: #360 Signed-off-by: Kevin Morris --- aurweb/pkgbase/actions.py | 11 +++ aurweb/routers/pkgbase.py | 19 +++-- templates/partials/packages/actions.html | 2 +- templates/pkgbase/disown.html | 38 +++++----- test/test_pkgbase_routes.py | 88 +++++++++++++++++++++--- 5 files changed, 123 insertions(+), 35 deletions(-) diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 46609f89..27143d51 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -50,6 +50,12 @@ def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)] is_maint = disowner == pkgbase.Maintainer + + comaint = pkgbase.comaintainers.filter( + PackageComaintainer.User == disowner + ).one_or_none() + is_comaint = comaint is not None + if is_maint: with db.begin(): # Comaintainer with the lowest Priority value; next-in-line. @@ -63,6 +69,11 @@ def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: else: # Otherwise, just orphan the package completely. pkgbase.Maintainer = None + elif is_comaint: + # This disown request is from a Comaintainer + with db.begin(): + notif = pkgbaseutil.remove_comaintainer(comaint) + notifs.append(notif) elif request.user.has_credential(creds.PKGBASE_DISOWN): # Otherwise, the request user performing this disownage is a # Trusted User and we treat it like a standard orphan request. diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index c735f474..1f09cfc8 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -545,15 +545,18 @@ async def pkgbase_disown_get(request: Request, name: str, next: str = Query(default=str())): pkgbase = get_pkg_or_base(name, PackageBase) + comaints = {c.User for c in pkgbase.comaintainers} + approved = [pkgbase.Maintainer] + list(comaints) has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=[pkgbase.Maintainer]) + approved=approved) if not has_cred: - return RedirectResponse(f"/pkgbase/{name}", - HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Disown Package") context["pkgbase"] = pkgbase context["next"] = next or "/pkgbase/{name}" + context["is_maint"] = request.user == pkgbase.Maintainer + context["is_comaint"] = request.user in comaints return render_template(request, "pkgbase/disown.html", context) @@ -566,8 +569,10 @@ async def pkgbase_disown_post(request: Request, name: str, next: str = Form(default=str())): pkgbase = get_pkg_or_base(name, PackageBase) + comaints = {c.User for c in pkgbase.comaintainers} + approved = [pkgbase.Maintainer] + list(comaints) has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=[pkgbase.Maintainer]) + approved=approved) if not has_cred: return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER) @@ -580,8 +585,9 @@ async def pkgbase_disown_post(request: Request, name: str, return render_template(request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST) - with db.begin(): - update_closure_comment(pkgbase, ORPHAN_ID, comments) + if request.user != pkgbase.Maintainer and request.user not in comaints: + with db.begin(): + update_closure_comment(pkgbase, ORPHAN_ID, comments) try: actions.pkgbase_disown_instance(request, pkgbase) @@ -862,7 +868,6 @@ async def pkgbase_merge_post(request: Request, name: str, comments: str = Form(default=str()), confirm: bool = Form(default=False), next: str = Form(default=str())): - pkgbase = get_pkg_or_base(name, PackageBase) context = await make_variable_context(request, "Package Merging") context["pkgbase"] = pkgbase diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index 2144b07a..fa8c994f 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -131,7 +131,7 @@ /> - {% elif request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) %} + {% elif request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer] + comaintainers) %}
    • {{ "Disown Package" | tr }} diff --git a/templates/pkgbase/disown.html b/templates/pkgbase/disown.html index 3cc7988d..1aedde4f 100644 --- a/templates/pkgbase/disown.html +++ b/templates/pkgbase/disown.html @@ -27,14 +27,16 @@ {% endfor %} -

      - {{ - "This action will close any pending package requests " - "related to it. If %sComments%s are omitted, a closure " - "comment will be autogenerated." - | tr | format("", "") | safe - }} -

      + {% if not is_maint and not is_comaint %} +

      + {{ + "This action will close any pending package requests " + "related to it. If %sComments%s are omitted, a closure " + "comment will be autogenerated." + | tr | format("", "") | safe + }} +

      + {% endif %}

      {{ @@ -47,14 +49,18 @@

      -

      - - -

      + {% if not is_maint and not is_comaint %} +

      + + +

      + {% else %} + + {% endif %}

      {pkg.Name}' - for pkg in provides - ]) + return ", ".join( + [f'{pkg.Name}' for pkg in provides] + ) def get_pkg_or_base( - name: str, - cls: Union[models.Package, models.PackageBase] = models.PackageBase) \ - -> Union[models.Package, models.PackageBase]: - """ Get a PackageBase instance by its name or raise a 404 if + name: str, cls: Union[models.Package, models.PackageBase] = models.PackageBase +) -> Union[models.Package, models.PackageBase]: + """Get a PackageBase instance by its name or raise a 404 if it can't be found in the database. :param name: {Package,PackageBase}.Name @@ -109,8 +106,7 @@ def get_pkg_or_base( return instance -def get_pkgbase_comment(pkgbase: models.PackageBase, id: int) \ - -> models.PackageComment: +def get_pkgbase_comment(pkgbase: models.PackageBase, id: int) -> models.PackageComment: comment = pkgbase.comments.filter(models.PackageComment.ID == id).first() if not comment: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) @@ -122,9 +118,8 @@ def out_of_date(packages: orm.Query) -> orm.Query: return packages.filter(models.PackageBase.OutOfDateTS.isnot(None)) -def updated_packages(limit: int = 0, - cache_ttl: int = 600) -> list[models.Package]: - """ Return a list of valid Package objects ordered by their +def updated_packages(limit: int = 0, cache_ttl: int = 600) -> list[models.Package]: + """Return a list of valid Package objects ordered by their ModifiedTS column in descending order from cache, after setting the cache when no key yet exists. @@ -139,10 +134,11 @@ def updated_packages(limit: int = 0, return orjson.loads(packages) with db.begin(): - query = db.query(models.Package).join(models.PackageBase).filter( - models.PackageBase.PackagerUID.isnot(None) - ).order_by( - models.PackageBase.ModifiedTS.desc() + query = ( + db.query(models.Package) + .join(models.PackageBase) + .filter(models.PackageBase.PackagerUID.isnot(None)) + .order_by(models.PackageBase.ModifiedTS.desc()) ) if limit: @@ -152,13 +148,13 @@ def updated_packages(limit: int = 0, for pkg in query: # For each Package returned by the query, append a dict # containing Package columns we're interested in. - packages.append({ - "Name": pkg.Name, - "Version": pkg.Version, - "PackageBase": { - "ModifiedTS": pkg.PackageBase.ModifiedTS + packages.append( + { + "Name": pkg.Name, + "Version": pkg.Version, + "PackageBase": {"ModifiedTS": pkg.PackageBase.ModifiedTS}, } - }) + ) # Store the JSON serialization of the package_updates key into Redis. redis.set("package_updates", orjson.dumps(packages)) @@ -168,9 +164,8 @@ def updated_packages(limit: int = 0, return packages -def query_voted(query: list[models.Package], - user: models.User) -> dict[int, bool]: - """ Produce a dictionary of package base ID keys to boolean values, +def query_voted(query: list[models.Package], user: models.User) -> dict[int, bool]: + """Produce a dictionary of package base ID keys to boolean values, which indicate whether or not the package base has a vote record related to user. @@ -180,20 +175,18 @@ def query_voted(query: list[models.Package], """ output = defaultdict(bool) query_set = {pkg.PackageBaseID for pkg in query} - voted = db.query(models.PackageVote).join( - models.PackageBase, - models.PackageBase.ID.in_(query_set) - ).filter( - models.PackageVote.UsersID == user.ID + voted = ( + db.query(models.PackageVote) + .join(models.PackageBase, models.PackageBase.ID.in_(query_set)) + .filter(models.PackageVote.UsersID == user.ID) ) for vote in voted: output[vote.PackageBase.ID] = True return output -def query_notified(query: list[models.Package], - user: models.User) -> dict[int, bool]: - """ Produce a dictionary of package base ID keys to boolean values, +def query_notified(query: list[models.Package], user: models.User) -> dict[int, bool]: + """Produce a dictionary of package base ID keys to boolean values, which indicate whether or not the package base has a notification record related to user. @@ -203,19 +196,17 @@ def query_notified(query: list[models.Package], """ output = defaultdict(bool) query_set = {pkg.PackageBaseID for pkg in query} - notified = db.query(models.PackageNotification).join( - models.PackageBase, - models.PackageBase.ID.in_(query_set) - ).filter( - models.PackageNotification.UserID == user.ID + notified = ( + db.query(models.PackageNotification) + .join(models.PackageBase, models.PackageBase.ID.in_(query_set)) + .filter(models.PackageNotification.UserID == user.ID) ) for notif in notified: output[notif.PackageBase.ID] = True return output -def pkg_required(pkgname: str, provides: list[str]) \ - -> list[PackageDependency]: +def pkg_required(pkgname: str, provides: list[str]) -> list[PackageDependency]: """ Get dependencies that match a string in `[pkgname] + provides`. @@ -225,9 +216,12 @@ def pkg_required(pkgname: str, provides: list[str]) \ :return: List of PackageDependency instances """ targets = set([pkgname] + provides) - query = db.query(PackageDependency).join(Package).filter( - PackageDependency.DepName.in_(targets) - ).order_by(Package.Name.asc()) + query = ( + db.query(PackageDependency) + .join(Package) + .filter(PackageDependency.DepName.in_(targets)) + .order_by(Package.Name.asc()) + ) return query diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 27143d51..4834f8dd 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -14,15 +14,15 @@ logger = logging.get_logger(__name__) def pkgbase_notify_instance(request: Request, pkgbase: PackageBase) -> None: - notif = db.query(pkgbase.notifications.filter( - PackageNotification.UserID == request.user.ID - ).exists()).scalar() + notif = db.query( + pkgbase.notifications.filter( + PackageNotification.UserID == request.user.ID + ).exists() + ).scalar() has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and not notif: with db.begin(): - db.create(PackageNotification, - PackageBase=pkgbase, - User=request.user) + db.create(PackageNotification, PackageBase=pkgbase, User=request.user) def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: @@ -36,8 +36,11 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: def pkgbase_unflag_instance(request: Request, pkgbase: PackageBase) -> None: - has_cred = request.user.has_credential(creds.PKGBASE_UNFLAG, approved=[ - pkgbase.Flagger, pkgbase.Maintainer] + [c.User for c in pkgbase.comaintainers]) + has_cred = request.user.has_credential( + creds.PKGBASE_UNFLAG, + approved=[pkgbase.Flagger, pkgbase.Maintainer] + + [c.User for c in pkgbase.comaintainers], + ) if has_cred: with db.begin(): pkgbase.OutOfDateTS = None @@ -93,9 +96,9 @@ def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: notif.send() -def pkgbase_delete_instance(request: Request, pkgbase: PackageBase, - comments: str = str()) \ - -> list[notify.Notification]: +def pkgbase_delete_instance( + request: Request, pkgbase: PackageBase, comments: str = str() +) -> list[notify.Notification]: notifs = handle_request(request, DELETION_ID, pkgbase) + [ notify.DeleteNotification(request.user.ID, pkgbase.ID) ] @@ -107,8 +110,9 @@ def pkgbase_delete_instance(request: Request, pkgbase: PackageBase, return notifs -def pkgbase_merge_instance(request: Request, pkgbase: PackageBase, - target: PackageBase, comments: str = str()) -> None: +def pkgbase_merge_instance( + request: Request, pkgbase: PackageBase, target: PackageBase, comments: str = str() +) -> None: pkgbasename = str(pkgbase.Name) # Create notifications. @@ -144,8 +148,10 @@ def pkgbase_merge_instance(request: Request, pkgbase: PackageBase, db.delete(pkgbase) # Log this out for accountability purposes. - logger.info(f"Trusted User '{request.user.Username}' merged " - f"'{pkgbasename}' into '{target.Name}'.") + logger.info( + f"Trusted User '{request.user.Username}' merged " + f"'{pkgbasename}' into '{target.Name}'." + ) # Send notifications. util.apply_all(notifs, lambda n: n.send()) diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 63621d63..223c3013 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -10,19 +10,23 @@ from aurweb.models.package_comment import PackageComment from aurweb.models.package_request import PENDING_ID, PackageRequest from aurweb.models.package_vote import PackageVote from aurweb.scripts import notify -from aurweb.templates import make_context as _make_context -from aurweb.templates import make_variable_context as _make_variable_context +from aurweb.templates import ( + make_context as _make_context, + make_variable_context as _make_variable_context, +) -async def make_variable_context(request: Request, pkgbase: PackageBase) \ - -> dict[str, Any]: +async def make_variable_context( + request: Request, pkgbase: PackageBase +) -> dict[str, Any]: ctx = await _make_variable_context(request, pkgbase.Name) return make_context(request, pkgbase, ctx) -def make_context(request: Request, pkgbase: PackageBase, - context: dict[str, Any] = None) -> dict[str, Any]: - """ Make a basic context for package or pkgbase. +def make_context( + request: Request, pkgbase: PackageBase, context: dict[str, Any] = None +) -> dict[str, Any]: + """Make a basic context for package or pkgbase. :param request: FastAPI request :param pkgbase: PackageBase instance @@ -34,14 +38,16 @@ def make_context(request: Request, pkgbase: PackageBase, # Per page and offset. offset, per_page = util.sanitize_params( request.query_params.get("O", defaults.O), - request.query_params.get("PP", defaults.COMMENTS_PER_PAGE)) + request.query_params.get("PP", defaults.COMMENTS_PER_PAGE), + ) context["O"] = offset context["PP"] = per_page context["git_clone_uri_anon"] = config.get("options", "git_clone_uri_anon") context["git_clone_uri_priv"] = config.get("options", "git_clone_uri_priv") context["pkgbase"] = pkgbase context["comaintainers"] = [ - c.User for c in pkgbase.comaintainers.order_by( + c.User + for c in pkgbase.comaintainers.order_by( PackageComaintainer.Priority.asc() ).all() ] @@ -53,9 +59,11 @@ def make_context(request: Request, pkgbase: PackageBase, context["comments_total"] = pkgbase.comments.order_by( PackageComment.CommentTS.desc() ).count() - context["comments"] = pkgbase.comments.order_by( - PackageComment.CommentTS.desc() - ).limit(per_page).offset(offset) + context["comments"] = ( + pkgbase.comments.order_by(PackageComment.CommentTS.desc()) + .limit(per_page) + .offset(offset) + ) context["pinned_comments"] = pkgbase.comments.filter( PackageComment.PinnedTS != 0 ).order_by(PackageComment.CommentTS.desc()) @@ -70,15 +78,15 @@ def make_context(request: Request, pkgbase: PackageBase, ).scalar() context["requests"] = pkgbase.requests.filter( - and_(PackageRequest.Status == PENDING_ID, - PackageRequest.ClosedTS.is_(None)) + and_(PackageRequest.Status == PENDING_ID, PackageRequest.ClosedTS.is_(None)) ).count() return context -def remove_comaintainer(comaint: PackageComaintainer) \ - -> notify.ComaintainerRemoveNotification: +def remove_comaintainer( + comaint: PackageComaintainer, +) -> notify.ComaintainerRemoveNotification: """ Remove a PackageComaintainer. @@ -107,9 +115,9 @@ def remove_comaintainers(pkgbase: PackageBase, usernames: list[str]) -> None: """ notifications = [] with db.begin(): - comaintainers = pkgbase.comaintainers.join(User).filter( - User.Username.in_(usernames) - ).all() + comaintainers = ( + pkgbase.comaintainers.join(User).filter(User.Username.in_(usernames)).all() + ) notifications = [ notify.ComaintainerRemoveNotification(co.User.ID, pkgbase.ID) for co in comaintainers @@ -133,23 +141,23 @@ def latest_priority(pkgbase: PackageBase) -> int: """ # Order comaintainers related to pkgbase by Priority DESC. - record = pkgbase.comaintainers.order_by( - PackageComaintainer.Priority.desc()).first() + record = pkgbase.comaintainers.order_by(PackageComaintainer.Priority.desc()).first() # Use Priority column if record exists, otherwise 0. return record.Priority if record else 0 class NoopComaintainerNotification: - """ A noop notification stub used as an error-state return value. """ + """A noop notification stub used as an error-state return value.""" def send(self) -> None: - """ noop """ + """noop""" return -def add_comaintainer(pkgbase: PackageBase, comaintainer: User) \ - -> notify.ComaintainerAddNotification: +def add_comaintainer( + pkgbase: PackageBase, comaintainer: User +) -> notify.ComaintainerAddNotification: """ Add a new comaintainer to `pkgbase`. @@ -165,14 +173,19 @@ def add_comaintainer(pkgbase: PackageBase, comaintainer: User) \ new_prio = latest_priority(pkgbase) + 1 with db.begin(): - db.create(PackageComaintainer, PackageBase=pkgbase, - User=comaintainer, Priority=new_prio) + db.create( + PackageComaintainer, + PackageBase=pkgbase, + User=comaintainer, + Priority=new_prio, + ) return notify.ComaintainerAddNotification(comaintainer.ID, pkgbase.ID) -def add_comaintainers(request: Request, pkgbase: PackageBase, - usernames: list[str]) -> None: +def add_comaintainers( + request: Request, pkgbase: PackageBase, usernames: list[str] +) -> None: """ Add comaintainers to `pkgbase`. @@ -216,7 +229,6 @@ def rotate_comaintainers(pkgbase: PackageBase) -> None: :param pkgbase: PackageBase instance """ - comaintainers = pkgbase.comaintainers.order_by( - PackageComaintainer.Priority.asc()) + comaintainers = pkgbase.comaintainers.order_by(PackageComaintainer.Priority.asc()) for i, comaint in enumerate(comaintainers): comaint.Priority = i + 1 diff --git a/aurweb/pkgbase/validate.py b/aurweb/pkgbase/validate.py index baefc415..3c50e578 100644 --- a/aurweb/pkgbase/validate.py +++ b/aurweb/pkgbase/validate.py @@ -5,9 +5,13 @@ from aurweb.exceptions import ValidationError from aurweb.models import PackageBase -def request(pkgbase: PackageBase, - type: str, comments: str, merge_into: str, - context: dict[str, Any]) -> None: +def request( + pkgbase: PackageBase, + type: str, + comments: str, + merge_into: str, + context: dict[str, Any], +) -> None: if not comments: raise ValidationError(["The comment field must not be empty."]) @@ -15,21 +19,16 @@ def request(pkgbase: PackageBase, # Perform merge-related checks. if not merge_into: # TODO: This error needs to be translated. - raise ValidationError( - ['The "Merge into" field must not be empty.']) + raise ValidationError(['The "Merge into" field must not be empty.']) - target = db.query(PackageBase).filter( - PackageBase.Name == merge_into - ).first() + target = db.query(PackageBase).filter(PackageBase.Name == merge_into).first() if not target: # TODO: This error needs to be translated. - raise ValidationError([ - "The package base you want to merge into does not exist." - ]) + raise ValidationError( + ["The package base you want to merge into does not exist."] + ) db.refresh(target) if target.ID == pkgbase.ID: # TODO: This error needs to be translated. - raise ValidationError([ - "You cannot merge a package base into itself." - ]) + raise ValidationError(["You cannot merge a package base into itself."]) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index 227d46ed..0bbea4be 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -19,8 +19,9 @@ def instrumentator(): # Their license is included in LICENSES/starlette_exporter. # The code has been modified to remove child route checks # (since we don't have any) and to stay within an 80-width limit. -def get_matching_route_path(scope: dict[Any, Any], routes: list[Route], - route_name: Optional[str] = None) -> str: +def get_matching_route_path( + scope: dict[Any, Any], routes: list[Route], route_name: Optional[str] = None +) -> str: """ Find a matching route and return its original path string @@ -34,7 +35,7 @@ def get_matching_route_path(scope: dict[Any, Any], routes: list[Route], if match == Match.FULL: route_name = route.path - ''' + """ # This path exists in the original function's code, but we # don't need it (currently), so it's been removed to avoid # useless test coverage. @@ -47,7 +48,7 @@ def get_matching_route_path(scope: dict[Any, Any], routes: list[Route], route_name = None else: route_name += child_route_name - ''' + """ return route_name elif match == Match.PARTIAL and route_name is None: @@ -55,9 +56,11 @@ def get_matching_route_path(scope: dict[Any, Any], routes: list[Route], def http_requests_total() -> Callable[[Info], None]: - metric = Counter("http_requests_total", - "Number of HTTP requests.", - labelnames=("method", "path", "status")) + metric = Counter( + "http_requests_total", + "Number of HTTP requests.", + labelnames=("method", "path", "status"), + ) def instrumentation(info: Info) -> None: if info.request.method.lower() in ("head", "options"): # pragma: no cover @@ -79,13 +82,13 @@ def http_requests_total() -> Callable[[Info], None]: if hasattr(app, "root_path"): app_root_path = getattr(app, "root_path") if root_path.startswith(app_root_path): - root_path = root_path[len(app_root_path):] + root_path = root_path[len(app_root_path) :] base_scope = { "type": scope.get("type"), "path": root_path + scope.get("path"), "path_params": scope.get("path_params", {}), - "method": scope.get("method") + "method": scope.get("method"), } method = scope.get("method") @@ -102,7 +105,8 @@ def http_api_requests_total() -> Callable[[Info], None]: metric = Counter( "http_api_requests", "Number of times an RPC API type has been requested.", - labelnames=("type", "status")) + labelnames=("type", "status"), + ) def instrumentation(info: Info) -> None: if info.request.method.lower() in ("head", "options"): # pragma: no cover diff --git a/aurweb/ratelimit.py b/aurweb/ratelimit.py index 86063f5d..cb08cdf5 100644 --- a/aurweb/ratelimit.py +++ b/aurweb/ratelimit.py @@ -38,8 +38,7 @@ def _update_ratelimit_db(request: Request): now = time.utcnow() time_to_delete = now - window_length - records = db.query(ApiRateLimit).filter( - ApiRateLimit.WindowStart < time_to_delete) + records = db.query(ApiRateLimit).filter(ApiRateLimit.WindowStart < time_to_delete) with db.begin(): db.delete_all(records) @@ -47,9 +46,7 @@ def _update_ratelimit_db(request: Request): record = db.query(ApiRateLimit, ApiRateLimit.IP == host).first() with db.begin(): if not record: - record = db.create(ApiRateLimit, - WindowStart=now, - IP=host, Requests=1) + record = db.create(ApiRateLimit, WindowStart=now, IP=host, Requests=1) else: record.Requests += 1 @@ -58,7 +55,7 @@ def _update_ratelimit_db(request: Request): def update_ratelimit(request: Request, pipeline: Pipeline): - """ Update the ratelimit stored in Redis or the database depending + """Update the ratelimit stored in Redis or the database depending on AUR_CONFIG's [options] cache setting. This Redis-capable function is slightly different than most. If Redis @@ -75,7 +72,7 @@ def update_ratelimit(request: Request, pipeline: Pipeline): def check_ratelimit(request: Request): - """ Increment and check to see if request has exceeded their rate limit. + """Increment and check to see if request has exceeded their rate limit. :param request: FastAPI request :returns: True if the request host has exceeded the rate limit else False diff --git a/aurweb/redis.py b/aurweb/redis.py index e29b8e37..af179b9b 100644 --- a/aurweb/redis.py +++ b/aurweb/redis.py @@ -1,9 +1,7 @@ import fakeredis - from redis import ConnectionPool, Redis import aurweb.config - from aurweb import logging logger = logging.get_logger(__name__) @@ -11,7 +9,7 @@ pool = None class FakeConnectionPool: - """ A fake ConnectionPool class which holds an internal reference + """A fake ConnectionPool class which holds an internal reference to a fakeredis handle. We normally deal with Redis by keeping its ConnectionPool globally diff --git a/aurweb/routers/__init__.py b/aurweb/routers/__init__.py index da79e38f..f77bce4f 100644 --- a/aurweb/routers/__init__.py +++ b/aurweb/routers/__init__.py @@ -3,7 +3,18 @@ API routers for FastAPI. See https://fastapi.tiangolo.com/tutorial/bigger-applications/ """ -from . import accounts, auth, html, packages, pkgbase, requests, rpc, rss, sso, trusted_user +from . import ( + accounts, + auth, + html, + packages, + pkgbase, + requests, + rpc, + rss, + sso, + trusted_user, +) """ aurweb application routes. This constant can be any iterable diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index dcac72b0..db05955a 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -1,6 +1,5 @@ import copy import typing - from http import HTTPStatus from typing import Any @@ -9,7 +8,6 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, or_ import aurweb.config - from aurweb import cookies, db, l10n, logging, models, util from aurweb.auth import account_type_required, requires_auth, requires_guest from aurweb.captcha import get_captcha_salts @@ -37,21 +35,23 @@ async def passreset(request: Request): @router.post("/passreset", response_class=HTMLResponse) @handle_form_exceptions @requires_guest -async def passreset_post(request: Request, - user: str = Form(...), - resetkey: str = Form(default=None), - password: str = Form(default=None), - confirm: str = Form(default=None)): +async def passreset_post( + request: Request, + user: str = Form(...), + resetkey: str = Form(default=None), + password: str = Form(default=None), + confirm: str = Form(default=None), +): context = await make_variable_context(request, "Password Reset") # The user parameter being required, we can match against criteria = or_(models.User.Username == user, models.User.Email == user) - db_user = db.query(models.User, - and_(criteria, models.User.Suspended == 0)).first() + db_user = db.query(models.User, and_(criteria, models.User.Suspended == 0)).first() if db_user is None: context["errors"] = ["Invalid e-mail."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.NOT_FOUND) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.NOT_FOUND + ) db.refresh(db_user) if resetkey: @@ -59,29 +59,34 @@ async def passreset_post(request: Request, if not db_user.ResetKey or resetkey != db_user.ResetKey: context["errors"] = ["Invalid e-mail."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.NOT_FOUND) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.NOT_FOUND + ) if not user or not password: context["errors"] = ["Missing a required field."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if password != confirm: # If the provided password does not match the provided confirm. context["errors"] = ["Password fields do not match."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if len(password) < models.User.minimum_passwd_length(): # Translate the error here, which simplifies error output # in the jinja2 template. _ = get_translator_for_request(request) - context["errors"] = [_( - "Your password must be at least %s characters.") % ( - str(models.User.minimum_passwd_length()))] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + _("Your password must be at least %s characters.") + % (str(models.User.minimum_passwd_length())) + ] + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST + ) # We got to this point; everything matched up. Update the password # and remove the ResetKey. @@ -92,8 +97,9 @@ async def passreset_post(request: Request, db_user.update_password(password) # Render ?step=complete. - return RedirectResponse(url="/passreset?step=complete", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + url="/passreset?step=complete", status_code=HTTPStatus.SEE_OTHER + ) # If we got here, we continue with issuing a resetkey for the user. resetkey = generate_resetkey() @@ -103,13 +109,13 @@ async def passreset_post(request: Request, ResetKeyNotification(db_user.ID).send() # Render ?step=confirm. - return RedirectResponse(url="/passreset?step=confirm", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + url="/passreset?step=confirm", status_code=HTTPStatus.SEE_OTHER + ) -def process_account_form(request: Request, user: models.User, - args: dict[str, Any]): - """ Process an account form. All fields are optional and only checks +def process_account_form(request: Request, user: models.User, args: dict[str, Any]): + """Process an account form. All fields are optional and only checks requirements in the case they are present. ``` @@ -146,7 +152,7 @@ def process_account_form(request: Request, user: models.User, validate.username_in_use, validate.email_in_use, validate.invalid_account_type, - validate.invalid_captcha + validate.invalid_captcha, ] try: @@ -158,11 +164,10 @@ def process_account_form(request: Request, user: models.User, return (True, []) -def make_account_form_context(context: dict, - request: Request, - user: models.User, - args: dict): - """ Modify a FastAPI context and add attributes for the account form. +def make_account_form_context( + context: dict, request: Request, user: models.User, args: dict +): + """Modify a FastAPI context and add attributes for the account form. :param context: FastAPI context :param request: FastAPI request @@ -173,15 +178,17 @@ def make_account_form_context(context: dict, # Do not modify the original context. context = copy.copy(context) - context["account_types"] = list(filter( - lambda e: request.user.AccountTypeID >= e[0], - [ - (at.USER_ID, f"Normal {at.USER}"), - (at.TRUSTED_USER_ID, at.TRUSTED_USER), - (at.DEVELOPER_ID, at.DEVELOPER), - (at.TRUSTED_USER_AND_DEV_ID, at.TRUSTED_USER_AND_DEV) - ] - )) + context["account_types"] = list( + filter( + lambda e: request.user.AccountTypeID >= e[0], + [ + (at.USER_ID, f"Normal {at.USER}"), + (at.TRUSTED_USER_ID, at.TRUSTED_USER), + (at.DEVELOPER_ID, at.DEVELOPER), + (at.TRUSTED_USER_AND_DEV_ID, at.TRUSTED_USER_AND_DEV), + ], + ) + ) if request.user.is_authenticated(): context["username"] = args.get("U", user.Username) @@ -229,24 +236,24 @@ def make_account_form_context(context: dict, @router.get("/register", response_class=HTMLResponse) @requires_guest -async def account_register(request: Request, - U: str = Form(default=str()), # Username - E: str = Form(default=str()), # Email - H: str = Form(default=False), # Hide Email - BE: str = Form(default=None), # Backup Email - R: str = Form(default=None), # Real Name - HP: str = Form(default=None), # Homepage - I: str = Form(default=None), # IRC Nick - K: str = Form(default=None), # PGP Key FP - L: str = Form(default=aurweb.config.get( - "options", "default_lang")), - TZ: str = Form(default=aurweb.config.get( - "options", "default_timezone")), - PK: str = Form(default=None), - CN: bool = Form(default=False), # Comment Notify - CU: bool = Form(default=False), # Update Notify - CO: bool = Form(default=False), # Owner Notify - captcha: str = Form(default=str())): +async def account_register( + request: Request, + U: str = Form(default=str()), # Username + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=None), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key FP + L: str = Form(default=aurweb.config.get("options", "default_lang")), + TZ: str = Form(default=aurweb.config.get("options", "default_timezone")), + PK: str = Form(default=None), + CN: bool = Form(default=False), # Comment Notify + CU: bool = Form(default=False), # Update Notify + CO: bool = Form(default=False), # Owner Notify + captcha: str = Form(default=str()), +): context = await make_variable_context(request, "Register") context["captcha_salt"] = get_captcha_salts()[0] context = make_account_form_context(context, request, None, dict()) @@ -256,32 +263,32 @@ async def account_register(request: Request, @router.post("/register", response_class=HTMLResponse) @handle_form_exceptions @requires_guest -async def account_register_post(request: Request, - U: str = Form(default=str()), # Username - E: str = Form(default=str()), # Email - H: str = Form(default=False), # Hide Email - BE: str = Form(default=None), # Backup Email - R: str = Form(default=''), # Real Name - HP: str = Form(default=None), # Homepage - I: str = Form(default=None), # IRC Nick - K: str = Form(default=None), # PGP Key - L: str = Form(default=aurweb.config.get( - "options", "default_lang")), - TZ: str = Form(default=aurweb.config.get( - "options", "default_timezone")), - PK: str = Form(default=str()), # SSH PubKey - CN: bool = Form(default=False), - UN: bool = Form(default=False), - ON: bool = Form(default=False), - captcha: str = Form(default=None), - captcha_salt: str = Form(...)): +async def account_register_post( + request: Request, + U: str = Form(default=str()), # Username + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=""), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key + L: str = Form(default=aurweb.config.get("options", "default_lang")), + TZ: str = Form(default=aurweb.config.get("options", "default_timezone")), + PK: str = Form(default=str()), # SSH PubKey + CN: bool = Form(default=False), + UN: bool = Form(default=False), + ON: bool = Form(default=False), + captcha: str = Form(default=None), + captcha_salt: str = Form(...), +): context = await make_variable_context(request, "Register") args = dict(await request.form()) args["K"] = args.get("K", str()).replace(" ", "") K = args.get("K") # Force "H" into a boolean. - args["H"] = H = (args.get("H", str()) == "on") + args["H"] = H = args.get("H", str()) == "on" context = make_account_form_context(context, request, None, args) ok, errors = process_account_form(request, request.user, args) @@ -289,30 +296,45 @@ async def account_register_post(request: Request, # If the field values given do not meet the requirements, # return HTTP 400 with an error. context["errors"] = errors - return render_template(request, "register.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "register.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if not captcha: context["errors"] = ["The CAPTCHA is missing."] - return render_template(request, "register.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "register.html", context, status_code=HTTPStatus.BAD_REQUEST + ) # Create a user with no password with a resetkey, then send # an email off about it. resetkey = generate_resetkey() # By default, we grab the User account type to associate with. - atype = db.query(models.AccountType, - models.AccountType.AccountType == "User").first() + atype = db.query( + models.AccountType, models.AccountType.AccountType == "User" + ).first() # Create a user given all parameters available. with db.begin(): - user = db.create(models.User, Username=U, - Email=E, HideEmail=H, BackupEmail=BE, - RealName=R, Homepage=HP, IRCNick=I, PGPKey=K, - LangPreference=L, Timezone=TZ, CommentNotify=CN, - UpdateNotify=UN, OwnershipNotify=ON, - ResetKey=resetkey, AccountType=atype) + user = db.create( + models.User, + Username=U, + Email=E, + HideEmail=H, + BackupEmail=BE, + RealName=R, + Homepage=HP, + IRCNick=I, + PGPKey=K, + LangPreference=L, + Timezone=TZ, + CommentNotify=CN, + UpdateNotify=UN, + OwnershipNotify=ON, + ResetKey=resetkey, + AccountType=atype, + ) # If a PK was given and either one does not exist or the given # PK mismatches the existing user's SSHPubKey.PubKey. @@ -323,8 +345,9 @@ async def account_register_post(request: Request, pk = " ".join(k) fprint = get_fingerprint(pk) with db.begin(): - db.create(models.SSHPubKey, UserID=user.ID, - PubKey=pk, Fingerprint=fprint) + db.create( + models.SSHPubKey, UserID=user.ID, PubKey=pk, Fingerprint=fprint + ) # Send a reset key notification to the new user. WelcomeNotification(user.ID).send() @@ -334,8 +357,9 @@ async def account_register_post(request: Request, return render_template(request, "register.html", context) -def cannot_edit(request: Request, user: models.User) \ - -> typing.Optional[RedirectResponse]: +def cannot_edit( + request: Request, user: models.User +) -> typing.Optional[RedirectResponse]: """ Decide if `request.user` cannot edit `user`. @@ -373,31 +397,30 @@ async def account_edit(request: Request, username: str): @router.post("/account/{username}/edit", response_class=HTMLResponse) @handle_form_exceptions @requires_auth -async def account_edit_post(request: Request, - username: str, - U: str = Form(default=str()), # Username - J: bool = Form(default=False), - E: str = Form(default=str()), # Email - H: str = Form(default=False), # Hide Email - BE: str = Form(default=None), # Backup Email - R: str = Form(default=None), # Real Name - HP: str = Form(default=None), # Homepage - I: str = Form(default=None), # IRC Nick - K: str = Form(default=None), # PGP Key - L: str = Form(aurweb.config.get( - "options", "default_lang")), - TZ: str = Form(aurweb.config.get( - "options", "default_timezone")), - P: str = Form(default=str()), # New Password - C: str = Form(default=None), # Password Confirm - PK: str = Form(default=None), # PubKey - CN: bool = Form(default=False), # Comment Notify - UN: bool = Form(default=False), # Update Notify - ON: bool = Form(default=False), # Owner Notify - T: int = Form(default=None), - passwd: str = Form(default=str())): - user = db.query(models.User).filter( - models.User.Username == username).first() +async def account_edit_post( + request: Request, + username: str, + U: str = Form(default=str()), # Username + J: bool = Form(default=False), + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=None), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key + L: str = Form(aurweb.config.get("options", "default_lang")), + TZ: str = Form(aurweb.config.get("options", "default_timezone")), + P: str = Form(default=str()), # New Password + C: str = Form(default=None), # Password Confirm + PK: str = Form(default=None), # PubKey + CN: bool = Form(default=False), # Comment Notify + UN: bool = Form(default=False), # Update Notify + ON: bool = Form(default=False), # Owner Notify + T: int = Form(default=None), + passwd: str = Form(default=str()), +): + user = db.query(models.User).filter(models.User.Username == username).first() response = cannot_edit(request, user) if response: return response @@ -416,13 +439,15 @@ async def account_edit_post(request: Request, if not passwd: context["errors"] = ["Invalid password."] - return render_template(request, "account/edit.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "account/edit.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if not ok: context["errors"] = errors - return render_template(request, "account/edit.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "account/edit.html", context, status_code=HTTPStatus.BAD_REQUEST + ) updates = [ update.simple, @@ -430,7 +455,7 @@ async def account_edit_post(request: Request, update.timezone, update.ssh_pubkey, update.account_type, - update.password + update.password, ] for f in updates: @@ -441,18 +466,17 @@ async def account_edit_post(request: Request, # Update cookies with requests, in case they were changed. response = render_template(request, "account/edit.html", context) - return cookies.update_response_cookies(request, response, - aurtz=TZ, aurlang=L) + return cookies.update_response_cookies(request, response, aurtz=TZ, aurlang=L) @router.get("/account/{username}") async def account(request: Request, username: str): _ = l10n.get_translator_for_request(request) - context = await make_variable_context( - request, _("Account") + " " + username) + context = await make_variable_context(request, _("Account") + " " + username) if not request.user.is_authenticated(): - return render_template(request, "account/show.html", context, - status_code=HTTPStatus.UNAUTHORIZED) + return render_template( + request, "account/show.html", context, status_code=HTTPStatus.UNAUTHORIZED + ) # Get related User record, if possible. user = get_user_by_name(username) @@ -460,11 +484,10 @@ async def account(request: Request, username: str): # Format PGPKey for display with a space between each 4 characters. k = user.PGPKey or str() - context["pgp_key"] = " ".join([k[i:i + 4] for i in range(0, len(k), 4)]) + context["pgp_key"] = " ".join([k[i : i + 4] for i in range(0, len(k), 4)]) login_ts = None - session = db.query(models.Session).filter( - models.Session.UsersID == user.ID).first() + session = db.query(models.Session).filter(models.Session.UsersID == user.ID).first() if session: login_ts = user.session.LastUpdateTS context["login_ts"] = login_ts @@ -480,15 +503,14 @@ async def account_comments(request: Request, username: str): context = make_context(request, "Accounts") context["username"] = username context["comments"] = user.package_comments.order_by( - models.PackageComment.CommentTS.desc()) + models.PackageComment.CommentTS.desc() + ) return render_template(request, "account/comments.html", context) @router.get("/accounts") @requires_auth -@account_type_required({at.TRUSTED_USER, - at.DEVELOPER, - at.TRUSTED_USER_AND_DEV}) +@account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) async def accounts(request: Request): context = make_context(request, "Accounts") return render_template(request, "account/search.html", context) @@ -497,19 +519,19 @@ async def accounts(request: Request): @router.post("/accounts") @handle_form_exceptions @requires_auth -@account_type_required({at.TRUSTED_USER, - at.DEVELOPER, - at.TRUSTED_USER_AND_DEV}) -async def accounts_post(request: Request, - O: int = Form(default=0), # Offset - SB: str = Form(default=str()), # Sort By - U: str = Form(default=str()), # Username - T: str = Form(default=str()), # Account Type - S: bool = Form(default=False), # Suspended - E: str = Form(default=str()), # Email - R: str = Form(default=str()), # Real Name - I: str = Form(default=str()), # IRC Nick - K: str = Form(default=str())): # PGP Key +@account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) +async def accounts_post( + request: Request, + O: int = Form(default=0), # Offset + SB: str = Form(default=str()), # Sort By + U: str = Form(default=str()), # Username + T: str = Form(default=str()), # Account Type + S: bool = Form(default=False), # Suspended + E: str = Form(default=str()), # Email + R: str = Form(default=str()), # Real Name + I: str = Form(default=str()), # IRC Nick + K: str = Form(default=str()), +): # PGP Key context = await make_variable_context(request, "Accounts") context["pp"] = pp = 50 # Hits per page. @@ -534,7 +556,7 @@ async def accounts_post(request: Request, "u": at.USER_ID, "t": at.TRUSTED_USER_ID, "d": at.DEVELOPER_ID, - "td": at.TRUSTED_USER_AND_DEV_ID + "td": at.TRUSTED_USER_AND_DEV_ID, } account_type_id = account_types.get(T, None) @@ -545,7 +567,8 @@ async def accounts_post(request: Request, # Populate this list with any additional statements to # be ANDed together. statements = [ - v for k, v in [ + v + for k, v in [ (account_type_id is not None, models.AccountType.ID == account_type_id), (bool(U), models.User.Username.like(f"%{U}%")), (bool(S), models.User.Suspended == S), @@ -553,7 +576,8 @@ async def accounts_post(request: Request, (bool(R), models.User.RealName.like(f"%{R}%")), (bool(I), models.User.IRCNick.like(f"%{I}%")), (bool(K), models.User.PGPKey.like(f"%{K}%")), - ] if k + ] + if k ] # Filter the query by coe-mbining all statements added above into @@ -571,9 +595,7 @@ async def accounts_post(request: Request, return render_template(request, "account/index.html", context) -def render_terms_of_service(request: Request, - context: dict, - terms: typing.Iterable): +def render_terms_of_service(request: Request, context: dict, terms: typing.Iterable): if not terms: return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) context["unaccepted_terms"] = terms @@ -585,14 +607,21 @@ def render_terms_of_service(request: Request, async def terms_of_service(request: Request): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. - diffs = db.query(models.Term).join(models.AcceptedTerm).filter( - models.AcceptedTerm.Revision < models.Term.Revision).all() + diffs = ( + db.query(models.Term) + .join(models.AcceptedTerm) + .filter(models.AcceptedTerm.Revision < models.Term.Revision) + .all() + ) # Query the database for any terms that have not yet been accepted. - unaccepted = db.query(models.Term).filter( - ~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))).all() + unaccepted = ( + db.query(models.Term) + .filter(~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))) + .all() + ) - for record in (diffs + unaccepted): + for record in diffs + unaccepted: db.refresh(record) # Translate the 'Terms of Service' part of our page title. @@ -607,16 +636,22 @@ async def terms_of_service(request: Request): @router.post("/tos") @handle_form_exceptions @requires_auth -async def terms_of_service_post(request: Request, - accept: bool = Form(default=False)): +async def terms_of_service_post(request: Request, accept: bool = Form(default=False)): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. - diffs = db.query(models.Term).join(models.AcceptedTerm).filter( - models.AcceptedTerm.Revision < models.Term.Revision).all() + diffs = ( + db.query(models.Term) + .join(models.AcceptedTerm) + .filter(models.AcceptedTerm.Revision < models.Term.Revision) + .all() + ) # Query the database for any terms that have not yet been accepted. - unaccepted = db.query(models.Term).filter( - ~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))).all() + unaccepted = ( + db.query(models.Term) + .filter(~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))) + .all() + ) if not accept: # Translate the 'Terms of Service' part of our page title. @@ -628,7 +663,8 @@ async def terms_of_service_post(request: Request, # them instead of reiterating the process in terms_of_service. accept_needed = sorted(unaccepted + diffs) return render_terms_of_service( - request, context, util.apply_all(accept_needed, db.refresh)) + request, context, util.apply_all(accept_needed, db.refresh) + ) with db.begin(): # For each term we found, query for the matching accepted term @@ -636,13 +672,18 @@ async def terms_of_service_post(request: Request, for term in diffs: db.refresh(term) accepted_term = request.user.accepted_terms.filter( - models.AcceptedTerm.TermsID == term.ID).first() + models.AcceptedTerm.TermsID == term.ID + ).first() accepted_term.Revision = term.Revision # For each term that was never accepted, accept it! for term in unaccepted: db.refresh(term) - db.create(models.AcceptedTerm, User=request.user, - Term=term, Revision=term.Revision) + db.create( + models.AcceptedTerm, + User=request.user, + Term=term, + Revision=term.Revision, + ) return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 50cec419..3f94952e 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -5,7 +5,6 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import or_ import aurweb.config - from aurweb import cookies, db from aurweb.auth import requires_auth, requires_guest from aurweb.exceptions import handle_form_exceptions @@ -17,7 +16,7 @@ router = APIRouter() async def login_template(request: Request, next: str, errors: list = None): - """ Provide login-specific template context to render_template. """ + """Provide login-specific template context to render_template.""" context = await make_variable_context(request, "Login", next) context["errors"] = errors context["url_base"] = f"{request.url.scheme}://{request.url.netloc}" @@ -32,55 +31,73 @@ async def login_get(request: Request, next: str = "/"): @router.post("/login", response_class=HTMLResponse) @handle_form_exceptions @requires_guest -async def login_post(request: Request, - next: str = Form(...), - user: str = Form(default=str()), - passwd: str = Form(default=str()), - remember_me: bool = Form(default=False)): +async def login_post( + request: Request, + next: str = Form(...), + user: str = Form(default=str()), + passwd: str = Form(default=str()), + remember_me: bool = Form(default=False), +): # TODO: Once the Origin header gets broader adoption, this code can be # slightly simplified to use it. login_path = aurweb.config.get("options", "aur_location") + "/login" referer = request.headers.get("Referer") if not referer or not referer.startswith(login_path): _ = get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, - detail=_("Bad Referer header.")) + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail=_("Bad Referer header.") + ) with db.begin(): - user = db.query(User).filter( - or_(User.Username == user, User.Email == user) - ).first() + user = ( + db.query(User) + .filter(or_(User.Username == user, User.Email == user)) + .first() + ) if not user: - return await login_template(request, next, - errors=["Bad username or password."]) + return await login_template(request, next, errors=["Bad username or password."]) if user.Suspended: - return await login_template(request, next, - errors=["Account Suspended"]) + return await login_template(request, next, errors=["Account Suspended"]) cookie_timeout = cookies.timeout(remember_me) sid = user.login(request, passwd, cookie_timeout) if not sid: - return await login_template(request, next, - errors=["Bad username or password."]) + return await login_template(request, next, errors=["Bad username or password."]) - response = RedirectResponse(url=next, - status_code=HTTPStatus.SEE_OTHER) + response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER) secure = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie("AURSID", sid, max_age=cookie_timeout, - secure=secure, httponly=secure, - samesite=cookies.samesite()) - response.set_cookie("AURTZ", user.Timezone, - secure=secure, httponly=secure, - samesite=cookies.samesite()) - response.set_cookie("AURLANG", user.LangPreference, - secure=secure, httponly=secure, - samesite=cookies.samesite()) - response.set_cookie("AURREMEMBER", remember_me, - secure=secure, httponly=secure, - samesite=cookies.samesite()) + response.set_cookie( + "AURSID", + sid, + max_age=cookie_timeout, + secure=secure, + httponly=secure, + samesite=cookies.samesite(), + ) + response.set_cookie( + "AURTZ", + user.Timezone, + secure=secure, + httponly=secure, + samesite=cookies.samesite(), + ) + response.set_cookie( + "AURLANG", + user.LangPreference, + secure=secure, + httponly=secure, + samesite=cookies.samesite(), + ) + response.set_cookie( + "AURREMEMBER", + remember_me, + secure=secure, + httponly=secure, + samesite=cookies.samesite(), + ) return response @@ -93,8 +110,7 @@ async def logout(request: Request, next: str = Form(default="/")): # Use 303 since we may be handling a post request, that'll get it # to redirect to a get request. - response = RedirectResponse(url=next, - status_code=HTTPStatus.SEE_OTHER) + response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER) response.delete_cookie("AURSID") response.delete_cookie("AURTZ") return response diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index d31a32c7..2148d535 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -2,17 +2,20 @@ decorators in some way; more complex routes should be defined in their own modules and imported here. """ import os - from http import HTTPStatus from fastapi import APIRouter, Form, HTTPException, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse -from prometheus_client import CONTENT_TYPE_LATEST, CollectorRegistry, generate_latest, multiprocess +from prometheus_client import ( + CONTENT_TYPE_LATEST, + CollectorRegistry, + generate_latest, + multiprocess, +) from sqlalchemy import and_, case, or_ import aurweb.config import aurweb.models.package_request - from aurweb import cookies, db, logging, models, time, util from aurweb.cache import db_count_cache from aurweb.exceptions import handle_form_exceptions @@ -27,17 +30,19 @@ router = APIRouter() @router.get("/favicon.ico") async def favicon(request: Request): - """ Some browsers attempt to find a website's favicon via root uri at - /favicon.ico, so provide a redirection here to our static icon. """ + """Some browsers attempt to find a website's favicon via root uri at + /favicon.ico, so provide a redirection here to our static icon.""" return RedirectResponse("/static/images/favicon.ico") @router.post("/language", response_class=RedirectResponse) @handle_form_exceptions -async def language(request: Request, - set_lang: str = Form(...), - next: str = Form(...), - q: str = Form(default=None)): +async def language( + request: Request, + set_lang: str = Form(...), + next: str = Form(...), + q: str = Form(default=None), +): """ A POST route used to set a session's language. @@ -45,7 +50,7 @@ async def language(request: Request, setting the language on any page, we want to preserve query parameters across the redirect. """ - if next[0] != '/': + if next[0] != "/": return HTMLResponse(b"Invalid 'next' parameter.", status_code=400) query_string = "?" + q if q else str() @@ -56,20 +61,21 @@ async def language(request: Request, request.user.LangPreference = set_lang # In any case, set the response's AURLANG cookie that never expires. - response = RedirectResponse(url=f"{next}{query_string}", - status_code=HTTPStatus.SEE_OTHER) + response = RedirectResponse( + url=f"{next}{query_string}", status_code=HTTPStatus.SEE_OTHER + ) secure = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie("AURLANG", set_lang, - secure=secure, httponly=secure, - samesite=cookies.samesite()) + response.set_cookie( + "AURLANG", set_lang, secure=secure, httponly=secure, samesite=cookies.samesite() + ) return response @router.get("/", response_class=HTMLResponse) async def index(request: Request): - """ Homepage route. """ + """Homepage route.""" context = make_context(request, "Home") - context['ssh_fingerprints'] = util.get_ssh_fingerprints() + context["ssh_fingerprints"] = util.get_ssh_fingerprints() bases = db.query(models.PackageBase) @@ -79,24 +85,33 @@ async def index(request: Request): # Package statistics. query = bases.filter(models.PackageBase.PackagerUID.isnot(None)) context["package_count"] = await db_count_cache( - redis, "package_count", query, expire=cache_expire) + redis, "package_count", query, expire=cache_expire + ) query = bases.filter( - and_(models.PackageBase.MaintainerUID.is_(None), - models.PackageBase.PackagerUID.isnot(None)) + and_( + models.PackageBase.MaintainerUID.is_(None), + models.PackageBase.PackagerUID.isnot(None), + ) ) context["orphan_count"] = await db_count_cache( - redis, "orphan_count", query, expire=cache_expire) + redis, "orphan_count", query, expire=cache_expire + ) query = db.query(models.User) context["user_count"] = await db_count_cache( - redis, "user_count", query, expire=cache_expire) + redis, "user_count", query, expire=cache_expire + ) query = query.filter( - or_(models.User.AccountTypeID == TRUSTED_USER_ID, - models.User.AccountTypeID == TRUSTED_USER_AND_DEV_ID)) + or_( + models.User.AccountTypeID == TRUSTED_USER_ID, + models.User.AccountTypeID == TRUSTED_USER_AND_DEV_ID, + ) + ) context["trusted_user_count"] = await db_count_cache( - redis, "trusted_user_count", query, expire=cache_expire) + redis, "trusted_user_count", query, expire=cache_expire + ) # Current timestamp. now = time.utcnow() @@ -106,31 +121,40 @@ async def index(request: Request): one_hour = 3600 updated = bases.filter( - and_(models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS >= one_hour, - models.PackageBase.PackagerUID.isnot(None)) + and_( + models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS >= one_hour, + models.PackageBase.PackagerUID.isnot(None), + ) ) query = bases.filter( - and_(models.PackageBase.SubmittedTS >= seven_days_ago, - models.PackageBase.PackagerUID.isnot(None)) + and_( + models.PackageBase.SubmittedTS >= seven_days_ago, + models.PackageBase.PackagerUID.isnot(None), + ) ) context["seven_days_old_added"] = await db_count_cache( - redis, "seven_days_old_added", query, expire=cache_expire) + redis, "seven_days_old_added", query, expire=cache_expire + ) query = updated.filter(models.PackageBase.ModifiedTS >= seven_days_ago) context["seven_days_old_updated"] = await db_count_cache( - redis, "seven_days_old_updated", query, expire=cache_expire) + redis, "seven_days_old_updated", query, expire=cache_expire + ) year = seven_days * 52 # Fifty two weeks worth: one year. year_ago = now - year query = updated.filter(models.PackageBase.ModifiedTS >= year_ago) context["year_old_updated"] = await db_count_cache( - redis, "year_old_updated", query, expire=cache_expire) + redis, "year_old_updated", query, expire=cache_expire + ) query = bases.filter( - models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS < 3600) + models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS < 3600 + ) context["never_updated"] = await db_count_cache( - redis, "never_updated", query, expire=cache_expire) + redis, "never_updated", query, expire=cache_expire + ) # Get the 15 most recently updated packages. context["package_updates"] = updated_packages(15, cache_expire) @@ -140,78 +164,92 @@ async def index(request: Request): # the dashboard display. packages = db.query(models.Package).join(models.PackageBase) - maintained = packages.join( - models.PackageComaintainer, - models.PackageComaintainer.PackageBaseID == models.PackageBase.ID, - isouter=True - ).join( - models.User, - or_(models.PackageBase.MaintainerUID == models.User.ID, - models.PackageComaintainer.UsersID == models.User.ID) - ).filter( - models.User.ID == request.user.ID + maintained = ( + packages.join( + models.PackageComaintainer, + models.PackageComaintainer.PackageBaseID == models.PackageBase.ID, + isouter=True, + ) + .join( + models.User, + or_( + models.PackageBase.MaintainerUID == models.User.ID, + models.PackageComaintainer.UsersID == models.User.ID, + ), + ) + .filter(models.User.ID == request.user.ID) ) # Packages maintained by the user that have been flagged. - context["flagged_packages"] = maintained.filter( - models.PackageBase.OutOfDateTS.isnot(None) - ).order_by( - models.PackageBase.ModifiedTS.desc(), models.Package.Name.asc() - ).limit(50).all() + context["flagged_packages"] = ( + maintained.filter(models.PackageBase.OutOfDateTS.isnot(None)) + .order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.asc()) + .limit(50) + .all() + ) # Flagged packages that request.user has voted for. context["flagged_packages_voted"] = query_voted( - context.get("flagged_packages"), request.user) + context.get("flagged_packages"), request.user + ) # Flagged packages that request.user is being notified about. context["flagged_packages_notified"] = query_notified( - context.get("flagged_packages"), request.user) + context.get("flagged_packages"), request.user + ) - archive_time = aurweb.config.getint('options', 'request_archive_time') + archive_time = aurweb.config.getint("options", "request_archive_time") start = now - archive_time # Package requests created by request.user. - context["package_requests"] = request.user.package_requests.filter( - models.PackageRequest.RequestTS >= start - ).order_by( - # Order primarily by the Status column being PENDING_ID, - # and secondarily by RequestTS; both in descending order. - case([(models.PackageRequest.Status == PENDING_ID, 1)], - else_=0).desc(), - models.PackageRequest.RequestTS.desc() - ).limit(50).all() + context["package_requests"] = ( + request.user.package_requests.filter( + models.PackageRequest.RequestTS >= start + ) + .order_by( + # Order primarily by the Status column being PENDING_ID, + # and secondarily by RequestTS; both in descending order. + case([(models.PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), + models.PackageRequest.RequestTS.desc(), + ) + .limit(50) + .all() + ) # Packages that the request user maintains or comaintains. - context["packages"] = maintained.filter( - models.User.ID == models.PackageBase.MaintainerUID - ).order_by( - models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc() - ).limit(50).all() + context["packages"] = ( + maintained.filter(models.User.ID == models.PackageBase.MaintainerUID) + .order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc()) + .limit(50) + .all() + ) # Packages that request.user has voted for. - context["packages_voted"] = query_voted( - context.get("packages"), request.user) + context["packages_voted"] = query_voted(context.get("packages"), request.user) # Packages that request.user is being notified about. context["packages_notified"] = query_notified( - context.get("packages"), request.user) + context.get("packages"), request.user + ) # Any packages that the request user comaintains. - context["comaintained"] = packages.join( - models.PackageComaintainer - ).filter( - models.PackageComaintainer.UsersID == request.user.ID - ).order_by( - models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc() - ).limit(50).all() + context["comaintained"] = ( + packages.join(models.PackageComaintainer) + .filter(models.PackageComaintainer.UsersID == request.user.ID) + .order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc()) + .limit(50) + .all() + ) # Comaintained packages that request.user has voted for. context["comaintained_voted"] = query_voted( - context.get("comaintained"), request.user) + context.get("comaintained"), request.user + ) # Comaintained packages that request.user is being notified about. context["comaintained_notified"] = query_notified( - context.get("comaintained"), request.user) + context.get("comaintained"), request.user + ) return render_template(request, "index.html", context) @@ -232,16 +270,15 @@ async def archive_sha256(request: Request, archive: str): @router.get("/metrics") async def metrics(request: Request): if not os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): - return Response("Prometheus metrics are not enabled.", - status_code=HTTPStatus.SERVICE_UNAVAILABLE) + return Response( + "Prometheus metrics are not enabled.", + status_code=HTTPStatus.SERVICE_UNAVAILABLE, + ) registry = CollectorRegistry() multiprocess.MultiProcessCollector(registry) data = generate_latest(registry) - headers = { - "Content-Type": CONTENT_TYPE_LATEST, - "Content-Length": str(len(data)) - } + headers = {"Content-Type": CONTENT_TYPE_LATEST, "Content-Length": str(len(data))} return Response(data, headers=headers) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 7bf4e3d4..55d2abf5 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -5,7 +5,6 @@ from typing import Any from fastapi import APIRouter, Form, Query, Request, Response import aurweb.filters # noqa: F401 - from aurweb import config, db, defaults, logging, models, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import InvariantError, handle_form_exceptions @@ -13,23 +12,24 @@ from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.packages import util as pkgutil from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base -from aurweb.pkgbase import actions as pkgbase_actions -from aurweb.pkgbase import util as pkgbaseutil +from aurweb.pkgbase import actions as pkgbase_actions, util as pkgbaseutil from aurweb.templates import make_context, make_variable_context, render_template logger = logging.get_logger(__name__) router = APIRouter() -async def packages_get(request: Request, context: dict[str, Any], - status_code: HTTPStatus = HTTPStatus.OK): +async def packages_get( + request: Request, context: dict[str, Any], status_code: HTTPStatus = HTTPStatus.OK +): # Query parameters used in this request. context["q"] = dict(request.query_params) # Per page and offset. offset, per_page = util.sanitize_params( request.query_params.get("O", defaults.O), - request.query_params.get("PP", defaults.PP)) + request.query_params.get("PP", defaults.PP), + ) context["O"] = offset # Limit PP to options.max_search_results @@ -82,8 +82,7 @@ async def packages_get(request: Request, context: dict[str, Any], if submit == "Orphans": # If the user clicked the "Orphans" button, we only want # orphaned packages. - search.query = search.query.filter( - models.PackageBase.MaintainerUID.is_(None)) + search.query = search.query.filter(models.PackageBase.MaintainerUID.is_(None)) # Collect search result count here; we've applied our keywords. # Including more query operations below, like ordering, will @@ -94,26 +93,31 @@ async def packages_get(request: Request, context: dict[str, Any], search.sort_by(sort_by, sort_order) # Insert search results into the context. - results = search.results().with_entities( - models.Package.ID, - models.Package.Name, - models.Package.PackageBaseID, - models.Package.Version, - models.Package.Description, - models.PackageBase.Popularity, - models.PackageBase.NumVotes, - models.PackageBase.OutOfDateTS, - models.User.Username.label("Maintainer"), - models.PackageVote.PackageBaseID.label("Voted"), - models.PackageNotification.PackageBaseID.label("Notify") - ).group_by(models.Package.Name) + results = ( + search.results() + .with_entities( + models.Package.ID, + models.Package.Name, + models.Package.PackageBaseID, + models.Package.Version, + models.Package.Description, + models.PackageBase.Popularity, + models.PackageBase.NumVotes, + models.PackageBase.OutOfDateTS, + models.User.Username.label("Maintainer"), + models.PackageVote.PackageBaseID.label("Voted"), + models.PackageNotification.PackageBaseID.label("Notify"), + ) + .group_by(models.Package.Name) + ) packages = results.limit(per_page).offset(offset) context["packages"] = packages context["packages_count"] = num_packages - return render_template(request, "packages/index.html", context, - status_code=status_code) + return render_template( + request, "packages/index.html", context, status_code=status_code + ) @router.get("/packages") @@ -123,9 +127,12 @@ async def packages(request: Request) -> Response: @router.get("/packages/{name}") -async def package(request: Request, name: str, - all_deps: bool = Query(default=False), - all_reqs: bool = Query(default=False)) -> Response: +async def package( + request: Request, + name: str, + all_deps: bool = Query(default=False), + all_reqs: bool = Query(default=False), +) -> Response: """ Get a package by name. @@ -156,26 +163,21 @@ async def package(request: Request, name: str, # Add our base information. context = await pkgbaseutil.make_variable_context(request, pkgbase) - context.update( - { - "all_deps": all_deps, - "all_reqs": all_reqs - } - ) + context.update({"all_deps": all_deps, "all_reqs": all_reqs}) context["package"] = pkg # Package sources. context["sources"] = pkg.package_sources.order_by( - models.PackageSource.Source.asc()).all() + models.PackageSource.Source.asc() + ).all() # Listing metadata. context["max_listing"] = max_listing = 20 # Package dependencies. deps = pkg.package_dependencies.order_by( - models.PackageDependency.DepTypeID.asc(), - models.PackageDependency.DepName.asc() + models.PackageDependency.DepTypeID.asc(), models.PackageDependency.DepName.asc() ) context["depends_count"] = deps.count() if not all_deps: @@ -183,8 +185,7 @@ async def package(request: Request, name: str, context["dependencies"] = deps.all() # Package requirements (other packages depend on this one). - reqs = pkgutil.pkg_required( - pkg.Name, [p.RelName for p in rels_data.get("p", [])]) + reqs = pkgutil.pkg_required(pkg.Name, [p.RelName for p in rels_data.get("p", [])]) context["reqs_count"] = reqs.count() if not all_reqs: reqs = reqs.limit(max_listing) @@ -210,8 +211,7 @@ async def package(request: Request, name: str, return render_template(request, "packages/show.html", context) -async def packages_unflag(request: Request, package_ids: list[int] = [], - **kwargs): +async def packages_unflag(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: return (False, ["You did not select any packages to unflag."]) @@ -220,11 +220,11 @@ async def packages_unflag(request: Request, package_ids: list[int] = [], bases = set() package_ids = set(package_ids) # Convert this to a set for O(1). - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: has_cred = request.user.has_credential( - creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger]) + creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger] + ) if not has_cred: return (False, ["You did not select any packages to unflag."]) @@ -236,20 +236,17 @@ async def packages_unflag(request: Request, package_ids: list[int] = [], return (True, ["The selected packages have been unflagged."]) -async def packages_notify(request: Request, package_ids: list[int] = [], - **kwargs): +async def packages_notify(request: Request, package_ids: list[int] = [], **kwargs): # In cases where we encounter errors with the request, we'll # use this error tuple as a return value. # TODO: This error does not yet have a translation. - error_tuple = (False, - ["You did not select any packages to be notified about."]) + error_tuple = (False, ["You did not select any packages to be notified about."]) if not package_ids: return error_tuple bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -257,9 +254,11 @@ async def packages_notify(request: Request, package_ids: list[int] = [], # Perform some checks on what the user selected for notify. for pkgbase in bases: - notif = db.query(pkgbase.notifications.filter( - models.PackageNotification.UserID == request.user.ID - ).exists()).scalar() + notif = db.query( + pkgbase.notifications.filter( + models.PackageNotification.UserID == request.user.ID + ).exists() + ).scalar() has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) # If the request user either does not have credentials @@ -275,23 +274,20 @@ async def packages_notify(request: Request, package_ids: list[int] = [], return (True, ["The selected packages' notifications have been enabled."]) -async def packages_unnotify(request: Request, package_ids: list[int] = [], - **kwargs): +async def packages_unnotify(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: # TODO: This error does not yet have a translation. - return (False, - ["You did not select any packages for notification removal."]) + return (False, ["You did not select any packages for notification removal."]) # TODO: This error does not yet have a translation. error_tuple = ( False, - ["A package you selected does not have notifications enabled."] + ["A package you selected does not have notifications enabled."], ) bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -299,9 +295,11 @@ async def packages_unnotify(request: Request, package_ids: list[int] = [], # Perform some checks on what the user selected for notify. for pkgbase in bases: - notif = db.query(pkgbase.notifications.filter( - models.PackageNotification.UserID == request.user.ID - ).exists()).scalar() + notif = db.query( + pkgbase.notifications.filter( + models.PackageNotification.UserID == request.user.ID + ).exists() + ).scalar() if not notif: return error_tuple @@ -312,19 +310,24 @@ async def packages_unnotify(request: Request, package_ids: list[int] = [], return (True, ["The selected packages' notifications have been removed."]) -async def packages_adopt(request: Request, package_ids: list[int] = [], - confirm: bool = False, **kwargs): +async def packages_adopt( + request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs +): if not package_ids: return (False, ["You did not select any packages to adopt."]) if not confirm: - return (False, ["The selected packages have not been adopted, " - "check the confirmation checkbox."]) + return ( + False, + [ + "The selected packages have not been adopted, " + "check the confirmation checkbox." + ], + ) bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -335,8 +338,10 @@ async def packages_adopt(request: Request, package_ids: list[int] = [], has_cred = request.user.has_credential(creds.PKGBASE_ADOPT) if not (has_cred or not pkgbase.Maintainer): # TODO: This error needs to be translated. - return (False, ["You are not allowed to adopt one of the " - "packages you selected."]) + return ( + False, + ["You are not allowed to adopt one of the " "packages you selected."], + ) # Now, really adopt the bases. for pkgbase in bases: @@ -345,8 +350,7 @@ async def packages_adopt(request: Request, package_ids: list[int] = [], return (True, ["The selected packages have been adopted."]) -def disown_all(request: Request, pkgbases: list[models.PackageBase]) \ - -> list[str]: +def disown_all(request: Request, pkgbases: list[models.PackageBase]) -> list[str]: errors = [] for pkgbase in pkgbases: try: @@ -356,19 +360,24 @@ def disown_all(request: Request, pkgbases: list[models.PackageBase]) \ return errors -async def packages_disown(request: Request, package_ids: list[int] = [], - confirm: bool = False, **kwargs): +async def packages_disown( + request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs +): if not package_ids: return (False, ["You did not select any packages to disown."]) if not confirm: - return (False, ["The selected packages have not been disowned, " - "check the confirmation checkbox."]) + return ( + False, + [ + "The selected packages have not been disowned, " + "check the confirmation checkbox." + ], + ) bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -376,12 +385,15 @@ async def packages_disown(request: Request, package_ids: list[int] = [], # Check that the user has credentials for every package they selected. for pkgbase in bases: - has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=[pkgbase.Maintainer]) + has_cred = request.user.has_credential( + creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer] + ) if not has_cred: # TODO: This error needs to be translated. - return (False, ["You are not allowed to disown one " - "of the packages you selected."]) + return ( + False, + ["You are not allowed to disown one " "of the packages you selected."], + ) # Now, disown all the bases if we can. if errors := disown_all(request, bases): @@ -390,23 +402,31 @@ async def packages_disown(request: Request, package_ids: list[int] = [], return (True, ["The selected packages have been disowned."]) -async def packages_delete(request: Request, package_ids: list[int] = [], - confirm: bool = False, merge_into: str = str(), - **kwargs): +async def packages_delete( + request: Request, + package_ids: list[int] = [], + confirm: bool = False, + merge_into: str = str(), + **kwargs, +): if not package_ids: return (False, ["You did not select any packages to delete."]) if not confirm: - return (False, ["The selected packages have not been deleted, " - "check the confirmation checkbox."]) + return ( + False, + [ + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ], + ) if not request.user.has_credential(creds.PKGBASE_DELETE): return (False, ["You do not have permission to delete packages."]) # set-ify package_ids and query the database for related records. package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() if len(packages) != len(package_ids): # Let the user know there was an issue with their input: they have @@ -422,12 +442,15 @@ async def packages_delete(request: Request, package_ids: list[int] = [], notifs += pkgbase_actions.pkgbase_delete_instance(request, pkgbase) # Log out the fact that this happened for accountability. - logger.info(f"Privileged user '{request.user.Username}' deleted the " - f"following package bases: {str(deleted_bases)}.") + logger.info( + f"Privileged user '{request.user.Username}' deleted the " + f"following package bases: {str(deleted_bases)}." + ) util.apply_all(notifs, lambda n: n.send()) return (True, ["The selected packages have been deleted."]) + # A mapping of action string -> callback functions used within the # `packages_post` route below. We expect any action callback to # return a tuple in the format: (succeeded: bool, message: list[str]). @@ -444,10 +467,12 @@ PACKAGE_ACTIONS = { @router.post("/packages") @handle_form_exceptions @requires_auth -async def packages_post(request: Request, - IDs: list[int] = Form(default=[]), - action: str = Form(default=str()), - confirm: bool = Form(default=False)): +async def packages_post( + request: Request, + IDs: list[int] = Form(default=[]), + action: str = Form(default=str()), + confirm: bool = Form(default=False), +): # If an invalid action is specified, just render GET /packages # with an BAD_REQUEST status_code. diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 1f09cfc8..913e3955 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -16,9 +16,7 @@ from aurweb.models.package_vote import PackageVote from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID from aurweb.packages.requests import update_closure_comment from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment -from aurweb.pkgbase import actions -from aurweb.pkgbase import util as pkgbaseutil -from aurweb.pkgbase import validate +from aurweb.pkgbase import actions, util as pkgbaseutil, validate from aurweb.scripts import notify, popupdate from aurweb.scripts.rendercomment import update_comment_render_fastapi from aurweb.templates import make_variable_context, render_template @@ -44,8 +42,9 @@ async def pkgbase(request: Request, name: str) -> Response: packages = pkgbase.packages.all() pkg = packages[0] if len(packages) == 1 and pkg.Name == pkgbase.Name: - return RedirectResponse(f"/packages/{pkg.Name}", - status_code=int(HTTPStatus.SEE_OTHER)) + return RedirectResponse( + f"/packages/{pkg.Name}", status_code=int(HTTPStatus.SEE_OTHER) + ) # Add our base information. context = pkgbaseutil.make_context(request, pkgbase) @@ -69,8 +68,7 @@ async def pkgbase_voters(request: Request, name: str) -> Response: pkgbase = get_pkg_or_base(name, PackageBase) if not request.user.has_credential(creds.PKGBASE_LIST_VOTERS): - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Voters") context["pkgbase"] = pkgbase @@ -82,8 +80,7 @@ async def pkgbase_flag_comment(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) if pkgbase.OutOfDateTS is None: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Flag Comment") context["pkgbase"] = pkgbase @@ -92,13 +89,15 @@ async def pkgbase_flag_comment(request: Request, name: str): @router.post("/pkgbase/{name}/keywords") @handle_form_exceptions -async def pkgbase_keywords(request: Request, name: str, - keywords: str = Form(default=str())): +async def pkgbase_keywords( + request: Request, name: str, keywords: str = Form(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) approved = [pkgbase.Maintainer] + [c.User for c in pkgbase.comaintainers] - has_cred = creds.has_credential(request.user, creds.PKGBASE_SET_KEYWORDS, - approved=approved) + has_cred = creds.has_credential( + request.user, creds.PKGBASE_SET_KEYWORDS, approved=approved + ) if not has_cred: return Response(status_code=HTTPStatus.UNAUTHORIZED) @@ -108,15 +107,14 @@ async def pkgbase_keywords(request: Request, name: str, # Delete all keywords which are not supplied by the user. with db.begin(): - other_keywords = pkgbase.keywords.filter( - ~PackageKeyword.Keyword.in_(keywords)) - other_keyword_strings = set( - kwd.Keyword.lower() for kwd in other_keywords) + other_keywords = pkgbase.keywords.filter(~PackageKeyword.Keyword.in_(keywords)) + other_keyword_strings = set(kwd.Keyword.lower() for kwd in other_keywords) existing_keywords = set( - kwd.Keyword.lower() for kwd in - pkgbase.keywords.filter( - ~PackageKeyword.Keyword.in_(other_keyword_strings)) + kwd.Keyword.lower() + for kwd in pkgbase.keywords.filter( + ~PackageKeyword.Keyword.in_(other_keyword_strings) + ) ) db.delete_all(other_keywords) @@ -124,8 +122,7 @@ async def pkgbase_keywords(request: Request, name: str, for keyword in new_keywords: db.create(PackageKeyword, PackageBase=pkgbase, Keyword=keyword) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/flag") @@ -135,8 +132,7 @@ async def pkgbase_flag_get(request: Request, name: str): has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if not has_cred or pkgbase.OutOfDateTS is not None: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Flag Package Out-Of-Date") context["pkgbase"] = pkgbase @@ -146,17 +142,20 @@ async def pkgbase_flag_get(request: Request, name: str): @router.post("/pkgbase/{name}/flag") @handle_form_exceptions @requires_auth -async def pkgbase_flag_post(request: Request, name: str, - comments: str = Form(default=str())): +async def pkgbase_flag_post( + request: Request, name: str, comments: str = Form(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) if not comments: context = templates.make_context(request, "Flag Package Out-Of-Date") context["pkgbase"] = pkgbase - context["errors"] = ["The selected packages have not been flagged, " - "please enter a comment."] - return render_template(request, "pkgbase/flag.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + "The selected packages have not been flagged, " "please enter a comment." + ] + return render_template( + request, "pkgbase/flag.html", context, status_code=HTTPStatus.BAD_REQUEST + ) has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if has_cred and not pkgbase.OutOfDateTS: @@ -168,18 +167,19 @@ async def pkgbase_flag_post(request: Request, name: str, notify.FlagNotification(request.user.ID, pkgbase.ID).send() - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/comments") @handle_form_exceptions @requires_auth async def pkgbase_comments_post( - request: Request, name: str, - comment: str = Form(default=str()), - enable_notifications: bool = Form(default=False)): - """ Add a new comment via POST request. """ + request: Request, + name: str, + comment: str = Form(default=str()), + enable_notifications: bool = Form(default=False), +): + """Add a new comment via POST request.""" pkgbase = get_pkg_or_base(name, PackageBase) if not comment: @@ -189,29 +189,34 @@ async def pkgbase_comments_post( # update the db record. now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, User=request.user, - PackageBase=pkgbase, - Comments=comment, RenderedComment=str(), - CommentTS=now) + comment = db.create( + PackageComment, + User=request.user, + PackageBase=pkgbase, + Comments=comment, + RenderedComment=str(), + CommentTS=now, + ) if enable_notifications and not request.user.notified(pkgbase): - db.create(PackageNotification, - User=request.user, - PackageBase=pkgbase) + db.create(PackageNotification, User=request.user, PackageBase=pkgbase) update_comment_render_fastapi(comment) notif = notify.CommentNotification(request.user.ID, pkgbase.ID, comment.ID) notif.send() # Redirect to the pkgbase page. - return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}", + status_code=HTTPStatus.SEE_OTHER, + ) @router.get("/pkgbase/{name}/comments/{id}/form") @requires_auth -async def pkgbase_comment_form(request: Request, name: str, id: int, - next: str = Query(default=None)): +async def pkgbase_comment_form( + request: Request, name: str, id: int, next: str = Query(default=None) +): """ Produce a comment form for comment {id}. @@ -244,14 +249,16 @@ async def pkgbase_comment_form(request: Request, name: str, id: int, context["next"] = next form = templates.render_raw_template( - request, "partials/packages/comment_form.html", context) + request, "partials/packages/comment_form.html", context + ) return JSONResponse({"form": form}) @router.get("/pkgbase/{name}/comments/{id}/edit") @requires_auth -async def pkgbase_comment_edit(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_edit( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Render the non-javascript edit form. @@ -276,11 +283,14 @@ async def pkgbase_comment_edit(request: Request, name: str, id: int, @handle_form_exceptions @requires_auth async def pkgbase_comment_post( - request: Request, name: str, id: int, - comment: str = Form(default=str()), - enable_notifications: bool = Form(default=False), - next: str = Form(default=None)): - """ Edit an existing comment. """ + request: Request, + name: str, + id: int, + comment: str = Form(default=str()), + enable_notifications: bool = Form(default=False), + next: str = Form(default=None), +): + """Edit an existing comment.""" pkgbase = get_pkg_or_base(name, PackageBase) db_comment = get_pkgbase_comment(pkgbase, id) @@ -302,24 +312,24 @@ async def pkgbase_comment_post( PackageNotification.PackageBaseID == pkgbase.ID ).first() if enable_notifications and not db_notif: - db.create(PackageNotification, - User=request.user, - PackageBase=pkgbase) + db.create(PackageNotification, User=request.user, PackageBase=pkgbase) update_comment_render_fastapi(db_comment) if not next: next = f"/pkgbase/{pkgbase.Name}" # Redirect to the pkgbase page anchored to the updated comment. - return RedirectResponse(f"{next}#comment-{db_comment.ID}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + f"{next}#comment-{db_comment.ID}", status_code=HTTPStatus.SEE_OTHER + ) @router.post("/pkgbase/{name}/comments/{id}/pin") @handle_form_exceptions @requires_auth -async def pkgbase_comment_pin(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_pin( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Pin a comment. @@ -332,13 +342,15 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential(creds.COMMENT_PIN, - approved=comment.maintainers()) + has_cred = request.user.has_credential( + creds.COMMENT_PIN, approved=comment.maintainers() + ) if not has_cred: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to pin this comment.")) + detail=_("You are not allowed to pin this comment."), + ) now = time.utcnow() with db.begin(): @@ -353,8 +365,9 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/unpin") @handle_form_exceptions @requires_auth -async def pkgbase_comment_unpin(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_unpin( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Unpin a comment. @@ -367,13 +380,15 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential(creds.COMMENT_PIN, - approved=comment.maintainers()) + has_cred = request.user.has_credential( + creds.COMMENT_PIN, approved=comment.maintainers() + ) if not has_cred: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to unpin this comment.")) + detail=_("You are not allowed to unpin this comment."), + ) with db.begin(): comment.PinnedTS = 0 @@ -387,8 +402,9 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/delete") @handle_form_exceptions @requires_auth -async def pkgbase_comment_delete(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_delete( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Delete a comment. @@ -405,13 +421,13 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - authorized = request.user.has_credential(creds.COMMENT_DELETE, - [comment.User]) + authorized = request.user.has_credential(creds.COMMENT_DELETE, [comment.User]) if not authorized: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to delete this comment.")) + detail=_("You are not allowed to delete this comment."), + ) now = time.utcnow() with db.begin(): @@ -427,8 +443,9 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/undelete") @handle_form_exceptions @requires_auth -async def pkgbase_comment_undelete(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_undelete( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Undelete a comment. @@ -445,13 +462,15 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential(creds.COMMENT_UNDELETE, - approved=[comment.User]) + has_cred = request.user.has_credential( + creds.COMMENT_UNDELETE, approved=[comment.User] + ) if not has_cred: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to undelete this comment.")) + detail=_("You are not allowed to undelete this comment."), + ) with db.begin(): comment.Deleter = None @@ -469,23 +488,17 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, async def pkgbase_vote(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) - vote = pkgbase.package_votes.filter( - PackageVote.UsersID == request.user.ID - ).first() + vote = pkgbase.package_votes.filter(PackageVote.UsersID == request.user.ID).first() has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and not vote: now = time.utcnow() with db.begin(): - db.create(PackageVote, - User=request.user, - PackageBase=pkgbase, - VoteTS=now) + db.create(PackageVote, User=request.user, PackageBase=pkgbase, VoteTS=now) # Update NumVotes/Popularity. popupdate.run_single(pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/unvote") @@ -494,9 +507,7 @@ async def pkgbase_vote(request: Request, name: str): async def pkgbase_unvote(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) - vote = pkgbase.package_votes.filter( - PackageVote.UsersID == request.user.ID - ).first() + vote = pkgbase.package_votes.filter(PackageVote.UsersID == request.user.ID).first() has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and vote: with db.begin(): @@ -505,8 +516,7 @@ async def pkgbase_unvote(request: Request, name: str): # Update NumVotes/Popularity. popupdate.run_single(pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/notify") @@ -515,8 +525,7 @@ async def pkgbase_unvote(request: Request, name: str): async def pkgbase_notify(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) actions.pkgbase_notify_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/unnotify") @@ -525,8 +534,7 @@ async def pkgbase_notify(request: Request, name: str): async def pkgbase_unnotify(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) actions.pkgbase_unnotify_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.post("/pkgbase/{name}/unflag") @@ -535,20 +543,19 @@ async def pkgbase_unnotify(request: Request, name: str): async def pkgbase_unflag(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) actions.pkgbase_unflag_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/disown") @requires_auth -async def pkgbase_disown_get(request: Request, name: str, - next: str = Query(default=str())): +async def pkgbase_disown_get( + request: Request, name: str, next: str = Query(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) comaints = {c.User for c in pkgbase.comaintainers} approved = [pkgbase.Maintainer] + list(comaints) - has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=approved) + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved) if not has_cred: return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER) @@ -563,27 +570,33 @@ async def pkgbase_disown_get(request: Request, name: str, @router.post("/pkgbase/{name}/disown") @handle_form_exceptions @requires_auth -async def pkgbase_disown_post(request: Request, name: str, - comments: str = Form(default=str()), - confirm: bool = Form(default=False), - next: str = Form(default=str())): +async def pkgbase_disown_post( + request: Request, + name: str, + comments: str = Form(default=str()), + confirm: bool = Form(default=False), + next: str = Form(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) comaints = {c.User for c in pkgbase.comaintainers} approved = [pkgbase.Maintainer] + list(comaints) - has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=approved) + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved) if not has_cred: - return RedirectResponse(f"/pkgbase/{name}", - HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Disown Package") context["pkgbase"] = pkgbase if not confirm: - context["errors"] = [("The selected packages have not been disowned, " - "check the confirmation checkbox.")] - return render_template(request, "pkgbase/disown.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + ( + "The selected packages have not been disowned, " + "check the confirmation checkbox." + ) + ] + return render_template( + request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if request.user != pkgbase.Maintainer and request.user not in comaints: with db.begin(): @@ -593,8 +606,9 @@ async def pkgbase_disown_post(request: Request, name: str, actions.pkgbase_disown_instance(request, pkgbase) except InvariantError as exc: context["errors"] = [str(exc)] - return render_template(request, "pkgbase/disown.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if not next: next = f"/pkgbase/{name}" @@ -615,8 +629,7 @@ async def pkgbase_adopt_post(request: Request, name: str): # if no maintainer currently exists. actions.pkgbase_adopt_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/comaintainers") @@ -627,20 +640,20 @@ async def pkgbase_comaintainers(request: Request, name: str) -> Response: # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, - approved=[pkgbase.Maintainer]) + has_creds = request.user.has_credential( + creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer] + ) if not has_creds: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) # Add our base information. context = templates.make_context(request, "Manage Co-maintainers") - context.update({ - "pkgbase": pkgbase, - "comaintainers": [ - c.User.Username for c in pkgbase.comaintainers - ] - }) + context.update( + { + "pkgbase": pkgbase, + "comaintainers": [c.User.Username for c in pkgbase.comaintainers], + } + ) return render_template(request, "pkgbase/comaintainers.html", context) @@ -648,50 +661,52 @@ async def pkgbase_comaintainers(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comaintainers") @handle_form_exceptions @requires_auth -async def pkgbase_comaintainers_post(request: Request, name: str, - users: str = Form(default=str())) \ - -> Response: +async def pkgbase_comaintainers_post( + request: Request, name: str, users: str = Form(default=str()) +) -> Response: # Get the PackageBase. pkgbase = get_pkg_or_base(name, PackageBase) # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, - approved=[pkgbase.Maintainer]) + has_creds = request.user.has_credential( + creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer] + ) if not has_creds: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) users = {e.strip() for e in users.split("\n") if bool(e.strip())} records = {c.User.Username for c in pkgbase.comaintainers} users_to_rm = records.difference(users) pkgbaseutil.remove_comaintainers(pkgbase, users_to_rm) - logger.debug(f"{request.user} removed comaintainers from " - f"{pkgbase.Name}: {users_to_rm}") + logger.debug( + f"{request.user} removed comaintainers from " f"{pkgbase.Name}: {users_to_rm}" + ) users_to_add = users.difference(records) error = pkgbaseutil.add_comaintainers(request, pkgbase, users_to_add) if error: context = templates.make_context(request, "Manage Co-maintainers") context["pkgbase"] = pkgbase - context["comaintainers"] = [ - c.User.Username for c in pkgbase.comaintainers - ] + context["comaintainers"] = [c.User.Username for c in pkgbase.comaintainers] context["errors"] = [error] return render_template(request, "pkgbase/comaintainers.html", context) - logger.debug(f"{request.user} added comaintainers to " - f"{pkgbase.Name}: {users_to_add}") + logger.debug( + f"{request.user} added comaintainers to " f"{pkgbase.Name}: {users_to_add}" + ) - return RedirectResponse(f"/pkgbase/{pkgbase.Name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + f"/pkgbase/{pkgbase.Name}", status_code=HTTPStatus.SEE_OTHER + ) @router.get("/pkgbase/{name}/request") @requires_auth -async def pkgbase_request(request: Request, name: str, - next: str = Query(default=str())): +async def pkgbase_request( + request: Request, name: str, next: str = Query(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) context = await make_variable_context(request, "Submit Request") context["pkgbase"] = pkgbase @@ -702,28 +717,28 @@ async def pkgbase_request(request: Request, name: str, @router.post("/pkgbase/{name}/request") @handle_form_exceptions @requires_auth -async def pkgbase_request_post(request: Request, name: str, - type: str = Form(...), - merge_into: str = Form(default=None), - comments: str = Form(default=str()), - next: str = Form(default=str())): +async def pkgbase_request_post( + request: Request, + name: str, + type: str = Form(...), + merge_into: str = Form(default=None), + comments: str = Form(default=str()), + next: str = Form(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) # Create our render context. context = await make_variable_context(request, "Submit Request") context["pkgbase"] = pkgbase - types = { - "deletion": DELETION_ID, - "merge": MERGE_ID, - "orphan": ORPHAN_ID - } + types = {"deletion": DELETION_ID, "merge": MERGE_ID, "orphan": ORPHAN_ID} if type not in types: # In the case that someone crafted a POST request with an invalid # type, just return them to the request form with BAD_REQUEST status. - return render_template(request, "pkgbase/request.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "pkgbase/request.html", context, status_code=HTTPStatus.BAD_REQUEST + ) try: validate.request(pkgbase, type, comments, merge_into, context) @@ -735,20 +750,26 @@ async def pkgbase_request_post(request: Request, name: str, # All good. Create a new PackageRequest based on the given type. now = time.utcnow() with db.begin(): - pkgreq = db.create(PackageRequest, - ReqTypeID=types.get(type), - User=request.user, - RequestTS=now, - PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - MergeBaseName=merge_into, - Comments=comments, - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=types.get(type), + User=request.user, + RequestTS=now, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + MergeBaseName=merge_into, + Comments=comments, + ClosureComment=str(), + ) # Prepare notification object. notif = notify.RequestOpenNotification( - request.user.ID, pkgreq.ID, type, - pkgreq.PackageBase.ID, merge_into=merge_into or None) + request.user.ID, + pkgreq.ID, + type, + pkgreq.PackageBase.ID, + merge_into=merge_into or None, + ) # Send the notification now that we're out of the DB scope. notif.send() @@ -767,13 +788,13 @@ async def pkgbase_request_post(request: Request, name: str, pkgbase.Maintainer = None pkgreq.Status = ACCEPTED_ID notif = notify.RequestCloseNotification( - request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display() + ) notif.send() logger.debug(f"New request #{pkgreq.ID} is marked for auto-orphan.") elif type == "deletion" and is_maintainer and outdated: # This request should be auto-accepted. - notifs = actions.pkgbase_delete_instance( - request, pkgbase, comments=comments) + notifs = actions.pkgbase_delete_instance(request, pkgbase, comments=comments) util.apply_all(notifs, lambda n: n.send()) logger.debug(f"New request #{pkgreq.ID} is marked for auto-deletion.") @@ -783,11 +804,11 @@ async def pkgbase_request_post(request: Request, name: str, @router.get("/pkgbase/{name}/delete") @requires_auth -async def pkgbase_delete_get(request: Request, name: str, - next: str = Query(default=str())): +async def pkgbase_delete_get( + request: Request, name: str, next: str = Query(default=str()) +): if not request.user.has_credential(creds.PKGBASE_DELETE): - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Package Deletion") context["pkgbase"] = get_pkg_or_base(name, PackageBase) @@ -798,53 +819,60 @@ async def pkgbase_delete_get(request: Request, name: str, @router.post("/pkgbase/{name}/delete") @handle_form_exceptions @requires_auth -async def pkgbase_delete_post(request: Request, name: str, - confirm: bool = Form(default=False), - comments: str = Form(default=str()), - next: str = Form(default="/packages")): +async def pkgbase_delete_post( + request: Request, + name: str, + confirm: bool = Form(default=False), + comments: str = Form(default=str()), + next: str = Form(default="/packages"), +): pkgbase = get_pkg_or_base(name, PackageBase) if not request.user.has_credential(creds.PKGBASE_DELETE): - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) if not confirm: context = templates.make_context(request, "Package Deletion") context["pkgbase"] = pkgbase - context["errors"] = [("The selected packages have not been deleted, " - "check the confirmation checkbox.")] - return render_template(request, "pkgbase/delete.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + ( + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ) + ] + return render_template( + request, "pkgbase/delete.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if comments: # Update any existing deletion requests' ClosureComment. with db.begin(): requests = pkgbase.requests.filter( - and_(PackageRequest.Status == PENDING_ID, - PackageRequest.ReqTypeID == DELETION_ID) + and_( + PackageRequest.Status == PENDING_ID, + PackageRequest.ReqTypeID == DELETION_ID, + ) ) for pkgreq in requests: pkgreq.ClosureComment = comments - notifs = actions.pkgbase_delete_instance( - request, pkgbase, comments=comments) + notifs = actions.pkgbase_delete_instance(request, pkgbase, comments=comments) util.apply_all(notifs, lambda n: n.send()) return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/merge") @requires_auth -async def pkgbase_merge_get(request: Request, name: str, - into: str = Query(default=str()), - next: str = Query(default=str())): +async def pkgbase_merge_get( + request: Request, + name: str, + into: str = Query(default=str()), + next: str = Query(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) context = templates.make_context(request, "Package Merging") - context.update({ - "pkgbase": pkgbase, - "into": into, - "next": next - }) + context.update({"pkgbase": pkgbase, "into": into, "next": next}) status_code = HTTPStatus.OK # TODO: Lookup errors from credential instead of hardcoding them. @@ -852,51 +880,58 @@ async def pkgbase_merge_get(request: Request, name: str, # Perhaps additionally: bad_credential_status_code(creds.PKGBASE_MERGE). # Don't take these examples verbatim. We should find good naming. if not request.user.has_credential(creds.PKGBASE_MERGE): - context["errors"] = [ - "Only Trusted Users and Developers can merge packages."] + context["errors"] = ["Only Trusted Users and Developers can merge packages."] status_code = HTTPStatus.UNAUTHORIZED - return render_template(request, "pkgbase/merge.html", context, - status_code=status_code) + return render_template( + request, "pkgbase/merge.html", context, status_code=status_code + ) @router.post("/pkgbase/{name}/merge") @handle_form_exceptions @requires_auth -async def pkgbase_merge_post(request: Request, name: str, - into: str = Form(default=str()), - comments: str = Form(default=str()), - confirm: bool = Form(default=False), - next: str = Form(default=str())): +async def pkgbase_merge_post( + request: Request, + name: str, + into: str = Form(default=str()), + comments: str = Form(default=str()), + confirm: bool = Form(default=False), + next: str = Form(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) context = await make_variable_context(request, "Package Merging") context["pkgbase"] = pkgbase # TODO: Lookup errors from credential instead of hardcoding them. if not request.user.has_credential(creds.PKGBASE_MERGE): - context["errors"] = [ - "Only Trusted Users and Developers can merge packages."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.UNAUTHORIZED) + context["errors"] = ["Only Trusted Users and Developers can merge packages."] + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.UNAUTHORIZED + ) if not confirm: - context["errors"] = ["The selected packages have not been deleted, " - "check the confirmation checkbox."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ] + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST + ) try: target = get_pkg_or_base(into, PackageBase) except HTTPException: - context["errors"] = [ - "Cannot find package to merge votes and comments into."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = ["Cannot find package to merge votes and comments into."] + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if pkgbase == target: context["errors"] = ["Cannot merge a package base with itself."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST + ) with db.begin(): update_closure_comment(pkgbase, MERGE_ID, comments, target=target) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index 086aa3bc..c7935575 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -18,9 +18,11 @@ router = APIRouter() @router.get("/requests") @requires_auth -async def requests(request: Request, - O: int = Query(default=defaults.O), - PP: int = Query(default=defaults.PP)): +async def requests( + request: Request, + O: int = Query(default=defaults.O), + PP: int = Query(default=defaults.PP), +): context = make_context(request, "Requests") context["q"] = dict(request.query_params) @@ -30,8 +32,7 @@ async def requests(request: Request, context["PP"] = PP # A PackageRequest query, with left inner joined User and RequestType. - query = db.query(PackageRequest).join( - User, User.ID == PackageRequest.UsersID) + query = db.query(PackageRequest).join(User, User.ID == PackageRequest.UsersID) # If the request user is not elevated (TU or Dev), then # filter PackageRequests which are owned by the request user. @@ -39,12 +40,17 @@ async def requests(request: Request, query = query.filter(PackageRequest.UsersID == request.user.ID) context["total"] = query.count() - context["results"] = query.order_by( - # Order primarily by the Status column being PENDING_ID, - # and secondarily by RequestTS; both in descending order. - case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), - PackageRequest.RequestTS.desc() - ).limit(PP).offset(O).all() + context["results"] = ( + query.order_by( + # Order primarily by the Status column being PENDING_ID, + # and secondarily by RequestTS; both in descending order. + case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), + PackageRequest.RequestTS.desc(), + ) + .limit(PP) + .offset(O) + .all() + ) return render_template(request, "requests.html", context) @@ -66,8 +72,9 @@ async def request_close(request: Request, id: int): @router.post("/requests/{id}/close") @handle_form_exceptions @requires_auth -async def request_close_post(request: Request, id: int, - comments: str = Form(default=str())): +async def request_close_post( + request: Request, id: int, comments: str = Form(default=str()) +): pkgreq = get_pkgreq_by_id(id) # `pkgreq`.User can close their own request. @@ -87,7 +94,8 @@ async def request_close_post(request: Request, id: int, pkgreq.Status = REJECTED_ID notify_ = notify.RequestCloseNotification( - request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display() + ) notify_.send() return RedirectResponse("/requests", status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index ff58063f..a0cf5019 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -1,12 +1,10 @@ import hashlib import re - from http import HTTPStatus from typing import Optional from urllib.parse import unquote import orjson - from fastapi import APIRouter, Form, Query, Request, Response from fastapi.responses import JSONResponse @@ -19,7 +17,7 @@ router = APIRouter() def parse_args(request: Request): - """ Handle legacy logic of 'arg' and 'arg[]' query parameter handling. + """Handle legacy logic of 'arg' and 'arg[]' query parameter handling. When 'arg' appears as the last argument given to the query string, that argument is used by itself as one single argument, regardless @@ -39,9 +37,7 @@ def parse_args(request: Request): # Create a list of (key, value) pairs of the given 'arg' and 'arg[]' # query parameters from last to first. query = list(reversed(unquote(request.url.query).split("&"))) - parts = [ - e.split("=", 1) for e in query if e.startswith(("arg=", "arg[]=")) - ] + parts = [e.split("=", 1) for e in query if e.startswith(("arg=", "arg[]="))] args = [] if parts: @@ -63,24 +59,28 @@ def parse_args(request: Request): return args -JSONP_EXPR = re.compile(r'^[a-zA-Z0-9()_.]{1,128}$') +JSONP_EXPR = re.compile(r"^[a-zA-Z0-9()_.]{1,128}$") -async def rpc_request(request: Request, - v: Optional[int] = None, - type: Optional[str] = None, - by: Optional[str] = defaults.RPC_SEARCH_BY, - arg: Optional[str] = None, - args: Optional[list[str]] = [], - callback: Optional[str] = None): +async def rpc_request( + request: Request, + v: Optional[int] = None, + type: Optional[str] = None, + by: Optional[str] = defaults.RPC_SEARCH_BY, + arg: Optional[str] = None, + args: Optional[list[str]] = [], + callback: Optional[str] = None, +): # Create a handle to our RPC class. rpc = RPC(version=v, type=type) # If ratelimit was exceeded, return a 429 Too Many Requests. if check_ratelimit(request): - return JSONResponse(rpc.error("Rate limit reached"), - status_code=int(HTTPStatus.TOO_MANY_REQUESTS)) + return JSONResponse( + rpc.error("Rate limit reached"), + status_code=int(HTTPStatus.TOO_MANY_REQUESTS), + ) # If `callback` was provided, produce a text/javascript response # valid for the jsonp callback. Otherwise, by default, return @@ -115,15 +115,11 @@ async def rpc_request(request: Request, # The ETag header expects quotes to surround any identifier. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag - headers = { - "Content-Type": content_type, - "ETag": f'"{etag}"' - } + headers = {"Content-Type": content_type, "ETag": f'"{etag}"'} if_none_match = request.headers.get("If-None-Match", str()) - if if_none_match and if_none_match.strip("\t\n\r\" ") == etag: - return Response(headers=headers, - status_code=int(HTTPStatus.NOT_MODIFIED)) + if if_none_match and if_none_match.strip('\t\n\r" ') == etag: + return Response(headers=headers, status_code=int(HTTPStatus.NOT_MODIFIED)) if callback: content = f"/**/{callback}({content.decode()})" @@ -135,13 +131,15 @@ async def rpc_request(request: Request, @router.get("/rpc.php") # Temporary! Remove on 03/04 @router.get("/rpc/") @router.get("/rpc") -async def rpc(request: Request, - v: Optional[int] = Query(default=None), - type: Optional[str] = Query(default=None), - by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), - arg: Optional[str] = Query(default=None), - args: Optional[list[str]] = Query(default=[], alias="arg[]"), - callback: Optional[str] = Query(default=None)): +async def rpc( + request: Request, + v: Optional[int] = Query(default=None), + type: Optional[str] = Query(default=None), + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), + arg: Optional[str] = Query(default=None), + args: Optional[list[str]] = Query(default=[], alias="arg[]"), + callback: Optional[str] = Query(default=None), +): if not request.url.query: return documentation() return await rpc_request(request, v, type, by, arg, args, callback) @@ -152,11 +150,13 @@ async def rpc(request: Request, @router.post("/rpc/") @router.post("/rpc") @handle_form_exceptions -async def rpc_post(request: Request, - v: Optional[int] = Form(default=None), - type: Optional[str] = Form(default=None), - by: Optional[str] = Form(default=defaults.RPC_SEARCH_BY), - arg: Optional[str] = Form(default=None), - args: Optional[list[str]] = Form(default=[], alias="arg[]"), - callback: Optional[str] = Form(default=None)): +async def rpc_post( + request: Request, + v: Optional[int] = Form(default=None), + type: Optional[str] = Form(default=None), + by: Optional[str] = Form(default=defaults.RPC_SEARCH_BY), + arg: Optional[str] = Form(default=None), + args: Optional[list[str]] = Form(default=[], alias="arg[]"), + callback: Optional[str] = Form(default=None), +): return await rpc_request(request, v, type, by, arg, args, callback) diff --git a/aurweb/routers/rss.py b/aurweb/routers/rss.py index 0996f3cd..ee85b738 100644 --- a/aurweb/routers/rss.py +++ b/aurweb/routers/rss.py @@ -10,9 +10,8 @@ from aurweb.models import Package, PackageBase router = APIRouter() -def make_rss_feed(request: Request, packages: list, - date_attr: str): - """ Create an RSS Feed string for some packages. +def make_rss_feed(request: Request, packages: list, date_attr: str): + """Create an RSS Feed string for some packages. :param request: A FastAPI request :param packages: A list of packages to add to the RSS feed @@ -26,10 +25,12 @@ def make_rss_feed(request: Request, packages: list, base = f"{request.url.scheme}://{request.url.netloc}" feed.link(href=base, rel="alternate") feed.link(href=f"{base}/rss", rel="self") - feed.image(title="AUR Newest Packages", - url=f"{base}/static/css/archnavbar/aurlogo.png", - link=base, - description="AUR Newest Packages Feed") + feed.image( + title="AUR Newest Packages", + url=f"{base}/static/css/archnavbar/aurlogo.png", + link=base, + description="AUR Newest Packages Feed", + ) for pkg in packages: entry = feed.add_entry(order="append") @@ -53,8 +54,12 @@ def make_rss_feed(request: Request, packages: list, @router.get("/rss/") async def rss(request: Request): - packages = db.query(Package).join(PackageBase).order_by( - PackageBase.SubmittedTS.desc()).limit(100) + packages = ( + db.query(Package) + .join(PackageBase) + .order_by(PackageBase.SubmittedTS.desc()) + .limit(100) + ) feed = make_rss_feed(request, packages, "SubmittedTS") response = Response(feed, media_type="application/rss+xml") @@ -69,8 +74,12 @@ async def rss(request: Request): @router.get("/rss/modified") async def rss_modified(request: Request): - packages = db.query(Package).join(PackageBase).order_by( - PackageBase.ModifiedTS.desc()).limit(100) + packages = ( + db.query(Package) + .join(PackageBase) + .order_by(PackageBase.ModifiedTS.desc()) + .limit(100) + ) feed = make_rss_feed(request, packages, "ModifiedTS") response = Response(feed, media_type="application/rss+xml") diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index eff1c63f..e1356cfb 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -1,11 +1,9 @@ import time import uuid - from http import HTTPStatus from urllib.parse import urlencode import fastapi - from authlib.integrations.starlette_client import OAuth, OAuthError from fastapi import Depends, HTTPException from fastapi.responses import RedirectResponse @@ -14,7 +12,6 @@ from starlette.requests import Request import aurweb.config import aurweb.db - from aurweb import util from aurweb.l10n import get_translator_for_request from aurweb.schema import Bans, Sessions, Users @@ -43,14 +40,18 @@ async def login(request: Request, redirect: str = None): The `redirect` argument is a query parameter specifying the post-login redirect URL. """ - authenticate_url = aurweb.config.get("options", "aur_location") + "/sso/authenticate" + authenticate_url = ( + aurweb.config.get("options", "aur_location") + "/sso/authenticate" + ) if redirect: authenticate_url = authenticate_url + "?" + urlencode([("redirect", redirect)]) return await oauth.sso.authorize_redirect(request, authenticate_url, prompt="login") def is_account_suspended(conn, user_id): - row = conn.execute(select([Users.c.Suspended]).where(Users.c.ID == user_id)).fetchone() + row = conn.execute( + select([Users.c.Suspended]).where(Users.c.ID == user_id) + ).fetchone() return row is not None and bool(row[0]) @@ -60,23 +61,27 @@ def open_session(request, conn, user_id): """ if is_account_suspended(conn, user_id): _ = get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.FORBIDDEN, - detail=_('Account suspended')) + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail=_("Account suspended") + ) # TODO This is a terrible message because it could imply the attempt at # logging in just caused the suspension. sid = uuid.uuid4().hex - conn.execute(Sessions.insert().values( - UsersID=user_id, - SessionID=sid, - LastUpdateTS=time.time(), - )) + conn.execute( + Sessions.insert().values( + UsersID=user_id, + SessionID=sid, + LastUpdateTS=time.time(), + ) + ) # Update user’s last login information. - conn.execute(Users.update() - .where(Users.c.ID == user_id) - .values(LastLogin=int(time.time()), - LastLoginIPAddress=request.client.host)) + conn.execute( + Users.update() + .where(Users.c.ID == user_id) + .values(LastLogin=int(time.time()), LastLoginIPAddress=request.client.host) + ) return sid @@ -98,7 +103,9 @@ def is_aur_url(url): @router.get("/sso/authenticate") -async def authenticate(request: Request, redirect: str = None, conn=Depends(aurweb.db.connect)): +async def authenticate( + request: Request, redirect: str = None, conn=Depends(aurweb.db.connect) +): """ Receive an OpenID Connect ID token, validate it, then process it to create an new AUR session. @@ -107,9 +114,12 @@ async def authenticate(request: Request, redirect: str = None, conn=Depends(aurw _ = get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.FORBIDDEN, - detail=_('The login form is currently disabled for your IP address, ' - 'probably due to sustained spam attacks. Sorry for the ' - 'inconvenience.')) + detail=_( + "The login form is currently disabled for your IP address, " + "probably due to sustained spam attacks. Sorry for the " + "inconvenience." + ), + ) try: token = await oauth.sso.authorize_access_token(request) @@ -120,30 +130,41 @@ async def authenticate(request: Request, redirect: str = None, conn=Depends(aurw _ = get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail=_('Bad OAuth token. Please retry logging in from the start.')) + detail=_("Bad OAuth token. Please retry logging in from the start."), + ) sub = user.get("sub") # this is the SSO account ID in JWT terminology if not sub: _ = get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, - detail=_("JWT is missing its `sub` field.")) + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=_("JWT is missing its `sub` field."), + ) - aur_accounts = conn.execute(select([Users.c.ID]).where(Users.c.SSOAccountID == sub)) \ - .fetchall() + aur_accounts = conn.execute( + select([Users.c.ID]).where(Users.c.SSOAccountID == sub) + ).fetchall() if not aur_accounts: return "Sorry, we don’t seem to know you Sir " + sub elif len(aur_accounts) == 1: sid = open_session(request, conn, aur_accounts[0][Users.c.ID]) - response = RedirectResponse(redirect if redirect and is_aur_url(redirect) else "/") + response = RedirectResponse( + redirect if redirect and is_aur_url(redirect) else "/" + ) secure_cookies = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie(key="AURSID", value=sid, httponly=True, - secure=secure_cookies) + response.set_cookie( + key="AURSID", value=sid, httponly=True, secure=secure_cookies + ) if "id_token" in token: # We save the id_token for the SSO logout. It’s not too important # though, so if we can’t find it, we can live without it. - response.set_cookie(key="SSO_ID_TOKEN", value=token["id_token"], - path="/sso/", httponly=True, - secure=secure_cookies) + response.set_cookie( + key="SSO_ID_TOKEN", + value=token["id_token"], + path="/sso/", + httponly=True, + secure=secure_cookies, + ) return util.add_samesite_fields(response, "strict") else: # We’ve got a severe integrity violation. @@ -165,8 +186,12 @@ async def logout(request: Request): return RedirectResponse("/") metadata = await oauth.sso.load_server_metadata() - query = urlencode({'post_logout_redirect_uri': aurweb.config.get('options', 'aur_location'), - 'id_token_hint': id_token}) - response = RedirectResponse(metadata["end_session_endpoint"] + '?' + query) + query = urlencode( + { + "post_logout_redirect_uri": aurweb.config.get("options", "aur_location"), + "id_token_hint": id_token, + } + ) + response = RedirectResponse(metadata["end_session_endpoint"] + "?" + query) response.delete_cookie("SSO_ID_TOKEN", path="/sso/") return response diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index e1267409..a84bb6bd 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -1,6 +1,5 @@ import html import typing - from http import HTTPStatus from typing import Any @@ -30,33 +29,36 @@ ADDVOTE_SPECIFICS = { "add_tu": (7 * 24 * 60 * 60, 0.66), "remove_tu": (7 * 24 * 60 * 60, 0.75), "remove_inactive_tu": (5 * 24 * 60 * 60, 0.66), - "bylaws": (7 * 24 * 60 * 60, 0.75) + "bylaws": (7 * 24 * 60 * 60, 0.75), } def populate_trusted_user_counts(context: dict[str, Any]) -> None: tu_query = db.query(User).filter( - or_(User.AccountTypeID == TRUSTED_USER_ID, - User.AccountTypeID == TRUSTED_USER_AND_DEV_ID) + or_( + User.AccountTypeID == TRUSTED_USER_ID, + User.AccountTypeID == TRUSTED_USER_AND_DEV_ID, + ) ) context["trusted_user_count"] = tu_query.count() # In case any records have a None InactivityTS. active_tu_query = tu_query.filter( - or_(User.InactivityTS.is_(None), - User.InactivityTS == 0) + or_(User.InactivityTS.is_(None), User.InactivityTS == 0) ) context["active_trusted_user_count"] = active_tu_query.count() @router.get("/tu") @requires_auth -async def trusted_user(request: Request, - coff: int = 0, # current offset - cby: str = "desc", # current by - poff: int = 0, # past offset - pby: str = "desc"): # past by - """ Proposal listings. """ +async def trusted_user( + request: Request, + coff: int = 0, # current offset + cby: str = "desc", # current by + poff: int = 0, # past offset + pby: str = "desc", +): # past by + """Proposal listings.""" if not request.user.has_credential(creds.TU_LIST_VOTES): return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) @@ -81,40 +83,47 @@ async def trusted_user(request: Request, past_by = "desc" context["past_by"] = past_by - current_votes = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.End > ts).order_by( - models.TUVoteInfo.Submitted.desc()) + current_votes = ( + db.query(models.TUVoteInfo) + .filter(models.TUVoteInfo.End > ts) + .order_by(models.TUVoteInfo.Submitted.desc()) + ) context["current_votes_count"] = current_votes.count() current_votes = current_votes.limit(pp).offset(current_off) - context["current_votes"] = reversed(current_votes.all()) \ - if current_by == "asc" else current_votes.all() + context["current_votes"] = ( + reversed(current_votes.all()) if current_by == "asc" else current_votes.all() + ) context["current_off"] = current_off - past_votes = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.End <= ts).order_by( - models.TUVoteInfo.Submitted.desc()) + past_votes = ( + db.query(models.TUVoteInfo) + .filter(models.TUVoteInfo.End <= ts) + .order_by(models.TUVoteInfo.Submitted.desc()) + ) context["past_votes_count"] = past_votes.count() past_votes = past_votes.limit(pp).offset(past_off) - context["past_votes"] = reversed(past_votes.all()) \ - if past_by == "asc" else past_votes.all() + context["past_votes"] = ( + reversed(past_votes.all()) if past_by == "asc" else past_votes.all() + ) context["past_off"] = past_off last_vote = func.max(models.TUVote.VoteID).label("LastVote") - last_votes_by_tu = db.query(models.TUVote).join(models.User).join( - models.TUVoteInfo, - models.TUVoteInfo.ID == models.TUVote.VoteID - ).filter( - and_(models.TUVote.VoteID == models.TUVoteInfo.ID, - models.User.ID == models.TUVote.UserID, - models.TUVoteInfo.End < ts, - or_(models.User.AccountTypeID == 2, - models.User.AccountTypeID == 4)) - ).with_entities( - models.TUVote.UserID, - last_vote, - models.User.Username - ).group_by(models.TUVote.UserID).order_by( - last_vote.desc(), models.User.Username.asc()) + last_votes_by_tu = ( + db.query(models.TUVote) + .join(models.User) + .join(models.TUVoteInfo, models.TUVoteInfo.ID == models.TUVote.VoteID) + .filter( + and_( + models.TUVote.VoteID == models.TUVoteInfo.ID, + models.User.ID == models.TUVote.UserID, + models.TUVoteInfo.End < ts, + or_(models.User.AccountTypeID == 2, models.User.AccountTypeID == 4), + ) + ) + .with_entities(models.TUVote.UserID, last_vote, models.User.Username) + .group_by(models.TUVote.UserID) + .order_by(last_vote.desc(), models.User.Username.asc()) + ) context["last_votes_by_tu"] = last_votes_by_tu.all() context["current_by_next"] = "asc" if current_by == "desc" else "desc" @@ -126,18 +135,22 @@ async def trusted_user(request: Request, "coff": current_off, "cby": current_by, "poff": past_off, - "pby": past_by + "pby": past_by, } return render_template(request, "tu/index.html", context) -def render_proposal(request: Request, context: dict, proposal: int, - voteinfo: models.TUVoteInfo, - voters: typing.Iterable[models.User], - vote: models.TUVote, - status_code: HTTPStatus = HTTPStatus.OK): - """ Render a single TU proposal. """ +def render_proposal( + request: Request, + context: dict, + proposal: int, + voteinfo: models.TUVoteInfo, + voters: typing.Iterable[models.User], + vote: models.TUVote, + status_code: HTTPStatus = HTTPStatus.OK, +): + """Render a single TU proposal.""" context["proposal"] = proposal context["voteinfo"] = voteinfo context["voters"] = voters.all() @@ -146,8 +159,9 @@ def render_proposal(request: Request, context: dict, proposal: int, participation = (total / voteinfo.ActiveTUs) if voteinfo.ActiveTUs else 0 context["participation"] = participation - accepted = (voteinfo.Yes > voteinfo.ActiveTUs / 2) or \ - (participation > voteinfo.Quorum and voteinfo.Yes > voteinfo.No) + accepted = (voteinfo.Yes > voteinfo.ActiveTUs / 2) or ( + participation > voteinfo.Quorum and voteinfo.Yes > voteinfo.No + ) context["accepted"] = accepted can_vote = voters.filter(models.TUVote.User == request.user).first() is None @@ -159,8 +173,7 @@ def render_proposal(request: Request, context: dict, proposal: int, context["vote"] = vote context["has_voted"] = vote is not None - return render_template(request, "tu/show.html", context, - status_code=status_code) + return render_template(request, "tu/show.html", context, status_code=status_code) @router.get("/tu/{proposal}") @@ -172,16 +185,27 @@ async def trusted_user_proposal(request: Request, proposal: int): context = await make_variable_context(request, "Trusted User") proposal = int(proposal) - voteinfo = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.ID == proposal).first() + voteinfo = ( + db.query(models.TUVoteInfo).filter(models.TUVoteInfo.ID == proposal).first() + ) if not voteinfo: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - voters = db.query(models.User).join(models.TUVote).filter( - models.TUVote.VoteID == voteinfo.ID) - vote = db.query(models.TUVote).filter( - and_(models.TUVote.UserID == request.user.ID, - models.TUVote.VoteID == voteinfo.ID)).first() + voters = ( + db.query(models.User) + .join(models.TUVote) + .filter(models.TUVote.VoteID == voteinfo.ID) + ) + vote = ( + db.query(models.TUVote) + .filter( + and_( + models.TUVote.UserID == request.user.ID, + models.TUVote.VoteID == voteinfo.ID, + ) + ) + .first() + ) if not request.user.has_credential(creds.TU_VOTE): context["error"] = "Only Trusted Users are allowed to vote." if voteinfo.User == request.user.Username: @@ -196,24 +220,36 @@ async def trusted_user_proposal(request: Request, proposal: int): @router.post("/tu/{proposal}") @handle_form_exceptions @requires_auth -async def trusted_user_proposal_post(request: Request, proposal: int, - decision: str = Form(...)): +async def trusted_user_proposal_post( + request: Request, proposal: int, decision: str = Form(...) +): if not request.user.has_credential(creds.TU_LIST_VOTES): return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) context = await make_variable_context(request, "Trusted User") proposal = int(proposal) # Make sure it's an int. - voteinfo = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.ID == proposal).first() + voteinfo = ( + db.query(models.TUVoteInfo).filter(models.TUVoteInfo.ID == proposal).first() + ) if not voteinfo: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - voters = db.query(models.User).join(models.TUVote).filter( - models.TUVote.VoteID == voteinfo.ID) - vote = db.query(models.TUVote).filter( - and_(models.TUVote.UserID == request.user.ID, - models.TUVote.VoteID == voteinfo.ID)).first() + voters = ( + db.query(models.User) + .join(models.TUVote) + .filter(models.TUVote.VoteID == voteinfo.ID) + ) + vote = ( + db.query(models.TUVote) + .filter( + and_( + models.TUVote.UserID == request.user.ID, + models.TUVote.VoteID == voteinfo.ID, + ) + ) + .first() + ) status_code = HTTPStatus.OK if not request.user.has_credential(creds.TU_VOTE): @@ -227,16 +263,15 @@ async def trusted_user_proposal_post(request: Request, proposal: int, status_code = HTTPStatus.BAD_REQUEST if status_code != HTTPStatus.OK: - return render_proposal(request, context, proposal, - voteinfo, voters, vote, - status_code=status_code) + return render_proposal( + request, context, proposal, voteinfo, voters, vote, status_code=status_code + ) if decision in {"Yes", "No", "Abstain"}: # Increment whichever decision was given to us. setattr(voteinfo, decision, getattr(voteinfo, decision) + 1) else: - return Response("Invalid 'decision' value.", - status_code=HTTPStatus.BAD_REQUEST) + return Response("Invalid 'decision' value.", status_code=HTTPStatus.BAD_REQUEST) with db.begin(): vote = db.create(models.TUVote, User=request.user, VoteInfo=voteinfo) @@ -247,8 +282,9 @@ async def trusted_user_proposal_post(request: Request, proposal: int, @router.get("/addvote") @requires_auth -async def trusted_user_addvote(request: Request, user: str = str(), - type: str = "add_tu", agenda: str = str()): +async def trusted_user_addvote( + request: Request, user: str = str(), type: str = "add_tu", agenda: str = str() +): if not request.user.has_credential(creds.TU_ADD_VOTE): return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) @@ -268,10 +304,12 @@ async def trusted_user_addvote(request: Request, user: str = str(), @router.post("/addvote") @handle_form_exceptions @requires_auth -async def trusted_user_addvote_post(request: Request, - user: str = Form(default=str()), - type: str = Form(default=str()), - agenda: str = Form(default=str())): +async def trusted_user_addvote_post( + request: Request, + user: str = Form(default=str()), + type: str = Form(default=str()), + agenda: str = Form(default=str()), +): if not request.user.has_credential(creds.TU_ADD_VOTE): return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) @@ -283,26 +321,29 @@ async def trusted_user_addvote_post(request: Request, context["agenda"] = agenda def render_addvote(context, status_code): - """ Simplify render_template a bit for this test. """ + """Simplify render_template a bit for this test.""" return render_template(request, "addvote.html", context, status_code) # Alright, get some database records, if we can. if type != "bylaws": - user_record = db.query(models.User).filter( - models.User.Username == user).first() + user_record = db.query(models.User).filter(models.User.Username == user).first() if user_record is None: context["error"] = "Username does not exist." return render_addvote(context, HTTPStatus.NOT_FOUND) utcnow = time.utcnow() - voteinfo = db.query(models.TUVoteInfo).filter( - and_(models.TUVoteInfo.User == user, - models.TUVoteInfo.End > utcnow)).count() + voteinfo = ( + db.query(models.TUVoteInfo) + .filter( + and_(models.TUVoteInfo.User == user, models.TUVoteInfo.End > utcnow) + ) + .count() + ) if voteinfo: _ = l10n.get_translator_for_request(request) - context["error"] = _( - "%s already has proposal running for them.") % ( - html.escape(user),) + context["error"] = _("%s already has proposal running for them.") % ( + html.escape(user), + ) return render_addvote(context, HTTPStatus.BAD_REQUEST) if type not in ADDVOTE_SPECIFICS: @@ -323,16 +364,27 @@ async def trusted_user_addvote_post(request: Request, # Create a new TUVoteInfo (proposal)! with db.begin(): - active_tus = db.query(User).filter( - and_(User.Suspended == 0, - User.InactivityTS.isnot(None), - User.AccountTypeID.in_(types)) - ).count() - voteinfo = db.create(models.TUVoteInfo, User=user, - Agenda=html.escape(agenda), - Submitted=timestamp, End=(timestamp + duration), - Quorum=quorum, ActiveTUs=active_tus, - Submitter=request.user) + active_tus = ( + db.query(User) + .filter( + and_( + User.Suspended == 0, + User.InactivityTS.isnot(None), + User.AccountTypeID.in_(types), + ) + ) + .count() + ) + voteinfo = db.create( + models.TUVoteInfo, + User=user, + Agenda=html.escape(agenda), + Submitted=timestamp, + End=(timestamp + duration), + Quorum=quorum, + ActiveTUs=active_tus, + Submitter=request.user, + ) # Redirect to the new proposal. endpoint = f"/tu/{voteinfo.ID}" diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 3ea7e070..26677f80 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -1,5 +1,4 @@ import os - from collections import defaultdict from typing import Any, Callable, NewType, Union @@ -7,7 +6,6 @@ from fastapi.responses import HTMLResponse from sqlalchemy import and_, literal, orm import aurweb.config as config - from aurweb import db, defaults, models from aurweb.exceptions import RPCError from aurweb.filters import number_format @@ -23,8 +21,7 @@ TYPE_MAPPING = { "replaces": "Replaces", } -DataGenerator = NewType("DataGenerator", - Callable[[models.Package], dict[str, Any]]) +DataGenerator = NewType("DataGenerator", Callable[[models.Package], dict[str, Any]]) def documentation(): @@ -40,7 +37,7 @@ def documentation(): class RPC: - """ RPC API handler class. + """RPC API handler class. There are various pieces to RPC's process, and encapsulating them inside of a class means that external users do not abuse the @@ -66,17 +63,25 @@ class RPC: # A set of RPC types supported by this API. EXPOSED_TYPES = { - "info", "multiinfo", - "search", "msearch", - "suggest", "suggest-pkgbase" + "info", + "multiinfo", + "search", + "msearch", + "suggest", + "suggest-pkgbase", } # A mapping of type aliases. TYPE_ALIASES = {"info": "multiinfo"} EXPOSED_BYS = { - "name-desc", "name", "maintainer", - "depends", "makedepends", "optdepends", "checkdepends" + "name-desc", + "name", + "maintainer", + "depends", + "makedepends", + "optdepends", + "checkdepends", } # A mapping of by aliases. @@ -92,7 +97,7 @@ class RPC: "results": [], "resultcount": 0, "type": "error", - "error": message + "error": message, } def _verify_inputs(self, by: str = [], args: list[str] = []) -> None: @@ -116,7 +121,7 @@ class RPC: raise RPCError("No request type/data specified.") def _get_json_data(self, package: models.Package) -> dict[str, Any]: - """ Produce dictionary data of one Package that can be JSON-serialized. + """Produce dictionary data of one Package that can be JSON-serialized. :param package: Package instance :returns: JSON-serializable dictionary @@ -143,7 +148,7 @@ class RPC: "Popularity": pop, "OutOfDate": package.OutOfDateTS, "FirstSubmitted": package.SubmittedTS, - "LastModified": package.ModifiedTS + "LastModified": package.ModifiedTS, } def _get_info_json_data(self, package: models.Package) -> dict[str, Any]: @@ -151,10 +156,7 @@ class RPC: # All info results have _at least_ an empty list of # License and Keywords. - data.update({ - "License": [], - "Keywords": [] - }) + data.update({"License": [], "Keywords": []}) # If we actually got extra_info records, update data with # them for this particular package. @@ -163,9 +165,9 @@ class RPC: return data - def _assemble_json_data(self, packages: list[models.Package], - data_generator: DataGenerator) \ - -> list[dict[str, Any]]: + def _assemble_json_data( + self, packages: list[models.Package], data_generator: DataGenerator + ) -> list[dict[str, Any]]: """ Assemble JSON data out of a list of packages. @@ -175,7 +177,7 @@ class RPC: return [data_generator(pkg) for pkg in packages] def _entities(self, query: orm.Query) -> orm.Query: - """ Select specific RPC columns on `query`. """ + """Select specific RPC columns on `query`.""" return query.with_entities( models.Package.ID, models.Package.Name, @@ -192,16 +194,22 @@ class RPC: models.User.Username.label("Maintainer"), ).group_by(models.Package.ID) - def _handle_multiinfo_type(self, args: list[str] = [], **kwargs) \ - -> list[dict[str, Any]]: + def _handle_multiinfo_type( + self, args: list[str] = [], **kwargs + ) -> list[dict[str, Any]]: self._enforce_args(args) args = set(args) - packages = db.query(models.Package).join(models.PackageBase).join( - models.User, - models.User.ID == models.PackageBase.MaintainerUID, - isouter=True - ).filter(models.Package.Name.in_(args)) + packages = ( + db.query(models.Package) + .join(models.PackageBase) + .join( + models.User, + models.User.ID == models.PackageBase.MaintainerUID, + isouter=True, + ) + .filter(models.Package.Name.in_(args)) + ) max_results = config.getint("options", "max_rpc_results") packages = self._entities(packages).limit(max_results + 1) @@ -217,65 +225,75 @@ class RPC: subqueries = [ # PackageDependency - db.query( - models.PackageDependency - ).join(models.DependencyType).filter( - models.PackageDependency.PackageID.in_(ids) - ).with_entities( + db.query(models.PackageDependency) + .join(models.DependencyType) + .filter(models.PackageDependency.PackageID.in_(ids)) + .with_entities( models.PackageDependency.PackageID.label("ID"), models.DependencyType.Name.label("Type"), models.PackageDependency.DepName.label("Name"), - models.PackageDependency.DepCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageDependency.DepCondition.label("Cond"), + ) + .distinct() + .order_by("Name"), # PackageRelation - db.query( - models.PackageRelation - ).join(models.RelationType).filter( - models.PackageRelation.PackageID.in_(ids) - ).with_entities( + db.query(models.PackageRelation) + .join(models.RelationType) + .filter(models.PackageRelation.PackageID.in_(ids)) + .with_entities( models.PackageRelation.PackageID.label("ID"), models.RelationType.Name.label("Type"), models.PackageRelation.RelName.label("Name"), - models.PackageRelation.RelCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageRelation.RelCondition.label("Cond"), + ) + .distinct() + .order_by("Name"), # Groups - db.query(models.PackageGroup).join( + db.query(models.PackageGroup) + .join( models.Group, - and_(models.PackageGroup.GroupID == models.Group.ID, - models.PackageGroup.PackageID.in_(ids)) - ).with_entities( + and_( + models.PackageGroup.GroupID == models.Group.ID, + models.PackageGroup.PackageID.in_(ids), + ), + ) + .with_entities( models.PackageGroup.PackageID.label("ID"), literal("Groups").label("Type"), models.Group.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), # Licenses - db.query(models.PackageLicense).join( - models.License, - models.PackageLicense.LicenseID == models.License.ID - ).filter( - models.PackageLicense.PackageID.in_(ids) - ).with_entities( + db.query(models.PackageLicense) + .join(models.License, models.PackageLicense.LicenseID == models.License.ID) + .filter(models.PackageLicense.PackageID.in_(ids)) + .with_entities( models.PackageLicense.PackageID.label("ID"), literal("License").label("Type"), models.License.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), # Keywords - db.query(models.PackageKeyword).join( + db.query(models.PackageKeyword) + .join( models.Package, - and_(Package.PackageBaseID == PackageKeyword.PackageBaseID, - Package.ID.in_(ids)) - ).with_entities( + and_( + Package.PackageBaseID == PackageKeyword.PackageBaseID, + Package.ID.in_(ids), + ), + ) + .with_entities( models.Package.ID.label("ID"), literal("Keywords").label("Type"), models.PackageKeyword.Keyword.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name") + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), ] # Union all subqueries together. @@ -295,8 +313,9 @@ class RPC: return self._assemble_json_data(packages, self._get_info_json_data) - def _handle_search_type(self, by: str = defaults.RPC_SEARCH_BY, - args: list[str] = []) -> list[dict[str, Any]]: + def _handle_search_type( + self, by: str = defaults.RPC_SEARCH_BY, args: list[str] = [] + ) -> list[dict[str, Any]]: # If `by` isn't maintainer and we don't have any args, raise an error. # In maintainer's case, return all orphans if there are no args, # so we need args to pass through to the handler without errors. @@ -318,50 +337,64 @@ class RPC: return self._assemble_json_data(results, self._get_json_data) - def _handle_msearch_type(self, args: list[str] = [], **kwargs)\ - -> list[dict[str, Any]]: + def _handle_msearch_type( + self, args: list[str] = [], **kwargs + ) -> list[dict[str, Any]]: return self._handle_search_type(by="m", args=args) - def _handle_suggest_type(self, args: list[str] = [], **kwargs)\ - -> list[str]: + def _handle_suggest_type(self, args: list[str] = [], **kwargs) -> list[str]: if not args: return [] arg = args[0] - packages = db.query(models.Package.Name).join( - models.PackageBase - ).filter( - and_(models.PackageBase.PackagerUID.isnot(None), - models.Package.Name.like(f"{arg}%")) - ).order_by(models.Package.Name.asc()).limit(20) + packages = ( + db.query(models.Package.Name) + .join(models.PackageBase) + .filter( + and_( + models.PackageBase.PackagerUID.isnot(None), + models.Package.Name.like(f"{arg}%"), + ) + ) + .order_by(models.Package.Name.asc()) + .limit(20) + ) return [pkg.Name for pkg in packages] - def _handle_suggest_pkgbase_type(self, args: list[str] = [], **kwargs)\ - -> list[str]: + def _handle_suggest_pkgbase_type(self, args: list[str] = [], **kwargs) -> list[str]: if not args: return [] arg = args[0] - packages = db.query(models.PackageBase.Name).filter( - and_(models.PackageBase.PackagerUID.isnot(None), - models.PackageBase.Name.like(f"{arg}%")) - ).order_by(models.PackageBase.Name.asc()).limit(20) + packages = ( + db.query(models.PackageBase.Name) + .filter( + and_( + models.PackageBase.PackagerUID.isnot(None), + models.PackageBase.Name.like(f"{arg}%"), + ) + ) + .order_by(models.PackageBase.Name.asc()) + .limit(20) + ) return [pkg.Name for pkg in packages] def _is_suggestion(self) -> bool: return self.type.startswith("suggest") - def _handle_callback(self, by: str, args: list[str])\ - -> Union[list[dict[str, Any]], list[str]]: + def _handle_callback( + self, by: str, args: list[str] + ) -> Union[list[dict[str, Any]], list[str]]: # Get a handle to our callback and trap an RPCError with # an empty list of results based on callback's execution. callback = getattr(self, f"_handle_{self.type.replace('-', '_')}_type") results = callback(by=by, args=args) return results - def handle(self, by: str = defaults.RPC_SEARCH_BY, args: list[str] = [])\ - -> Union[list[dict[str, Any]], dict[str, Any]]: - """ Request entrypoint. A router should pass v, type and args + def handle( + self, by: str = defaults.RPC_SEARCH_BY, args: list[str] = [] + ) -> Union[list[dict[str, Any]], dict[str, Any]]: + """Request entrypoint. A router should pass v, type and args to this function and expect an output dictionary to be returned. :param v: RPC version argument @@ -392,8 +425,5 @@ class RPC: return results # Return JSON output. - data.update({ - "resultcount": len(results), - "results": results - }) + data.update({"resultcount": len(results), "results": results}) return data diff --git a/aurweb/schema.py b/aurweb/schema.py index d2644541..b3b36195 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -6,7 +6,18 @@ usually be automatically generated. See `migrations/README` for details. """ -from sqlalchemy import CHAR, TIMESTAMP, Column, ForeignKey, Index, MetaData, String, Table, Text, text +from sqlalchemy import ( + CHAR, + TIMESTAMP, + Column, + ForeignKey, + Index, + MetaData, + String, + Table, + Text, + text, +) from sqlalchemy.dialects.mysql import BIGINT, DECIMAL, INTEGER, TINYINT from sqlalchemy.ext.compiler import compiles @@ -15,13 +26,13 @@ import aurweb.config db_backend = aurweb.config.get("database", "backend") -@compiles(TINYINT, 'sqlite') +@compiles(TINYINT, "sqlite") def compile_tinyint_sqlite(type_, compiler, **kw): # pragma: no cover """TINYINT is not supported on SQLite. Substitute it with INTEGER.""" - return 'INTEGER' + return "INTEGER" -@compiles(BIGINT, 'sqlite') +@compiles(BIGINT, "sqlite") def compile_bigint_sqlite(type_, compiler, **kw): # pragma: no cover """ For SQLite's AUTOINCREMENT to work on BIGINT columns, we need to map BIGINT @@ -29,429 +40,567 @@ def compile_bigint_sqlite(type_, compiler, **kw): # pragma: no cover See https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#allowing-autoincrement-behavior-sqlalchemy-types-other-than-integer-integer """ # noqa: E501 - return 'INTEGER' + return "INTEGER" metadata = MetaData() # Define the Account Types for the AUR. AccountTypes = Table( - 'AccountTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('AccountType', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci' + "AccountTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("AccountType", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # User information for each user regardless of type. Users = Table( - 'Users', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('AccountTypeID', ForeignKey('AccountTypes.ID', ondelete="NO ACTION"), nullable=False, server_default=text("1")), - Column('Suspended', TINYINT(unsigned=True), nullable=False, server_default=text("0")), - Column('Username', String(32), nullable=False, unique=True), - Column('Email', String(254), nullable=False, unique=True), - Column('BackupEmail', String(254)), - Column('HideEmail', TINYINT(unsigned=True), nullable=False, server_default=text("0")), - Column('Passwd', String(255), nullable=False), - Column('Salt', CHAR(32), nullable=False, server_default=text("''")), - Column('ResetKey', CHAR(32), nullable=False, server_default=text("''")), - Column('RealName', String(64), nullable=False, server_default=text("''")), - Column('LangPreference', String(6), nullable=False, server_default=text("'en'")), - Column('Timezone', String(32), nullable=False, server_default=text("'UTC'")), - Column('Homepage', Text), - Column('IRCNick', String(32), nullable=False, server_default=text("''")), - Column('PGPKey', String(40)), - Column('LastLogin', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('LastLoginIPAddress', String(45)), - Column('LastSSHLogin', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('LastSSHLoginIPAddress', String(45)), - Column('InactivityTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('RegistrationTS', TIMESTAMP, nullable=False, server_default=text("CURRENT_TIMESTAMP")), - Column('CommentNotify', TINYINT(1), nullable=False, server_default=text("1")), - Column('UpdateNotify', TINYINT(1), nullable=False, server_default=text("0")), - Column('OwnershipNotify', TINYINT(1), nullable=False, server_default=text("1")), - Column('SSOAccountID', String(255), nullable=True, unique=True), - Index('UsersAccountTypeID', 'AccountTypeID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Users", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column( + "AccountTypeID", + ForeignKey("AccountTypes.ID", ondelete="NO ACTION"), + nullable=False, + server_default=text("1"), + ), + Column( + "Suspended", TINYINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("Username", String(32), nullable=False, unique=True), + Column("Email", String(254), nullable=False, unique=True), + Column("BackupEmail", String(254)), + Column( + "HideEmail", TINYINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("Passwd", String(255), nullable=False), + Column("Salt", CHAR(32), nullable=False, server_default=text("''")), + Column("ResetKey", CHAR(32), nullable=False, server_default=text("''")), + Column("RealName", String(64), nullable=False, server_default=text("''")), + Column("LangPreference", String(6), nullable=False, server_default=text("'en'")), + Column("Timezone", String(32), nullable=False, server_default=text("'UTC'")), + Column("Homepage", Text), + Column("IRCNick", String(32), nullable=False, server_default=text("''")), + Column("PGPKey", String(40)), + Column( + "LastLogin", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("LastLoginIPAddress", String(45)), + Column( + "LastSSHLogin", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("LastSSHLoginIPAddress", String(45)), + Column( + "InactivityTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column( + "RegistrationTS", + TIMESTAMP, + nullable=False, + server_default=text("CURRENT_TIMESTAMP"), + ), + Column("CommentNotify", TINYINT(1), nullable=False, server_default=text("1")), + Column("UpdateNotify", TINYINT(1), nullable=False, server_default=text("0")), + Column("OwnershipNotify", TINYINT(1), nullable=False, server_default=text("1")), + Column("SSOAccountID", String(255), nullable=True, unique=True), + Index("UsersAccountTypeID", "AccountTypeID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # SSH public keys used for the aurweb SSH/Git interface. SSHPubKeys = Table( - 'SSHPubKeys', metadata, - Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('Fingerprint', String(44), primary_key=True), - Column('PubKey', String(4096), nullable=False), - mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', + "SSHPubKeys", + metadata, + Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("Fingerprint", String(44), primary_key=True), + Column("PubKey", String(4096), nullable=False), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_bin", ) # Track Users logging in/out of AUR web site. Sessions = Table( - 'Sessions', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('SessionID', CHAR(32), nullable=False, unique=True), - Column('LastUpdateTS', BIGINT(unsigned=True), nullable=False), - mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', + "Sessions", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("SessionID", CHAR(32), nullable=False, unique=True), + Column("LastUpdateTS", BIGINT(unsigned=True), nullable=False), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_bin", ) # Information on package bases PackageBases = Table( - 'PackageBases', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(255), nullable=False, unique=True), - Column('NumVotes', INTEGER(unsigned=True), nullable=False, server_default=text("0")), - Column('Popularity', - DECIMAL(10, 6, unsigned=True) - if db_backend == "mysql" else String(17), - nullable=False, server_default=text("0")), - Column('OutOfDateTS', BIGINT(unsigned=True)), - Column('FlaggerComment', Text, nullable=False), - Column('SubmittedTS', BIGINT(unsigned=True), nullable=False), - Column('ModifiedTS', BIGINT(unsigned=True), nullable=False), - Column('FlaggerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # who flagged the package out-of-date? + "PackageBases", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(255), nullable=False, unique=True), + Column( + "NumVotes", INTEGER(unsigned=True), nullable=False, server_default=text("0") + ), + Column( + "Popularity", + DECIMAL(10, 6, unsigned=True) if db_backend == "mysql" else String(17), + nullable=False, + server_default=text("0"), + ), + Column("OutOfDateTS", BIGINT(unsigned=True)), + Column("FlaggerComment", Text, nullable=False), + Column("SubmittedTS", BIGINT(unsigned=True), nullable=False), + Column("ModifiedTS", BIGINT(unsigned=True), nullable=False), + Column( + "FlaggerUID", ForeignKey("Users.ID", ondelete="SET NULL") + ), # who flagged the package out-of-date? # deleting a user will cause packages to be orphaned, not deleted - Column('SubmitterUID', ForeignKey('Users.ID', ondelete='SET NULL')), # who submitted it? - Column('MaintainerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # User - Column('PackagerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # Last packager - Index('BasesMaintainerUID', 'MaintainerUID'), - Index('BasesNumVotes', 'NumVotes'), - Index('BasesPackagerUID', 'PackagerUID'), - Index('BasesSubmitterUID', 'SubmitterUID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + Column( + "SubmitterUID", ForeignKey("Users.ID", ondelete="SET NULL") + ), # who submitted it? + Column("MaintainerUID", ForeignKey("Users.ID", ondelete="SET NULL")), # User + Column("PackagerUID", ForeignKey("Users.ID", ondelete="SET NULL")), # Last packager + Index("BasesMaintainerUID", "MaintainerUID"), + Index("BasesNumVotes", "NumVotes"), + Index("BasesPackagerUID", "PackagerUID"), + Index("BasesSubmitterUID", "SubmitterUID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Keywords of package bases PackageKeywords = Table( - 'PackageKeywords', metadata, - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - Column('Keyword', String(255), primary_key=True, nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageKeywords", + metadata, + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + Column( + "Keyword", + String(255), + primary_key=True, + nullable=False, + server_default=text("''"), + ), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about the actual packages Packages = Table( - 'Packages', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('Name', String(255), nullable=False, unique=True), - Column('Version', String(255), nullable=False, server_default=text("''")), - Column('Description', String(255)), - Column('URL', String(8000)), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Packages", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("Name", String(255), nullable=False, unique=True), + Column("Version", String(255), nullable=False, server_default=text("''")), + Column("Description", String(255)), + Column("URL", String(8000)), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about licenses Licenses = Table( - 'Licenses', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(255), nullable=False, unique=True), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Licenses", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(255), nullable=False, unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about package-license-relations PackageLicenses = Table( - 'PackageLicenses', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - Column('LicenseID', ForeignKey('Licenses.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - mysql_engine='InnoDB', + "PackageLicenses", + metadata, + Column( + "PackageID", + ForeignKey("Packages.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + Column( + "LicenseID", + ForeignKey("Licenses.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + mysql_engine="InnoDB", ) # Information about groups Groups = Table( - 'Groups', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(255), nullable=False, unique=True), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Groups", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(255), nullable=False, unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about package-group-relations PackageGroups = Table( - 'PackageGroups', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - Column('GroupID', ForeignKey('Groups.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - mysql_engine='InnoDB', + "PackageGroups", + metadata, + Column( + "PackageID", + ForeignKey("Packages.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + Column( + "GroupID", + ForeignKey("Groups.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + mysql_engine="InnoDB", ) # Define the package dependency types DependencyTypes = Table( - 'DependencyTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('Name', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "DependencyTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("Name", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track which dependencies a package has PackageDepends = Table( - 'PackageDepends', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), - Column('DepTypeID', ForeignKey('DependencyTypes.ID', ondelete="NO ACTION"), nullable=False), - Column('DepName', String(255), nullable=False), - Column('DepDesc', String(255)), - Column('DepCondition', String(255)), - Column('DepArch', String(255)), - Index('DependsDepName', 'DepName'), - Index('DependsPackageID', 'PackageID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageDepends", + metadata, + Column("PackageID", ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False), + Column( + "DepTypeID", + ForeignKey("DependencyTypes.ID", ondelete="NO ACTION"), + nullable=False, + ), + Column("DepName", String(255), nullable=False), + Column("DepDesc", String(255)), + Column("DepCondition", String(255)), + Column("DepArch", String(255)), + Index("DependsDepName", "DepName"), + Index("DependsPackageID", "PackageID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Define the package relation types RelationTypes = Table( - 'RelationTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('Name', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "RelationTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("Name", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track which conflicts, provides and replaces a package has PackageRelations = Table( - 'PackageRelations', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), - Column('RelTypeID', ForeignKey('RelationTypes.ID', ondelete="NO ACTION"), nullable=False), - Column('RelName', String(255), nullable=False), - Column('RelCondition', String(255)), - Column('RelArch', String(255)), - Index('RelationsPackageID', 'PackageID'), - Index('RelationsRelName', 'RelName'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageRelations", + metadata, + Column("PackageID", ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False), + Column( + "RelTypeID", + ForeignKey("RelationTypes.ID", ondelete="NO ACTION"), + nullable=False, + ), + Column("RelName", String(255), nullable=False), + Column("RelCondition", String(255)), + Column("RelArch", String(255)), + Index("RelationsPackageID", "PackageID"), + Index("RelationsRelName", "RelName"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track which sources a package has PackageSources = Table( - 'PackageSources', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), - Column('Source', String(8000), nullable=False, server_default=text("'/dev/null'")), - Column('SourceArch', String(255)), - Index('SourcesPackageID', 'PackageID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageSources", + metadata, + Column("PackageID", ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False), + Column("Source", String(8000), nullable=False, server_default=text("'/dev/null'")), + Column("SourceArch", String(255)), + Index("SourcesPackageID", "PackageID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track votes for packages PackageVotes = Table( - 'PackageVotes', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('VoteTS', BIGINT(unsigned=True), nullable=False), - Index('VoteUsersIDPackageID', 'UsersID', 'PackageBaseID', unique=True), - Index('VotesPackageBaseID', 'PackageBaseID'), - Index('VotesUsersID', 'UsersID'), - mysql_engine='InnoDB', + "PackageVotes", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("VoteTS", BIGINT(unsigned=True), nullable=False), + Index("VoteUsersIDPackageID", "UsersID", "PackageBaseID", unique=True), + Index("VotesPackageBaseID", "PackageBaseID"), + Index("VotesUsersID", "UsersID"), + mysql_engine="InnoDB", ) # Record comments for packages PackageComments = Table( - 'PackageComments', metadata, - Column('ID', BIGINT(unsigned=True), primary_key=True), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('UsersID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('Comments', Text, nullable=False), - Column('RenderedComment', Text, nullable=False), - Column('CommentTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('EditedTS', BIGINT(unsigned=True)), - Column('EditedUsersID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('DelTS', BIGINT(unsigned=True)), - Column('DelUsersID', ForeignKey('Users.ID', ondelete='CASCADE')), - Column('PinnedTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Index('CommentsPackageBaseID', 'PackageBaseID'), - Index('CommentsUsersID', 'UsersID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageComments", + metadata, + Column("ID", BIGINT(unsigned=True), primary_key=True), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("Comments", Text, nullable=False), + Column("RenderedComment", Text, nullable=False), + Column( + "CommentTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("EditedTS", BIGINT(unsigned=True)), + Column("EditedUsersID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("DelTS", BIGINT(unsigned=True)), + Column("DelUsersID", ForeignKey("Users.ID", ondelete="CASCADE")), + Column("PinnedTS", BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Index("CommentsPackageBaseID", "PackageBaseID"), + Index("CommentsUsersID", "UsersID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Package base co-maintainers PackageComaintainers = Table( - 'PackageComaintainers', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('Priority', INTEGER(unsigned=True), nullable=False), - Index('ComaintainersPackageBaseID', 'PackageBaseID'), - Index('ComaintainersUsersID', 'UsersID'), - mysql_engine='InnoDB', + "PackageComaintainers", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("Priority", INTEGER(unsigned=True), nullable=False), + Index("ComaintainersPackageBaseID", "PackageBaseID"), + Index("ComaintainersUsersID", "UsersID"), + mysql_engine="InnoDB", ) # Package base notifications PackageNotifications = Table( - 'PackageNotifications', metadata, - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Index('NotifyUserIDPkgID', 'UserID', 'PackageBaseID', unique=True), - mysql_engine='InnoDB', + "PackageNotifications", + metadata, + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Index("NotifyUserIDPkgID", "UserID", "PackageBaseID", unique=True), + mysql_engine="InnoDB", ) # Package name blacklist PackageBlacklist = Table( - 'PackageBlacklist', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(64), nullable=False, unique=True), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageBlacklist", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(64), nullable=False, unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Providers in the official repositories OfficialProviders = Table( - 'OfficialProviders', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(64), nullable=False), - Column('Repo', String(64), nullable=False), - Column('Provides', String(64), nullable=False), - Index('ProviderNameProvides', 'Name', 'Provides', unique=True), - mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', + "OfficialProviders", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(64), nullable=False), + Column("Repo", String(64), nullable=False), + Column("Provides", String(64), nullable=False), + Index("ProviderNameProvides", "Name", "Provides", unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_bin", ) # Define package request types RequestTypes = Table( - 'RequestTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('Name', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "RequestTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("Name", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Package requests PackageRequests = Table( - 'PackageRequests', metadata, - Column('ID', BIGINT(unsigned=True), primary_key=True), - Column('ReqTypeID', ForeignKey('RequestTypes.ID', ondelete="NO ACTION"), nullable=False), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='SET NULL')), - Column('PackageBaseName', String(255), nullable=False), - Column('MergeBaseName', String(255)), - Column('UsersID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('Comments', Text, nullable=False), - Column('ClosureComment', Text, nullable=False), - Column('RequestTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('ClosedTS', BIGINT(unsigned=True)), - Column('ClosedUID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('Status', TINYINT(unsigned=True), nullable=False, server_default=text("0")), - Index('RequestsPackageBaseID', 'PackageBaseID'), - Index('RequestsUsersID', 'UsersID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageRequests", + metadata, + Column("ID", BIGINT(unsigned=True), primary_key=True), + Column( + "ReqTypeID", ForeignKey("RequestTypes.ID", ondelete="NO ACTION"), nullable=False + ), + Column("PackageBaseID", ForeignKey("PackageBases.ID", ondelete="SET NULL")), + Column("PackageBaseName", String(255), nullable=False), + Column("MergeBaseName", String(255)), + Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("Comments", Text, nullable=False), + Column("ClosureComment", Text, nullable=False), + Column( + "RequestTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("ClosedTS", BIGINT(unsigned=True)), + Column("ClosedUID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("Status", TINYINT(unsigned=True), nullable=False, server_default=text("0")), + Index("RequestsPackageBaseID", "PackageBaseID"), + Index("RequestsUsersID", "UsersID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Vote information TU_VoteInfo = Table( - 'TU_VoteInfo', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Agenda', Text, nullable=False), - Column('User', String(32), nullable=False), - Column('Submitted', BIGINT(unsigned=True), nullable=False), - Column('End', BIGINT(unsigned=True), nullable=False), - Column('Quorum', - DECIMAL(2, 2, unsigned=True) - if db_backend == "mysql" else String(5), - nullable=False), - Column('SubmitterID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('Yes', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - Column('No', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - Column('Abstain', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - Column('ActiveTUs', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "TU_VoteInfo", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Agenda", Text, nullable=False), + Column("User", String(32), nullable=False), + Column("Submitted", BIGINT(unsigned=True), nullable=False), + Column("End", BIGINT(unsigned=True), nullable=False), + Column( + "Quorum", + DECIMAL(2, 2, unsigned=True) if db_backend == "mysql" else String(5), + nullable=False, + ), + Column("SubmitterID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("Yes", INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), + Column("No", INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), + Column( + "Abstain", INTEGER(unsigned=True), nullable=False, server_default=text("'0'") + ), + Column( + "ActiveTUs", INTEGER(unsigned=True), nullable=False, server_default=text("'0'") + ), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Individual vote records TU_Votes = Table( - 'TU_Votes', metadata, - Column('VoteID', ForeignKey('TU_VoteInfo.ID', ondelete='CASCADE'), nullable=False), - Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - mysql_engine='InnoDB', + "TU_Votes", + metadata, + Column("VoteID", ForeignKey("TU_VoteInfo.ID", ondelete="CASCADE"), nullable=False), + Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + mysql_engine="InnoDB", ) # Malicious user banning Bans = Table( - 'Bans', metadata, - Column('IPAddress', String(45), primary_key=True), - Column('BanTS', TIMESTAMP, nullable=False), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Bans", + metadata, + Column("IPAddress", String(45), primary_key=True), + Column("BanTS", TIMESTAMP, nullable=False), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Terms and Conditions Terms = Table( - 'Terms', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Description', String(255), nullable=False), - Column('URL', String(8000), nullable=False), - Column('Revision', INTEGER(unsigned=True), nullable=False, server_default=text("1")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Terms", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Description", String(255), nullable=False), + Column("URL", String(8000), nullable=False), + Column( + "Revision", INTEGER(unsigned=True), nullable=False, server_default=text("1") + ), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Terms and Conditions accepted by users AcceptedTerms = Table( - 'AcceptedTerms', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('TermsID', ForeignKey('Terms.ID', ondelete='CASCADE'), nullable=False), - Column('Revision', INTEGER(unsigned=True), nullable=False, server_default=text("0")), - mysql_engine='InnoDB', + "AcceptedTerms", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("TermsID", ForeignKey("Terms.ID", ondelete="CASCADE"), nullable=False), + Column( + "Revision", INTEGER(unsigned=True), nullable=False, server_default=text("0") + ), + mysql_engine="InnoDB", ) # Rate limits for API ApiRateLimit = Table( - 'ApiRateLimit', metadata, - Column('IP', String(45), primary_key=True, unique=True, default=str()), - Column('Requests', INTEGER(11), nullable=False), - Column('WindowStart', BIGINT(20), nullable=False), - Index('ApiRateLimitWindowStart', 'WindowStart'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "ApiRateLimit", + metadata, + Column("IP", String(45), primary_key=True, unique=True, default=str()), + Column("Requests", INTEGER(11), nullable=False), + Column("WindowStart", BIGINT(20), nullable=False), + Index("ApiRateLimitWindowStart", "WindowStart"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) diff --git a/aurweb/scripts/adduser.py b/aurweb/scripts/adduser.py index 4cc059d1..cf933c71 100644 --- a/aurweb/scripts/adduser.py +++ b/aurweb/scripts/adduser.py @@ -11,7 +11,6 @@ import sys import traceback import aurweb.models.account_type as at - from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint @@ -30,8 +29,9 @@ def parse_args(): parser.add_argument("--ssh-pubkey", help="SSH PubKey") choices = at.ACCOUNT_TYPE_NAME.values() - parser.add_argument("-t", "--type", help="Account Type", - choices=choices, default=at.USER) + parser.add_argument( + "-t", "--type", help="Account Type", choices=choices, default=at.USER + ) return parser.parse_args() @@ -40,25 +40,29 @@ def main(): args = parse_args() db.get_engine() - type = db.query(AccountType, - AccountType.AccountType == args.type).first() + type = db.query(AccountType, AccountType.AccountType == args.type).first() with db.begin(): - user = db.create(User, Username=args.username, - Email=args.email, Passwd=args.password, - RealName=args.realname, IRCNick=args.ircnick, - PGPKey=args.pgp_key, AccountType=type) + user = db.create( + User, + Username=args.username, + Email=args.email, + Passwd=args.password, + RealName=args.realname, + IRCNick=args.ircnick, + PGPKey=args.pgp_key, + AccountType=type, + ) if args.ssh_pubkey: pubkey = args.ssh_pubkey.strip() # Remove host from the pubkey if it's there. - pubkey = ' '.join(pubkey.split(' ')[:2]) + pubkey = " ".join(pubkey.split(" ")[:2]) with db.begin(): - db.create(SSHPubKey, - User=user, - PubKey=pubkey, - Fingerprint=get_fingerprint(pubkey)) + db.create( + SSHPubKey, User=user, PubKey=pubkey, Fingerprint=get_fingerprint(pubkey) + ) print(user.json()) return 0 diff --git a/aurweb/scripts/aurblup.py b/aurweb/scripts/aurblup.py index 9c9059ec..340d1ccd 100755 --- a/aurweb/scripts/aurblup.py +++ b/aurweb/scripts/aurblup.py @@ -3,11 +3,9 @@ import re import pyalpm - from sqlalchemy import and_ import aurweb.config - from aurweb import db, util from aurweb.models import OfficialProvider @@ -18,8 +16,8 @@ def _main(force: bool = False): repomap = dict() db_path = aurweb.config.get("aurblup", "db-path") - sync_dbs = aurweb.config.get('aurblup', 'sync-dbs').split(' ') - server = aurweb.config.get('aurblup', 'server') + sync_dbs = aurweb.config.get("aurblup", "sync-dbs").split(" ") + server = aurweb.config.get("aurblup", "server") h = pyalpm.Handle("/", db_path) for sync_db in sync_dbs: @@ -35,28 +33,35 @@ def _main(force: bool = False): providers.add((pkg.name, pkg.name)) repomap[(pkg.name, pkg.name)] = repo.name for provision in pkg.provides: - provisionname = re.sub(r'(<|=|>).*', '', provision) + provisionname = re.sub(r"(<|=|>).*", "", provision) providers.add((pkg.name, provisionname)) repomap[(pkg.name, provisionname)] = repo.name with db.begin(): old_providers = set( - db.query(OfficialProvider).with_entities( + db.query(OfficialProvider) + .with_entities( OfficialProvider.Name.label("Name"), - OfficialProvider.Provides.label("Provides") - ).distinct().order_by("Name").all() + OfficialProvider.Provides.label("Provides"), + ) + .distinct() + .order_by("Name") + .all() ) for name, provides in old_providers.difference(providers): - db.delete_all(db.query(OfficialProvider).filter( - and_(OfficialProvider.Name == name, - OfficialProvider.Provides == provides) - )) + db.delete_all( + db.query(OfficialProvider).filter( + and_( + OfficialProvider.Name == name, + OfficialProvider.Provides == provides, + ) + ) + ) for name, provides in providers.difference(old_providers): repo = repomap.get((name, provides)) - db.create(OfficialProvider, Name=name, - Repo=repo, Provides=provides) + db.create(OfficialProvider, Name=name, Repo=repo, Provides=provides) def main(force: bool = False): @@ -64,5 +69,5 @@ def main(force: bool = False): _main(force) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/config.py b/aurweb/scripts/config.py index e7c91dd1..1d90f525 100644 --- a/aurweb/scripts/config.py +++ b/aurweb/scripts/config.py @@ -50,12 +50,12 @@ def parse_args(): actions = ["get", "set", "unset"] parser = argparse.ArgumentParser( description="aurweb configuration tool", - formatter_class=lambda prog: fmt_cls(prog=prog, max_help_position=80)) + formatter_class=lambda prog: fmt_cls(prog=prog, max_help_position=80), + ) parser.add_argument("action", choices=actions, help="script action") parser.add_argument("section", help="config section") parser.add_argument("option", help="config option") - parser.add_argument("value", nargs="?", default=0, - help="config option value") + parser.add_argument("value", nargs="?", default=0, help="config option value") return parser.parse_args() diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 888e346c..7ca171ab 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -25,16 +25,13 @@ import os import shutil import sys import tempfile - from collections import defaultdict from typing import Any import orjson - from sqlalchemy import literal, orm import aurweb.config - from aurweb import db, filters, logging, models, util from aurweb.benchmark import Benchmark from aurweb.models import Package, PackageBase, User @@ -90,65 +87,68 @@ def get_extended_dict(query: orm.Query): def get_extended_fields(): subqueries = [ # PackageDependency - db.query( - models.PackageDependency - ).join(models.DependencyType).with_entities( + db.query(models.PackageDependency) + .join(models.DependencyType) + .with_entities( models.PackageDependency.PackageID.label("ID"), models.DependencyType.Name.label("Type"), models.PackageDependency.DepName.label("Name"), - models.PackageDependency.DepCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageDependency.DepCondition.label("Cond"), + ) + .distinct() + .order_by("Name"), # PackageRelation - db.query( - models.PackageRelation - ).join(models.RelationType).with_entities( + db.query(models.PackageRelation) + .join(models.RelationType) + .with_entities( models.PackageRelation.PackageID.label("ID"), models.RelationType.Name.label("Type"), models.PackageRelation.RelName.label("Name"), - models.PackageRelation.RelCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageRelation.RelCondition.label("Cond"), + ) + .distinct() + .order_by("Name"), # Groups - db.query(models.PackageGroup).join( - models.Group, - models.PackageGroup.GroupID == models.Group.ID - ).with_entities( + db.query(models.PackageGroup) + .join(models.Group, models.PackageGroup.GroupID == models.Group.ID) + .with_entities( models.PackageGroup.PackageID.label("ID"), literal("Groups").label("Type"), models.Group.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), # Licenses - db.query(models.PackageLicense).join( - models.License, - models.PackageLicense.LicenseID == models.License.ID - ).with_entities( + db.query(models.PackageLicense) + .join(models.License, models.PackageLicense.LicenseID == models.License.ID) + .with_entities( models.PackageLicense.PackageID.label("ID"), literal("License").label("Type"), models.License.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), # Keywords - db.query(models.PackageKeyword).join( - models.Package, - Package.PackageBaseID == models.PackageKeyword.PackageBaseID - ).with_entities( + db.query(models.PackageKeyword) + .join( + models.Package, Package.PackageBaseID == models.PackageKeyword.PackageBaseID + ) + .with_entities( models.Package.ID.label("ID"), literal("Keywords").label("Type"), models.PackageKeyword.Keyword.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name") + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), ] query = subqueries[0].union_all(*subqueries[1:]) return get_extended_dict(query) -EXTENDED_FIELD_HANDLERS = { - "--extended": get_extended_fields -} +EXTENDED_FIELD_HANDLERS = {"--extended": get_extended_fields} def as_dict(package: Package) -> dict[str, Any]: @@ -181,37 +181,38 @@ def _main(): archivedir = aurweb.config.get("mkpkglists", "archivedir") os.makedirs(archivedir, exist_ok=True) - PACKAGES = aurweb.config.get('mkpkglists', 'packagesfile') - META = aurweb.config.get('mkpkglists', 'packagesmetafile') - META_EXT = aurweb.config.get('mkpkglists', 'packagesmetaextfile') - PKGBASE = aurweb.config.get('mkpkglists', 'pkgbasefile') - USERS = aurweb.config.get('mkpkglists', 'userfile') + PACKAGES = aurweb.config.get("mkpkglists", "packagesfile") + META = aurweb.config.get("mkpkglists", "packagesmetafile") + META_EXT = aurweb.config.get("mkpkglists", "packagesmetaextfile") + PKGBASE = aurweb.config.get("mkpkglists", "pkgbasefile") + USERS = aurweb.config.get("mkpkglists", "userfile") bench = Benchmark() logger.info("Started re-creating archives, wait a while...") - query = db.query(Package).join( - PackageBase, - PackageBase.ID == Package.PackageBaseID - ).join( - User, - PackageBase.MaintainerUID == User.ID, - isouter=True - ).filter(PackageBase.PackagerUID.isnot(None)).with_entities( - Package.ID, - Package.Name, - PackageBase.ID.label("PackageBaseID"), - PackageBase.Name.label("PackageBase"), - Package.Version, - Package.Description, - Package.URL, - PackageBase.NumVotes, - PackageBase.Popularity, - PackageBase.OutOfDateTS.label("OutOfDate"), - User.Username.label("Maintainer"), - PackageBase.SubmittedTS.label("FirstSubmitted"), - PackageBase.ModifiedTS.label("LastModified") - ).distinct().order_by("Name") + query = ( + db.query(Package) + .join(PackageBase, PackageBase.ID == Package.PackageBaseID) + .join(User, PackageBase.MaintainerUID == User.ID, isouter=True) + .filter(PackageBase.PackagerUID.isnot(None)) + .with_entities( + Package.ID, + Package.Name, + PackageBase.ID.label("PackageBaseID"), + PackageBase.Name.label("PackageBase"), + Package.Version, + Package.Description, + Package.URL, + PackageBase.NumVotes, + PackageBase.Popularity, + PackageBase.OutOfDateTS.label("OutOfDate"), + User.Username.label("Maintainer"), + PackageBase.SubmittedTS.label("FirstSubmitted"), + PackageBase.ModifiedTS.label("LastModified"), + ) + .distinct() + .order_by("Name") + ) # Produce packages-meta-v1.json.gz output = list() @@ -252,7 +253,7 @@ def _main(): # We stream out package json objects line per line, so # we also need to include the ',' character at the end # of package lines (excluding the last package). - suffix = b",\n" if i < n else b'\n' + suffix = b",\n" if i < n else b"\n" # Write out to packagesmetafile output.append(item) @@ -273,8 +274,7 @@ def _main(): util.apply_all(gzips.values(), lambda gz: gz.close()) # Produce pkgbase.gz - query = db.query(PackageBase.Name).filter( - PackageBase.PackagerUID.isnot(None)).all() + query = db.query(PackageBase.Name).filter(PackageBase.PackagerUID.isnot(None)).all() tmp_pkgbase = os.path.join(tmpdir, os.path.basename(PKGBASE)) with gzip.open(tmp_pkgbase, "wt") as f: f.writelines([f"{base.Name}\n" for i, base in enumerate(query)]) @@ -317,5 +317,5 @@ def main(): _main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 6afa65ae..f19438bb 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -13,7 +13,6 @@ import aurweb.config import aurweb.db import aurweb.filters import aurweb.l10n - from aurweb import db, logging from aurweb.models import PackageBase, User from aurweb.models.package_comaintainer import PackageComaintainer @@ -25,15 +24,15 @@ from aurweb.models.tu_vote import TUVote logger = logging.get_logger(__name__) -aur_location = aurweb.config.get('options', 'aur_location') +aur_location = aurweb.config.get("options", "aur_location") def headers_msgid(thread_id): - return {'Message-ID': thread_id} + return {"Message-ID": thread_id} def headers_reply(thread_id): - return {'In-Reply-To': thread_id, 'References': thread_id} + return {"In-Reply-To": thread_id, "References": thread_id} class Notification: @@ -47,67 +46,64 @@ class Notification: return [] def get_body_fmt(self, lang): - body = '' + body = "" for line in self.get_body(lang).splitlines(): - if line == '--': - body += '--\n' + if line == "--": + body += "--\n" continue - body += textwrap.fill(line, break_long_words=False) + '\n' + body += textwrap.fill(line, break_long_words=False) + "\n" for i, ref in enumerate(self.get_refs()): - body += '\n' + '[%d] %s' % (i + 1, ref) + body += "\n" + "[%d] %s" % (i + 1, ref) return body.rstrip() def _send(self) -> None: - sendmail = aurweb.config.get('notifications', 'sendmail') - sender = aurweb.config.get('notifications', 'sender') - reply_to = aurweb.config.get('notifications', 'reply-to') + sendmail = aurweb.config.get("notifications", "sendmail") + sender = aurweb.config.get("notifications", "sender") + reply_to = aurweb.config.get("notifications", "reply-to") reason = self.__class__.__name__ - if reason.endswith('Notification'): - reason = reason[:-len('Notification')] + if reason.endswith("Notification"): + reason = reason[: -len("Notification")] for recipient in self.get_recipients(): to, lang = recipient - msg = email.mime.text.MIMEText(self.get_body_fmt(lang), - 'plain', 'utf-8') - msg['Subject'] = self.get_subject(lang) - msg['From'] = sender - msg['Reply-to'] = reply_to - msg['To'] = to + msg = email.mime.text.MIMEText(self.get_body_fmt(lang), "plain", "utf-8") + msg["Subject"] = self.get_subject(lang) + msg["From"] = sender + msg["Reply-to"] = reply_to + msg["To"] = to if self.get_cc(): - msg['Cc'] = str.join(', ', self.get_cc()) - msg['X-AUR-Reason'] = reason - msg['Date'] = email.utils.formatdate(localtime=True) + msg["Cc"] = str.join(", ", self.get_cc()) + msg["X-AUR-Reason"] = reason + msg["Date"] = email.utils.formatdate(localtime=True) for key, value in self.get_headers().items(): msg[key] = value - sendmail = aurweb.config.get('notifications', 'sendmail') + sendmail = aurweb.config.get("notifications", "sendmail") if sendmail: # send email using the sendmail binary specified in the # configuration file - p = subprocess.Popen([sendmail, '-t', '-oi'], - stdin=subprocess.PIPE) + p = subprocess.Popen([sendmail, "-t", "-oi"], stdin=subprocess.PIPE) p.communicate(msg.as_bytes()) else: # send email using smtplib; no local MTA required - server_addr = aurweb.config.get('notifications', 'smtp-server') - server_port = aurweb.config.getint('notifications', - 'smtp-port') - use_ssl = aurweb.config.getboolean('notifications', - 'smtp-use-ssl') - use_starttls = aurweb.config.getboolean('notifications', - 'smtp-use-starttls') - user = aurweb.config.get('notifications', 'smtp-user') - passwd = aurweb.config.get('notifications', 'smtp-password') + server_addr = aurweb.config.get("notifications", "smtp-server") + server_port = aurweb.config.getint("notifications", "smtp-port") + use_ssl = aurweb.config.getboolean("notifications", "smtp-use-ssl") + use_starttls = aurweb.config.getboolean( + "notifications", "smtp-use-starttls" + ) + user = aurweb.config.get("notifications", "smtp-user") + passwd = aurweb.config.get("notifications", "smtp-password") classes = { False: smtplib.SMTP, True: smtplib.SMTP_SSL, } - smtp_timeout = aurweb.config.getint("notifications", - "smtp-timeout") - server = classes[use_ssl](server_addr, server_port, - timeout=smtp_timeout) + smtp_timeout = aurweb.config.getint("notifications", "smtp-timeout") + server = classes[use_ssl]( + server_addr, server_port, timeout=smtp_timeout + ) if use_starttls: server.ehlo() @@ -126,23 +122,29 @@ class Notification: try: self._send() except OSError as exc: - logger.error("Unable to emit notification due to an " - "OSError (precise exception following).") + logger.error( + "Unable to emit notification due to an " + "OSError (precise exception following)." + ) logger.error(str(exc)) class ResetKeyNotification(Notification): def __init__(self, uid): - user = db.query(User).filter( - and_(User.ID == uid, User.Suspended == 0) - ).with_entities( - User.Username, - User.Email, - User.BackupEmail, - User.LangPreference, - User.ResetKey - ).order_by(User.Username.asc()).first() + user = ( + db.query(User) + .filter(and_(User.ID == uid, User.Suspended == 0)) + .with_entities( + User.Username, + User.Email, + User.BackupEmail, + User.LangPreference, + User.ResetKey, + ) + .order_by(User.Username.asc()) + .first() + ) self._username = user.Username self._to = user.Email @@ -159,55 +161,66 @@ class ResetKeyNotification(Notification): return [(self._to, self._lang)] def get_subject(self, lang): - return aurweb.l10n.translator.translate('AUR Password Reset', lang) + return aurweb.l10n.translator.translate("AUR Password Reset", lang) def get_body(self, lang): return aurweb.l10n.translator.translate( - 'A password reset request was submitted for the account ' - '{user} associated with your email address. If you wish to ' - 'reset your password follow the link [1] below, otherwise ' - 'ignore this message and nothing will happen.', - lang).format(user=self._username) + "A password reset request was submitted for the account " + "{user} associated with your email address. If you wish to " + "reset your password follow the link [1] below, otherwise " + "ignore this message and nothing will happen.", + lang, + ).format(user=self._username) def get_refs(self): - return (aur_location + '/passreset/?resetkey=' + self._resetkey,) + return (aur_location + "/passreset/?resetkey=" + self._resetkey,) class WelcomeNotification(ResetKeyNotification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'Welcome to the Arch User Repository', - lang) + "Welcome to the Arch User Repository", lang + ) def get_body(self, lang): return aurweb.l10n.translator.translate( - 'Welcome to the Arch User Repository! In order to set an ' - 'initial password for your new account, please click the ' - 'link [1] below. If the link does not work, try copying and ' - 'pasting it into your browser.', lang) + "Welcome to the Arch User Repository! In order to set an " + "initial password for your new account, please click the " + "link [1] below. If the link does not work, try copying and " + "pasting it into your browser.", + lang, + ) class CommentNotification(Notification): def __init__(self, uid, pkgbase_id, comment_id): - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - query = db.query(User).join(PackageNotification).filter( - and_(User.CommentNotify == 1, - PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + User.CommentNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() + ) self._recipients = [(u.Email, u.LangPreference) for u in query] - pkgcomment = db.query(PackageComment.Comments).filter( - PackageComment.ID == comment_id).first() + pkgcomment = ( + db.query(PackageComment.Comments) + .filter(PackageComment.ID == comment_id) + .first() + ) self._text = pkgcomment.Comments super().__init__() @@ -217,49 +230,56 @@ class CommentNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Comment for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Comment for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_body(self, lang): body = aurweb.l10n.translator.translate( - '{user} [1] added the following comment to {pkgbase} [2]:', - lang).format(user=self._user, pkgbase=self._pkgbase) - body += '\n\n' + self._text + '\n\n--\n' - dnlabel = aurweb.l10n.translator.translate( - 'Disable notifications', lang) + "{user} [1] added the following comment to {pkgbase} [2]:", lang + ).format(user=self._user, pkgbase=self._pkgbase) + body += "\n\n" + self._text + "\n\n--\n" + dnlabel = aurweb.l10n.translator.translate("Disable notifications", lang) body += aurweb.l10n.translator.translate( - 'If you no longer wish to receive notifications about this ' - 'package, please go to the package page [2] and select ' - '"{label}".', lang).format(label=dnlabel) + "If you no longer wish to receive notifications about this " + "package, please go to the package page [2] and select " + '"{label}".', + lang, + ).format(label=dnlabel) return body def get_refs(self): - return (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._pkgbase + '/') + return ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._pkgbase + "/", + ) def get_headers(self): - thread_id = '' + thread_id = "" return headers_reply(thread_id) class UpdateNotification(Notification): def __init__(self, uid, pkgbase_id): - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - query = db.query(User).join(PackageNotification).filter( - and_(User.UpdateNotify == 1, - PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + User.UpdateNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() + ) self._recipients = [(u.Email, u.LangPreference) for u in query] super().__init__() @@ -269,55 +289,63 @@ class UpdateNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Package Update: {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Package Update: {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_body(self, lang): body = aurweb.l10n.translator.translate( - '{user} [1] pushed a new commit to {pkgbase} [2].', - lang).format(user=self._user, pkgbase=self._pkgbase) - body += '\n\n--\n' - dnlabel = aurweb.l10n.translator.translate( - 'Disable notifications', lang) + "{user} [1] pushed a new commit to {pkgbase} [2].", lang + ).format(user=self._user, pkgbase=self._pkgbase) + body += "\n\n--\n" + dnlabel = aurweb.l10n.translator.translate("Disable notifications", lang) body += aurweb.l10n.translator.translate( - 'If you no longer wish to receive notifications about this ' - 'package, please go to the package page [2] and select ' - '"{label}".', lang).format(label=dnlabel) + "If you no longer wish to receive notifications about this " + "package, please go to the package page [2] and select " + '"{label}".', + lang, + ).format(label=dnlabel) return body def get_refs(self): - return (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._pkgbase + '/') + return ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._pkgbase + "/", + ) def get_headers(self): - thread_id = '' + thread_id = "" return headers_reply(thread_id) class FlagNotification(Notification): def __init__(self, uid, pkgbase_id): - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - query = db.query(User).join(PackageComaintainer, isouter=True).join( - PackageBase, - or_(PackageBase.MaintainerUID == User.ID, - PackageBase.ID == PackageComaintainer.PackageBaseID) - ).filter( - and_(PackageBase.ID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageComaintainer, isouter=True) + .join( + PackageBase, + or_( + PackageBase.MaintainerUID == User.ID, + PackageBase.ID == PackageComaintainer.PackageBaseID, + ), + ) + .filter(and_(PackageBase.ID == pkgbase_id, User.Suspended == 0)) + .with_entities(User.Email, User.LangPreference) + .distinct() + ) self._recipients = [(u.Email, u.LangPreference) for u in query] - pkgbase = db.query(PackageBase.FlaggerComment).filter( - PackageBase.ID == pkgbase_id).first() + pkgbase = ( + db.query(PackageBase.FlaggerComment) + .filter(PackageBase.ID == pkgbase_id) + .first() + ) self._text = pkgbase.FlaggerComment super().__init__() @@ -327,43 +355,53 @@ class FlagNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Out-of-date Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Out-of-date Notification for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_body(self, lang): body = aurweb.l10n.translator.translate( - 'Your package {pkgbase} [1] has been flagged out-of-date by ' - '{user} [2]:', lang).format(pkgbase=self._pkgbase, - user=self._user) - body += '\n\n' + self._text + "Your package {pkgbase} [1] has been flagged out-of-date by " "{user} [2]:", + lang, + ).format(pkgbase=self._pkgbase, user=self._user) + body += "\n\n" + self._text return body def get_refs(self): - return (aur_location + '/pkgbase/' + self._pkgbase + '/', - aur_location + '/account/' + self._user + '/') + return ( + aur_location + "/pkgbase/" + self._pkgbase + "/", + aur_location + "/account/" + self._user + "/", + ) class OwnershipEventNotification(Notification): def __init__(self, uid, pkgbase_id): - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - query = db.query(User).join(PackageNotification).filter( - and_(User.OwnershipNotify == 1, - PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + User.OwnershipNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() + ) self._recipients = [(u.Email, u.LangPreference) for u in query] - pkgbase = db.query(PackageBase.FlaggerComment).filter( - PackageBase.ID == pkgbase_id).first() + pkgbase = ( + db.query(PackageBase.FlaggerComment) + .filter(PackageBase.ID == pkgbase_id) + .first() + ) self._text = pkgbase.FlaggerComment super().__init__() @@ -373,39 +411,43 @@ class OwnershipEventNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Ownership Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Ownership Notification for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_refs(self): - return (aur_location + '/pkgbase/' + self._pkgbase + '/', - aur_location + '/account/' + self._user + '/') + return ( + aur_location + "/pkgbase/" + self._pkgbase + "/", + aur_location + "/account/" + self._user + "/", + ) class AdoptNotification(OwnershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'The package {pkgbase} [1] was adopted by {user} [2].', - lang).format(pkgbase=self._pkgbase, user=self._user) + "The package {pkgbase} [1] was adopted by {user} [2].", lang + ).format(pkgbase=self._pkgbase, user=self._user) class DisownNotification(OwnershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'The package {pkgbase} [1] was disowned by {user} ' - '[2].', lang).format(pkgbase=self._pkgbase, - user=self._user) + "The package {pkgbase} [1] was disowned by {user} " "[2].", lang + ).format(pkgbase=self._pkgbase, user=self._user) class ComaintainershipEventNotification(Notification): def __init__(self, uid, pkgbase_id): - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - user = db.query(User).filter(User.ID == uid).with_entities( - User.Email, - User.LangPreference - ).first() + user = ( + db.query(User) + .filter(User.ID == uid) + .with_entities(User.Email, User.LangPreference) + .first() + ) self._to = user.Email self._lang = user.LangPreference @@ -417,247 +459,59 @@ class ComaintainershipEventNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Co-Maintainer Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Co-Maintainer Notification for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_refs(self): - return (aur_location + '/pkgbase/' + self._pkgbase + '/',) + return (aur_location + "/pkgbase/" + self._pkgbase + "/",) class ComaintainerAddNotification(ComaintainershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'You were added to the co-maintainer list of {pkgbase} [1].', - lang).format(pkgbase=self._pkgbase) + "You were added to the co-maintainer list of {pkgbase} [1].", lang + ).format(pkgbase=self._pkgbase) class ComaintainerRemoveNotification(ComaintainershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'You were removed from the co-maintainer list of {pkgbase} ' - '[1].', lang).format(pkgbase=self._pkgbase) + "You were removed from the co-maintainer list of {pkgbase} " "[1].", lang + ).format(pkgbase=self._pkgbase) class DeleteNotification(Notification): def __init__(self, uid, old_pkgbase_id, new_pkgbase_id=None): - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._old_pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == old_pkgbase_id).first().Name + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._old_pkgbase = ( + db.query(PackageBase.Name) + .filter(PackageBase.ID == old_pkgbase_id) + .first() + .Name + ) self._new_pkgbase = None if new_pkgbase_id: - self._new_pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == new_pkgbase_id).first().Name + self._new_pkgbase = ( + db.query(PackageBase.Name) + .filter(PackageBase.ID == new_pkgbase_id) + .first() + .Name + ) - query = db.query(User).join(PackageNotification).filter( - and_(PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == old_pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() - self._recipients = [(u.Email, u.LangPreference) for u in query] - - super().__init__() - - def get_recipients(self): - return self._recipients - - def get_subject(self, lang): - return aurweb.l10n.translator.translate( - 'AUR Package deleted: {pkgbase}', - lang).format(pkgbase=self._old_pkgbase) - - def get_body(self, lang): - if self._new_pkgbase: - dnlabel = aurweb.l10n.translator.translate( - 'Disable notifications', lang) - return aurweb.l10n.translator.translate( - '{user} [1] merged {old} [2] into {new} [3].\n\n' - '--\n' - 'If you no longer wish receive notifications about the ' - 'new package, please go to [3] and click "{label}".', - lang).format(user=self._user, old=self._old_pkgbase, - new=self._new_pkgbase, label=dnlabel) - else: - return aurweb.l10n.translator.translate( - '{user} [1] deleted {pkgbase} [2].\n\n' - 'You will no longer receive notifications about this ' - 'package.', lang).format(user=self._user, - pkgbase=self._old_pkgbase) - - def get_refs(self): - refs = (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._old_pkgbase + '/') - if self._new_pkgbase: - refs += (aur_location + '/pkgbase/' + self._new_pkgbase + '/',) - return refs - - -class RequestOpenNotification(Notification): - def __init__(self, uid, reqid, reqtype, pkgbase_id, merge_into=None): - - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name - - self._to = aurweb.config.get('options', 'aur_request_ml') - - query = db.query(PackageRequest).join(PackageBase).join( - PackageComaintainer, - PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, - isouter=True - ).join( - User, - or_(User.ID == PackageRequest.UsersID, - User.ID == PackageBase.MaintainerUID, - User.ID == PackageComaintainer.UsersID) - ).filter( - and_(PackageRequest.ID == reqid, - User.Suspended == 0) - ).with_entities( - User.Email - ).distinct() - self._cc = [u.Email for u in query] - - pkgreq = db.query(PackageRequest.Comments).filter( - PackageRequest.ID == reqid).first() - - self._text = pkgreq.Comments - self._reqid = int(reqid) - self._reqtype = reqtype - self._merge_into = merge_into - - def get_recipients(self): - return [(self._to, 'en')] - - def get_cc(self): - return self._cc - - def get_subject(self, lang): - return '[PRQ#%d] %s Request for %s' % \ - (self._reqid, self._reqtype.title(), self._pkgbase) - - def get_body(self, lang): - if self._merge_into: - body = '%s [1] filed a request to merge %s [2] into %s [3]:' % \ - (self._user, self._pkgbase, self._merge_into) - body += '\n\n' + self._text - else: - an = 'an' if self._reqtype[0] in 'aeiou' else 'a' - body = '%s [1] filed %s %s request for %s [2]:' % \ - (self._user, an, self._reqtype, self._pkgbase) - body += '\n\n' + self._text - return body - - def get_refs(self): - refs = (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._pkgbase + '/') - if self._merge_into: - refs += (aur_location + '/pkgbase/' + self._merge_into + '/',) - return refs - - def get_headers(self): - thread_id = '' - # Use a deterministic Message-ID for the first email referencing a - # request. - headers = headers_msgid(thread_id) - return headers - - -class RequestCloseNotification(Notification): - - def __init__(self, uid, reqid, reason): - user = db.query(User.Username).filter(User.ID == uid).first() - self._user = user.Username if user else None - - self._to = aurweb.config.get('options', 'aur_request_ml') - - query = db.query(PackageRequest).join(PackageBase).join( - PackageComaintainer, - PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, - isouter=True - ).join( - User, - or_(User.ID == PackageRequest.UsersID, - User.ID == PackageBase.MaintainerUID, - User.ID == PackageComaintainer.UsersID) - ).filter( - and_(PackageRequest.ID == reqid, - User.Suspended == 0) - ).with_entities( - User.Email - ).distinct() - self._cc = [u.Email for u in query] - - pkgreq = db.query(PackageRequest).join(RequestType).filter( - PackageRequest.ID == reqid - ).with_entities( - PackageRequest.ClosureComment, - RequestType.Name, - PackageRequest.PackageBaseName - ).first() - - self._text = pkgreq.ClosureComment - self._reqtype = pkgreq.Name - self._pkgbase = pkgreq.PackageBaseName - - self._reqid = int(reqid) - self._reason = reason - - def get_recipients(self): - return [(self._to, 'en')] - - def get_cc(self): - return self._cc - - def get_subject(self, lang): - return '[PRQ#%d] %s Request for %s %s' % (self._reqid, - self._reqtype.title(), - self._pkgbase, - self._reason.title()) - - def get_body(self, lang): - if self._user: - body = 'Request #%d has been %s by %s [1]' % \ - (self._reqid, self._reason, self._user) - else: - body = 'Request #%d has been %s automatically by the Arch User ' \ - 'Repository package request system' % \ - (self._reqid, self._reason) - if self._text.strip() == '': - body += '.' - else: - body += ':\n\n' + self._text - return body - - def get_refs(self): - if self._user: - return (aur_location + '/account/' + self._user + '/',) - else: - return () - - def get_headers(self): - thread_id = '' - headers = headers_reply(thread_id) - return headers - - -class TUVoteReminderNotification(Notification): - def __init__(self, vote_id): - self._vote_id = int(vote_id) - - subquery = db.query(TUVote.UserID).filter(TUVote.VoteID == vote_id) - query = db.query(User).filter( - and_(User.AccountTypeID.in_((2, 4)), - ~User.ID.in_(subquery), - User.Suspended == 0) - ).with_entities( - User.Email, User.LangPreference + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == old_pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() ) self._recipients = [(u.Email, u.LangPreference) for u in query] @@ -668,36 +522,280 @@ class TUVoteReminderNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'TU Vote Reminder: Proposal {id}', - lang).format(id=self._vote_id) + "AUR Package deleted: {pkgbase}", lang + ).format(pkgbase=self._old_pkgbase) + + def get_body(self, lang): + if self._new_pkgbase: + dnlabel = aurweb.l10n.translator.translate("Disable notifications", lang) + return aurweb.l10n.translator.translate( + "{user} [1] merged {old} [2] into {new} [3].\n\n" + "--\n" + "If you no longer wish receive notifications about the " + 'new package, please go to [3] and click "{label}".', + lang, + ).format( + user=self._user, + old=self._old_pkgbase, + new=self._new_pkgbase, + label=dnlabel, + ) + else: + return aurweb.l10n.translator.translate( + "{user} [1] deleted {pkgbase} [2].\n\n" + "You will no longer receive notifications about this " + "package.", + lang, + ).format(user=self._user, pkgbase=self._old_pkgbase) + + def get_refs(self): + refs = ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._old_pkgbase + "/", + ) + if self._new_pkgbase: + refs += (aur_location + "/pkgbase/" + self._new_pkgbase + "/",) + return refs + + +class RequestOpenNotification(Notification): + def __init__(self, uid, reqid, reqtype, pkgbase_id, merge_into=None): + + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) + + self._to = aurweb.config.get("options", "aur_request_ml") + + query = ( + db.query(PackageRequest) + .join(PackageBase) + .join( + PackageComaintainer, + PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, + isouter=True, + ) + .join( + User, + or_( + User.ID == PackageRequest.UsersID, + User.ID == PackageBase.MaintainerUID, + User.ID == PackageComaintainer.UsersID, + ), + ) + .filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) + .with_entities(User.Email) + .distinct() + ) + self._cc = [u.Email for u in query] + + pkgreq = ( + db.query(PackageRequest.Comments).filter(PackageRequest.ID == reqid).first() + ) + + self._text = pkgreq.Comments + self._reqid = int(reqid) + self._reqtype = reqtype + self._merge_into = merge_into + + def get_recipients(self): + return [(self._to, "en")] + + def get_cc(self): + return self._cc + + def get_subject(self, lang): + return "[PRQ#%d] %s Request for %s" % ( + self._reqid, + self._reqtype.title(), + self._pkgbase, + ) + + def get_body(self, lang): + if self._merge_into: + body = "%s [1] filed a request to merge %s [2] into %s [3]:" % ( + self._user, + self._pkgbase, + self._merge_into, + ) + body += "\n\n" + self._text + else: + an = "an" if self._reqtype[0] in "aeiou" else "a" + body = "%s [1] filed %s %s request for %s [2]:" % ( + self._user, + an, + self._reqtype, + self._pkgbase, + ) + body += "\n\n" + self._text + return body + + def get_refs(self): + refs = ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._pkgbase + "/", + ) + if self._merge_into: + refs += (aur_location + "/pkgbase/" + self._merge_into + "/",) + return refs + + def get_headers(self): + thread_id = "" + # Use a deterministic Message-ID for the first email referencing a + # request. + headers = headers_msgid(thread_id) + return headers + + +class RequestCloseNotification(Notification): + def __init__(self, uid, reqid, reason): + user = db.query(User.Username).filter(User.ID == uid).first() + self._user = user.Username if user else None + + self._to = aurweb.config.get("options", "aur_request_ml") + + query = ( + db.query(PackageRequest) + .join(PackageBase) + .join( + PackageComaintainer, + PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, + isouter=True, + ) + .join( + User, + or_( + User.ID == PackageRequest.UsersID, + User.ID == PackageBase.MaintainerUID, + User.ID == PackageComaintainer.UsersID, + ), + ) + .filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) + .with_entities(User.Email) + .distinct() + ) + self._cc = [u.Email for u in query] + + pkgreq = ( + db.query(PackageRequest) + .join(RequestType) + .filter(PackageRequest.ID == reqid) + .with_entities( + PackageRequest.ClosureComment, + RequestType.Name, + PackageRequest.PackageBaseName, + ) + .first() + ) + + self._text = pkgreq.ClosureComment + self._reqtype = pkgreq.Name + self._pkgbase = pkgreq.PackageBaseName + + self._reqid = int(reqid) + self._reason = reason + + def get_recipients(self): + return [(self._to, "en")] + + def get_cc(self): + return self._cc + + def get_subject(self, lang): + return "[PRQ#%d] %s Request for %s %s" % ( + self._reqid, + self._reqtype.title(), + self._pkgbase, + self._reason.title(), + ) + + def get_body(self, lang): + if self._user: + body = "Request #%d has been %s by %s [1]" % ( + self._reqid, + self._reason, + self._user, + ) + else: + body = ( + "Request #%d has been %s automatically by the Arch User " + "Repository package request system" % (self._reqid, self._reason) + ) + if self._text.strip() == "": + body += "." + else: + body += ":\n\n" + self._text + return body + + def get_refs(self): + if self._user: + return (aur_location + "/account/" + self._user + "/",) + else: + return () + + def get_headers(self): + thread_id = "" + headers = headers_reply(thread_id) + return headers + + +class TUVoteReminderNotification(Notification): + def __init__(self, vote_id): + self._vote_id = int(vote_id) + + subquery = db.query(TUVote.UserID).filter(TUVote.VoteID == vote_id) + query = ( + db.query(User) + .filter( + and_( + User.AccountTypeID.in_((2, 4)), + ~User.ID.in_(subquery), + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + ) + self._recipients = [(u.Email, u.LangPreference) for u in query] + + super().__init__() + + def get_recipients(self): + return self._recipients + + def get_subject(self, lang): + return aurweb.l10n.translator.translate( + "TU Vote Reminder: Proposal {id}", lang + ).format(id=self._vote_id) def get_body(self, lang): return aurweb.l10n.translator.translate( - 'Please remember to cast your vote on proposal {id} [1]. ' - 'The voting period ends in less than 48 hours.', - lang).format(id=self._vote_id) + "Please remember to cast your vote on proposal {id} [1]. " + "The voting period ends in less than 48 hours.", + lang, + ).format(id=self._vote_id) def get_refs(self): - return (aur_location + '/tu/?id=' + str(self._vote_id),) + return (aur_location + "/tu/?id=" + str(self._vote_id),) def main(): db.get_engine() action = sys.argv[1] action_map = { - 'send-resetkey': ResetKeyNotification, - 'welcome': WelcomeNotification, - 'comment': CommentNotification, - 'update': UpdateNotification, - 'flag': FlagNotification, - 'adopt': AdoptNotification, - 'disown': DisownNotification, - 'comaintainer-add': ComaintainerAddNotification, - 'comaintainer-remove': ComaintainerRemoveNotification, - 'delete': DeleteNotification, - 'request-open': RequestOpenNotification, - 'request-close': RequestCloseNotification, - 'tu-vote-reminder': TUVoteReminderNotification, + "send-resetkey": ResetKeyNotification, + "welcome": WelcomeNotification, + "comment": CommentNotification, + "update": UpdateNotification, + "flag": FlagNotification, + "adopt": AdoptNotification, + "disown": DisownNotification, + "comaintainer-add": ComaintainerAddNotification, + "comaintainer-remove": ComaintainerRemoveNotification, + "delete": DeleteNotification, + "request-open": RequestOpenNotification, + "request-close": RequestCloseNotification, + "tu-vote-reminder": TUVoteReminderNotification, } with db.begin(): @@ -705,5 +803,5 @@ def main(): notification.send() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/pkgmaint.py b/aurweb/scripts/pkgmaint.py index 2a2c638a..9d7cf53b 100755 --- a/aurweb/scripts/pkgmaint.py +++ b/aurweb/scripts/pkgmaint.py @@ -11,8 +11,8 @@ def _main(): limit_to = time.utcnow() - 86400 query = db.query(PackageBase).filter( - and_(PackageBase.SubmittedTS < limit_to, - PackageBase.PackagerUID.is_(None))) + and_(PackageBase.SubmittedTS < limit_to, PackageBase.PackagerUID.is_(None)) + ) db.delete_all(query) @@ -22,5 +22,5 @@ def main(): _main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index 637173eb..aa163be1 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 from sqlalchemy import and_, func -from sqlalchemy.sql.functions import coalesce -from sqlalchemy.sql.functions import sum as _sum +from sqlalchemy.sql.functions import coalesce, sum as _sum from aurweb import db, time from aurweb.models import PackageBase, PackageVote @@ -20,18 +19,26 @@ def run_variable(pkgbases: list[PackageBase] = []) -> None: now = time.utcnow() # NumVotes subquery. - votes_subq = db.get_session().query( - func.count("*") - ).select_from(PackageVote).filter( - PackageVote.PackageBaseID == PackageBase.ID + votes_subq = ( + db.get_session() + .query(func.count("*")) + .select_from(PackageVote) + .filter(PackageVote.PackageBaseID == PackageBase.ID) ) # Popularity subquery. - pop_subq = db.get_session().query( - coalesce(_sum(func.pow(0.98, (now - PackageVote.VoteTS) / 86400)), 0.0), - ).select_from(PackageVote).filter( - and_(PackageVote.PackageBaseID == PackageBase.ID, - PackageVote.VoteTS.isnot(None)) + pop_subq = ( + db.get_session() + .query( + coalesce(_sum(func.pow(0.98, (now - PackageVote.VoteTS) / 86400)), 0.0), + ) + .select_from(PackageVote) + .filter( + and_( + PackageVote.PackageBaseID == PackageBase.ID, + PackageVote.VoteTS.isnot(None), + ) + ) ) with db.begin(): @@ -42,14 +49,16 @@ def run_variable(pkgbases: list[PackageBase] = []) -> None: ids = {pkgbase.ID for pkgbase in pkgbases} query = query.filter(PackageBase.ID.in_(ids)) - query.update({ - "NumVotes": votes_subq.scalar_subquery(), - "Popularity": pop_subq.scalar_subquery() - }) + query.update( + { + "NumVotes": votes_subq.scalar_subquery(), + "Popularity": pop_subq.scalar_subquery(), + } + ) def run_single(pkgbase: PackageBase) -> None: - """ A single popupdate. The given pkgbase instance will be + """A single popupdate. The given pkgbase instance will be refreshed after the database update is done. NOTE: This function is compatible only with aurweb FastAPI. @@ -65,5 +74,5 @@ def main(): run_variable() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 87f8b89f..ff6fe09c 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import sys - from urllib.parse import quote_plus from xml.etree.ElementTree import Element @@ -10,7 +9,6 @@ import markdown import pygit2 import aurweb.config - from aurweb import db, logging, util from aurweb.models import PackageComment @@ -25,13 +23,15 @@ class LinkifyExtension(markdown.extensions.Extension): # Captures http(s) and ftp URLs until the first non URL-ish character. # Excludes trailing punctuation. - _urlre = (r'(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' - r'(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))') + _urlre = ( + r"(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?" + r"(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))" + ) def extendMarkdown(self, md): processor = markdown.inlinepatterns.AutolinkInlineProcessor(self._urlre, md) # Register it right after the default <>-link processor (priority 120). - md.inlinePatterns.register(processor, 'linkify', 119) + md.inlinePatterns.register(processor, "linkify", 119) class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): @@ -43,16 +43,16 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): """ def handleMatch(self, m, data): - el = Element('a') - el.set('href', f'https://bugs.archlinux.org/task/{m.group(1)}') + el = Element("a") + el.set("href", f"https://bugs.archlinux.org/task/{m.group(1)}") el.text = markdown.util.AtomicString(m.group(0)) return (el, m.start(0), m.end(0)) class FlysprayLinksExtension(markdown.extensions.Extension): def extendMarkdown(self, md): - processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b', md) - md.inlinePatterns.register(processor, 'flyspray-links', 118) + processor = FlysprayLinksInlineProcessor(r"\bFS#(\d+)\b", md) + md.inlinePatterns.register(processor, "flyspray-links", 118) class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): @@ -65,10 +65,10 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): """ def __init__(self, md, head): - repo_path = aurweb.config.get('serve', 'repo-path') + repo_path = aurweb.config.get("serve", "repo-path") self._repo = pygit2.Repository(repo_path) self._head = head - super().__init__(r'\b([0-9a-f]{7,40})\b', md) + super().__init__(r"\b([0-9a-f]{7,40})\b", md) def handleMatch(self, m, data): oid = m.group(1) @@ -76,13 +76,12 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): # Unknown OID; preserve the orginal text. return (None, None, None) - el = Element('a') + el = Element("a") commit_uri = aurweb.config.get("options", "commit_uri") prefixlen = util.git_search(self._repo, oid) - el.set('href', commit_uri % ( - quote_plus(self._head), - quote_plus(oid[:prefixlen]) - )) + el.set( + "href", commit_uri % (quote_plus(self._head), quote_plus(oid[:prefixlen])) + ) el.text = markdown.util.AtomicString(oid[:prefixlen]) return (el, m.start(0), m.end(0)) @@ -97,7 +96,7 @@ class GitCommitsExtension(markdown.extensions.Extension): def extendMarkdown(self, md): try: processor = GitCommitsInlineProcessor(md, self._head) - md.inlinePatterns.register(processor, 'git-commits', 117) + md.inlinePatterns.register(processor, "git-commits", 117) except pygit2.GitError: logger.error(f"No git repository found for '{self._head}'.") @@ -105,16 +104,16 @@ class GitCommitsExtension(markdown.extensions.Extension): class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): def run(self, doc): for elem in doc: - if elem.tag == 'h1': - elem.tag = 'h5' - elif elem.tag in ['h2', 'h3', 'h4', 'h5']: - elem.tag = 'h6' + if elem.tag == "h1": + elem.tag = "h5" + elif elem.tag in ["h2", "h3", "h4", "h5"]: + elem.tag = "h6" class HeadingExtension(markdown.extensions.Extension): def extendMarkdown(self, md): # Priority doesn't matter since we don't conflict with other processors. - md.treeprocessors.register(HeadingTreeprocessor(md), 'heading', 30) + md.treeprocessors.register(HeadingTreeprocessor(md), "heading", 30) def save_rendered_comment(comment: PackageComment, html: str): @@ -130,16 +129,26 @@ def update_comment_render(comment: PackageComment) -> None: text = comment.Comments pkgbasename = comment.PackageBase.Name - html = markdown.markdown(text, extensions=[ - 'fenced_code', - LinkifyExtension(), - FlysprayLinksExtension(), - GitCommitsExtension(pkgbasename), - HeadingExtension() - ]) + html = markdown.markdown( + text, + extensions=[ + "fenced_code", + LinkifyExtension(), + FlysprayLinksExtension(), + GitCommitsExtension(pkgbasename), + HeadingExtension(), + ], + ) - allowed_tags = (bleach.sanitizer.ALLOWED_TAGS - + ['p', 'pre', 'h4', 'h5', 'h6', 'br', 'hr']) + allowed_tags = bleach.sanitizer.ALLOWED_TAGS + [ + "p", + "pre", + "h4", + "h5", + "h6", + "br", + "hr", + ] html = bleach.clean(html, tags=allowed_tags) save_rendered_comment(comment, html) db.refresh(comment) @@ -148,11 +157,9 @@ def update_comment_render(comment: PackageComment) -> None: def main(): db.get_engine() comment_id = int(sys.argv[1]) - comment = db.query(PackageComment).filter( - PackageComment.ID == comment_id - ).first() + comment = db.query(PackageComment).filter(PackageComment.ID == comment_id).first() update_comment_render(comment) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/tuvotereminder.py b/aurweb/scripts/tuvotereminder.py index 742fa6d4..aa59d911 100755 --- a/aurweb/scripts/tuvotereminder.py +++ b/aurweb/scripts/tuvotereminder.py @@ -3,12 +3,11 @@ from sqlalchemy import and_ import aurweb.config - from aurweb import db, time from aurweb.models import TUVoteInfo from aurweb.scripts import notify -notify_cmd = aurweb.config.get('notifications', 'notify-cmd') +notify_cmd = aurweb.config.get("notifications", "notify-cmd") def main(): @@ -23,13 +22,12 @@ def main(): filter_to = now + end query = db.query(TUVoteInfo.ID).filter( - and_(TUVoteInfo.End >= filter_from, - TUVoteInfo.End <= filter_to) + and_(TUVoteInfo.End >= filter_from, TUVoteInfo.End <= filter_to) ) for voteinfo in query: notif = notify.TUVoteReminderNotification(voteinfo.ID) notif.send() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/usermaint.py b/aurweb/scripts/usermaint.py index 69f9db04..fb79aeaf 100755 --- a/aurweb/scripts/usermaint.py +++ b/aurweb/scripts/usermaint.py @@ -9,14 +9,16 @@ from aurweb.models import User def _main(): limit_to = time.utcnow() - 86400 * 7 - update_ = update(User).where( - User.LastLogin < limit_to - ).values(LastLoginIPAddress=None) + update_ = ( + update(User).where(User.LastLogin < limit_to).values(LastLoginIPAddress=None) + ) db.get_session().execute(update_) - update_ = update(User).where( - User.LastSSHLogin < limit_to - ).values(LastSSHLoginIPAddress=None) + update_ = ( + update(User) + .where(User.LastSSHLogin < limit_to) + .values(LastSSHLoginIPAddress=None) + ) db.get_session().execute(update_) @@ -26,5 +28,5 @@ def main(): _main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/spawn.py b/aurweb/spawn.py index c7d54c4e..29162f33 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -16,18 +16,16 @@ import subprocess import sys import tempfile import time - from typing import Iterable import aurweb.config import aurweb.schema - from aurweb.exceptions import AurwebException children = [] temporary_dir = None verbosity = 0 -asgi_backend = '' +asgi_backend = "" workers = 1 PHP_BINARY = os.environ.get("PHP_BINARY", "php") @@ -60,22 +58,21 @@ def validate_php_config() -> None: :return: None """ try: - proc = subprocess.Popen([PHP_BINARY, "-m"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + proc = subprocess.Popen( + [PHP_BINARY, "-m"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) out, _ = proc.communicate() except FileNotFoundError: - raise AurwebException(f"Unable to locate the '{PHP_BINARY}' " - "executable.") + raise AurwebException(f"Unable to locate the '{PHP_BINARY}' " "executable.") - assert proc.returncode == 0, ("Received non-zero error code " - f"{proc.returncode} from '{PHP_BINARY}'.") + assert proc.returncode == 0, ( + "Received non-zero error code " f"{proc.returncode} from '{PHP_BINARY}'." + ) modules = out.decode().splitlines() for module in PHP_MODULES: if module not in modules: - raise AurwebException( - f"PHP does not have the '{module}' module enabled.") + raise AurwebException(f"PHP does not have the '{module}' module enabled.") def generate_nginx_config(): @@ -91,7 +88,8 @@ def generate_nginx_config(): config_path = os.path.join(temporary_dir, "nginx.conf") config = open(config_path, "w") # We double nginx's braces because they conflict with Python's f-strings. - config.write(f""" + config.write( + f""" events {{}} daemon off; error_log /dev/stderr info; @@ -124,7 +122,8 @@ def generate_nginx_config(): }} }} }} - """) + """ + ) return config_path @@ -146,20 +145,23 @@ def start(): return atexit.register(stop) - if 'AUR_CONFIG' in os.environ: - os.environ['AUR_CONFIG'] = os.path.realpath(os.environ['AUR_CONFIG']) + if "AUR_CONFIG" in os.environ: + os.environ["AUR_CONFIG"] = os.path.realpath(os.environ["AUR_CONFIG"]) try: terminal_width = os.get_terminal_size().columns except OSError: terminal_width = 80 - print("{ruler}\n" - "Spawing PHP and FastAPI, then nginx as a reverse proxy.\n" - "Check out {aur_location}\n" - "Hit ^C to terminate everything.\n" - "{ruler}" - .format(ruler=("-" * terminal_width), - aur_location=aurweb.config.get('options', 'aur_location'))) + print( + "{ruler}\n" + "Spawing PHP and FastAPI, then nginx as a reverse proxy.\n" + "Check out {aur_location}\n" + "Hit ^C to terminate everything.\n" + "{ruler}".format( + ruler=("-" * terminal_width), + aur_location=aurweb.config.get("options", "aur_location"), + ) + ) # PHP php_address = aurweb.config.get("php", "bind_address") @@ -168,8 +170,9 @@ def start(): spawn_child(["php", "-S", php_address, "-t", htmldir]) # FastAPI - fastapi_host, fastapi_port = aurweb.config.get( - "fastapi", "bind_address").rsplit(":", 1) + fastapi_host, fastapi_port = aurweb.config.get("fastapi", "bind_address").rsplit( + ":", 1 + ) # Logging config. aurwebdir = aurweb.config.get("options", "aurwebdir") @@ -178,20 +181,33 @@ def start(): backend_args = { "hypercorn": ["-b", f"{fastapi_host}:{fastapi_port}"], "uvicorn": ["--host", fastapi_host, "--port", fastapi_port], - "gunicorn": ["--bind", f"{fastapi_host}:{fastapi_port}", - "-k", "uvicorn.workers.UvicornWorker", - "-w", str(workers)] + "gunicorn": [ + "--bind", + f"{fastapi_host}:{fastapi_port}", + "-k", + "uvicorn.workers.UvicornWorker", + "-w", + str(workers), + ], } backend_args = backend_args.get(asgi_backend) - spawn_child([ - "python", "-m", asgi_backend, - "--log-config", fastapi_log_config, - ] + backend_args + ["aurweb.asgi:app"]) + spawn_child( + [ + "python", + "-m", + asgi_backend, + "--log-config", + fastapi_log_config, + ] + + backend_args + + ["aurweb.asgi:app"] + ) # nginx spawn_child(["nginx", "-p", temporary_dir, "-c", generate_nginx_config()]) - print(f""" + print( + f""" > Started nginx. > > PHP backend: http://{php_address} @@ -201,11 +217,13 @@ def start(): > FastAPI frontend: http://{fastapi_host}:{FASTAPI_NGINX_PORT} > > Frontends are hosted via nginx and should be preferred. -""") +""" + ) -def _kill_children(children: Iterable, exceptions: list[Exception] = []) \ - -> list[Exception]: +def _kill_children( + children: Iterable, exceptions: list[Exception] = [] +) -> list[Exception]: """ Kill each process found in `children`. @@ -223,8 +241,9 @@ def _kill_children(children: Iterable, exceptions: list[Exception] = []) \ return exceptions -def _wait_for_children(children: Iterable, exceptions: list[Exception] = []) \ - -> list[Exception]: +def _wait_for_children( + children: Iterable, exceptions: list[Exception] = [] +) -> list[Exception]: """ Wait for each process to end found in `children`. @@ -261,21 +280,31 @@ def stop() -> None: exceptions = _wait_for_children(children, exceptions) children = [] if exceptions: - raise ProcessExceptions("Errors terminating the child processes:", - exceptions) + raise ProcessExceptions("Errors terminating the child processes:", exceptions) -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser( - prog='python -m aurweb.spawn', - description='Start aurweb\'s test server.') - parser.add_argument('-v', '--verbose', action='count', default=0, - help='increase verbosity') - choices = ['hypercorn', 'gunicorn', 'uvicorn'] - parser.add_argument('-b', '--backend', choices=choices, default='uvicorn', - help='asgi backend used to launch the python server') - parser.add_argument("-w", "--workers", default=1, type=int, - help="number of workers to use in gunicorn") + prog="python -m aurweb.spawn", description="Start aurweb's test server." + ) + parser.add_argument( + "-v", "--verbose", action="count", default=0, help="increase verbosity" + ) + choices = ["hypercorn", "gunicorn", "uvicorn"] + parser.add_argument( + "-b", + "--backend", + choices=choices, + default="uvicorn", + help="asgi backend used to launch the python server", + ) + parser.add_argument( + "-w", + "--workers", + default=1, + type=int, + help="number of workers to use in gunicorn", + ) args = parser.parse_args() try: diff --git a/aurweb/templates.py b/aurweb/templates.py index 6520bedf..781826ea 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -1,28 +1,27 @@ import copy import functools import os - from http import HTTPStatus from typing import Callable import jinja2 - from fastapi import Request from fastapi.responses import HTMLResponse import aurweb.config - from aurweb import cookies, l10n, time # Prepare jinja2 objects. -_loader = jinja2.FileSystemLoader(os.path.join( - aurweb.config.get("options", "aurwebdir"), "templates")) -_env = jinja2.Environment(loader=_loader, autoescape=True, - extensions=["jinja2.ext.i18n"]) +_loader = jinja2.FileSystemLoader( + os.path.join(aurweb.config.get("options", "aurwebdir"), "templates") +) +_env = jinja2.Environment( + loader=_loader, autoescape=True, extensions=["jinja2.ext.i18n"] +) def register_filter(name: str) -> Callable: - """ A decorator that can be used to register a filter. + """A decorator that can be used to register a filter. Example @register_filter("some_filter") @@ -35,31 +34,36 @@ def register_filter(name: str) -> Callable: :param name: Filter name :return: Callable used for filter """ + def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + _env.filters[name] = wrapper return wrapper + return decorator def register_function(name: str) -> Callable: - """ A decorator that can be used to register a function. - """ + """A decorator that can be used to register a function.""" + def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + if name in _env.globals: raise KeyError(f"Jinja already has a function named '{name}'") _env.globals[name] = wrapper return wrapper + return decorator def make_context(request: Request, title: str, next: str = None): - """ Create a context for a jinja2 TemplateResponse. """ + """Create a context for a jinja2 TemplateResponse.""" import aurweb.auth.creds commit_url = aurweb.config.get_with_fallback("devel", "commit_url", None) @@ -85,17 +89,19 @@ def make_context(request: Request, title: str, next: str = None): "config": aurweb.config, "creds": aurweb.auth.creds, "next": next if next else request.url.path, - "version": os.environ.get("COMMIT_HASH", aurweb.config.AURWEB_VERSION) + "version": os.environ.get("COMMIT_HASH", aurweb.config.AURWEB_VERSION), } async def make_variable_context(request: Request, title: str, next: str = None): - """ Make a context with variables provided by the user - (query params via GET or form data via POST). """ + """Make a context with variables provided by the user + (query params via GET or form data via POST).""" context = make_context(request, title, next) - to_copy = dict(request.query_params) \ - if request.method.lower() == "get" \ + to_copy = ( + dict(request.query_params) + if request.method.lower() == "get" else dict(await request.form()) + ) for k, v in to_copy.items(): context[k] = v @@ -111,7 +117,7 @@ def base_template(path: str): def render_raw_template(request: Request, path: str, context: dict): - """ Render a Jinja2 multi-lingual template with some context. """ + """Render a Jinja2 multi-lingual template with some context.""" # Create a deep copy of our jinja2 _environment. The _environment in # total by itself is 48 bytes large (according to sys.getsizeof). # This is done so we can install gettext translations on the template @@ -126,11 +132,10 @@ def render_raw_template(request: Request, path: str, context: dict): return template.render(context) -def render_template(request: Request, - path: str, - context: dict, - status_code: HTTPStatus = HTTPStatus.OK): - """ Render a template as an HTMLResponse. """ +def render_template( + request: Request, path: str, context: dict, status_code: HTTPStatus = HTTPStatus.OK +): + """Render a template as an HTMLResponse.""" rendered = render_raw_template(request, path, context) response = HTMLResponse(rendered, status_code=int(status_code)) diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 8261051d..4451eb3a 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -1,10 +1,9 @@ import aurweb.db - from aurweb import models def setup_test_db(*args): - """ This function is to be used to setup a test database before + """This function is to be used to setup a test database before using it. It takes a variable number of table strings, and for each table in that set of table strings, it deletes all records. diff --git a/aurweb/testing/alpm.py b/aurweb/testing/alpm.py index ce30d042..ddafb710 100644 --- a/aurweb/testing/alpm.py +++ b/aurweb/testing/alpm.py @@ -17,6 +17,7 @@ class AlpmDatabase: This class can be used to add or remove packages from a test repository. """ + repo = "test" def __init__(self, database_root: str): @@ -35,13 +36,14 @@ class AlpmDatabase: os.makedirs(pkgdir) return pkgdir - def add(self, pkgname: str, pkgver: str, arch: str, - provides: list[str] = []) -> None: + def add( + self, pkgname: str, pkgver: str, arch: str, provides: list[str] = [] + ) -> None: context = { "pkgname": pkgname, "pkgver": pkgver, "arch": arch, - "provides": provides + "provides": provides, } template = base_template("testing/alpm_package.j2") pkgdir = self._get_pkgdir(pkgname, pkgver, self.repo) @@ -76,8 +78,9 @@ class AlpmDatabase: self.clean() cmdline = ["bash", "-c", "bsdtar -czvf ../test.db *"] proc = subprocess.run(cmdline, cwd=self.repopath) - assert proc.returncode == 0, \ - f"Bad return code while creating alpm database: {proc.returncode}" + assert ( + proc.returncode == 0 + ), f"Bad return code while creating alpm database: {proc.returncode}" # Print out the md5 hash value of the new test.db. test_db = os.path.join(self.remote, "test.db") diff --git a/aurweb/testing/email.py b/aurweb/testing/email.py index b3e3990b..057ff792 100644 --- a/aurweb/testing/email.py +++ b/aurweb/testing/email.py @@ -5,7 +5,6 @@ import email import os import re import sys - from typing import TextIO @@ -28,6 +27,7 @@ class Email: print(email.headers) """ + TEST_DIR = "test-emails" def __init__(self, serial: int = 1, autoparse: bool = True): @@ -61,7 +61,7 @@ class Email: value = os.environ.get("PYTEST_CURRENT_TEST", "email").split(" ")[0] if suite: value = value.split(":")[0] - return re.sub(r'(\/|\.|,|:)', "_", value) + return re.sub(r"(\/|\.|,|:)", "_", value) @staticmethod def count() -> int: @@ -159,6 +159,6 @@ class Email: lines += [ f"== Email #{i + 1} ==", email.glue(), - f"== End of Email #{i + 1}" + f"== End of Email #{i + 1}", ] print("\n".join(lines), file=file) diff --git a/aurweb/testing/filelock.py b/aurweb/testing/filelock.py index 3a18c153..33b42cb3 100644 --- a/aurweb/testing/filelock.py +++ b/aurweb/testing/filelock.py @@ -1,6 +1,5 @@ import hashlib import os - from typing import Callable from posix_ipc import O_CREAT, Semaphore diff --git a/aurweb/testing/git.py b/aurweb/testing/git.py index 019d870f..216515c8 100644 --- a/aurweb/testing/git.py +++ b/aurweb/testing/git.py @@ -1,6 +1,5 @@ import os import shlex - from subprocess import PIPE, Popen from typing import Tuple diff --git a/aurweb/testing/html.py b/aurweb/testing/html.py index 8c923438..16b7322b 100644 --- a/aurweb/testing/html.py +++ b/aurweb/testing/html.py @@ -6,7 +6,7 @@ parser = etree.HTMLParser() def parse_root(html: str) -> etree.Element: - """ Parse an lxml.etree.ElementTree root from html content. + """Parse an lxml.etree.ElementTree root from html content. :param html: HTML markup :return: etree.Element diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py index c97d1532..98312e9e 100644 --- a/aurweb/testing/requests.py +++ b/aurweb/testing/requests.py @@ -2,7 +2,8 @@ import aurweb.config class User: - """ A fake User model. """ + """A fake User model.""" + # Fake columns. LangPreference = aurweb.config.get("options", "default_lang") Timezone = aurweb.config.get("options", "default_timezone") @@ -15,7 +16,8 @@ class User: class Client: - """ A fake FastAPI Request.client object. """ + """A fake FastAPI Request.client object.""" + # A fake host. host = "127.0.0.1" @@ -25,16 +27,19 @@ class URL: class Request: - """ A fake Request object which mimics a FastAPI Request for tests. """ + """A fake Request object which mimics a FastAPI Request for tests.""" + client = Client() url = URL() - def __init__(self, - user: User = User(), - authenticated: bool = False, - method: str = "GET", - headers: dict[str, str] = dict(), - cookies: dict[str, str] = dict()) -> "Request": + def __init__( + self, + user: User = User(), + authenticated: bool = False, + method: str = "GET", + headers: dict[str, str] = dict(), + cookies: dict[str, str] = dict(), + ) -> "Request": self.user = user self.user.authenticated = authenticated diff --git a/aurweb/testing/smtp.py b/aurweb/testing/smtp.py index e5d67991..7596fbe9 100644 --- a/aurweb/testing/smtp.py +++ b/aurweb/testing/smtp.py @@ -2,7 +2,7 @@ class FakeSMTP: - """ A fake version of smtplib.SMTP used for testing. """ + """A fake version of smtplib.SMTP used for testing.""" starttls_enabled = False use_ssl = False @@ -41,5 +41,6 @@ class FakeSMTP: class FakeSMTP_SSL(FakeSMTP): - """ A fake version of smtplib.SMTP_SSL used for testing. """ + """A fake version of smtplib.SMTP_SSL used for testing.""" + use_ssl = True diff --git a/aurweb/time.py b/aurweb/time.py index a97ca986..505f17f5 100644 --- a/aurweb/time.py +++ b/aurweb/time.py @@ -1,5 +1,4 @@ import zoneinfo - from collections import OrderedDict from datetime import datetime from urllib.parse import unquote @@ -11,7 +10,7 @@ import aurweb.config def tz_offset(name: str): - """ Get a timezone offset in the form "+00:00" by its name. + """Get a timezone offset in the form "+00:00" by its name. Example: tz_offset('America/Los_Angeles') @@ -24,7 +23,7 @@ def tz_offset(name: str): offset = dt.utcoffset().total_seconds() / 60 / 60 # Prefix the offset string with a - or +. - offset_string = '-' if offset < 0 else '+' + offset_string = "-" if offset < 0 else "+" # Remove any negativity from the offset. We want a good offset. :) offset = abs(offset) @@ -42,19 +41,25 @@ def tz_offset(name: str): return offset_string -SUPPORTED_TIMEZONES = OrderedDict({ - # Flatten out the list of tuples into an OrderedDict. - timezone: offset for timezone, offset in sorted([ - # Comprehend a list of tuples (timezone, offset display string) - # and sort them by (offset, timezone). - (tz, "(UTC%s) %s" % (tz_offset(tz), tz)) - for tz in zoneinfo.available_timezones() - ], key=lambda element: (tz_offset(element[0]), element[0])) -}) +SUPPORTED_TIMEZONES = OrderedDict( + { + # Flatten out the list of tuples into an OrderedDict. + timezone: offset + for timezone, offset in sorted( + [ + # Comprehend a list of tuples (timezone, offset display string) + # and sort them by (offset, timezone). + (tz, "(UTC%s) %s" % (tz_offset(tz), tz)) + for tz in zoneinfo.available_timezones() + ], + key=lambda element: (tz_offset(element[0]), element[0]), + ) + } +) def get_request_timezone(request: Request): - """ Get a request's timezone by its AURTZ cookie. We use the + """Get a request's timezone by its AURTZ cookie. We use the configuration's [options] default_timezone otherwise. @param request FastAPI request diff --git a/aurweb/users/update.py b/aurweb/users/update.py index ffea1f2f..51f2d2e0 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -8,12 +8,23 @@ from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.util import strtobool -def simple(U: str = str(), E: str = str(), H: bool = False, - BE: str = str(), R: str = str(), HP: str = str(), - I: str = str(), K: str = str(), J: bool = False, - CN: bool = False, UN: bool = False, ON: bool = False, - S: bool = False, user: models.User = None, - **kwargs) -> None: +def simple( + U: str = str(), + E: str = str(), + H: bool = False, + BE: str = str(), + R: str = str(), + HP: str = str(), + I: str = str(), + K: str = str(), + J: bool = False, + CN: bool = False, + UN: bool = False, + ON: bool = False, + S: bool = False, + user: models.User = None, + **kwargs, +) -> None: now = time.utcnow() with db.begin(): user.Username = U or user.Username @@ -31,22 +42,26 @@ def simple(U: str = str(), E: str = str(), H: bool = False, user.OwnershipNotify = strtobool(ON) -def language(L: str = str(), - request: Request = None, - user: models.User = None, - context: dict[str, Any] = {}, - **kwargs) -> None: +def language( + L: str = str(), + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: if L and L != user.LangPreference: with db.begin(): user.LangPreference = L context["language"] = L -def timezone(TZ: str = str(), - request: Request = None, - user: models.User = None, - context: dict[str, Any] = {}, - **kwargs) -> None: +def timezone( + TZ: str = str(), + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: if TZ and TZ != user.Timezone: with db.begin(): user.Timezone = TZ @@ -67,8 +82,7 @@ def ssh_pubkey(PK: str = str(), user: models.User = None, **kwargs) -> None: with db.begin(): # Delete any existing keys we can't find. - to_remove = user.ssh_pub_keys.filter( - ~SSHPubKey.Fingerprint.in_(fprints)) + to_remove = user.ssh_pub_keys.filter(~SSHPubKey.Fingerprint.in_(fprints)) db.delete_all(to_remove) # For each key, if it does not yet exist, create it. @@ -79,24 +93,27 @@ def ssh_pubkey(PK: str = str(), user: models.User = None, **kwargs) -> None: ).exists() if not db.query(exists).scalar(): # No public key exists, create one. - db.create(models.SSHPubKey, UserID=user.ID, - PubKey=" ".join([prefix, key]), - Fingerprint=fprints[i]) + db.create( + models.SSHPubKey, + UserID=user.ID, + PubKey=" ".join([prefix, key]), + Fingerprint=fprints[i], + ) -def account_type(T: int = None, - user: models.User = None, - **kwargs) -> None: +def account_type(T: int = None, user: models.User = None, **kwargs) -> None: if T is not None and (T := int(T)) != user.AccountTypeID: with db.begin(): user.AccountTypeID = T -def password(P: str = str(), - request: Request = None, - user: models.User = None, - context: dict[str, Any] = {}, - **kwargs) -> None: +def password( + P: str = str(), + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: if P and not user.valid_password(P): # Remove the fields we consumed for passwords. context["P"] = context["C"] = str() diff --git a/aurweb/users/validate.py b/aurweb/users/validate.py index de51e3ff..6c27a0b7 100644 --- a/aurweb/users/validate.py +++ b/aurweb/users/validate.py @@ -25,42 +25,44 @@ def invalid_fields(E: str = str(), U: str = str(), **kwargs) -> None: raise ValidationError(["Missing a required field."]) -def invalid_suspend_permission(request: Request = None, - user: models.User = None, - S: str = "False", - **kwargs) -> None: +def invalid_suspend_permission( + request: Request = None, user: models.User = None, S: str = "False", **kwargs +) -> None: if not request.user.is_elevated() and strtobool(S) != bool(user.Suspended): - raise ValidationError([ - "You do not have permission to suspend accounts."]) + raise ValidationError(["You do not have permission to suspend accounts."]) -def invalid_username(request: Request = None, U: str = str(), - _: l10n.Translator = None, - **kwargs) -> None: +def invalid_username( + request: Request = None, U: str = str(), _: l10n.Translator = None, **kwargs +) -> None: if not util.valid_username(U): username_min_len = config.getint("options", "username_min_len") username_max_len = config.getint("options", "username_max_len") - raise ValidationError([ - "The username is invalid.", + raise ValidationError( [ - _("It must be between %s and %s characters long") % ( - username_min_len, username_max_len), - "Start and end with a letter or number", - "Can contain only one period, underscore or hyphen.", + "The username is invalid.", + [ + _("It must be between %s and %s characters long") + % (username_min_len, username_max_len), + "Start and end with a letter or number", + "Can contain only one period, underscore or hyphen.", + ], ] - ]) + ) -def invalid_password(P: str = str(), C: str = str(), - _: l10n.Translator = None, **kwargs) -> None: +def invalid_password( + P: str = str(), C: str = str(), _: l10n.Translator = None, **kwargs +) -> None: if P: if not util.valid_password(P): - username_min_len = config.getint( - "options", "username_min_len") - raise ValidationError([ - _("Your password must be at least %s characters.") % ( - username_min_len) - ]) + username_min_len = config.getint("options", "username_min_len") + raise ValidationError( + [ + _("Your password must be at least %s characters.") + % (username_min_len) + ] + ) elif not C: raise ValidationError(["Please confirm your new password."]) elif P != C: @@ -71,15 +73,18 @@ def is_banned(request: Request = None, **kwargs) -> None: host = request.client.host exists = db.query(models.Ban, models.Ban.IPAddress == host).exists() if db.query(exists).scalar(): - raise ValidationError([ - "Account registration has been disabled for your " - "IP address, probably due to sustained spam attacks. " - "Sorry for the inconvenience." - ]) + raise ValidationError( + [ + "Account registration has been disabled for your " + "IP address, probably due to sustained spam attacks. " + "Sorry for the inconvenience." + ] + ) -def invalid_user_password(request: Request = None, passwd: str = str(), - **kwargs) -> None: +def invalid_user_password( + request: Request = None, passwd: str = str(), **kwargs +) -> None: if request.user.is_authenticated(): if not request.user.valid_password(passwd): raise ValidationError(["Invalid password."]) @@ -97,8 +102,9 @@ def invalid_backup_email(BE: str = str(), **kwargs) -> None: def invalid_homepage(HP: str = str(), **kwargs) -> None: if HP and not util.valid_homepage(HP): - raise ValidationError([ - "The home page is invalid, please specify the full HTTP(s) URL."]) + raise ValidationError( + ["The home page is invalid, please specify the full HTTP(s) URL."] + ) def invalid_pgp_key(K: str = str(), **kwargs) -> None: @@ -106,8 +112,9 @@ def invalid_pgp_key(K: str = str(), **kwargs) -> None: raise ValidationError(["The PGP key fingerprint is invalid."]) -def invalid_ssh_pubkey(PK: str = str(), user: models.User = None, - _: l10n.Translator = None, **kwargs) -> None: +def invalid_ssh_pubkey( + PK: str = str(), user: models.User = None, _: l10n.Translator = None, **kwargs +) -> None: if not PK: return @@ -119,15 +126,23 @@ def invalid_ssh_pubkey(PK: str = str(), user: models.User = None, for prefix, key in keys: fingerprint = get_fingerprint(f"{prefix} {key}") - exists = db.query(models.SSHPubKey).filter( - and_(models.SSHPubKey.UserID != user.ID, - models.SSHPubKey.Fingerprint == fingerprint) - ).exists() + exists = ( + db.query(models.SSHPubKey) + .filter( + and_( + models.SSHPubKey.UserID != user.ID, + models.SSHPubKey.Fingerprint == fingerprint, + ) + ) + .exists() + ) if db.query(exists).scalar(): - raise ValidationError([ - _("The SSH public key, %s%s%s, is already in use.") % ( - "", fingerprint, "") - ]) + raise ValidationError( + [ + _("The SSH public key, %s%s%s, is already in use.") + % ("", fingerprint, "") + ] + ) def invalid_language(L: str = str(), **kwargs) -> None: @@ -140,60 +155,78 @@ def invalid_timezone(TZ: str = str(), **kwargs) -> None: raise ValidationError(["Timezone is not currently supported."]) -def username_in_use(U: str = str(), user: models.User = None, - _: l10n.Translator = None, **kwargs) -> None: - exists = db.query(models.User).filter( - and_(models.User.ID != user.ID, - models.User.Username == U) - ).exists() +def username_in_use( + U: str = str(), user: models.User = None, _: l10n.Translator = None, **kwargs +) -> None: + exists = ( + db.query(models.User) + .filter(and_(models.User.ID != user.ID, models.User.Username == U)) + .exists() + ) if db.query(exists).scalar(): # If the username already exists... - raise ValidationError([ - _("The username, %s%s%s, is already in use.") % ( - "", U, "") - ]) + raise ValidationError( + [ + _("The username, %s%s%s, is already in use.") + % ("", U, "") + ] + ) -def email_in_use(E: str = str(), user: models.User = None, - _: l10n.Translator = None, **kwargs) -> None: - exists = db.query(models.User).filter( - and_(models.User.ID != user.ID, - models.User.Email == E) - ).exists() +def email_in_use( + E: str = str(), user: models.User = None, _: l10n.Translator = None, **kwargs +) -> None: + exists = ( + db.query(models.User) + .filter(and_(models.User.ID != user.ID, models.User.Email == E)) + .exists() + ) if db.query(exists).scalar(): # If the email already exists... - raise ValidationError([ - _("The address, %s%s%s, is already in use.") % ( - "", E, "") - ]) + raise ValidationError( + [ + _("The address, %s%s%s, is already in use.") + % ("", E, "") + ] + ) -def invalid_account_type(T: int = None, request: Request = None, - user: models.User = None, - _: l10n.Translator = None, - **kwargs) -> None: +def invalid_account_type( + T: int = None, + request: Request = None, + user: models.User = None, + _: l10n.Translator = None, + **kwargs, +) -> None: if T is not None and (T := int(T)) != user.AccountTypeID: name = ACCOUNT_TYPE_NAME.get(T, None) has_cred = request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) if name is None: raise ValidationError(["Invalid account type provided."]) elif not has_cred: - raise ValidationError([ - "You do not have permission to change account types."]) + raise ValidationError( + ["You do not have permission to change account types."] + ) elif T > request.user.AccountTypeID: # If the chosen account type is higher than the editor's account # type, the editor doesn't have permission to set the new type. - error = _("You do not have permission to change " - "this user's account type to %s.") % name + error = ( + _( + "You do not have permission to change " + "this user's account type to %s." + ) + % name + ) raise ValidationError([error]) - logger.debug(f"Trusted User '{request.user.Username}' has " - f"modified '{user.Username}' account's type to" - f" {name}.") + logger.debug( + f"Trusted User '{request.user.Username}' has " + f"modified '{user.Username}' account's type to" + f" {name}." + ) -def invalid_captcha(captcha_salt: str = None, captcha: str = None, - **kwargs) -> None: +def invalid_captcha(captcha_salt: str = None, captcha: str = None, **kwargs) -> None: if captcha_salt and captcha_salt not in get_captcha_salts(): raise ValidationError(["This CAPTCHA has expired. Please try again."]) diff --git a/aurweb/util.py b/aurweb/util.py index 8291b578..4f1bd64e 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -2,7 +2,6 @@ import math import re import secrets import string - from datetime import datetime from http import HTTPStatus from subprocess import PIPE, Popen @@ -11,12 +10,10 @@ from urllib.parse import urlparse import fastapi import pygit2 - from email_validator import EmailSyntaxError, validate_email from fastapi.responses import JSONResponse import aurweb.config - from aurweb import defaults, logging logger = logging.get_logger(__name__) @@ -24,15 +21,15 @@ logger = logging.get_logger(__name__) def make_random_string(length: int) -> str: alphanumerics = string.ascii_lowercase + string.digits - return ''.join([secrets.choice(alphanumerics) for i in range(length)]) + return "".join([secrets.choice(alphanumerics) for i in range(length)]) def make_nonce(length: int = 8): - """ Generate a single random nonce. Here, token_hex generates a hex + """Generate a single random nonce. Here, token_hex generates a hex string of 2 hex characters per byte, where the length give is nbytes. This means that to get our proper string length, we need to cut it in half and truncate off any remaining (in the case that - length was uneven). """ + length was uneven).""" return secrets.token_hex(math.ceil(length / 2))[:length] @@ -45,7 +42,7 @@ def valid_username(username): # Check that username contains: one or more alphanumeric # characters, an optional separator of '.', '-' or '_', followed # by alphanumeric characters. - return re.match(r'^[a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$', username) + return re.match(r"^[a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$", username) def valid_email(email): @@ -82,7 +79,7 @@ def valid_pgp_fingerprint(fp): def jsonify(obj): - """ Perform a conversion on obj if it's needed. """ + """Perform a conversion on obj if it's needed.""" if isinstance(obj, datetime): obj = int(obj.timestamp()) return obj @@ -151,8 +148,7 @@ def git_search(repo: pygit2.Repository, commit_hash: str) -> int: return prefixlen -async def error_or_result(next: Callable, *args, **kwargs) \ - -> fastapi.Response: +async def error_or_result(next: Callable, *args, **kwargs) -> fastapi.Response: """ Try to return a response from `next`. @@ -174,9 +170,9 @@ async def error_or_result(next: Callable, *args, **kwargs) \ def parse_ssh_key(string: str) -> Tuple[str, str]: - """ Parse an SSH public key. """ + """Parse an SSH public key.""" invalid_exc = ValueError("The SSH public key is invalid.") - parts = re.sub(r'\s\s+', ' ', string.strip()).split() + parts = re.sub(r"\s\s+", " ", string.strip()).split() if len(parts) < 2: raise invalid_exc @@ -185,8 +181,7 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: if prefix not in prefixes: raise invalid_exc - proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, - stderr=PIPE) + proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, stderr=PIPE) out, _ = proc.communicate(f"{prefix} {key}".encode()) if proc.returncode: raise invalid_exc @@ -195,5 +190,5 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: def parse_ssh_keys(string: str) -> list[Tuple[str, str]]: - """ Parse a list of SSH public keys. """ + """Parse a list of SSH public keys.""" return [parse_ssh_key(e) for e in string.splitlines()] diff --git a/doc/web-auth.md b/doc/web-auth.md index 17284889..dbb4403d 100644 --- a/doc/web-auth.md +++ b/doc/web-auth.md @@ -108,4 +108,3 @@ The following list of steps describes exactly how this verification works: - `options.disable_http_login: 1` - `options.login_timeout: ` - `options.persistent_cookie_timeout: ` - diff --git a/docker-compose.yml b/docker-compose.yml index 9edffeeb..a1c2bb42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: healthcheck: test: "bash /docker/health/memcached.sh" interval: 3s - + redis: image: aurweb:latest init: true diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index 9fdf6015..99804d1d 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -147,4 +147,3 @@ http { '' close; } } - diff --git a/migrations/env.py b/migrations/env.py index 774ecdeb..dcc0329d 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -2,7 +2,6 @@ import logging import logging.config import sqlalchemy - from alembic import context import aurweb.db @@ -69,9 +68,7 @@ def run_migrations_online(): ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/po/ar.po b/po/ar.po index 676a5025..ea0e03cf 100644 --- a/po/ar.po +++ b/po/ar.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # safa1996alfulaij , 2015 # صفا الفليج , 2015-2016 diff --git a/po/ast.po b/po/ast.po index 16c363a6..2075edc1 100644 --- a/po/ast.po +++ b/po/ast.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # enolp , 2014-2015,2017 # Ḷḷumex03 , 2014 diff --git a/po/az.po b/po/az.po index 7e534b4c..1c7ca207 100644 --- a/po/az.po +++ b/po/az.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/az_AZ.po b/po/az_AZ.po index e903027b..2f5ceabd 100644 --- a/po/az_AZ.po +++ b/po/az_AZ.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/bg.po b/po/bg.po index 7864f5dc..c7c70021 100644 --- a/po/bg.po +++ b/po/bg.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/ca.po b/po/ca.po index 391dd146..d43c84dc 100644 --- a/po/ca.po +++ b/po/ca.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Adolfo Jayme-Barrientos, 2014 # Hector Mtz-Seara , 2011,2013 diff --git a/po/ca_ES.po b/po/ca_ES.po index bad69bd1..aac7b03f 100644 --- a/po/ca_ES.po +++ b/po/ca_ES.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/cs.po b/po/cs.po index b9bd739a..59a24007 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Daniel Milde , 2017 # Daniel Peukert , 2021 diff --git a/po/da.po b/po/da.po index a6f290ea..822b5506 100644 --- a/po/da.po +++ b/po/da.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Linuxbruger , 2018 # Louis Tim Larsen , 2015 diff --git a/po/de.po b/po/de.po index ec0a0fbe..a0f8fb0f 100644 --- a/po/de.po +++ b/po/de.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # 9d91e189c22376bb4ee81489bc27fc28, 2013 # 9d91e189c22376bb4ee81489bc27fc28, 2013-2014 diff --git a/po/el.po b/po/el.po index f1fe704e..37db785c 100644 --- a/po/el.po +++ b/po/el.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Achilleas Pipinellis, 2014 # Achilleas Pipinellis, 2013 diff --git a/po/es.po b/po/es.po index ea7ac099..9cbe98a6 100644 --- a/po/es.po +++ b/po/es.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Adolfo Jayme-Barrientos, 2015 # Angel Velasquez , 2011 diff --git a/po/es_419.po b/po/es_419.po index 444eccb7..e2b96ae6 100644 --- a/po/es_419.po +++ b/po/es_419.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Angel Velasquez , 2011 # juantascon , 2011 diff --git a/po/et.po b/po/et.po index 9b6493b5..44f2b3a0 100644 --- a/po/et.po +++ b/po/et.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/fi.po b/po/fi.po index 39cfe626..636681b7 100644 --- a/po/fi.po +++ b/po/fi.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Elias Autio, 2016 # Jesse Jaara , 2011-2012,2015 diff --git a/po/fi_FI.po b/po/fi_FI.po index f3253433..17a58b4a 100644 --- a/po/fi_FI.po +++ b/po/fi_FI.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/fr.po b/po/fr.po index 99d01460..03192d48 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Alexandre Macabies , 2018 # Antoine Lubineau , 2012 diff --git a/po/he.po b/po/he.po index cd4a0f87..936e93a1 100644 --- a/po/he.po +++ b/po/he.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # GenghisKhan , 2016 # Lukas Fleischer , 2011 diff --git a/po/hi_IN.po b/po/hi_IN.po index 37fd082e..114c9461 100644 --- a/po/hi_IN.po +++ b/po/hi_IN.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Panwar108 , 2018,2020-2021 msgid "" diff --git a/po/hr.po b/po/hr.po index 4932bd7e..fe1857c1 100644 --- a/po/hr.po +++ b/po/hr.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 msgid "" diff --git a/po/hu.po b/po/hu.po index 51894457..e6ebd451 100644 --- a/po/hu.po +++ b/po/hu.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Balló György , 2013 # Balló György , 2011,2013-2016 diff --git a/po/id.po b/po/id.po index 75a6c98b..103c47e6 100644 --- a/po/id.po +++ b/po/id.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # se7entime , 2013 # se7entime , 2016 diff --git a/po/id_ID.po b/po/id_ID.po index d01294c8..c3acb167 100644 --- a/po/id_ID.po +++ b/po/id_ID.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/is.po b/po/is.po index a7a88b04..aee80ce5 100644 --- a/po/is.po +++ b/po/is.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/it.po b/po/it.po index 436b6459..f583cb2f 100644 --- a/po/it.po +++ b/po/it.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Fanfurlio Farolfi , 2021-2022 # Giovanni Scafora , 2011-2015 diff --git a/po/ja.po b/po/ja.po index 55d056bf..280edb46 100644 --- a/po/ja.po +++ b/po/ja.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # kusakata, 2013 # kusakata, 2013 diff --git a/po/ko.po b/po/ko.po index 808ffe27..6da57759 100644 --- a/po/ko.po +++ b/po/ko.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/lt.po b/po/lt.po index d126f193..c9f55632 100644 --- a/po/lt.po +++ b/po/lt.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/nb.po b/po/nb.po index 1cc090f1..307a80d6 100644 --- a/po/nb.po +++ b/po/nb.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Alexander F. Rødseth , 2015,2017-2019 # Alexander F. Rødseth , 2011,2013-2014 diff --git a/po/nb_NO.po b/po/nb_NO.po index 74af6936..5d958172 100644 --- a/po/nb_NO.po +++ b/po/nb_NO.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Kim Nordmo , 2017,2019 # Lukas Fleischer , 2011 diff --git a/po/nl.po b/po/nl.po index 282b5b40..54519d21 100644 --- a/po/nl.po +++ b/po/nl.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Heimen Stoffels , 2021-2022 # Heimen Stoffels , 2015,2021 diff --git a/po/pl.po b/po/pl.po index 4856f22b..94a6fb67 100644 --- a/po/pl.po +++ b/po/pl.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Bartłomiej Piotrowski , 2011 # Bartłomiej Piotrowski , 2014 diff --git a/po/pt.po b/po/pt.po index b2cf86b2..aed32031 100644 --- a/po/pt.po +++ b/po/pt.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 msgid "" diff --git a/po/pt_BR.po b/po/pt_BR.po index c9c15d72..d29a9448 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Albino Biasutti Neto Bino , 2011 # Fábio Nogueira , 2016 diff --git a/po/pt_PT.po b/po/pt_PT.po index 3518cb7b..7f6ea67a 100644 --- a/po/pt_PT.po +++ b/po/pt_PT.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Christophe Silva , 2018 # Gaspar Santos , 2011 diff --git a/po/ro.po b/po/ro.po index fa159928..4409b698 100644 --- a/po/ro.po +++ b/po/ro.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Arthur Țițeică , 2013-2015 # Lukas Fleischer , 2011 diff --git a/po/ru.po b/po/ru.po index 75550c8c..44f000dd 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Alex , 2021 # Evgeniy Alekseev , 2014-2015 diff --git a/po/sk.po b/po/sk.po index 76d3d1a8..853fc198 100644 --- a/po/sk.po +++ b/po/sk.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # archetyp , 2013-2016 # Jose Riha , 2018 diff --git a/po/sr.po b/po/sr.po index dae37bcd..426ce599 100644 --- a/po/sr.po +++ b/po/sr.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 # Slobodan Terzić , 2011-2012,2015-2017 diff --git a/po/sr_RS.po b/po/sr_RS.po index 985ee007..b7560965 100644 --- a/po/sr_RS.po +++ b/po/sr_RS.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Nikola Stojković , 2013 msgid "" diff --git a/po/sv_SE.po b/po/sv_SE.po index 6abb8452..4887fdde 100644 --- a/po/sv_SE.po +++ b/po/sv_SE.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Johannes Löthberg , 2015-2016 # Kevin Morris , 2022 diff --git a/po/tr.po b/po/tr.po index 83b1e4df..559a0008 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # tarakbumba , 2011,2013-2015 # tarakbumba , 2012,2014 diff --git a/po/uk.po b/po/uk.po index a4410185..3bffe4f6 100644 --- a/po/uk.po +++ b/po/uk.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 # Rax Garfield , 2012 diff --git a/po/vi.po b/po/vi.po index 3ea5bad3..87f7faac 100644 --- a/po/vi.po +++ b/po/vi.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/zh.po b/po/zh.po index 04fe06f3..c932df9c 100644 --- a/po/zh.po +++ b/po/zh.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" diff --git a/po/zh_CN.po b/po/zh_CN.po index 53d42bc8..675d15a3 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Feng Chao , 2015-2016 # dongfengweixiao , 2015 diff --git a/po/zh_TW.po b/po/zh_TW.po index e7399a19..1526b4a9 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # pan93412 , 2018 # 黃柏諺 , 2014-2017 diff --git a/schema/gendummydata.py b/schema/gendummydata.py index aedfda7e..fa59855f 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -15,27 +15,26 @@ import os import random import sys import time - from datetime import datetime import bcrypt LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" -USER_ID = 5 # Users.ID of first bogus user -PKG_ID = 1 # Packages.ID of first package +USER_ID = 5 # Users.ID of first bogus user +PKG_ID = 1 # Packages.ID of first package # how many users to 'register' MAX_USERS = int(os.environ.get("MAX_USERS", 38000)) -MAX_DEVS = .1 # what percentage of MAX_USERS are Developers -MAX_TUS = .2 # what percentage of MAX_USERS are Trusted Users +MAX_DEVS = 0.1 # what percentage of MAX_USERS are Developers +MAX_TUS = 0.2 # what percentage of MAX_USERS are Trusted Users # how many packages to load MAX_PKGS = int(os.environ.get("MAX_PKGS", 32000)) -PKG_DEPS = (1, 15) # min/max depends a package has -PKG_RELS = (1, 5) # min/max relations a package has -PKG_SRC = (1, 3) # min/max sources a package has -PKG_CMNTS = (1, 5) # min/max number of comments a package has +PKG_DEPS = (1, 15) # min/max depends a package has +PKG_RELS = (1, 5) # min/max relations a package has +PKG_SRC = (1, 3) # min/max sources a package has +PKG_CMNTS = (1, 5) # min/max number of comments a package has CATEGORIES_COUNT = 17 # the number of categories from aur-schema -VOTING = (0, .001) # percentage range for package voting +VOTING = (0, 0.001) # percentage range for package voting # number of open trusted user proposals OPEN_PROPOSALS = int(os.environ.get("OPEN_PROPOSALS", 15)) # number of closed trusted user proposals @@ -113,10 +112,10 @@ if not len(contents) - MAX_USERS > MAX_PKGS: def normalize(unicode_data): - """ We only accept ascii for usernames. Also use this to normalize + """We only accept ascii for usernames. Also use this to normalize package names; our database utf8mb4 collations compare with Unicode - Equivalence. """ - return unicode_data.encode('ascii', 'ignore').decode('ascii') + Equivalence.""" + return unicode_data.encode("ascii", "ignore").decode("ascii") # select random usernames @@ -196,10 +195,12 @@ for u in user_keys: # "{salt}{username}" to_hash = f"{salt}{u}" - h = hashlib.new('md5') + h = hashlib.new("md5") h.update(to_hash.encode()) - s = ("INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd, Salt)" - " VALUES (%d, %d, '%s', '%s@example.com', '%s', '%s');\n") + s = ( + "INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd, Salt)" + " VALUES (%d, %d, '%s', '%s@example.com', '%s', '%s');\n" + ) s = s % (seen_users[u], account_type, u, u, h.hexdigest(), salt) out.write(s) @@ -230,13 +231,17 @@ for p in list(seen_pkgs.keys()): uuid = genUID() # the submitter/user - s = ("INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " - "SubmitterUID, MaintainerUID, PackagerUID) VALUES (%d, '%s', '', %d, %d, %d, %s, %s);\n") + s = ( + "INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " + "SubmitterUID, MaintainerUID, PackagerUID) VALUES (%d, '%s', '', %d, %d, %d, %s, %s);\n" + ) s = s % (seen_pkgs[p], p, NOW, NOW, uuid, muid, puid) out.write(s) - s = ("INSERT INTO Packages (ID, PackageBaseID, Name, Version) VALUES " - "(%d, %d, '%s', '%s');\n") + s = ( + "INSERT INTO Packages (ID, PackageBaseID, Name, Version) VALUES " + "(%d, %d, '%s', '%s');\n" + ) s = s % (seen_pkgs[p], seen_pkgs[p], p, genVersion()) out.write(s) @@ -247,8 +252,10 @@ for p in list(seen_pkgs.keys()): num_comments = random.randrange(PKG_CMNTS[0], PKG_CMNTS[1]) for i in range(0, num_comments): now = NOW + random.randrange(400, 86400 * 3) - s = ("INSERT INTO PackageComments (PackageBaseID, UsersID," - " Comments, RenderedComment, CommentTS) VALUES (%d, %d, '%s', '', %d);\n") + s = ( + "INSERT INTO PackageComments (PackageBaseID, UsersID," + " Comments, RenderedComment, CommentTS) VALUES (%d, %d, '%s', '', %d);\n" + ) s = s % (seen_pkgs[p], genUID(), genFortune(), now) out.write(s) @@ -258,14 +265,17 @@ utcnow = int(datetime.utcnow().timestamp()) track_votes = {} log.debug("Casting votes for packages.") for u in user_keys: - num_votes = random.randrange(int(len(seen_pkgs) * VOTING[0]), - int(len(seen_pkgs) * VOTING[1])) + num_votes = random.randrange( + int(len(seen_pkgs) * VOTING[0]), int(len(seen_pkgs) * VOTING[1]) + ) pkgvote = {} for v in range(num_votes): pkg = random.randrange(1, len(seen_pkgs) + 1) if pkg not in pkgvote: - s = ("INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS)" - " VALUES (%d, %d, %d);\n") + s = ( + "INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS)" + " VALUES (%d, %d, %d);\n" + ) s = s % (seen_users[u], pkg, utcnow) pkgvote[pkg] = 1 if pkg not in track_votes: @@ -310,9 +320,12 @@ for p in seen_pkgs_keys: src_file = user_keys[random.randrange(0, len(user_keys))] src = "%s%s.%s/%s/%s-%s.tar.gz" % ( RANDOM_URL[random.randrange(0, len(RANDOM_URL))], - p, RANDOM_TLDS[random.randrange(0, len(RANDOM_TLDS))], + p, + RANDOM_TLDS[random.randrange(0, len(RANDOM_TLDS))], RANDOM_LOCS[random.randrange(0, len(RANDOM_LOCS))], - src_file, genVersion()) + src_file, + genVersion(), + ) s = "INSERT INTO PackageSources(PackageID, Source) VALUES (%d, '%s');\n" s = s % (seen_pkgs[p], src) out.write(s) @@ -334,8 +347,10 @@ for t in range(0, OPEN_PROPOSALS + CLOSE_PROPOSALS): else: user = user_keys[random.randrange(0, len(user_keys))] suid = trustedusers[random.randrange(0, len(trustedusers))] - s = ("INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End," - " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n") + s = ( + "INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End," + " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n" + ) s = s % (genFortune(), user, start, end, suid) out.write(s) count += 1 diff --git a/templates/addvote.html b/templates/addvote.html index 4d2b0292..8777cbf3 100644 --- a/templates/addvote.html +++ b/templates/addvote.html @@ -65,4 +65,3 @@ {% endblock %} - diff --git a/templates/home.html b/templates/home.html index c1f172f4..6a5fca69 100644 --- a/templates/home.html +++ b/templates/home.html @@ -5,7 +5,7 @@ | tr | format('', "", '', "") - | safe + | safe }} {{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!" | tr @@ -61,7 +61,7 @@ {% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %}

        - {% for keytype in ssh_fingerprints %} + {% for keytype in ssh_fingerprints %}
      • {{ keytype }}: {{ ssh_fingerprints[keytype] }} {% endfor %}
      @@ -85,7 +85,7 @@ | tr | format('', "", "", "") - | safe + | safe }}

      diff --git a/templates/packages/index.html b/templates/packages/index.html index 6034d2f6..58ce8648 100644 --- a/templates/packages/index.html +++ b/templates/packages/index.html @@ -12,7 +12,7 @@ {% elif not packages_count %} - {% include "partials/packages/search.html" %} + {% include "partials/packages/search.html" %}

      {{ "No packages matched your search criteria." | tr }}

      diff --git a/templates/partials/account/results.html b/templates/partials/account/results.html index 1c398ce1..ef8d927a 100644 --- a/templates/partials/account/results.html +++ b/templates/partials/account/results.html @@ -79,4 +79,3 @@
    • - diff --git a/templates/tu/show.html b/templates/tu/show.html index c36a3e8f..f4214018 100644 --- a/templates/tu/show.html +++ b/templates/tu/show.html @@ -4,7 +4,7 @@
      {% include "partials/tu/proposal/details.html" %}
      - + {% if utcnow >= voteinfo.End %}
      {% include "partials/tu/proposal/voters.html" %} diff --git a/test/conftest.py b/test/conftest.py index 283c979a..aac221f7 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -39,12 +39,10 @@ ahead of each function takes too long when compared to this method. """ import os import pathlib - from multiprocessing import Lock import py import pytest - from posix_ipc import O_CREAT, Semaphore from sqlalchemy import create_engine from sqlalchemy.engine import URL @@ -54,7 +52,6 @@ from sqlalchemy.orm import scoped_session import aurweb.config import aurweb.db - from aurweb import initdb, logging, testing from aurweb.testing.email import Email from aurweb.testing.filelock import FileLock @@ -78,13 +75,10 @@ def test_engine() -> Engine: unix_socket = aurweb.config.get_with_fallback("database", "socket", None) kwargs = { "username": aurweb.config.get("database", "user"), - "password": aurweb.config.get_with_fallback( - "database", "password", None), + "password": aurweb.config.get_with_fallback("database", "password", None), "host": aurweb.config.get("database", "host"), "port": aurweb.config.get_with_fallback("database", "port", None), - "query": { - "unix_socket": unix_socket - } + "query": {"unix_socket": unix_socket}, } backend = aurweb.config.get("database", "backend") @@ -99,6 +93,7 @@ class AlembicArgs: This structure is needed to pass conftest-specific arguments to initdb.run duration database creation. """ + verbose = False use_alembic = True @@ -156,7 +151,7 @@ def setup_email(): @pytest.fixture(scope="module") def setup_database(tmp_path_factory: pathlib.Path, worker_id: str) -> None: - """ Create and drop a database for the suite this fixture is used in. """ + """Create and drop a database for the suite this fixture is used in.""" engine = test_engine() dbname = aurweb.db.name() diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py index 2af7127b..9af19105 100644 --- a/test/test_accepted_term.py +++ b/test/test_accepted_term.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,17 +16,21 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def term() -> Term: with db.begin(): - term = db.create(Term, Description="Test term", - URL="https://test.term") + term = db.create(Term, Description="Test term", URL="https://test.term") yield term diff --git a/test/test_account_type.py b/test/test_account_type.py index 1d71f878..4b56b7ff 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -22,26 +22,30 @@ def account_type() -> AccountType: def test_account_type(account_type): - """ Test creating an AccountType, and reading its columns. """ + """Test creating an AccountType, and reading its columns.""" # Make sure it got db.created and was given an ID. assert bool(account_type.ID) # Next, test our string functions. assert str(account_type) == "TestUser" - assert repr(account_type) == \ - "" % ( - account_type.ID) + assert repr(account_type) == "" % ( + account_type.ID + ) - record = db.query(AccountType, - AccountType.AccountType == "TestUser").first() + record = db.query(AccountType, AccountType.AccountType == "TestUser").first() assert account_type == record def test_user_account_type_relationship(account_type): with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountType=account_type, + ) assert user.AccountType == account_type diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 37b3d130..eab8fa4f 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -1,6 +1,5 @@ import re import tempfile - from datetime import datetime from http import HTTPStatus from logging import DEBUG @@ -8,17 +7,21 @@ from subprocess import Popen import lxml.html import pytest - from fastapi.testclient import TestClient import aurweb.models.account_type as at - from aurweb import captcha, db, logging, time from aurweb.asgi import app from aurweb.db import create, query from aurweb.models.accepted_term import AcceptedTerm -from aurweb.models.account_type import (DEVELOPER_ID, TRUSTED_USER, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID, - AccountType) +from aurweb.models.account_type import ( + DEVELOPER_ID, + TRUSTED_USER, + TRUSTED_USER_AND_DEV_ID, + TRUSTED_USER_ID, + USER_ID, + AccountType, +) from aurweb.models.ban import Ban from aurweb.models.session import Session from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint @@ -39,8 +42,11 @@ def make_ssh_pubkey(): # dependency to passing this test). with tempfile.TemporaryDirectory() as tmpdir: with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) + proc = Popen( + ["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, + stderr=null, + ) proc.wait() assert proc.returncode == 0 @@ -60,9 +66,13 @@ def client() -> TestClient: def create_user(username: str) -> User: email = f"{username}@example.org" - user = create(User, Username=username, Email=email, - Passwd="testPassword", - AccountTypeID=USER_ID) + user = create( + User, + Username=username, + Email=email, + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @@ -85,8 +95,9 @@ def test_get_passreset_authed_redirects(client: TestClient, user: User): assert sid is not None with client as request: - response = request.get("/passreset", cookies={"AURSID": sid}, - allow_redirects=False) + response = request.get( + "/passreset", cookies={"AURSID": sid}, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -129,10 +140,12 @@ def test_post_passreset_authed_redirects(client: TestClient, user: User): assert sid is not None with client as request: - response = request.post("/passreset", - cookies={"AURSID": sid}, - data={"user": "blah"}, - allow_redirects=False) + response = request.post( + "/passreset", + cookies={"AURSID": sid}, + data={"user": "blah"}, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -166,8 +179,9 @@ def test_post_passreset_user_suspended(client: TestClient, user: User): def test_post_passreset_resetkey(client: TestClient, user: User): with db.begin(): - user.session = Session(UsersID=user.ID, SessionID="blah", - LastUpdateTS=time.utcnow()) + user.session = Session( + UsersID=user.ID, SessionID="blah", LastUpdateTS=time.utcnow() + ) # Prepare a password reset. with client as request: @@ -182,7 +196,7 @@ def test_post_passreset_resetkey(client: TestClient, user: User): "user": TEST_USERNAME, "resetkey": resetkey, "password": "abcd1234", - "confirm": "abcd1234" + "confirm": "abcd1234", } with client as request: @@ -200,10 +214,7 @@ def make_resetkey(client: TestClient, user: User): def make_passreset_data(user: User, resetkey: str): - return { - "user": user.Username, - "resetkey": resetkey - } + return {"user": user.Username, "resetkey": resetkey} def test_post_passreset_error_invalid_email(client: TestClient, user: User): @@ -240,8 +251,7 @@ def test_post_passreset_error_missing_field(client: TestClient, user: User): assert error in response.content.decode("utf-8") -def test_post_passreset_error_password_mismatch(client: TestClient, - user: User): +def test_post_passreset_error_password_mismatch(client: TestClient, user: User): resetkey = make_resetkey(client, user) post_data = make_passreset_data(user, resetkey) @@ -257,8 +267,7 @@ def test_post_passreset_error_password_mismatch(client: TestClient, assert error in response.content.decode("utf-8") -def test_post_passreset_error_password_requirements(client: TestClient, - user: User): +def test_post_passreset_error_password_requirements(client: TestClient, user: User): resetkey = make_resetkey(client, user) post_data = make_passreset_data(user, resetkey) @@ -284,7 +293,7 @@ def test_get_register(client: TestClient): def post_register(request, **kwargs): - """ A simple helper that allows overrides to test defaults. """ + """A simple helper that allows overrides to test defaults.""" salt = captcha.get_captcha_salts()[0] token = captcha.get_captcha_token(salt) answer = captcha.get_captcha_answer(token) @@ -297,7 +306,7 @@ def post_register(request, **kwargs): "L": "en", "TZ": "UTC", "captcha": answer, - "captcha_salt": salt + "captcha_salt": salt, } # For any kwargs given, override their k:v pairs in data. @@ -380,9 +389,11 @@ def test_post_register_error_ip_banned(client: TestClient): assert response.status_code == int(HTTPStatus.BAD_REQUEST) content = response.content.decode() - assert ("Account registration has been disabled for your IP address, " + - "probably due to sustained spam attacks. Sorry for the " + - "inconvenience.") in content + assert ( + "Account registration has been disabled for your IP address, " + + "probably due to sustained spam attacks. Sorry for the " + + "inconvenience." + ) in content def test_post_register_error_missing_username(client: TestClient): @@ -489,7 +500,7 @@ def test_post_register_error_invalid_pgp_fingerprints(client: TestClient): expected = "The PGP key fingerprint is invalid." assert expected in content - pk = 'z' + ('a' * 39) + pk = "z" + ("a" * 39) with client as request: response = post_register(request, K=pk) @@ -569,8 +580,11 @@ def test_post_register_error_ssh_pubkey_taken(client: TestClient, user: User): # dependency to passing this test). with tempfile.TemporaryDirectory() as tmpdir: with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) + proc = Popen( + ["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, + stderr=null, + ) proc.wait() assert proc.returncode == 0 @@ -602,8 +616,11 @@ def test_post_register_with_ssh_pubkey(client: TestClient): # dependency to passing this test). with tempfile.TemporaryDirectory() as tmpdir: with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) + proc = Popen( + ["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, + stderr=null, + ) proc.wait() assert proc.returncode == 0 @@ -617,7 +634,7 @@ def test_post_register_with_ssh_pubkey(client: TestClient): def test_get_account_edit_tu_as_tu(client: TestClient, tu_user: User): - """ Test edit get route of another TU as a TU. """ + """Test edit get route of another TU as a TU.""" with db.begin(): user2 = create_user("test2") user2.AccountTypeID = at.TRUSTED_USER_ID @@ -643,7 +660,7 @@ def test_get_account_edit_tu_as_tu(client: TestClient, tu_user: User): def test_get_account_edit_as_tu(client: TestClient, tu_user: User): - """ Test edit get route of another user as a TU. """ + """Test edit get route of another user as a TU.""" with db.begin(): user2 = create_user("test2") @@ -669,7 +686,7 @@ def test_get_account_edit_as_tu(client: TestClient, tu_user: User): def test_get_account_edit_type(client: TestClient, user: User): - """ Test that users do not have an Account Type field. """ + """Test that users do not have an Account Type field.""" cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{user.Username}/edit" @@ -700,14 +717,18 @@ def test_get_account_edit_unauthorized(client: TestClient, user: User): sid = user.login(request, "testPassword") with db.begin(): - user2 = create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user2 = create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) endpoint = f"/account/{user2.Username}/edit" with client as request: # Try to edit `test2` while authenticated as `test`. - response = request.get(endpoint, cookies={"AURSID": sid}, - allow_redirects=False) + response = request.get(endpoint, cookies={"AURSID": sid}, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" @@ -718,16 +739,15 @@ def test_post_account_edit(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test666@example.org", - "passwd": "testPassword" - } + post_data = {"U": "test", "E": "test666@example.org", "passwd": "testPassword"} with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) @@ -772,8 +792,7 @@ def test_post_account_edit_type_as_dev(client: TestClient, tu_user: User): assert user2.AccountTypeID == at.DEVELOPER_ID -def test_post_account_edit_invalid_type_as_tu(client: TestClient, - tu_user: User): +def test_post_account_edit_invalid_type_as_tu(client: TestClient, tu_user: User): with db.begin(): user2 = create_user("test_tu") tu_user.AccountTypeID = at.TRUSTED_USER_ID @@ -792,8 +811,10 @@ def test_post_account_edit_invalid_type_as_tu(client: TestClient, assert user2.AccountTypeID == at.USER_ID errors = get_errors(resp.text) - expected = ("You do not have permission to change this user's " - f"account type to {at.DEVELOPER}.") + expected = ( + "You do not have permission to change this user's " + f"account type to {at.DEVELOPER}." + ) assert errors[0].text.strip() == expected @@ -807,16 +828,13 @@ def test_post_account_edit_dev(client: TestClient, tu_user: User): request = Request() sid = tu_user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test666@example.org", - "passwd": "testPassword" - } + post_data = {"U": "test", "E": "test666@example.org", "passwd": "testPassword"} endpoint = f"/account/{tu_user.Username}/edit" with client as request: - response = request.post(endpoint, cookies={"AURSID": sid}, - data=post_data, allow_redirects=False) + response = request.post( + endpoint, cookies={"AURSID": sid}, data=post_data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) expected = "The account, test, " @@ -832,13 +850,16 @@ def test_post_account_edit_language(client: TestClient, user: User): "U": "test", "E": "test@example.org", "L": "de", # German - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) @@ -859,33 +880,33 @@ def test_post_account_edit_timezone(client: TestClient, user: User): "U": "test", "E": "test@example.org", "TZ": "CET", - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) -def test_post_account_edit_error_missing_password(client: TestClient, - user: User): +def test_post_account_edit_error_missing_password(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test@example.org", - "TZ": "CET", - "passwd": "" - } + post_data = {"U": "test", "E": "test@example.org", "TZ": "CET", "passwd": ""} with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -893,22 +914,19 @@ def test_post_account_edit_error_missing_password(client: TestClient, assert "Invalid password." in content -def test_post_account_edit_error_invalid_password(client: TestClient, - user: User): +def test_post_account_edit_error_invalid_password(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test@example.org", - "TZ": "CET", - "passwd": "invalid" - } + post_data = {"U": "test", "E": "test@example.org", "TZ": "CET", "passwd": "invalid"} with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -916,18 +934,18 @@ def test_post_account_edit_error_invalid_password(client: TestClient, assert "Invalid password." in content -def test_post_account_edit_suspend_unauthorized(client: TestClient, - user: User): +def test_post_account_edit_suspend_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} post_data = { "U": "test", "E": "test@example.org", "S": True, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - resp = request.post(f"/account/{user.Username}/edit", data=post_data, - cookies=cookies) + resp = request.post( + f"/account/{user.Username}/edit", data=post_data, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -945,11 +963,12 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): "U": "test", "E": "test@example.org", "J": True, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - resp = request.post(f"/account/{user.Username}/edit", data=post_data, - cookies=cookies) + resp = request.post( + f"/account/{user.Username}/edit", data=post_data, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.OK) # Make sure the user record got updated correctly. @@ -957,8 +976,9 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): post_data.update({"J": False}) with client as request: - resp = request.post(f"/account/{user.Username}/edit", data=post_data, - cookies=cookies) + resp = request.post( + f"/account/{user.Username}/edit", data=post_data, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.OK) assert user.InactivityTS == 0 @@ -974,7 +994,7 @@ def test_post_account_edit_suspended(client: TestClient, user: User): "U": "test", "E": "test@example.org", "S": True, - "passwd": "testPassword" + "passwd": "testPassword", } endpoint = f"/account/{user.Username}/edit" with client as request: @@ -997,21 +1017,27 @@ def test_post_account_edit_error_unauthorized(client: TestClient, user: User): sid = user.login(request, "testPassword") with db.begin(): - user2 = create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user2 = create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) post_data = { "U": "test", "E": "test@example.org", "TZ": "CET", - "passwd": "testPassword" + "passwd": "testPassword", } endpoint = f"/account/{user2.Username}/edit" with client as request: # Attempt to edit 'test2' while logged in as 'test'. - response = request.post(endpoint, cookies={"AURSID": sid}, - data=post_data, allow_redirects=False) + response = request.post( + endpoint, cookies={"AURSID": sid}, data=post_data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" @@ -1026,13 +1052,16 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): "U": "test", "E": "test@example.org", "PK": make_ssh_pubkey(), - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1040,9 +1069,12 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): post_data["PK"] = make_ssh_pubkey() with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1055,13 +1087,16 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): "U": user.Username, "E": user.Email, "PK": make_ssh_pubkey(), - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1069,13 +1104,16 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): "U": user.Username, "E": user.Email, "PK": str(), # Pass an empty string now to walk the delete path. - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1087,12 +1125,13 @@ def test_post_account_edit_invalid_ssh_pubkey(client: TestClient, user: User): "U": "test", "E": "test@example.org", "PK": pubkey, - "passwd": "testPassword" + "passwd": "testPassword", } cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.post("/account/test/edit", data=data, - cookies=cookies, allow_redirects=False) + response = request.post( + "/account/test/edit", data=data, cookies=cookies, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1106,13 +1145,16 @@ def test_post_account_edit_password(client: TestClient, user: User): "E": "test@example.org", "P": "newPassword", "C": "newPassword", - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + response = request.post( + "/account/test/edit", + cookies={"AURSID": sid}, + data=post_data, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1132,7 +1174,7 @@ def test_post_account_edit_self_type_as_user(client: TestClient, user: User): "U": user.Username, "E": user.Email, "T": TRUSTED_USER_ID, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: resp = request.post(endpoint, data=data, cookies=cookies) @@ -1151,8 +1193,7 @@ def test_post_account_edit_other_user_as_user(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: - resp = request.get(endpoint, cookies=cookies, - allow_redirects=False) + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/account/{user2.Username}" @@ -1172,7 +1213,7 @@ def test_post_account_edit_self_type_as_tu(client: TestClient, tu_user: User): "U": tu_user.Username, "E": tu_user.Email, "T": USER_ID, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: resp = request.post(endpoint, data=data, cookies=cookies) @@ -1182,7 +1223,8 @@ def test_post_account_edit_self_type_as_tu(client: TestClient, tu_user: User): def test_post_account_edit_other_user_type_as_tu( - client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture): + client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture +): caplog.set_level(DEBUG) with db.begin(): @@ -1202,7 +1244,7 @@ def test_post_account_edit_other_user_type_as_tu( "U": user2.Username, "E": user2.Email, "T": TRUSTED_USER_ID, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: resp = request.post(endpoint, data=data, cookies=cookies) @@ -1212,14 +1254,17 @@ def test_post_account_edit_other_user_type_as_tu( assert user2.AccountTypeID == TRUSTED_USER_ID # and also that this got logged out at DEBUG level. - expected = (f"Trusted User '{tu_user.Username}' has " - f"modified '{user2.Username}' account's type to" - f" {TRUSTED_USER}.") + expected = ( + f"Trusted User '{tu_user.Username}' has " + f"modified '{user2.Username}' account's type to" + f" {TRUSTED_USER}." + ) assert expected in caplog.text def test_post_account_edit_other_user_type_as_tu_invalid_type( - client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture): + client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture +): with db.begin(): user2 = create_user("test2") @@ -1227,12 +1272,7 @@ def test_post_account_edit_other_user_type_as_tu_invalid_type( endpoint = f"/account/{user2.Username}/edit" # As a TU, we can modify other user's account types. - data = { - "U": user2.Username, - "E": user2.Email, - "T": 0, - "passwd": "testPassword" - } + data = {"U": user2.Username, "E": user2.Email, "T": 0, "passwd": "testPassword"} with client as request: resp = request.post(endpoint, data=data, cookies=cookies) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1247,8 +1287,9 @@ def test_get_account(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get("/account/test", cookies={"AURSID": sid}, - allow_redirects=False) + response = request.get( + "/account/test", cookies={"AURSID": sid}, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) @@ -1258,8 +1299,9 @@ def test_get_account_not_found(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get("/account/not_found", cookies={"AURSID": sid}, - allow_redirects=False) + response = request.get( + "/account/not_found", cookies={"AURSID": sid}, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -1274,8 +1316,8 @@ def test_get_account_unauthenticated(client: TestClient, user: User): def test_get_accounts(client: TestClient, user: User, tu_user: User): - """ Test that we can GET request /accounts and receive - a form which can be used to POST /accounts. """ + """Test that we can GET request /accounts and receive + a form which can be used to POST /accounts.""" sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} @@ -1296,8 +1338,8 @@ def test_get_accounts(client: TestClient, user: User, tu_user: User): assert form.attrib.get("action") == "/accounts" def field(element): - """ Return the given element string as a valid - selector in the form. """ + """Return the given element string as a valid + selector in the form.""" return f"./fieldset/p/{element}" username = form.xpath(field('input[@id="id_username"]')) @@ -1360,8 +1402,7 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User): columns = rows[i].xpath("./td") assert len(columns) == 7 - username, atype, suspended, real_name, \ - irc_nick, pgp_key, edit = columns + username, atype, suspended, real_name, irc_nick, pgp_key, edit = columns username = next(iter(username.xpath("./a"))) assert username.text.strip() == _user.Username @@ -1379,8 +1420,10 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User): else: assert not edit - logger.debug('Checked user row {"id": %s, "username": "%s"}.' - % (_user.ID, _user.Username)) + logger.debug( + 'Checked user row {"id": %s, "username": "%s"}.' + % (_user.ID, _user.Username) + ) def test_post_accounts_username(client: TestClient, user: User, tu_user: User): @@ -1389,8 +1432,7 @@ def test_post_accounts_username(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, - data={"U": user.Username}) + response = request.post("/accounts", cookies=cookies, data={"U": user.Username}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1403,34 +1445,33 @@ def test_post_accounts_username(client: TestClient, user: User, tu_user: User): assert username.text.strip() == user.Username -def test_post_accounts_account_type(client: TestClient, user: User, - tu_user: User): +def test_post_accounts_account_type(client: TestClient, user: User, tu_user: User): # Check the different account type options. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} # Make a user with the "User" role here so we can # test the `u` parameter. - account_type = query(AccountType, - AccountType.AccountType == "User").first() + account_type = query(AccountType, AccountType.AccountType == "User").first() with db.begin(): - create(User, Username="test_2", - Email="test_2@example.org", - RealName="Test User 2", - Passwd="testPassword", - AccountType=account_type) + create( + User, + Username="test_2", + Email="test_2@example.org", + RealName="Test User 2", + Passwd="testPassword", + AccountType=account_type, + ) # Expect no entries; we marked our only user as a User type. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "t"}) + response = request.post("/accounts", cookies=cookies, data={"T": "t"}) assert response.status_code == int(HTTPStatus.OK) assert len(get_rows(response.text)) == 0 # So, let's also ensure that specifying "u" returns our user. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "u"}) + response = request.post("/accounts", cookies=cookies, data={"T": "u"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1443,13 +1484,12 @@ def test_post_accounts_account_type(client: TestClient, user: User, # Set our only user to a Trusted User. with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == TRUSTED_USER_ID - ).first() + user.AccountType = ( + query(AccountType).filter(AccountType.ID == TRUSTED_USER_ID).first() + ) with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "t"}) + response = request.post("/accounts", cookies=cookies, data={"T": "t"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1461,13 +1501,12 @@ def test_post_accounts_account_type(client: TestClient, user: User, assert type.text.strip() == "Trusted User" with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == DEVELOPER_ID - ).first() + user.AccountType = ( + query(AccountType).filter(AccountType.ID == DEVELOPER_ID).first() + ) with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "d"}) + response = request.post("/accounts", cookies=cookies, data={"T": "d"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1479,13 +1518,12 @@ def test_post_accounts_account_type(client: TestClient, user: User, assert type.text.strip() == "Developer" with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == TRUSTED_USER_AND_DEV_ID - ).first() + user.AccountType = ( + query(AccountType).filter(AccountType.ID == TRUSTED_USER_AND_DEV_ID).first() + ) with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "td"}) + response = request.post("/accounts", cookies=cookies, data={"T": "td"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1517,8 +1555,7 @@ def test_post_accounts_status(client: TestClient, user: User, tu_user: User): user.Suspended = True with client as request: - response = request.post("/accounts", cookies=cookies, - data={"S": True}) + response = request.post("/accounts", cookies=cookies, data={"S": True}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1535,8 +1572,7 @@ def test_post_accounts_email(client: TestClient, user: User, tu_user: User): # Search via email. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"E": user.Email}) + response = request.post("/accounts", cookies=cookies, data={"E": user.Email}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1549,8 +1585,7 @@ def test_post_accounts_realname(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, - data={"R": user.RealName}) + response = request.post("/accounts", cookies=cookies, data={"R": user.RealName}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1563,8 +1598,7 @@ def test_post_accounts_irc(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, - data={"I": user.IRCNick}) + response = request.post("/accounts", cookies=cookies, data={"I": user.IRCNick}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1589,22 +1623,19 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): first_rows = rows with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "u"}) + response = request.post("/accounts", cookies=cookies, data={"SB": "u"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 def compare_text_values(column, lhs, rhs): - return [row[column].text for row in lhs] \ - == [row[column].text for row in rhs] + return [row[column].text for row in lhs] == [row[column].text for row in rhs] # Test the username rows are ordered the same. assert compare_text_values(0, first_rows, rows) is True with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "i"}) + response = request.post("/accounts", cookies=cookies, data={"SB": "i"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1614,8 +1645,7 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Sort by "i" -> RealName. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "r"}) + response = request.post("/accounts", cookies=cookies, data={"SB": "r"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1624,9 +1654,9 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): assert compare_text_values(4, first_rows, reversed(rows)) is True with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == TRUSTED_USER_AND_DEV_ID - ).first() + user.AccountType = ( + query(AccountType).filter(AccountType.ID == TRUSTED_USER_AND_DEV_ID).first() + ) # Fetch first_rows again with our new AccountType ordering. with client as request: @@ -1638,8 +1668,7 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Sort by "t" -> AccountType. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "t"}) + response = request.post("/accounts", cookies=cookies, data={"SB": "t"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1657,8 +1686,7 @@ def test_post_accounts_pgp_key(client: TestClient, user: User, tu_user: User): # Search via PGPKey. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"K": user.PGPKey}) + response = request.post("/accounts", cookies=cookies, data={"K": user.PGPKey}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1668,15 +1696,17 @@ def test_post_accounts_pgp_key(client: TestClient, user: User, tu_user: User): def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): # Create 150 users. users = [user] - account_type = query(AccountType, - AccountType.AccountType == "User").first() + account_type = query(AccountType, AccountType.AccountType == "User").first() with db.begin(): for i in range(150): - _user = create(User, Username=f"test_#{i}", - Email=f"test_#{i}@example.org", - RealName=f"Test User #{i}", - Passwd="testPassword", - AccountType=account_type) + _user = create( + User, + Username=f"test_#{i}", + Email=f"test_#{i}@example.org", + RealName=f"Test User #{i}", + Passwd="testPassword", + AccountType=account_type, + ) users.append(_user) sid = user.login(Request(), "testPassword") @@ -1709,8 +1739,9 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): assert "disabled" not in page_next.attrib with client as request: - response = request.post("/accounts", cookies=cookies, - data={"O": 50}) # +50 offset. + response = request.post( + "/accounts", cookies=cookies, data={"O": 50} + ) # +50 offset. assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1724,8 +1755,9 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): assert username.text.strip() == _user.Username with client as request: - response = request.post("/accounts", cookies=cookies, - data={"O": 101}) # Last page. + response = request.post( + "/accounts", cookies=cookies, data={"O": 101} + ) # Last page. assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1741,8 +1773,9 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): def test_get_terms_of_service(client: TestClient, user: User): with db.begin(): - term = create(Term, Description="Test term.", - URL="http://localhost", Revision=1) + term = create( + Term, Description="Test term.", URL="http://localhost", Revision=1 + ) with client as request: response = request.get("/tos", allow_redirects=False) @@ -1764,8 +1797,9 @@ def test_get_terms_of_service(client: TestClient, user: User): assert response.status_code == int(HTTPStatus.OK) with db.begin(): - accepted_term = create(AcceptedTerm, User=user, - Term=term, Revision=term.Revision) + accepted_term = create( + AcceptedTerm, User=user, Term=term, Revision=term.Revision + ) with client as request: response = request.get("/tos", cookies=cookies, allow_redirects=False) @@ -1800,8 +1834,9 @@ def test_post_terms_of_service(client: TestClient, user: User): # Create a fresh Term. with db.begin(): - term = create(Term, Description="Test term.", - URL="http://localhost", Revision=1) + term = create( + Term, Description="Test term.", URL="http://localhost", Revision=1 + ) # Test that the term we just created is listed. with client as request: @@ -1810,8 +1845,7 @@ def test_post_terms_of_service(client: TestClient, user: User): # Make a POST request to /tos with the agree checkbox disabled (False). with client as request: - response = request.post("/tos", data={"accept": False}, - cookies=cookies) + response = request.post("/tos", data={"accept": False}, cookies=cookies) assert response.status_code == int(HTTPStatus.OK) # Make a POST request to /tos with the agree checkbox enabled (True). @@ -1820,8 +1854,7 @@ def test_post_terms_of_service(client: TestClient, user: User): assert response.status_code == int(HTTPStatus.SEE_OTHER) # Query the db for the record created by the post request. - accepted_term = query(AcceptedTerm, - AcceptedTerm.TermsID == term.ID).first() + accepted_term = query(AcceptedTerm, AcceptedTerm.TermsID == term.ID).first() assert accepted_term.User == user assert accepted_term.Term == term diff --git a/test/test_adduser.py b/test/test_adduser.py index 65968d40..2cb71f3b 100644 --- a/test/test_adduser.py +++ b/test/test_adduser.py @@ -3,16 +3,17 @@ from unittest import mock import pytest import aurweb.models.account_type as at - from aurweb import db from aurweb.models import User from aurweb.scripts import adduser from aurweb.testing.requests import Request -TEST_SSH_PUBKEY = ("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI" - "bmlzdHAyNTYAAABBBEURnkiY6JoLyqDE8Li1XuAW+LHmkmLDMW/GL5wY" - "7k4/A+Ta7bjA3MOKrF9j4EuUTvCuNXULxvpfSqheTFWZc+g= " - "kevr@volcano") +TEST_SSH_PUBKEY = ( + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI" + "bmlzdHAyNTYAAABBBEURnkiY6JoLyqDE8Li1XuAW+LHmkmLDMW/GL5wY" + "7k4/A+Ta7bjA3MOKrF9j4EuUTvCuNXULxvpfSqheTFWZc+g= " + "kevr@volcano" +) @pytest.fixture(autouse=True) @@ -38,18 +39,36 @@ def test_adduser(): def test_adduser_tu(): - run_main([ - "-u", "test", "-e", "test@example.org", "-p", "abcd1234", - "-t", at.TRUSTED_USER - ]) + run_main( + [ + "-u", + "test", + "-e", + "test@example.org", + "-p", + "abcd1234", + "-t", + at.TRUSTED_USER, + ] + ) test = db.query(User).filter(User.Username == "test").first() assert test is not None assert test.AccountTypeID == at.TRUSTED_USER_ID def test_adduser_ssh_pk(): - run_main(["-u", "test", "-e", "test@example.org", "-p", "abcd1234", - "--ssh-pubkey", TEST_SSH_PUBKEY]) + run_main( + [ + "-u", + "test", + "-e", + "test@example.org", + "-p", + "abcd1234", + "--ssh-pubkey", + TEST_SSH_PUBKEY, + ] + ) test = db.query(User).filter(User.Username == "test").first() assert test is not None assert TEST_SSH_PUBKEY.startswith(test.ssh_pub_keys.first().PubKey) diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py index 82805ecf..c67aa57d 100644 --- a/test/test_api_rate_limit.py +++ b/test/test_api_rate_limit.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -13,8 +12,7 @@ def setup(db_test): def test_api_rate_key_creation(): with db.begin(): - rate = db.create(ApiRateLimit, IP="127.0.0.1", Requests=10, - WindowStart=1) + rate = db.create(ApiRateLimit, IP="127.0.0.1", Requests=10, WindowStart=1) assert rate.IP == "127.0.0.1" assert rate.Requests == 10 assert rate.WindowStart == 1 diff --git a/test/test_asgi.py b/test/test_asgi.py index c693a3a9..6ff80fa3 100644 --- a/test/test_asgi.py +++ b/test/test_asgi.py @@ -1,20 +1,17 @@ import http import os import re - from typing import Callable from unittest import mock import fastapi import pytest - from fastapi import HTTPException from fastapi.testclient import TestClient import aurweb.asgi import aurweb.config import aurweb.redis - from aurweb.exceptions import handle_form_exceptions from aurweb.testing.requests import Request @@ -33,7 +30,9 @@ def mock_glab_request(monkeypatch): if side_effect: return side_effect # pragma: no cover return return_value + monkeypatch.setattr("requests.post", what_to_return) + return wrapped @@ -47,13 +46,14 @@ def mock_glab_config(project: str = "test/project", token: str = "test-token"): elif key == "error-token": return token return config_get(section, key) + return wrapper @pytest.mark.asyncio async def test_asgi_startup_session_secret_exception(monkeypatch): - """ Test that we get an IOError on app_startup when we cannot - connect to options.redis_address. """ + """Test that we get an IOError on app_startup when we cannot + connect to options.redis_address.""" redis_addr = aurweb.config.get("options", "redis_address") @@ -110,8 +110,9 @@ async def test_asgi_app_disabled_metrics(caplog: pytest.LogCaptureFixture): with mock.patch.dict(os.environ, env): await aurweb.asgi.app_startup() - expected = ("$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " - "endpoint is disabled.") + expected = ( + "$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " "endpoint is disabled." + ) assert expected in caplog.text @@ -134,9 +135,12 @@ class FakeResponse: self.text = text -def test_internal_server_error_bad_glab(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error_bad_glab( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.get("/internal_server_error") async def internal_server_error(request: fastapi.Request): raise ValueError("test exception") @@ -154,9 +158,12 @@ def test_internal_server_error_bad_glab(setup: None, use_traceback: None, assert re.search(expr, caplog.text) -def test_internal_server_error_no_token(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error_no_token( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.get("/internal_server_error") async def internal_server_error(request: fastapi.Request): raise ValueError("test exception") @@ -175,9 +182,12 @@ def test_internal_server_error_no_token(setup: None, use_traceback: None, assert re.search(expr, caplog.text) -def test_internal_server_error(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.get("/internal_server_error") async def internal_server_error(request: fastapi.Request): raise ValueError("test exception") @@ -203,9 +213,12 @@ def test_internal_server_error(setup: None, use_traceback: None, assert "FATAL" not in caplog.text -def test_internal_server_error_post(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error_post( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.post("/internal_server_error") @handle_form_exceptions async def internal_server_error(request: fastapi.Request): diff --git a/test/test_aurblup.py b/test/test_aurblup.py index 0b499d57..93a832f9 100644 --- a/test/test_aurblup.py +++ b/test/test_aurblup.py @@ -1,5 +1,4 @@ import tempfile - from unittest import mock import py @@ -32,7 +31,7 @@ def setup(db_test, alpm_db: AlpmDatabase, tempdir: py.path.local) -> None: if key == "db-path": return alpm_db.local elif key == "server": - return f'file://{alpm_db.remote}' + return f"file://{alpm_db.remote}" elif key == "sync-dbs": return alpm_db.repo return value @@ -51,8 +50,7 @@ def test_aurblup(alpm_db: AlpmDatabase): # Test that the package got added to the database. for name in ("pkg", "pkg2"): - pkg = db.query(OfficialProvider).filter( - OfficialProvider.Name == name).first() + pkg = db.query(OfficialProvider).filter(OfficialProvider.Name == name).first() assert pkg is not None # Test that we can remove the package. @@ -62,11 +60,9 @@ def test_aurblup(alpm_db: AlpmDatabase): aurblup.main(True) # Expect that the database got updated accordingly. - pkg = db.query(OfficialProvider).filter( - OfficialProvider.Name == "pkg").first() + pkg = db.query(OfficialProvider).filter(OfficialProvider.Name == "pkg").first() assert pkg is None - pkg2 = db.query(OfficialProvider).filter( - OfficialProvider.Name == "pkg2").first() + pkg2 = db.query(OfficialProvider).filter(OfficialProvider.Name == "pkg2").first() assert pkg2 is not None @@ -78,14 +74,16 @@ def test_aurblup_cleanup(alpm_db: AlpmDatabase): # Now, let's insert an OfficialPackage that doesn't exist, # then exercise the old provider deletion path. with db.begin(): - db.create(OfficialProvider, Name="fake package", - Repo="test", Provides="package") + db.create( + OfficialProvider, Name="fake package", Repo="test", Provides="package" + ) # Run aurblup again. aurblup.main() # Expect that the fake package got deleted because it's # not in alpm_db anymore. - providers = db.query(OfficialProvider).filter( - OfficialProvider.Name == "fake package").all() + providers = ( + db.query(OfficialProvider).filter(OfficialProvider.Name == "fake package").all() + ) assert len(providers) == 0 diff --git a/test/test_auth.py b/test/test_auth.py index b8221c19..4a4318e8 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -1,11 +1,15 @@ import fastapi import pytest - from fastapi import HTTPException from sqlalchemy.exc import IntegrityError from aurweb import config, db, time -from aurweb.auth import AnonymousUser, BasicAuthBackend, _auth_required, account_type_required +from aurweb.auth import ( + AnonymousUser, + BasicAuthBackend, + _auth_required, + account_type_required, +) from aurweb.models.account_type import USER, USER_ID from aurweb.models.session import Session from aurweb.models.user import User @@ -20,9 +24,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.com", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.com", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -55,8 +64,7 @@ async def test_auth_backend_invalid_user_id(): # Create a new session with a fake user id. now_ts = time.utcnow() with pytest.raises(IntegrityError): - Session(UsersID=666, SessionID="realSession", - LastUpdateTS=now_ts + 5) + Session(UsersID=666, SessionID="realSession", LastUpdateTS=now_ts + 5) @pytest.mark.asyncio @@ -65,8 +73,9 @@ async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): # equal the real_user. now_ts = time.utcnow() with db.begin(): - db.create(Session, UsersID=user.ID, SessionID="realSession", - LastUpdateTS=now_ts + 5) + db.create( + Session, UsersID=user.ID, SessionID="realSession", LastUpdateTS=now_ts + 5 + ) request = Request() request.cookies["AURSID"] = "realSession" @@ -76,7 +85,7 @@ async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): @pytest.mark.asyncio async def test_expired_session(backend: BasicAuthBackend, user: User): - """ Login, expire the session manually, then authenticate. """ + """Login, expire the session manually, then authenticate.""" # First, build a Request with a logged in user. request = Request() request.user = user @@ -115,8 +124,8 @@ async def test_auth_required_redirection_bad_referrer(): def test_account_type_required(): - """ This test merely asserts that a few different paths - do not raise exceptions. """ + """This test merely asserts that a few different paths + do not raise exceptions.""" # This one shouldn't raise. account_type_required({USER}) @@ -125,7 +134,7 @@ def test_account_type_required(): # But this one should! We have no "FAKE" key. with pytest.raises(KeyError): - account_type_required({'FAKE'}) + account_type_required({"FAKE"}) def test_is_trusted_user(): diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 5942edcf..87ad86f6 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -1,14 +1,11 @@ import re - from http import HTTPStatus from unittest import mock import pytest - from fastapi.testclient import TestClient import aurweb.config - from aurweb import db, time from aurweb.asgi import app from aurweb.models.account_type import USER_ID @@ -42,39 +39,41 @@ def client() -> TestClient: @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=TEST_USERNAME, + Email=TEST_EMAIL, + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user def test_login_logout(client: TestClient, user: User): - post_data = { - "user": "test", - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": "test", "passwd": "testPassword", "next": "/"} with client as request: # First, let's test get /login. response = request.get("/login") assert response.status_code == int(HTTPStatus.OK) - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) # Simulate following the redirect location from above's response. response = request.get(response.headers.get("location")) assert response.status_code == int(HTTPStatus.OK) - response = request.post("/logout", data=post_data, - allow_redirects=False) + response = request.post("/logout", data=post_data, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) - response = request.post("/logout", data=post_data, cookies={ - "AURSID": response.cookies.get("AURSID") - }, allow_redirects=False) + response = request.post( + "/logout", + data=post_data, + cookies={"AURSID": response.cookies.get("AURSID")}, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" not in response.cookies @@ -84,11 +83,7 @@ def test_login_suspended(client: TestClient, user: User): with db.begin(): user.Suspended = 1 - data = { - "user": user.Username, - "passwd": "testPassword", - "next": "/" - } + data = {"user": user.Username, "passwd": "testPassword", "next": "/"} with client as request: resp = request.post("/login", data=data) errors = get_errors(resp.text) @@ -96,23 +91,17 @@ def test_login_suspended(client: TestClient, user: User): def test_login_email(client: TestClient, user: user): - post_data = { - "user": user.Email, - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": user.Email, "passwd": "testPassword", "next": "/"} with client as request: - resp = request.post("/login", data=post_data, - allow_redirects=False) + resp = request.post("/login", data=post_data, allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" in resp.cookies def mock_getboolean(**overrided_configs): mocked_config = { - tuple(config.split("__")): value - for config, value in overrided_configs.items() + tuple(config.split("__")): value for config, value in overrided_configs.items() } def side_effect(*args): @@ -123,19 +112,14 @@ def mock_getboolean(**overrided_configs): @mock.patch( "aurweb.config.getboolean", - side_effect=mock_getboolean(options__disable_http_login=False) + side_effect=mock_getboolean(options__disable_http_login=False), ) def test_insecure_login(getboolean: mock.Mock, client: TestClient, user: User): - post_data = { - "user": user.Username, - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": user.Username, "passwd": "testPassword", "next": "/"} # Perform a login request with the data matching our user. with client as request: - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data, allow_redirects=False) # Make sure we got the expected status out of it. assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -152,17 +136,17 @@ def test_insecure_login(getboolean: mock.Mock, client: TestClient, user: User): @mock.patch( "aurweb.config.getboolean", - side_effect=mock_getboolean(options__disable_http_login=True) + side_effect=mock_getboolean(options__disable_http_login=True), ) def test_secure_login(getboolean: mock.Mock, client: TestClient, user: User): - """ In this test, we check to verify the course of action taken + """In this test, we check to verify the course of action taken by starlette when providing secure=True to a response cookie. This is achieved by mocking aurweb.config.getboolean to return True (or 1) when looking for `options.disable_http_login`. When we receive a response with `disable_http_login` enabled, we check the fields in cookies received for the secure and httponly fields, in addition to the rest of the fields given - on such a request. """ + on such a request.""" # Create a local TestClient here since we mocked configuration. # client = TestClient(app) @@ -172,16 +156,11 @@ def test_secure_login(getboolean: mock.Mock, client: TestClient, user: User): # client.headers.update(TEST_REFERER) # Data used for our upcoming http post request. - post_data = { - "user": user.Username, - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": user.Username, "passwd": "testPassword", "next": "/"} # Perform a login request with the data matching our user. with client as request: - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data, allow_redirects=False) # Make sure we got the expected status out of it. assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -203,16 +182,11 @@ def test_secure_login(getboolean: mock.Mock, client: TestClient, user: User): def test_authenticated_login(client: TestClient, user: User): - post_data = { - "user": user.Username, - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": user.Username, "passwd": "testPassword", "next": "/"} with client as request: # Try to login. - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -220,8 +194,9 @@ def test_authenticated_login(client: TestClient, user: User): # when requesting GET /login as an authenticated user. # Now, let's verify that we receive 403 Forbidden when we # try to get /login as an authenticated user. - response = request.get("/login", cookies=response.cookies, - allow_redirects=False) + response = request.get( + "/login", cookies=response.cookies, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) assert "Logged-in as: test" in response.text @@ -236,10 +211,7 @@ def test_unauthenticated_logout_unauthorized(client: TestClient): def test_login_missing_username(client: TestClient): - post_data = { - "passwd": "testPassword", - "next": "/" - } + post_data = {"passwd": "testPassword", "next": "/"} with client as request: response = request.post("/login", data=post_data) @@ -256,17 +228,15 @@ def test_login_remember_me(client: TestClient, user: User): "user": "test", "passwd": "testPassword", "next": "/", - "remember_me": True + "remember_me": True, } with client as request: - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" in response.cookies - cookie_timeout = aurweb.config.getint( - "options", "persistent_cookie_timeout") + cookie_timeout = aurweb.config.getint("options", "persistent_cookie_timeout") now_ts = time.utcnow() session = db.query(Session).filter(Session.UsersID == user.ID).first() @@ -280,7 +250,7 @@ def test_login_incorrect_password_remember_me(client: TestClient, user: User): "user": "test", "passwd": "badPassword", "next": "/", - "remember_me": "on" + "remember_me": "on", } with client as request: @@ -295,10 +265,7 @@ def test_login_incorrect_password_remember_me(client: TestClient, user: User): def test_login_missing_password(client: TestClient): - post_data = { - "user": "test", - "next": "/" - } + post_data = {"user": "test", "next": "/"} with client as request: response = request.post("/login", data=post_data) @@ -310,11 +277,7 @@ def test_login_missing_password(client: TestClient): def test_login_incorrect_password(client: TestClient): - post_data = { - "user": "test", - "passwd": "badPassword", - "next": "/" - } + post_data = {"user": "test", "passwd": "badPassword", "next": "/"} with client as request: response = request.post("/login", data=post_data) @@ -350,8 +313,9 @@ def test_login_bad_referer(client: TestClient): assert "AURSID" not in response.cookies -def test_generate_unique_sid_exhausted(client: TestClient, user: User, - caplog: pytest.LogCaptureFixture): +def test_generate_unique_sid_exhausted( + client: TestClient, user: User, caplog: pytest.LogCaptureFixture +): """ In this test, we mock up generate_unique_sid() to infinitely return the same SessionID given to `user`. Within that mocking, we try @@ -364,13 +328,17 @@ def test_generate_unique_sid_exhausted(client: TestClient, user: User, now = time.utcnow() with db.begin(): # Create a second user; we'll login with this one. - user2 = db.create(User, Username="test2", Email="test2@example.org", - ResetKey="testReset", Passwd="testPassword", - AccountTypeID=USER_ID) + user2 = db.create( + User, + Username="test2", + Email="test2@example.org", + ResetKey="testReset", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) # Create a session with ID == "testSession" for `user`. - db.create(Session, User=user, SessionID="testSession", - LastUpdateTS=now) + db.create(Session, User=user, SessionID="testSession", LastUpdateTS=now) # Mock out generate_unique_sid; always return "testSession" which # causes us to eventually error out and raise an internal error. diff --git a/test/test_ban.py b/test/test_ban.py index ff49f7e2..9db62296 100644 --- a/test/test_ban.py +++ b/test/test_ban.py @@ -1,9 +1,7 @@ import warnings - from datetime import datetime, timedelta import pytest - from sqlalchemy import exc as sa_exc from aurweb import db diff --git a/test/test_cache.py b/test/test_cache.py index b49ee386..83a9755a 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -11,7 +11,7 @@ def setup(db_test): class StubRedis: - """ A class which acts as a RedisConnection without using Redis. """ + """A class which acts as a RedisConnection without using Redis.""" cache = dict() expires = dict() @@ -39,10 +39,13 @@ def redis(): @pytest.mark.asyncio async def test_db_count_cache(redis): - db.create(User, Username="user1", - Email="user1@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + db.create( + User, + Username="user1", + Email="user1@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) query = db.query(User) @@ -57,10 +60,13 @@ async def test_db_count_cache(redis): @pytest.mark.asyncio async def test_db_count_cache_expires(redis): - db.create(User, Username="user1", - Email="user1@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + db.create( + User, + Username="user1", + Email="user1@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) query = db.query(User) diff --git a/test/test_captcha.py b/test/test_captcha.py index e5f8c71a..fa6fcbcc 100644 --- a/test/test_captcha.py +++ b/test/test_captcha.py @@ -11,14 +11,14 @@ def setup(db_test): def test_captcha_salts(): - """ Make sure we can get some captcha salts. """ + """Make sure we can get some captcha salts.""" salts = captcha.get_captcha_salts() assert len(salts) == 6 def test_captcha_token(): - """ Make sure getting a captcha salt's token matches up against - the first three digits of the md5 hash of the salt. """ + """Make sure getting a captcha salt's token matches up against + the first three digits of the md5 hash of the salt.""" salts = captcha.get_captcha_salts() salt = salts[0] @@ -29,9 +29,9 @@ def test_captcha_token(): def test_captcha_challenge_answer(): - """ Make sure that executing the captcha challenge via shell + """Make sure that executing the captcha challenge via shell produces the correct result by comparing it against a straight - up token conversion. """ + up token conversion.""" salts = captcha.get_captcha_salts() salt = salts[0] @@ -44,7 +44,7 @@ def test_captcha_challenge_answer(): def test_captcha_salt_filter(): - """ Make sure captcha_salt_filter returns the first salt from + """Make sure captcha_salt_filter returns the first salt from get_captcha_salts(). Example usage: @@ -55,7 +55,7 @@ def test_captcha_salt_filter(): def test_captcha_cmdline_filter(): - """ Make sure that the captcha_cmdline filter gives us the + """Make sure that the captcha_cmdline filter gives us the same challenge that get_captcha_challenge does. Example usage: diff --git a/test/test_config.py b/test/test_config.py index f451d8b3..c7a3610e 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -2,7 +2,6 @@ import configparser import io import os import re - from unittest import mock import py @@ -35,6 +34,7 @@ def mock_config_get(): if option == "salt_rounds": return "666" return config_get(section, option) + return _mock_config_get @@ -59,7 +59,7 @@ def test_config_main_get_unknown_section(get: str): main() # With an invalid section, we should get a usage error. - expected = r'^error: no section found$' + expected = r"^error: no section found$" assert re.match(expected, stderr.getvalue().strip()) @@ -140,8 +140,7 @@ def test_config_main_set_immutable(): args = ["aurweb-config", "set", "options", "salt_rounds", "666"] with mock.patch.dict(os.environ, {"AUR_CONFIG_IMMUTABLE": "1"}): with mock.patch("sys.argv", args): - with mock.patch("aurweb.config.set_option", - side_effect=mock_set_option): + with mock.patch("aurweb.config.set_option", side_effect=mock_set_option): main() expected = None @@ -170,8 +169,7 @@ def test_config_main_set_unknown_section(save: None): args = ["aurweb-config", "set", "options", "salt_rounds", "666"] with mock.patch("sys.argv", args): with mock.patch("sys.stderr", stderr): - with mock.patch("aurweb.config.set_option", - side_effect=mock_set_option): + with mock.patch("aurweb.config.set_option", side_effect=mock_set_option): main() assert stderr.getvalue().strip() == "error: no section found" diff --git a/test/test_db.py b/test/test_db.py index f36fff2c..8ac5607d 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -2,26 +2,26 @@ import os import re import sqlite3 import tempfile - from unittest import mock import pytest import aurweb.config import aurweb.initdb - from aurweb import db from aurweb.models.account_type import AccountType class Args: - """ Stub arguments used for running aurweb.initdb. """ + """Stub arguments used for running aurweb.initdb.""" + use_alembic = True verbose = True class DBCursor: - """ A fake database cursor object used in tests. """ + """A fake database cursor object used in tests.""" + items = [] def execute(self, *args, **kwargs): @@ -33,7 +33,8 @@ class DBCursor: class DBConnection: - """ A fake database connection object used in tests. """ + """A fake database connection object used in tests.""" + @staticmethod def cursor(): return DBCursor() @@ -44,7 +45,7 @@ class DBConnection: def make_temp_config(*replacements): - """ Generate a temporary config file with a set of replacements. + """Generate a temporary config file with a set of replacements. :param *replacements: A variable number of tuple regex replacement pairs :return: A tuple containing (temp directory, temp config file) @@ -85,13 +86,16 @@ def make_temp_config(*replacements): def make_temp_sqlite_config(): - return make_temp_config((r"backend = .*", "backend = sqlite"), - (r"name = .*", "name = /tmp/aurweb.sqlite3")) + return make_temp_config( + (r"backend = .*", "backend = sqlite"), + (r"name = .*", "name = /tmp/aurweb.sqlite3"), + ) def make_temp_mysql_config(): - return make_temp_config((r"backend = .*", "backend = mysql"), - (r"name = .*", "name = aurweb_test")) + return make_temp_config( + (r"backend = .*", "backend = mysql"), (r"name = .*", "name = aurweb_test") + ) @pytest.fixture(autouse=True) @@ -150,7 +154,7 @@ def test_sqlalchemy_unknown_backend(): def test_db_connects_without_fail(): - """ This only tests the actual config supplied to pytest. """ + """This only tests the actual config supplied to pytest.""" db.connect() diff --git a/test/test_dependency_type.py b/test/test_dependency_type.py index c5afd38d..e172782b 100644 --- a/test/test_dependency_type.py +++ b/test/test_dependency_type.py @@ -12,8 +12,7 @@ def setup(db_test): def test_dependency_types(): dep_types = ["depends", "makedepends", "checkdepends", "optdepends"] for dep_type in dep_types: - dependency_type = query(DependencyType, - DependencyType.Name == dep_type).first() + dependency_type = query(DependencyType, DependencyType.Name == dep_type).first() assert dependency_type is not None diff --git a/test/test_email.py b/test/test_email.py index 873feffe..81abd507 100644 --- a/test/test_email.py +++ b/test/test_email.py @@ -1,5 +1,4 @@ import io - from subprocess import PIPE, Popen import pytest @@ -23,7 +22,7 @@ def sendmail(from_: str, to_: str, content: str) -> Email: def test_email_glue(): - """ Test that Email.glue() decodes both base64 and decoded content. """ + """Test that Email.glue() decodes both base64 and decoded content.""" body = "Test email." sendmail("test@example.org", "test@example.org", body) assert Email.count() == 1 @@ -34,7 +33,7 @@ def test_email_glue(): def test_email_dump(): - """ Test that Email.dump() dumps a single email. """ + """Test that Email.dump() dumps a single email.""" body = "Test email." sendmail("test@example.org", "test@example.org", body) assert Email.count() == 1 @@ -46,7 +45,7 @@ def test_email_dump(): def test_email_dump_multiple(): - """ Test that Email.dump() dumps multiple emails. """ + """Test that Email.dump() dumps multiple emails.""" body = "Test email." sendmail("test@example.org", "test@example.org", body) sendmail("test2@example.org", "test2@example.org", body) diff --git a/test/test_filelock.py b/test/test_filelock.py index 70aa7580..c0580642 100644 --- a/test/test_filelock.py +++ b/test/test_filelock.py @@ -1,5 +1,4 @@ import py - from _pytest.logging import LogCaptureFixture from aurweb.testing.filelock import FileLock diff --git a/test/test_filters.py b/test/test_filters.py index 558911f5..e74ddb87 100644 --- a/test/test_filters.py +++ b/test/test_filters.py @@ -22,7 +22,7 @@ def test_number_format(): def test_extend_query(): - """ Test extension of a query via extend_query. """ + """Test extension of a query via extend_query.""" query = {"a": "b"} extended = filters.extend_query(query, ("a", "c"), ("b", "d")) assert extended.get("a") == "c" @@ -30,7 +30,7 @@ def test_extend_query(): def test_to_qs(): - """ Test conversion from a query dictionary to a query string. """ + """Test conversion from a query dictionary to a query string.""" query = {"a": "b", "c": [1, 2, 3]} qs = filters.to_qs(query) assert qs == "a=b&c=1&c=2&c=3" diff --git a/test/test_group.py b/test/test_group.py index 82b82464..a1c563b6 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db diff --git a/test/test_homepage.py b/test/test_homepage.py index 63b832e3..5490a244 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -1,10 +1,8 @@ import re - from http import HTTPStatus from unittest.mock import patch import pytest - from fastapi.testclient import TestClient from aurweb import db, time @@ -31,16 +29,26 @@ def setup(db_test): @pytest.fixture def user(): with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user2(): with db.begin(): - user = db.create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -50,10 +58,17 @@ def redis(): def delete_keys(): # Cleanup keys if they exist. - for key in ("package_count", "orphan_count", "user_count", - "trusted_user_count", "seven_days_old_added", - "seven_days_old_updated", "year_old_updated", - "never_updated", "package_updates"): + for key in ( + "package_count", + "orphan_count", + "user_count", + "trusted_user_count", + "seven_days_old_added", + "seven_days_old_updated", + "year_old_updated", + "never_updated", + "package_updates", + ): if redis.get(key) is not None: redis.delete(key) @@ -66,16 +81,21 @@ def redis(): def package(user: User) -> Package: now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="test-pkg", - Maintainer=user, Packager=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="test-pkg", + Maintainer=user, + Packager=user, + SubmittedTS=now, + ModifiedTS=now, + ) pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield pkg @pytest.fixture def packages(user): - """ Yield a list of num_packages Package objects maintained by user. """ + """Yield a list of num_packages Package objects maintained by user.""" num_packages = 50 # Tunable # For i..num_packages, create a package named pkg_{i}. @@ -83,9 +103,14 @@ def packages(user): now = time.utcnow() with db.begin(): for i in range(num_packages): - pkgbase = db.create(PackageBase, Name=f"pkg_{i}", - Maintainer=user, Packager=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=user, + Packager=user, + SubmittedTS=now, + ModifiedTS=now, + ) pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) pkgs.append(pkg) now += 1 @@ -99,9 +124,9 @@ def test_homepage(): assert response.status_code == int(HTTPStatus.OK) -@patch('aurweb.util.get_ssh_fingerprints') +@patch("aurweb.util.get_ssh_fingerprints") def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock): - fingerprints = {'Ed25519': "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"} + fingerprints = {"Ed25519": "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"} get_ssh_fingerprints_mock.return_value = fingerprints with client as request: @@ -110,17 +135,23 @@ def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock): for key, value in fingerprints.items(): assert key in response.content.decode() assert value in response.content.decode() - assert 'The following SSH fingerprints are used for the AUR' in response.content.decode() + assert ( + "The following SSH fingerprints are used for the AUR" + in response.content.decode() + ) -@patch('aurweb.util.get_ssh_fingerprints') +@patch("aurweb.util.get_ssh_fingerprints") def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock): get_ssh_fingerprints_mock.return_value = {} with client as request: response = request.get("/") - assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode() + assert ( + "The following SSH fingerprints are used for the AUR" + not in response.content.decode() + ) def test_homepage_stats(redis, packages): @@ -131,20 +162,20 @@ def test_homepage_stats(redis, packages): root = parse_root(response.text) expectations = [ - ("Packages", r'\d+'), - ("Orphan Packages", r'\d+'), - ("Packages added in the past 7 days", r'\d+'), - ("Packages updated in the past 7 days", r'\d+'), - ("Packages updated in the past year", r'\d+'), - ("Packages never updated", r'\d+'), - ("Registered Users", r'\d+'), - ("Trusted Users", r'\d+') + ("Packages", r"\d+"), + ("Orphan Packages", r"\d+"), + ("Packages added in the past 7 days", r"\d+"), + ("Packages updated in the past 7 days", r"\d+"), + ("Packages updated in the past year", r"\d+"), + ("Packages never updated", r"\d+"), + ("Registered Users", r"\d+"), + ("Trusted Users", r"\d+"), ] stats = root.xpath('//div[@id="pkg-stats"]//tr') for i, expected in enumerate(expectations): expected_key, expected_regex = expected - key, value = stats[i].xpath('./td') + key, value = stats[i].xpath("./td") assert key.text.strip() == expected_key assert re.match(expected_regex, value.text.strip()) @@ -165,7 +196,7 @@ def test_homepage_updates(redis, packages): expectations = [f"pkg_{i}" for i in range(50 - 1, 50 - 1 - 15, -1)] updates = root.xpath('//div[@id="pkg-updates"]/table/tbody/tr') for i, expected in enumerate(expectations): - pkgname = updates[i].xpath('./td/a').pop(0) + pkgname = updates[i].xpath("./td/a").pop(0) assert pkgname.text.strip() == expected @@ -173,9 +204,9 @@ def test_homepage_dashboard(redis, packages, user): # Create Comaintainer records for all of the packages. with db.begin(): for pkg in packages: - db.create(PackageComaintainer, - PackageBase=pkg.PackageBase, - User=user, Priority=1) + db.create( + PackageComaintainer, PackageBase=pkg.PackageBase, User=user, Priority=1 + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -189,16 +220,18 @@ def test_homepage_dashboard(redis, packages, user): expectations = [f"pkg_{i}" for i in range(50 - 1, 0, -1)] my_packages = root.xpath('//table[@id="my-packages"]/tbody/tr') for i, expected in enumerate(expectations): - name, version, votes, pop, voted, notify, desc, maint \ - = my_packages[i].xpath('./td') - assert name.xpath('./a').pop(0).text.strip() == expected + name, version, votes, pop, voted, notify, desc, maint = my_packages[i].xpath( + "./td" + ) + assert name.xpath("./a").pop(0).text.strip() == expected # Do the same for the Comaintained Packages table. my_packages = root.xpath('//table[@id="comaintained-packages"]/tbody/tr') for i, expected in enumerate(expectations): - name, version, votes, pop, voted, notify, desc, maint \ - = my_packages[i].xpath('./td') - assert name.xpath('./a').pop(0).text.strip() == expected + name, version, votes, pop, voted, notify, desc, maint = my_packages[i].xpath( + "./td" + ) + assert name.xpath("./a").pop(0).text.strip() == expected def test_homepage_dashboard_requests(redis, packages, user): @@ -207,11 +240,16 @@ def test_homepage_dashboard_requests(redis, packages, user): pkg = packages[0] reqtype = db.query(RequestType, RequestType.ID == DELETION_ID).first() with db.begin(): - pkgreq = db.create(PackageRequest, PackageBase=pkg.PackageBase, - PackageBaseName=pkg.PackageBase.Name, - User=user, Comments=str(), - ClosureComment=str(), RequestTS=now, - RequestType=reqtype) + pkgreq = db.create( + PackageRequest, + PackageBase=pkg.PackageBase, + PackageBaseName=pkg.PackageBase.Name, + User=user, + Comments=str(), + ClosureComment=str(), + RequestTS=now, + RequestType=reqtype, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -220,7 +258,7 @@ def test_homepage_dashboard_requests(redis, packages, user): root = parse_root(response.text) request = root.xpath('//table[@id="pkgreq-results"]/tbody/tr').pop(0) - pkgname = request.xpath('./td/a').pop(0) + pkgname = request.xpath("./td/a").pop(0) assert pkgname.text.strip() == pkgreq.PackageBaseName @@ -238,7 +276,7 @@ def test_homepage_dashboard_flagged_packages(redis, packages, user): # Check to see that the package showed up in the Flagged Packages table. root = parse_root(response.text) flagged_pkg = root.xpath('//table[@id="flagged-packages"]/tbody/tr').pop(0) - flagged_name = flagged_pkg.xpath('./td/a').pop(0) + flagged_name = flagged_pkg.xpath("./td/a").pop(0) assert flagged_name.text.strip() == pkg.Name @@ -247,8 +285,7 @@ def test_homepage_dashboard_flagged(user: User, user2: User, package: Package): now = time.utcnow() with db.begin(): - db.create(PackageComaintainer, User=user2, - PackageBase=pkgbase, Priority=1) + db.create(PackageComaintainer, User=user2, PackageBase=pkgbase, Priority=1) pkgbase.OutOfDateTS = now - 5 pkgbase.Flagger = user diff --git a/test/test_html.py b/test/test_html.py index ffe2a9f2..88c75a7c 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -2,13 +2,11 @@ import hashlib import os import tempfile - from http import HTTPStatus from unittest import mock import fastapi import pytest - from fastapi import HTTPException from fastapi.testclient import TestClient @@ -33,8 +31,13 @@ def client() -> TestClient: @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -53,12 +56,7 @@ def pkgbase(user: User) -> PackageBase: def test_archdev_navbar(client: TestClient): - expected = [ - "AUR Home", - "Packages", - "Register", - "Login" - ] + expected = ["AUR Home", "Packages", "Register", "Login"] with client as request: resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) @@ -70,13 +68,7 @@ def test_archdev_navbar(client: TestClient): def test_archdev_navbar_authenticated(client: TestClient, user: User): - expected = [ - "Dashboard", - "Packages", - "Requests", - "My Account", - "Logout" - ] + expected = ["Dashboard", "Packages", "Requests", "My Account", "Logout"] cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: resp = request.get("/", cookies=cookies) @@ -88,8 +80,7 @@ def test_archdev_navbar_authenticated(client: TestClient, user: User): assert item.text.strip() == expected[i] -def test_archdev_navbar_authenticated_tu(client: TestClient, - trusted_user: User): +def test_archdev_navbar_authenticated_tu(client: TestClient, trusted_user: User): expected = [ "Dashboard", "Packages", @@ -97,7 +88,7 @@ def test_archdev_navbar_authenticated_tu(client: TestClient, "Accounts", "My Account", "Trusted User", - "Logout" + "Logout", ] cookies = {"AURSID": trusted_user.login(Request(), "testPassword")} with client as request: @@ -131,7 +122,7 @@ def test_get_successes(): def test_archive_sig(client: TestClient): - hash_value = hashlib.sha256(b'test').hexdigest() + hash_value = hashlib.sha256(b"test").hexdigest() with tempfile.TemporaryDirectory() as tmpdir: packages_sha256 = os.path.join(tmpdir, "packages.gz.sha256") @@ -179,12 +170,7 @@ def test_disabled_metrics(client: TestClient): def test_rtl(client: TestClient): responses = {} - expected = [ - [], - [], - ['rtl'], - ['rtl'] - ] + expected = [[], [], ["rtl"], ["rtl"]] with client as request: responses["default"] = request.get("/") responses["de"] = request.get("/", cookies={"AURLANG": "de"}) @@ -193,11 +179,11 @@ def test_rtl(client: TestClient): for i, (lang, resp) in enumerate(responses.items()): assert resp.status_code == int(HTTPStatus.OK) t = parse_root(resp.text) - assert t.xpath('//html/@dir') == expected[i] + assert t.xpath("//html/@dir") == expected[i] def test_404_with_valid_pkgbase(client: TestClient, pkgbase: PackageBase): - """ Test HTTPException with status_code == 404 and valid pkgbase. """ + """Test HTTPException with status_code == 404 and valid pkgbase.""" endpoint = f"/{pkgbase.Name}" with client as request: response = request.get(endpoint) @@ -209,7 +195,7 @@ def test_404_with_valid_pkgbase(client: TestClient, pkgbase: PackageBase): def test_404(client: TestClient): - """ Test HTTPException with status_code == 404 without a valid pkgbase. """ + """Test HTTPException with status_code == 404 without a valid pkgbase.""" with client as request: response = request.get("/nonexistentroute") assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -221,7 +207,8 @@ def test_404(client: TestClient): def test_503(client: TestClient): - """ Test HTTPException with status_code == 503 (Service Unavailable). """ + """Test HTTPException with status_code == 503 (Service Unavailable).""" + @asgi.app.get("/raise-503") async def raise_503(request: fastapi.Request): raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE) diff --git a/test/test_initdb.py b/test/test_initdb.py index 44681d8e..db5edf74 100644 --- a/test/test_initdb.py +++ b/test/test_initdb.py @@ -3,7 +3,6 @@ import pytest import aurweb.config import aurweb.db import aurweb.initdb - from aurweb.models.account_type import AccountType @@ -19,11 +18,11 @@ class Args: def test_run(): from aurweb.schema import metadata + aurweb.db.kill_engine() metadata.drop_all(aurweb.db.get_engine()) aurweb.initdb.run(Args()) # Check that constant table rows got added via initdb. - record = aurweb.db.query(AccountType, - AccountType.AccountType == "User").first() + record = aurweb.db.query(AccountType, AccountType.AccountType == "User").first() assert record is not None diff --git a/test/test_l10n.py b/test/test_l10n.py index c24c5f55..818d517f 100644 --- a/test/test_l10n.py +++ b/test/test_l10n.py @@ -4,13 +4,13 @@ from aurweb.testing.requests import Request def test_translator(): - """ Test creating l10n translation tools. """ + """Test creating l10n translation tools.""" de_home = l10n.translator.translate("Home", "de") assert de_home == "Startseite" def test_get_request_language(): - """ First, tests default_lang, then tests a modified AURLANG cookie. """ + """First, tests default_lang, then tests a modified AURLANG cookie.""" request = Request() assert l10n.get_request_language(request) == "en" @@ -19,18 +19,17 @@ def test_get_request_language(): def test_get_raw_translator_for_request(): - """ Make sure that get_raw_translator_for_request is giving us - the translator we expect. """ + """Make sure that get_raw_translator_for_request is giving us + the translator we expect.""" request = Request() request.cookies["AURLANG"] = "de" translator = l10n.get_raw_translator_for_request(request) - assert translator.gettext("Home") == \ - l10n.translator.translate("Home", "de") + assert translator.gettext("Home") == l10n.translator.translate("Home", "de") def test_get_translator_for_request(): - """ Make sure that get_translator_for_request is giving us back - our expected translation function. """ + """Make sure that get_translator_for_request is giving us back + our expected translation function.""" request = Request() request.cookies["AURLANG"] = "de" @@ -43,10 +42,8 @@ def test_tn_filter(): request.cookies["AURLANG"] = "en" context = {"language": "en", "request": request} - translated = filters.tn(context, 1, "%d package found.", - "%d packages found.") + translated = filters.tn(context, 1, "%d package found.", "%d packages found.") assert translated == "%d package found." - translated = filters.tn(context, 2, "%d package found.", - "%d packages found.") + translated = filters.tn(context, 2, "%d package found.", "%d packages found.") assert translated == "%d packages found." diff --git a/test/test_license.py b/test/test_license.py index b34bd260..cea76e7d 100644 --- a/test/test_license.py +++ b/test/test_license.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db diff --git a/test/test_mkpkglists.py b/test/test_mkpkglists.py index 9bc1073b..3c105817 100644 --- a/test/test_mkpkglists.py +++ b/test/test_mkpkglists.py @@ -1,14 +1,20 @@ import gzip import json import os - from unittest import mock import py import pytest from aurweb import config, db -from aurweb.models import License, Package, PackageBase, PackageDependency, PackageLicense, User +from aurweb.models import ( + License, + Package, + PackageBase, + PackageDependency, + PackageLicense, + User, +) from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID @@ -38,10 +44,13 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", - Email="test@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -52,16 +61,18 @@ def packages(user: User) -> list[Package]: lic = db.create(License, Name="GPL") for i in range(5): # Create the package. - pkgbase = db.create(PackageBase, Name=f"pkgbase_{i}", - Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create(PackageBase, Name=f"pkgbase_{i}", Packager=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") # Create some related records. db.create(PackageLicense, Package=pkg, License=lic) - db.create(PackageDependency, DepTypeID=DEPENDS_ID, - Package=pkg, DepName=f"dep_{i}", - DepCondition=">=1.0") + db.create( + PackageDependency, + DepTypeID=DEPENDS_ID, + Package=pkg, + DepName=f"dep_{i}", + DepCondition=">=1.0", + ) # Add the package to our output list. output.append(pkg) @@ -88,8 +99,11 @@ def config_mock(tmpdir: py.path.local) -> None: config.rehash() -def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packages: list[Package]): +def test_mkpkglists( + tmpdir: py.path.local, config_mock: None, user: User, packages: list[Package] +): from aurweb.scripts import mkpkglists + mkpkglists.main() PACKAGES = config.get("mkpkglists", "packagesfile") @@ -106,10 +120,7 @@ def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packag PKGBASE, "pkgbase_0\npkgbase_1\npkgbase_2\npkgbase_3\npkgbase_4\n", ), - ( - USERS, - "test\n" - ), + (USERS, "test\n"), ] for (file, expected_content) in expectations: @@ -136,6 +147,7 @@ def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packag @mock.patch("sys.argv", ["mkpkglists", "--extended"]) def test_mkpkglists_extended_empty(config_mock: None): from aurweb.scripts import mkpkglists + mkpkglists.main() PACKAGES = config.get("mkpkglists", "packagesfile") @@ -166,9 +178,9 @@ def test_mkpkglists_extended_empty(config_mock: None): @mock.patch("sys.argv", ["mkpkglists", "--extended"]) -def test_mkpkglists_extended(config_mock: None, user: User, - packages: list[Package]): +def test_mkpkglists_extended(config_mock: None, user: User, packages: list[Package]): from aurweb.scripts import mkpkglists + mkpkglists.main() PACKAGES = config.get("mkpkglists", "packagesfile") @@ -186,10 +198,7 @@ def test_mkpkglists_extended(config_mock: None, user: User, PKGBASE, "pkgbase_0\npkgbase_1\npkgbase_2\npkgbase_3\npkgbase_4\n", ), - ( - USERS, - "test\n" - ), + (USERS, "test\n"), ] for (file, expected_content) in expectations: diff --git a/test/test_notify.py b/test/test_notify.py index bbcc6b5a..9e61d9ee 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -23,24 +23,39 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user1() -> User: with db.begin(): - user1 = db.create(User, Username="user1", Email="user1@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user1 = db.create( + User, + Username="user1", + Email="user1@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user1 @pytest.fixture def user2() -> User: with db.begin(): - user2 = db.create(User, Username="user2", Email="user2@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user2 = db.create( + User, + Username="user2", + Email="user2@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user2 @@ -52,11 +67,15 @@ def pkgbases(user: User) -> list[PackageBase]: with db.begin(): for i in range(5): output.append( - db.create(PackageBase, Name=f"pkgbase_{i}", - Maintainer=user, SubmittedTS=now, - ModifiedTS=now)) - db.create(models.PackageNotification, PackageBase=output[-1], - User=user) + db.create( + PackageBase, + Name=f"pkgbase_{i}", + Maintainer=user, + SubmittedTS=now, + ModifiedTS=now, + ) + ) + db.create(models.PackageNotification, PackageBase=output[-1], User=user) yield output @@ -64,11 +83,15 @@ def pkgbases(user: User) -> list[PackageBase]: def pkgreq(user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): - pkgreq_ = db.create(PackageRequest, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, User=user2, - ReqTypeID=ORPHAN_ID, - Comments="This is a request test comment.", - ClosureComment=str()) + pkgreq_ = db.create( + PackageRequest, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + User=user2, + ReqTypeID=ORPHAN_ID, + Comments="This is a request test comment.", + ClosureComment=str(), + ) yield pkgreq_ @@ -78,21 +101,24 @@ def packages(pkgbases: list[PackageBase]) -> list[Package]: with db.begin(): for i, pkgbase in enumerate(pkgbases): output.append( - db.create(Package, PackageBase=pkgbase, - Name=f"pkg_{i}", Version=f"{i}.0")) + db.create( + Package, PackageBase=pkgbase, Name=f"pkg_{i}", Version=f"{i}.0" + ) + ) yield output -def test_out_of_date(user: User, user1: User, user2: User, - pkgbases: list[PackageBase]): +def test_out_of_date(user: User, user1: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] # Create two comaintainers. We'll pass the maintainer uid to # FlagNotification, so we should expect to get two emails. with db.begin(): - db.create(models.PackageComaintainer, - PackageBase=pkgbase, User=user1, Priority=1) - db.create(models.PackageComaintainer, - PackageBase=pkgbase, User=user2, Priority=2) + db.create( + models.PackageComaintainer, PackageBase=pkgbase, User=user1, Priority=1 + ) + db.create( + models.PackageComaintainer, PackageBase=pkgbase, User=user2, Priority=2 + ) # Send the notification for pkgbases[0]. notif = notify.FlagNotification(user.ID, pkgbases[0].ID) @@ -165,8 +191,12 @@ def test_comment(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): - comment = db.create(models.PackageComment, PackageBase=pkgbase, - User=user2, Comments="This is a test comment.") + comment = db.create( + models.PackageComment, + PackageBase=pkgbase, + User=user2, + Comments="This is a test comment.", + ) rendercomment.update_comment_render_fastapi(comment) notif = notify.CommentNotification(user2.ID, pkgbase.ID, comment.ID) @@ -366,15 +396,16 @@ def set_tu(users: list[User]) -> User: user.AccountTypeID = TRUSTED_USER_ID -def test_open_close_request(user: User, user2: User, - pkgreq: PackageRequest, - pkgbases: list[PackageBase]): +def test_open_close_request( + user: User, user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): set_tu([user]) pkgbase = pkgbases[0] # Send an open request notification. notif = notify.RequestOpenNotification( - user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID) + user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID + ) notif.send() assert Email.count() == 1 @@ -420,22 +451,24 @@ Request #{pkgreq.ID} has been rejected by {user2.Username} [1]. email = Email(3).parse() assert email.headers.get("To") == aur_request_ml assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) - expected = (f"[PRQ#{pkgreq.ID}] Orphan Request for " - f"{pkgbase.Name} Accepted") + expected = f"[PRQ#{pkgreq.ID}] Orphan Request for " f"{pkgbase.Name} Accepted" assert email.headers.get("Subject") == expected - expected = (f"Request #{pkgreq.ID} has been accepted automatically " - "by the Arch User Repository\npackage request system.") + expected = ( + f"Request #{pkgreq.ID} has been accepted automatically " + "by the Arch User Repository\npackage request system." + ) assert email.body == expected -def test_close_request_comaintainer_cc(user: User, user2: User, - pkgreq: PackageRequest, - pkgbases: list[PackageBase]): +def test_close_request_comaintainer_cc( + user: User, user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): pkgbase = pkgbases[0] with db.begin(): - db.create(models.PackageComaintainer, PackageBase=pkgbase, - User=user2, Priority=1) + db.create( + models.PackageComaintainer, PackageBase=pkgbase, User=user2, Priority=1 + ) notif = notify.RequestCloseNotification(0, pkgreq.ID, "accepted") notif.send() @@ -446,9 +479,9 @@ def test_close_request_comaintainer_cc(user: User, user2: User, assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) -def test_close_request_closure_comment(user: User, user2: User, - pkgreq: PackageRequest, - pkgbases: list[PackageBase]): +def test_close_request_closure_comment( + user: User, user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): pkgbase = pkgbases[0] with db.begin(): pkgreq.ClosureComment = "This is a test closure comment." @@ -496,7 +529,7 @@ ends in less than 48 hours. def test_notify_main(user: User): - """ Test TU vote reminder through aurweb.notify.main(). """ + """Test TU vote reminder through aurweb.notify.main().""" set_tu([user]) vote_id = 1 @@ -539,6 +572,7 @@ def mock_smtp_config(cls): elif key == "smtp-password": return cls() return cls(config_get(section, key)) + return _mock_smtp_config @@ -574,6 +608,7 @@ def mock_smtp_starttls_config(cls): elif key == "smtp-password": return cls("password") return cls(config_get(section, key)) + return _mock_smtp_starttls_config @@ -590,8 +625,7 @@ def test_smtp_starttls(user: User): get = "aurweb.config.get" getboolean = "aurweb.config.getboolean" with mock.patch(get, side_effect=mock_smtp_starttls_config(str)): - with mock.patch( - getboolean, side_effect=mock_smtp_starttls_config(bool)): + with mock.patch(getboolean, side_effect=mock_smtp_starttls_config(bool)): with mock.patch("smtplib.SMTP", side_effect=smtp): notif = notify.WelcomeNotification(user.ID) notif.send() @@ -621,6 +655,7 @@ def mock_smtp_ssl_config(cls): elif key == "smtp-password": return cls("password") return cls(config_get(section, key)) + return _mock_smtp_ssl_config @@ -651,7 +686,7 @@ def test_notification_defaults(): def test_notification_oserror(user: User, caplog: pytest.LogCaptureFixture): - """ Try sending a notification with a bad SMTP configuration. """ + """Try sending a notification with a bad SMTP configuration.""" caplog.set_level(ERROR) config_get = config.get config_getint = config.getint diff --git a/test/test_official_provider.py b/test/test_official_provider.py index 9287ea2d..b36fff5a 100644 --- a/test/test_official_provider.py +++ b/test/test_official_provider.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -13,10 +12,12 @@ def setup(db_test): def test_official_provider_creation(): with db.begin(): - oprovider = db.create(OfficialProvider, - Name="some-name", - Repo="some-repo", - Provides="some-provides") + oprovider = db.create( + OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides", + ) assert bool(oprovider.ID) assert oprovider.Name == "some-name" assert oprovider.Repo == "some-repo" @@ -24,19 +25,23 @@ def test_official_provider_creation(): def test_official_provider_cs(): - """ Test case sensitivity of the database table. """ + """Test case sensitivity of the database table.""" with db.begin(): - oprovider = db.create(OfficialProvider, - Name="some-name", - Repo="some-repo", - Provides="some-provides") + oprovider = db.create( + OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides", + ) assert bool(oprovider.ID) with db.begin(): - oprovider_cs = db.create(OfficialProvider, - Name="SOME-NAME", - Repo="SOME-REPO", - Provides="SOME-PROVIDES") + oprovider_cs = db.create( + OfficialProvider, + Name="SOME-NAME", + Repo="SOME-REPO", + Provides="SOME-PROVIDES", + ) assert bool(oprovider_cs.ID) assert oprovider.ID != oprovider_cs.ID diff --git a/test/test_package.py b/test/test_package.py index 1408a182..2a9df483 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy import and_ from sqlalchemy.exc import IntegrityError @@ -20,20 +19,28 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def package(user: User) -> Package: with db.begin(): - pkgbase = db.create(PackageBase, Name="beautiful-package", - Maintainer=user) - package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) + package = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package", + ) yield package @@ -48,21 +55,28 @@ def test_package(package: Package): package.Version = "1.2.3" # Make sure it got updated in the database. - record = db.query(Package).filter( - and_(Package.ID == package.ID, - Package.Version == "1.2.3") - ).first() + record = ( + db.query(Package) + .filter(and_(Package.ID == package.ID, Package.Version == "1.2.3")) + .first() + ) assert record is not None def test_package_null_pkgbase_raises(): with pytest.raises(IntegrityError): - Package(Name="some-package", Description="Some description.", - URL="https://some.package") + Package( + Name="some-package", + Description="Some description.", + URL="https://some.package", + ) def test_package_null_name_raises(package: Package): pkgbase = package.PackageBase with pytest.raises(IntegrityError): - Package(PackageBase=pkgbase, Description="Some description.", - URL="https://some.package") + Package( + PackageBase=pkgbase, + Description="Some description.", + URL="https://some.package", + ) diff --git a/test/test_package_base.py b/test/test_package_base.py index 5be7e40b..feea8183 100644 --- a/test/test_package_base.py +++ b/test/test_package_base.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -16,17 +15,21 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def pkgbase(user: User) -> PackageBase: with db.begin(): - pkgbase = db.create(PackageBase, Name="beautiful-package", - Maintainer=user) + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) yield pkgbase @@ -44,7 +47,7 @@ def test_package_base(user: User, pkgbase: PackageBase): def test_package_base_ci(user: User, pkgbase: PackageBase): - """ Test case insensitivity of the database table. """ + """Test case insensitivity of the database table.""" with pytest.raises(IntegrityError): with db.begin(): db.create(PackageBase, Name=pkgbase.Name.upper(), Maintainer=user) diff --git a/test/test_package_blacklist.py b/test/test_package_blacklist.py index 427c3be4..44de1830 100644 --- a/test/test_package_blacklist.py +++ b/test/test_package_blacklist.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db diff --git a/test/test_package_comaintainer.py b/test/test_package_comaintainer.py index e377edc0..52075887 100644 --- a/test/test_package_comaintainer.py +++ b/test/test_package_comaintainer.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,9 +16,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -32,8 +36,9 @@ def pkgbase(user: User) -> PackageBase: def test_package_comaintainer_creation(user: User, pkgbase: PackageBase): with db.begin(): - package_comaintainer = db.create(PackageComaintainer, User=user, - PackageBase=pkgbase, Priority=5) + package_comaintainer = db.create( + PackageComaintainer, User=user, PackageBase=pkgbase, Priority=5 + ) assert bool(package_comaintainer) assert package_comaintainer.User == user assert package_comaintainer.PackageBase == pkgbase @@ -50,7 +55,6 @@ def test_package_comaintainer_null_pkgbase_raises(user: User): PackageComaintainer(User=user, Priority=1) -def test_package_comaintainer_null_priority_raises(user: User, - pkgbase: PackageBase): +def test_package_comaintainer_null_priority_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageComaintainer(User=user, PackageBase=pkgbase) diff --git a/test/test_package_comment.py b/test/test_package_comment.py index c89e23af..74f2895d 100644 --- a/test/test_package_comment.py +++ b/test/test_package_comment.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,9 +16,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -32,35 +36,46 @@ def pkgbase(user: User) -> PackageBase: def test_package_comment_creation(user: User, pkgbase: PackageBase): with db.begin(): - package_comment = db.create(PackageComment, PackageBase=pkgbase, - User=user, Comments="Test comment.", - RenderedComment="Test rendered comment.") + package_comment = db.create( + PackageComment, + PackageBase=pkgbase, + User=user, + Comments="Test comment.", + RenderedComment="Test rendered comment.", + ) assert bool(package_comment.ID) def test_package_comment_null_pkgbase_raises(user: User): with pytest.raises(IntegrityError): - PackageComment(User=user, Comments="Test comment.", - RenderedComment="Test rendered comment.") + PackageComment( + User=user, + Comments="Test comment.", + RenderedComment="Test rendered comment.", + ) def test_package_comment_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageComment(PackageBase=pkgbase, - Comments="Test comment.", - RenderedComment="Test rendered comment.") + PackageComment( + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment="Test rendered comment.", + ) -def test_package_comment_null_comments_raises(user: User, - pkgbase: PackageBase): +def test_package_comment_null_comments_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageComment(PackageBase=pkgbase, User=user, - RenderedComment="Test rendered comment.") + PackageComment( + PackageBase=pkgbase, User=user, RenderedComment="Test rendered comment." + ) -def test_package_comment_null_renderedcomment_defaults(user: User, - pkgbase: PackageBase): +def test_package_comment_null_renderedcomment_defaults( + user: User, pkgbase: PackageBase +): with db.begin(): - record = db.create(PackageComment, PackageBase=pkgbase, - User=user, Comments="Test comment.") + record = db.create( + PackageComment, PackageBase=pkgbase, User=user, Comments="Test comment." + ) assert record.RenderedComment == str() diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py index 2afbc1e3..9366bb55 100644 --- a/test/test_package_dependency.py +++ b/test/test_package_dependency.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -19,9 +18,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @@ -29,16 +33,21 @@ def user() -> User: def package(user: User) -> Package: with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) - package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + package = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package", + ) yield package def test_package_dependencies(user: User, package: Package): with db.begin(): - pkgdep = db.create(PackageDependency, Package=package, - DepTypeID=DEPENDS_ID, DepName="test-dep") + pkgdep = db.create( + PackageDependency, Package=package, DepTypeID=DEPENDS_ID, DepName="test-dep" + ) assert pkgdep.DepName == "test-dep" assert pkgdep.Package == package assert pkgdep in package.package_dependencies diff --git a/test/test_package_group.py b/test/test_package_group.py index 0cb83ee2..163f693d 100644 --- a/test/test_package_group.py +++ b/test/test_package_group.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -19,9 +18,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py index ff466efc..b52547f9 100644 --- a/test/test_package_keyword.py +++ b/test/test_package_keyword.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,24 +16,27 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def pkgbase(user: User) -> PackageBase: with db.begin(): - pkgbase = db.create(PackageBase, Name="beautiful-package", - Maintainer=user) + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) yield pkgbase def test_package_keyword(pkgbase: PackageBase): with db.begin(): - pkg_keyword = db.create(PackageKeyword, PackageBase=pkgbase, - Keyword="test") + pkg_keyword = db.create(PackageKeyword, PackageBase=pkgbase, Keyword="test") assert pkg_keyword in pkgbase.keywords assert pkgbase == pkg_keyword.PackageBase diff --git a/test/test_package_license.py b/test/test_package_license.py index c43423b8..b9242647 100644 --- a/test/test_package_license.py +++ b/test/test_package_license.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -19,9 +18,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -42,8 +46,7 @@ def package(user: User, license: License): def test_package_license(license: License, package: Package): with db.begin(): - package_license = db.create(PackageLicense, Package=package, - License=license) + package_license = db.create(PackageLicense, Package=package, License=license) assert package_license.License == license assert package_license.Package == package diff --git a/test/test_package_notification.py b/test/test_package_notification.py index e7a72a43..27a03e84 100644 --- a/test/test_package_notification.py +++ b/test/test_package_notification.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -16,8 +15,13 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + ) yield user @@ -31,7 +35,8 @@ def pkgbase(user: User) -> PackageBase: def test_package_notification_creation(user: User, pkgbase: PackageBase): with db.begin(): package_notification = db.create( - PackageNotification, User=user, PackageBase=pkgbase) + PackageNotification, User=user, PackageBase=pkgbase + ) assert bool(package_notification) assert package_notification.User == user assert package_notification.PackageBase == pkgbase diff --git a/test/test_package_relation.py b/test/test_package_relation.py index 6e9a5545..c20b1394 100644 --- a/test/test_package_relation.py +++ b/test/test_package_relation.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -19,9 +18,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -29,17 +33,24 @@ def user() -> User: def package(user: User) -> Package: with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) - package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + package = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package", + ) yield package def test_package_relation(package: Package): with db.begin(): - pkgrel = db.create(PackageRelation, Package=package, - RelTypeID=CONFLICTS_ID, - RelName="test-relation") + pkgrel = db.create( + PackageRelation, + Package=package, + RelTypeID=CONFLICTS_ID, + RelName="test-relation", + ) assert pkgrel.RelName == "test-relation" assert pkgrel.Package == package diff --git a/test/test_package_request.py b/test/test_package_request.py index 3474c565..a69a0617 100644 --- a/test/test_package_request.py +++ b/test/test_package_request.py @@ -1,12 +1,20 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db, time from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase -from aurweb.models.package_request import (ACCEPTED, ACCEPTED_ID, CLOSED, CLOSED_ID, PENDING, PENDING_ID, REJECTED, - REJECTED_ID, PackageRequest) +from aurweb.models.package_request import ( + ACCEPTED, + ACCEPTED_ID, + CLOSED, + CLOSED_ID, + PENDING, + PENDING_ID, + REJECTED, + REJECTED_ID, + PackageRequest, +) from aurweb.models.request_type import MERGE_ID from aurweb.models.user import User @@ -19,9 +27,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -34,10 +47,15 @@ def pkgbase(user: User) -> PackageBase: def test_package_request_creation(user: User, pkgbase: PackageBase): with db.begin(): - package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + package_request = db.create( + PackageRequest, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) assert bool(package_request.ID) assert package_request.User == user @@ -54,11 +72,17 @@ def test_package_request_creation(user: User, pkgbase: PackageBase): def test_package_request_closed(user: User, pkgbase: PackageBase): ts = time.utcnow() with db.begin(): - package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Closer=user, ClosedTS=ts, - Comments=str(), ClosureComment=str()) + package_request = db.create( + PackageRequest, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Closer=user, + ClosedTS=ts, + Comments=str(), + ClosureComment=str(), + ) assert package_request.Closer == user assert package_request.ClosedTS == ts @@ -67,61 +91,87 @@ def test_package_request_closed(user: User, pkgbase: PackageBase): assert package_request in user.closed_requests -def test_package_request_null_request_type_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_request_type_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + PackageRequest( + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) def test_package_request_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) -def test_package_request_null_package_base_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_package_base_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, - User=user, PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) -def test_package_request_null_package_base_name_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_package_base_name_raises( + user: User, pkgbase: PackageBase +): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - Comments=str(), ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + Comments=str(), + ClosureComment=str(), + ) -def test_package_request_null_comments_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_comments_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, User=user, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + ClosureComment=str(), + ) -def test_package_request_null_closure_comment_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_closure_comment_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, User=user, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - Comments=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ) def test_package_request_status_display(user: User, pkgbase: PackageBase): - """ Test status_display() based on the Status column value. """ + """Test status_display() based on the Status column value.""" with db.begin(): - pkgreq = db.create(PackageRequest, ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str(), - Status=PENDING_ID) + pkgreq = db.create( + PackageRequest, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + Status=PENDING_ID, + ) assert pkgreq.status_display() == PENDING with db.begin(): diff --git a/test/test_package_source.py b/test/test_package_source.py index e5797f90..06230580 100644 --- a/test/test_package_source.py +++ b/test/test_package_source.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -18,9 +17,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user diff --git a/test/test_package_vote.py b/test/test_package_vote.py index 24d2fdd2..9a868262 100644 --- a/test/test_package_vote.py +++ b/test/test_package_vote.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db, time @@ -17,9 +16,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @@ -34,8 +38,7 @@ def test_package_vote_creation(user: User, pkgbase: PackageBase): ts = time.utcnow() with db.begin(): - package_vote = db.create(PackageVote, User=user, - PackageBase=pkgbase, VoteTS=ts) + package_vote = db.create(PackageVote, User=user, PackageBase=pkgbase, VoteTS=ts) assert bool(package_vote) assert package_vote.User == user assert package_vote.PackageBase == pkgbase diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 62f89e23..a707bbac 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1,10 +1,8 @@ import re - from http import HTTPStatus from unittest import mock import pytest - from fastapi.testclient import TestClient from aurweb import asgi, db, time @@ -22,7 +20,12 @@ from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation from aurweb.models.package_request import PackageRequest from aurweb.models.package_vote import PackageVote -from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID, RelationType +from aurweb.models.relation_type import ( + CONFLICTS_ID, + PROVIDES_ID, + REPLACES_ID, + RelationType, +) from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User from aurweb.testing.html import get_errors, get_successes, parse_root @@ -34,30 +37,24 @@ def package_endpoint(package: Package) -> str: def create_package(pkgname: str, maintainer: User) -> Package: - pkgbase = db.create(PackageBase, - Name=pkgname, - Maintainer=maintainer) + pkgbase = db.create(PackageBase, Name=pkgname, Maintainer=maintainer) return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) -def create_package_dep(package: Package, depname: str, - dep_type_name: str = "depends") -> PackageDependency: - dep_type = db.query(DependencyType, - DependencyType.Name == dep_type_name).first() - return db.create(PackageDependency, - DependencyType=dep_type, - Package=package, - DepName=depname) +def create_package_dep( + package: Package, depname: str, dep_type_name: str = "depends" +) -> PackageDependency: + dep_type = db.query(DependencyType, DependencyType.Name == dep_type_name).first() + return db.create( + PackageDependency, DependencyType=dep_type, Package=package, DepName=depname + ) -def create_package_rel(package: Package, - relname: str) -> PackageRelation: - rel_type = db.query(RelationType, - RelationType.ID == PROVIDES_ID).first() - return db.create(PackageRelation, - RelationType=rel_type, - Package=package, - RelName=relname) +def create_package_rel(package: Package, relname: str) -> PackageRelation: + rel_type = db.query(RelationType, RelationType.ID == PROVIDES_ID).first() + return db.create( + PackageRelation, RelationType=rel_type, Package=package, RelName=relname + ) @pytest.fixture(autouse=True) @@ -67,64 +64,73 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: - """ Yield a FastAPI TestClient. """ + """Yield a FastAPI TestClient.""" yield TestClient(app=asgi.app) def create_user(username: str) -> User: with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @pytest.fixture def user() -> User: - """ Yield a user. """ + """Yield a user.""" user = create_user("test") yield user @pytest.fixture def maintainer() -> User: - """ Yield a specific User used to maintain packages. """ + """Yield a specific User used to maintain packages.""" account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@example.org", - Passwd="testPassword", - AccountType=account_type) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountType=account_type, + ) yield maintainer @pytest.fixture def tu_user(): - tu_type = db.query(AccountType, - AccountType.AccountType == "Trusted User").first() + tu_type = db.query(AccountType, AccountType.AccountType == "Trusted User").first() with db.begin(): - tu_user = db.create(User, Username="test_tu", - Email="test_tu@example.org", - RealName="Test TU", Passwd="testPassword", - AccountType=tu_type) + tu_user = db.create( + User, + Username="test_tu", + Email="test_tu@example.org", + RealName="Test TU", + Passwd="testPassword", + AccountType=tu_type, + ) yield tu_user @pytest.fixture def package(maintainer: User) -> Package: - """ Yield a Package created by user. """ + """Yield a Package created by user.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, - Name="test-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name) + pkgbase = db.create( + PackageBase, + Name="test-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield package @@ -135,29 +141,34 @@ def pkgbase(package: Package) -> PackageBase: @pytest.fixture def target(maintainer: User) -> PackageBase: - """ Merge target. """ + """Merge target.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="target-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="target-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield pkgbase @pytest.fixture def pkgreq(user: User, pkgbase: PackageBase) -> PackageRequest: - """ Yield a PackageRequest related to `pkgbase`. """ + """Yield a PackageRequest related to `pkgbase`.""" with db.begin(): - pkgreq = db.create(PackageRequest, - ReqTypeID=DELETION_ID, - User=user, - PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=f"Deletion request for {pkgbase.Name}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=DELETION_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=f"Deletion request for {pkgbase.Name}", + ClosureComment=str(), + ) yield pkgreq @@ -166,31 +177,33 @@ def comment(user: User, package: Package) -> PackageComment: pkgbase = package.PackageBase now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, - User=user, - PackageBase=pkgbase, - Comments="Test comment.", - RenderedComment=str(), - CommentTS=now) + comment = db.create( + PackageComment, + User=user, + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment=str(), + CommentTS=now, + ) yield comment @pytest.fixture def packages(maintainer: User) -> list[Package]: - """ Yield 55 packages named pkg_0 .. pkg_54. """ + """Yield 55 packages named pkg_0 .. pkg_54.""" packages_ = [] now = time.utcnow() with db.begin(): for i in range(55): - pkgbase = db.create(PackageBase, - Name=f"pkg_{i}", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") packages_.append(package) yield packages_ @@ -203,40 +216,56 @@ def test_package_not_found(client: TestClient): def test_package(client: TestClient, package: Package): - """ Test a single / packages / {name} route. """ + """Test a single / packages / {name} route.""" with db.begin(): - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=PROVIDES_ID, - RelName="test_provider1") - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=PROVIDES_ID, - RelName="test_provider2") + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=PROVIDES_ID, + RelName="test_provider1", + ) + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=PROVIDES_ID, + RelName="test_provider2", + ) - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=REPLACES_ID, - RelName="test_replacer1") - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=REPLACES_ID, - RelName="test_replacer2") + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=REPLACES_ID, + RelName="test_replacer1", + ) + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=REPLACES_ID, + RelName="test_replacer2", + ) - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=CONFLICTS_ID, - RelName="test_conflict1") - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=CONFLICTS_ID, - RelName="test_conflict2") + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=CONFLICTS_ID, + RelName="test_conflict1", + ) + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=CONFLICTS_ID, + RelName="test_conflict2", + ) # Create some licenses. licenses = [ db.create(License, Name="test_license1"), - db.create(License, Name="test_license2") + db.create(License, Name="test_license2"), ] - db.create(PackageLicense, PackageID=package.ID, - License=licenses[0]) - db.create(PackageLicense, PackageID=package.ID, - License=licenses[1]) + db.create(PackageLicense, PackageID=package.ID, License=licenses[0]) + db.create(PackageLicense, PackageID=package.ID, License=licenses[1]) with client as request: resp = request.get(package_endpoint(package)) @@ -311,7 +340,7 @@ def paged_depends_required(client: TestClient, package: Package): params={ "all_deps": True, "all_reqs": True, - } + }, ) assert resp.status_code == int(HTTPStatus.OK) @@ -321,10 +350,15 @@ def paged_depends_required(client: TestClient, package: Package): def test_package_comments(client: TestClient, user: User, package: Package): - now = (time.utcnow()) + now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, PackageBase=package.PackageBase, - User=user, Comments="Test comment", CommentTS=now) + comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Test comment", + CommentTS=now, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -332,17 +366,18 @@ def test_package_comments(client: TestClient, user: User, package: Package): assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) - expected = [ - comment.Comments - ] - comments = root.xpath('.//div[contains(@class, "package-comments")]' - '/div[@class="article-content"]/div/text()') + expected = [comment.Comments] + comments = root.xpath( + './/div[contains(@class, "package-comments")]' + '/div[@class="article-content"]/div/text()' + ) for i, row in enumerate(expected): assert comments[i].strip() == row -def test_package_requests_display(client: TestClient, user: User, - package: Package, pkgreq: PackageRequest): +def test_package_requests_display( + client: TestClient, user: User, package: Package, pkgreq: PackageRequest +): # Test that a single request displays "1 pending request". with client as request: resp = request.get(package_endpoint(package)) @@ -355,11 +390,15 @@ def test_package_requests_display(client: TestClient, user: User, type_ = db.query(RequestType, RequestType.ID == DELETION_ID).first() with db.begin(): - db.create(PackageRequest, PackageBase=package.PackageBase, - PackageBaseName=package.PackageBase.Name, - User=user, RequestType=type_, - Comments="Test comment2.", - ClosureComment=str()) + db.create( + PackageRequest, + PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + User=user, + RequestType=type_, + Comments="Test comment2.", + ClosureComment=str(), + ) # Test that a two requests display "2 pending requests". with client as request: @@ -372,11 +411,10 @@ def test_package_requests_display(client: TestClient, user: User, assert target.text.strip() == "2 pending requests" -def test_package_authenticated(client: TestClient, user: User, - package: Package): - """ We get the same here for either authenticated or not +def test_package_authenticated(client: TestClient, user: User, package: Package): + """We get the same here for either authenticated or not authenticated. Form inputs are presented to maintainers. - This process also occurs when pkgbase.html is rendered. """ + This process also occurs when pkgbase.html is rendered.""" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: resp = request.get(package_endpoint(package), cookies=cookies) @@ -390,7 +428,7 @@ def test_package_authenticated(client: TestClient, user: User, "Flag package out-of-date", "Vote for this package", "Enable notifications", - "Submit Request" + "Submit Request", ] for expected_text in expected: assert expected_text in resp.text @@ -402,9 +440,9 @@ def test_package_authenticated(client: TestClient, user: User, assert len(target) == 0 -def test_package_authenticated_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_package_authenticated_maintainer( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: resp = request.get(package_endpoint(package), cookies=cookies) @@ -420,15 +458,13 @@ def test_package_authenticated_maintainer(client: TestClient, "Enable notifications", "Manage Co-Maintainers", "Submit Request", - "Disown Package" + "Disown Package", ] for expected_text in expected: assert expected_text in resp.text -def test_package_authenticated_tu(client: TestClient, - tu_user: User, - package: Package): +def test_package_authenticated_tu(client: TestClient, tu_user: User, package: Package): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: resp = request.get(package_endpoint(package), cookies=cookies) @@ -446,14 +482,13 @@ def test_package_authenticated_tu(client: TestClient, "Submit Request", "Delete Package", "Merge Package", - "Disown Package" + "Disown Package", ] for expected_text in expected: assert expected_text in resp.text -def test_package_dependencies(client: TestClient, maintainer: User, - package: Package): +def test_package_dependencies(client: TestClient, maintainer: User, package: Package): # Create a normal dependency of type depends. with db.begin(): dep_pkg = create_package("test-dep-1", maintainer) @@ -461,32 +496,32 @@ def test_package_dependencies(client: TestClient, maintainer: User, # Also, create a makedepends. make_dep_pkg = create_package("test-dep-2", maintainer) - make_dep = create_package_dep(package, make_dep_pkg.Name, - dep_type_name="makedepends") + make_dep = create_package_dep( + package, make_dep_pkg.Name, dep_type_name="makedepends" + ) make_dep.DepArch = "x86_64" # And... a checkdepends! check_dep_pkg = create_package("test-dep-3", maintainer) - create_package_dep(package, check_dep_pkg.Name, - dep_type_name="checkdepends") + create_package_dep(package, check_dep_pkg.Name, dep_type_name="checkdepends") # Geez. Just stop. This is optdepends. opt_dep_pkg = create_package("test-dep-4", maintainer) - create_package_dep(package, opt_dep_pkg.Name, - dep_type_name="optdepends") + create_package_dep(package, opt_dep_pkg.Name, dep_type_name="optdepends") # Heh. Another optdepends to test one with a description. opt_desc_dep_pkg = create_package("test-dep-5", maintainer) - opt_desc_dep = create_package_dep(package, opt_desc_dep_pkg.Name, - dep_type_name="optdepends") + opt_desc_dep = create_package_dep( + package, opt_desc_dep_pkg.Name, dep_type_name="optdepends" + ) opt_desc_dep.DepDesc = "Test description." - broken_dep = create_package_dep(package, "test-dep-6", - dep_type_name="depends") + broken_dep = create_package_dep(package, "test-dep-6", dep_type_name="depends") # Create an official provider record. - db.create(OfficialProvider, Name="test-dep-99", - Repo="core", Provides="test-dep-99") + db.create( + OfficialProvider, Name="test-dep-99", Repo="core", Provides="test-dep-99" + ) create_package_dep(package, "test-dep-99") # Also, create a provider who provides our test-dep-99. @@ -498,13 +533,14 @@ def test_package_dependencies(client: TestClient, maintainer: User, assert resp.status_code == int(HTTPStatus.OK) # Let's make sure all the non-broken deps are ordered as we expect. - expected = list(filter( - lambda e: e.is_package(), - package.package_dependencies.order_by( - PackageDependency.DepTypeID.asc(), - PackageDependency.DepName.asc() - ).all() - )) + expected = list( + filter( + lambda e: e.is_package(), + package.package_dependencies.order_by( + PackageDependency.DepTypeID.asc(), PackageDependency.DepName.asc() + ).all(), + ) + ) root = parse_root(resp.text) pkgdeps = root.findall('.//ul[@id="pkgdepslist"]/li/a') for i, expectation in enumerate(expected): @@ -512,7 +548,7 @@ def test_package_dependencies(client: TestClient, maintainer: User, # Let's make sure the DepArch was displayed for our target make dep. arch = root.findall('.//ul[@id="pkgdepslist"]/li')[3] - arch = arch.xpath('./em')[0] + arch = arch.xpath("./em")[0] assert arch.text.strip() == "(make, x86_64)" # And let's make sure that the broken package was displayed. @@ -522,16 +558,19 @@ def test_package_dependencies(client: TestClient, maintainer: User, def test_packages(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "X", # "X" isn't valid, defaults to "nd" - "PP": "1 or 1", - "O": "0 or 0" - }) + response = request.get( + "/packages", + params={ + "SeB": "X", # "X" isn't valid, defaults to "nd" + "PP": "1 or 1", + "O": "0 or 0", + }, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) stats = root.xpath('//div[@class="pkglist-stats"]/p')[0] - pager_text = re.sub(r'\s+', " ", stats.text.replace("\n", "").strip()) + pager_text = re.sub(r"\s+", " ", stats.text.replace("\n", "").strip()) assert pager_text == "55 packages found. Page 1 of 2." rows = root.xpath('//table[@class="results"]/tbody/tr') @@ -551,10 +590,7 @@ def test_packages_empty(client: TestClient): def test_packages_search_by_name(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "n", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "n", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -563,13 +599,9 @@ def test_packages_search_by_name(client: TestClient, packages: list[Package]): assert len(rows) == 50 # Default per-page -def test_packages_search_by_exact_name(client: TestClient, - packages: list[Package]): +def test_packages_search_by_exact_name(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "N", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "N", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -579,10 +611,7 @@ def test_packages_search_by_exact_name(client: TestClient, assert len(rows) == 0 with client as request: - response = request.get("/packages", params={ - "SeB": "N", - "K": "pkg_1" - }) + response = request.get("/packages", params={"SeB": "N", "K": "pkg_1"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -592,13 +621,9 @@ def test_packages_search_by_exact_name(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_pkgbase(client: TestClient, - packages: list[Package]): +def test_packages_search_by_pkgbase(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "b", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "b", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -607,13 +632,9 @@ def test_packages_search_by_pkgbase(client: TestClient, assert len(rows) == 50 -def test_packages_search_by_exact_pkgbase(client: TestClient, - packages: list[Package]): +def test_packages_search_by_exact_pkgbase(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "B", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "B", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -621,10 +642,7 @@ def test_packages_search_by_exact_pkgbase(client: TestClient, assert len(rows) == 0 with client as request: - response = request.get("/packages", params={ - "SeB": "B", - "K": "pkg_1" - }) + response = request.get("/packages", params={"SeB": "B", "K": "pkg_1"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -632,14 +650,10 @@ def test_packages_search_by_exact_pkgbase(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_keywords(client: TestClient, - packages: list[Package]): +def test_packages_search_by_keywords(client: TestClient, packages: list[Package]): # None of our packages have keywords, so this query should return nothing. with client as request: - response = request.get("/packages", params={ - "SeB": "k", - "K": "testKeyword" - }) + response = request.get("/packages", params={"SeB": "k", "K": "testKeyword"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -649,16 +663,13 @@ def test_packages_search_by_keywords(client: TestClient, # But now, let's create the keyword for the first package. package = packages[0] with db.begin(): - db.create(PackageKeyword, - PackageBase=package.PackageBase, - Keyword="testKeyword") + db.create( + PackageKeyword, PackageBase=package.PackageBase, Keyword="testKeyword" + ) # And request packages with that keyword, we should get 1 result. with client as request: - response = request.get("/packages", params={ - "SeB": "k", - "K": "testKeyword" - }) + response = request.get("/packages", params={"SeB": "k", "K": "testKeyword"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -666,16 +677,15 @@ def test_packages_search_by_keywords(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_maintainer( + client: TestClient, maintainer: User, package: Package +): # We should expect that searching by `package`'s maintainer # returns `package` in the results. with client as request: - response = request.get("/packages", params={ - "SeB": "m", - "K": maintainer.Username - }) + response = request.get( + "/packages", params={"SeB": "m", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') @@ -704,15 +714,14 @@ def test_packages_search_by_maintainer(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_comaintainer(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_comaintainer( + client: TestClient, maintainer: User, package: Package +): # Nobody's a comaintainer yet. with client as request: - response = request.get("/packages", params={ - "SeB": "c", - "K": maintainer.Username - }) + response = request.get( + "/packages", params={"SeB": "c", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -721,17 +730,18 @@ def test_packages_search_by_comaintainer(client: TestClient, # Now, we create a comaintainer. with db.begin(): - db.create(PackageComaintainer, - PackageBase=package.PackageBase, - User=maintainer, - Priority=1) + db.create( + PackageComaintainer, + PackageBase=package.PackageBase, + User=maintainer, + Priority=1, + ) # Then test that it's returned by our search. with client as request: - response = request.get("/packages", params={ - "SeB": "c", - "K": maintainer.Username - }) + response = request.get( + "/packages", params={"SeB": "c", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -739,15 +749,18 @@ def test_packages_search_by_comaintainer(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_co_or_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_co_or_maintainer( + client: TestClient, maintainer: User, package: Package +): with client as request: - response = request.get("/packages", params={ - "SeB": "M", - "SB": "BLAH", # Invalid SB; gets reset to default "n". - "K": maintainer.Username - }) + response = request.get( + "/packages", + params={ + "SeB": "M", + "SB": "BLAH", # Invalid SB; gets reset to default "n". + "K": maintainer.Username, + }, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -755,19 +768,18 @@ def test_packages_search_by_co_or_maintainer(client: TestClient, assert len(rows) == 1 with db.begin(): - user = db.create(User, Username="comaintainer", - Email="comaintainer@example.org", - Passwd="testPassword") - db.create(PackageComaintainer, - PackageBase=package.PackageBase, - User=user, - Priority=1) + user = db.create( + User, + Username="comaintainer", + Email="comaintainer@example.org", + Passwd="testPassword", + ) + db.create( + PackageComaintainer, PackageBase=package.PackageBase, User=user, Priority=1 + ) with client as request: - response = request.get("/packages", params={ - "SeB": "M", - "K": user.Username - }) + response = request.get("/packages", params={"SeB": "M", "K": user.Username}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -775,14 +787,13 @@ def test_packages_search_by_co_or_maintainer(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_submitter(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_submitter( + client: TestClient, maintainer: User, package: Package +): with client as request: - response = request.get("/packages", params={ - "SeB": "s", - "K": maintainer.Username - }) + response = request.get( + "/packages", params={"SeB": "s", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -792,184 +803,184 @@ def test_packages_search_by_submitter(client: TestClient, def test_packages_sort_by_name(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SB": "n", # Name - "SO": "a", # Ascending - "PP": "150" - }) + response = request.get( + "/packages", params={"SB": "n", "SO": "a", "PP": "150"} # Name # Ascending + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - rows = [row.xpath('./td/a')[0].text.strip() for row in rows] + rows = [row.xpath("./td/a")[0].text.strip() for row in rows] with client as request: - response2 = request.get("/packages", params={ - "SB": "n", # Name - "SO": "d", # Ascending - "PP": "150" - }) + response2 = request.get( + "/packages", params={"SB": "n", "SO": "d", "PP": "150"} # Name # Ascending + ) assert response2.status_code == int(HTTPStatus.OK) root = parse_root(response2.text) rows2 = root.xpath('//table[@class="results"]/tbody/tr') - rows2 = [row.xpath('./td/a')[0].text.strip() for row in rows2] + rows2 = [row.xpath("./td/a")[0].text.strip() for row in rows2] assert rows == list(reversed(rows2)) -def test_packages_sort_by_votes(client: TestClient, - maintainer: User, - packages: list[Package]): +def test_packages_sort_by_votes( + client: TestClient, maintainer: User, packages: list[Package] +): # Set the first package's NumVotes to 1. with db.begin(): packages[0].PackageBase.NumVotes = 1 # Test that, by default, the first result is what we just set above. with client as request: - response = request.get("/packages", params={ - "SB": "v" # Votes. - }) + response = request.get("/packages", params={"SB": "v"}) # Votes. assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - votes = rows[0].xpath('./td')[2] # The third column of the first row. + votes = rows[0].xpath("./td")[2] # The third column of the first row. assert votes.text.strip() == "1" # Now, test that with an ascending order, the last result is # the one we set, since the default (above) is descending. with client as request: - response = request.get("/packages", params={ - "SB": "v", # Votes. - "SO": "a", # Ascending. - "O": "50" # Second page. - }) + response = request.get( + "/packages", + params={ + "SB": "v", # Votes. + "SO": "a", # Ascending. + "O": "50", # Second page. + }, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - votes = rows[-1].xpath('./td')[2] # The third column of the last row. + votes = rows[-1].xpath("./td")[2] # The third column of the last row. assert votes.text.strip() == "1" -def test_packages_sort_by_popularity(client: TestClient, - maintainer: User, - packages: list[Package]): +def test_packages_sort_by_popularity( + client: TestClient, maintainer: User, packages: list[Package] +): # Set the first package's Popularity to 0.50. with db.begin(): packages[0].PackageBase.Popularity = "0.50" # Test that, by default, the first result is what we just set above. with client as request: - response = request.get("/packages", params={ - "SB": "p" # Popularity - }) + response = request.get("/packages", params={"SB": "p"}) # Popularity assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - pop = rows[0].xpath('./td')[3] # The fourth column of the first row. + pop = rows[0].xpath("./td")[3] # The fourth column of the first row. assert pop.text.strip() == "0.50" -def test_packages_sort_by_voted(client: TestClient, - maintainer: User, - packages: list[Package]): +def test_packages_sort_by_voted( + client: TestClient, maintainer: User, packages: list[Package] +): now = time.utcnow() with db.begin(): - db.create(PackageVote, PackageBase=packages[0].PackageBase, - User=maintainer, VoteTS=now) + db.create( + PackageVote, + PackageBase=packages[0].PackageBase, + User=maintainer, + VoteTS=now, + ) # Test that, by default, the first result is what we just set above. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - response = request.get("/packages", params={ - "SB": "w", # Voted - "SO": "d" # Descending, Voted first. - }, cookies=cookies) + response = request.get( + "/packages", + params={"SB": "w", "SO": "d"}, # Voted # Descending, Voted first. + cookies=cookies, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - voted = rows[0].xpath('./td')[5] # The sixth column of the first row. + voted = rows[0].xpath("./td")[5] # The sixth column of the first row. assert voted.text.strip() == "Yes" # Conversely, everything else was not voted on. - voted = rows[1].xpath('./td')[5] # The sixth column of the second row. + voted = rows[1].xpath("./td")[5] # The sixth column of the second row. assert voted.text.strip() == str() # Empty. -def test_packages_sort_by_notify(client: TestClient, - maintainer: User, - packages: list[Package]): - db.create(PackageNotification, - PackageBase=packages[0].PackageBase, - User=maintainer) +def test_packages_sort_by_notify( + client: TestClient, maintainer: User, packages: list[Package] +): + db.create(PackageNotification, PackageBase=packages[0].PackageBase, User=maintainer) # Test that, by default, the first result is what we just set above. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - response = request.get("/packages", params={ - "SB": "o", # Voted - "SO": "d" # Descending, Voted first. - }, cookies=cookies) + response = request.get( + "/packages", + params={"SB": "o", "SO": "d"}, # Voted # Descending, Voted first. + cookies=cookies, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - notify = rows[0].xpath('./td')[6] # The sixth column of the first row. + notify = rows[0].xpath("./td")[6] # The sixth column of the first row. assert notify.text.strip() == "Yes" # Conversely, everything else was not voted on. - notify = rows[1].xpath('./td')[6] # The sixth column of the second row. + notify = rows[1].xpath("./td")[6] # The sixth column of the second row. assert notify.text.strip() == str() # Empty. -def test_packages_sort_by_maintainer(client: TestClient, - maintainer: User, - package: Package): - """ Sort a package search by the maintainer column. """ +def test_packages_sort_by_maintainer( + client: TestClient, maintainer: User, package: Package +): + """Sort a package search by the maintainer column.""" # Create a second package, so the two can be ordered and checked. with db.begin(): - maintainer2 = db.create(User, Username="maintainer2", - Email="maintainer2@example.org", - Passwd="testPassword") - base2 = db.create(PackageBase, Name="pkg_2", Maintainer=maintainer2, - Submitter=maintainer2, Packager=maintainer2) + maintainer2 = db.create( + User, + Username="maintainer2", + Email="maintainer2@example.org", + Passwd="testPassword", + ) + base2 = db.create( + PackageBase, + Name="pkg_2", + Maintainer=maintainer2, + Submitter=maintainer2, + Packager=maintainer2, + ) db.create(Package, Name="pkg_2", PackageBase=base2) # Check the descending order route. with client as request: - response = request.get("/packages", params={ - "SB": "m", - "SO": "d" - }) + response = request.get("/packages", params={"SB": "m", "SO": "d"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - col = rows[0].xpath('./td')[5].xpath('./a')[0] # Last column. + col = rows[0].xpath("./td")[5].xpath("./a")[0] # Last column. assert col.text.strip() == maintainer.Username # On the other hand, with ascending, we should get reverse ordering. with client as request: - response = request.get("/packages", params={ - "SB": "m", - "SO": "a" - }) + response = request.get("/packages", params={"SB": "m", "SO": "a"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - col = rows[0].xpath('./td')[5].xpath('./a')[0] # Last column. + col = rows[0].xpath("./td")[5].xpath("./a")[0] # Last column. assert col.text.strip() == maintainer2.Username -def test_packages_sort_by_last_modified(client: TestClient, - packages: list[Package]): +def test_packages_sort_by_last_modified(client: TestClient, packages: list[Package]): now = time.utcnow() # Set the first package's ModifiedTS to be 1000 seconds before now. package = packages[0] @@ -977,10 +988,10 @@ def test_packages_sort_by_last_modified(client: TestClient, package.PackageBase.ModifiedTS = now - 1000 with client as request: - response = request.get("/packages", params={ - "SB": "l", - "SO": "a" # Ascending; oldest modification first. - }) + response = request.get( + "/packages", + params={"SB": "l", "SO": "a"}, # Ascending; oldest modification first. + ) assert response.status_code == int(HTTPStatus.OK) # We should have 50 (default per page) results. @@ -990,12 +1001,13 @@ def test_packages_sort_by_last_modified(client: TestClient, # Let's assert that the first item returned was the one we modified above. row = rows[0] - col = row.xpath('./td')[0].xpath('./a')[0] + col = row.xpath("./td")[0].xpath("./a")[0] assert col.text.strip() == package.Name -def test_packages_flagged(client: TestClient, maintainer: User, - packages: list[Package]): +def test_packages_flagged( + client: TestClient, maintainer: User, packages: list[Package] +): package = packages[0] now = time.utcnow() @@ -1005,9 +1017,7 @@ def test_packages_flagged(client: TestClient, maintainer: User, package.PackageBase.Flagger = maintainer with client as request: - response = request.get("/packages", params={ - "outdated": "on" - }) + response = request.get("/packages", params={"outdated": "on"}) assert response.status_code == int(HTTPStatus.OK) # We should only get one result from this query; the package we flagged. @@ -1016,9 +1026,7 @@ def test_packages_flagged(client: TestClient, maintainer: User, assert len(rows) == 1 with client as request: - response = request.get("/packages", params={ - "outdated": "off" - }) + response = request.get("/packages", params={"outdated": "off"}) assert response.status_code == int(HTTPStatus.OK) # In this case, we should get 54 results, which means that the first @@ -1044,14 +1052,17 @@ def test_packages_orphans(client: TestClient, packages: list[Package]): def test_packages_per_page(client: TestClient, maintainer: User): - """ Test the ability for /packages to deal with the PP query - argument specifications (50, 100, 250; default: 50). """ + """Test the ability for /packages to deal with the PP query + argument specifications (50, 100, 250; default: 50).""" with db.begin(): for i in range(255): - base = db.create(PackageBase, Name=f"pkg_{i}", - Maintainer=maintainer, - Submitter=maintainer, - Packager=maintainer) + base = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Submitter=maintainer, + Packager=maintainer, + ) db.create(Package, PackageBase=base, Name=base.Name) # Test default case, PP of 50. @@ -1079,18 +1090,20 @@ def test_packages_per_page(client: TestClient, maintainer: User): assert len(rows) == 250 -def test_packages_post_unknown_action(client: TestClient, user: User, - package: Package): +def test_packages_post_unknown_action(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "unknown"}, - cookies=cookies, allow_redirects=False) + resp = request.post( + "/packages", + data={"action": "unknown"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) def test_packages_post_error(client: TestClient, user: User, package: Package): - async def stub_action(request: Request, **kwargs): return (False, ["Some error."]) @@ -1098,8 +1111,12 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + resp = request.post( + "/packages", + data={"action": "stub"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1108,7 +1125,6 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): def test_packages_post(client: TestClient, user: User, package: Package): - async def stub_action(request: Request, **kwargs): return (True, ["Some success."]) @@ -1116,8 +1132,12 @@ def test_packages_post(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + resp = request.post( + "/packages", + data={"action": "stub"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.OK) errors = get_successes(resp.text) @@ -1125,8 +1145,9 @@ def test_packages_post(client: TestClient, user: User, package: Package): assert errors[0].text.strip() == expected -def test_packages_post_unflag(client: TestClient, user: User, - maintainer: User, package: Package): +def test_packages_post_unflag( + client: TestClient, user: User, maintainer: User, package: Package +): # Flag `package` as `user`. now = time.utcnow() with db.begin(): @@ -1181,8 +1202,7 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # an error to be rendered. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "notify"}, - cookies=cookies) + resp = request.post("/packages", data={"action": "notify"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to be notified about." @@ -1190,10 +1210,9 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # Now let's actually enable notifications on `package`. with client as request: - resp = request.post("/packages", data={ - "action": "notify", - "IDs": [package.ID] - }, cookies=cookies) + resp = request.post( + "/packages", data={"action": "notify", "IDs": [package.ID]}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.OK) expected = "The selected packages' notifications have been enabled." successes = get_successes(resp.text) @@ -1202,31 +1221,27 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # Try to enable notifications when they're already enabled, # causing an error to be rendered. with client as request: - resp = request.post("/packages", data={ - "action": "notify", - "IDs": [package.ID] - }, cookies=cookies) + resp = request.post( + "/packages", data={"action": "notify", "IDs": [package.ID]}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to be notified about." assert errors[0].text.strip() == expected -def test_packages_post_unnotify(client: TestClient, user: User, - package: Package): +def test_packages_post_unnotify(client: TestClient, user: User, package: Package): # Create a notification record. with db.begin(): - notif = db.create(PackageNotification, - PackageBase=package.PackageBase, - User=user) + notif = db.create( + PackageNotification, PackageBase=package.PackageBase, User=user + ) assert notif is not None # Request removal of the notification without any IDs. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "unnotify" - }, cookies=cookies) + resp = request.post("/packages", data={"action": "unnotify"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages for notification removal." @@ -1234,10 +1249,11 @@ def test_packages_post_unnotify(client: TestClient, user: User, # Request removal of the notification; really. with client as request: - resp = request.post("/packages", data={ - "action": "unnotify", - "IDs": [package.ID] - }, cookies=cookies) + resp = request.post( + "/packages", + data={"action": "unnotify", "IDs": [package.ID]}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) expected = "The selected packages' notifications have been removed." @@ -1251,25 +1267,23 @@ def test_packages_post_unnotify(client: TestClient, user: User, # Try it again. The notif no longer exists. with client as request: - resp = request.post("/packages", data={ - "action": "unnotify", - "IDs": [package.ID] - }, cookies=cookies) + resp = request.post( + "/packages", + data={"action": "unnotify", "IDs": [package.ID]}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "A package you selected does not have notifications enabled." assert errors[0].text.strip() == expected -def test_packages_post_adopt(client: TestClient, user: User, - package: Package): +def test_packages_post_adopt(client: TestClient, user: User, package: Package): # Try to adopt an empty list of packages. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "adopt" - }, cookies=cookies) + resp = request.post("/packages", data={"action": "adopt"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to adopt." @@ -1277,11 +1291,11 @@ def test_packages_post_adopt(client: TestClient, user: User, # Now, let's try to adopt a package that's already maintained. with client as request: - resp = request.post("/packages", data={ - "action": "adopt", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + resp = request.post( + "/packages", + data={"action": "adopt", "IDs": [package.ID], "confirm": True}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You are not allowed to adopt one of the packages you selected." @@ -1294,33 +1308,34 @@ def test_packages_post_adopt(client: TestClient, user: User, # Now, let's try to adopt without confirming. with client as request: - resp = request.post("/packages", data={ - "action": "adopt", - "IDs": [package.ID] - }, cookies=cookies) + resp = request.post( + "/packages", data={"action": "adopt", "IDs": [package.ID]}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) - expected = ("The selected packages have not been adopted, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been adopted, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected # Let's do it again now that there is no maintainer. with client as request: - resp = request.post("/packages", data={ - "action": "adopt", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + resp = request.post( + "/packages", + data={"action": "adopt", "IDs": [package.ID], "confirm": True}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) expected = "The selected packages have been adopted." assert successes[0].text.strip() == expected -def test_packages_post_disown_as_maintainer(client: TestClient, user: User, - maintainer: User, - package: Package): - """ Disown packages as a maintainer. """ +def test_packages_post_disown_as_maintainer( + client: TestClient, user: User, maintainer: User, package: Package +): + """Disown packages as a maintainer.""" # Initially prove that we have a maintainer. assert package.PackageBase.Maintainer is not None assert package.PackageBase.Maintainer == maintainer @@ -1328,9 +1343,7 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, # Try to run the disown action with no IDs; get an error. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "disown" - }, cookies=cookies) + resp = request.post("/packages", data={"action": "disown"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to disown." @@ -1339,25 +1352,26 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, # Try to disown `package` without giving the confirm argument. with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID] - }, cookies=cookies) + resp = request.post( + "/packages", data={"action": "disown", "IDs": [package.ID]}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert package.PackageBase.Maintainer is not None errors = get_errors(resp.text) - expected = ("The selected packages have not been disowned, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been disowned, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected # Now, try to disown `package` without credentials (as `user`). user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID], - "confirm": True - }, cookies=user_cookies) + resp = request.post( + "/packages", + data={"action": "disown", "IDs": [package.ID], "confirm": True}, + cookies=user_cookies, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert package.PackageBase.Maintainer is not None errors = get_errors(resp.text) @@ -1366,11 +1380,11 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, # Now, let's really disown `package` as `maintainer`. with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + resp = request.post( + "/packages", + data={"action": "disown", "IDs": [package.ID], "confirm": True}, + cookies=cookies, + ) assert package.PackageBase.Maintainer is None successes = get_successes(resp.text) @@ -1378,30 +1392,36 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, assert successes[0].text.strip() == expected -def test_packages_post_disown(client: TestClient, tu_user: User, - maintainer: User, package: Package): - """ Disown packages as a Trusted User, which cannot bypass idle time. """ +def test_packages_post_disown( + client: TestClient, tu_user: User, maintainer: User, package: Package +): + """Disown packages as a Trusted User, which cannot bypass idle time.""" cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + resp = request.post( + "/packages", + data={"action": "disown", "IDs": [package.ID], "confirm": True}, + cookies=cookies, + ) errors = get_errors(resp.text) expected = r"^No due existing orphan requests to accept for .+\.$" assert re.match(expected, errors[0].text.strip()) -def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, - user: User, tu_user: User, package: Package): +def test_packages_post_delete( + caplog: pytest.fixture, + client: TestClient, + user: User, + tu_user: User, + package: Package, +): # First, let's try to use the delete action with no packages IDs. user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "delete" - }, cookies=user_cookies) + resp = request.post( + "/packages", data={"action": "delete"}, cookies=user_cookies + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to delete." @@ -1409,23 +1429,26 @@ def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, # Now, let's try to delete real packages without supplying "confirm". with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [package.ID] - }, cookies=user_cookies) + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [package.ID]}, + cookies=user_cookies, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) - expected = ("The selected packages have not been deleted, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected # And again, with everything, but `user` doesn't have permissions. with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [package.ID], - "confirm": True - }, cookies=user_cookies) + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [package.ID], "confirm": True}, + cookies=user_cookies, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You do not have permission to delete packages." @@ -1436,11 +1459,11 @@ def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, # an invalid package ID. tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [0], - "confirm": True - }, cookies=tu_cookies) + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [0], "confirm": True}, + cookies=tu_cookies, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "One of the packages you selected does not exist." @@ -1449,11 +1472,11 @@ def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, # Whoo. Now, let's finally make a valid request as `tu_user` # to delete `package`. with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [package.ID], - "confirm": True - }, cookies=tu_cookies) + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [package.ID], "confirm": True}, + cookies=tu_cookies, + ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) expected = "The selected packages have been deleted." @@ -1461,15 +1484,17 @@ def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, # Expect that the package deletion was logged. pkgbases = [package.PackageBase.Name] - expected = (f"Privileged user '{tu_user.Username}' deleted the " - f"following package bases: {str(pkgbases)}.") + expected = ( + f"Privileged user '{tu_user.Username}' deleted the " + f"following package bases: {str(pkgbases)}." + ) assert expected in caplog.text def test_account_comments_unauthorized(client: TestClient, user: User): - """ This test may seem out of place, but it requires packages, + """This test may seem out of place, but it requires packages, so its being included in the packages routes test suite to - leverage existing fixtures. """ + leverage existing fixtures.""" endpoint = f"/account/{user.Username}/comments" with client as request: resp = request.get(endpoint, allow_redirects=False) @@ -1478,22 +1503,28 @@ def test_account_comments_unauthorized(client: TestClient, user: User): def test_account_comments(client: TestClient, user: User, package: Package): - """ This test may seem out of place, but it requires packages, + """This test may seem out of place, but it requires packages, so its being included in the packages routes test suite to - leverage existing fixtures. """ + leverage existing fixtures.""" now = time.utcnow() with db.begin(): # This comment's CommentTS is `now + 1`, so it is found in rendered # HTML before the rendered_comment, which has a CommentTS of `now`. - comment = db.create(PackageComment, - PackageBase=package.PackageBase, - User=user, Comments="Test comment", - CommentTS=now + 1) - rendered_comment = db.create(PackageComment, - PackageBase=package.PackageBase, - User=user, Comments="Test comment", - RenderedComment="

      Test comment

      ", - CommentTS=now) + comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Test comment", + CommentTS=now + 1, + ) + rendered_comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Test comment", + RenderedComment="

      Test comment

      ", + CommentTS=now, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{user.Username}/comments" @@ -1508,7 +1539,6 @@ def test_account_comments(client: TestClient, user: User, package: Package): assert comments[0].text.strip() == comment.Comments # And from the second, we have rendered content. - rendered = comments[1].xpath('./p') - expected = rendered_comment.RenderedComment.replace( - "

      ", "").replace("

      ", "") + rendered = comments[1].xpath("./p") + expected = rendered_comment.RenderedComment.replace("

      ", "").replace("

      ", "") assert rendered[0].text.strip() == expected diff --git a/test/test_packages_util.py b/test/test_packages_util.py index 02f84601..0042cd71 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -1,5 +1,4 @@ import pytest - from fastapi.testclient import TestClient from aurweb import asgi, config, db, time @@ -23,18 +22,22 @@ def setup(db_test): @pytest.fixture def maintainer() -> User: with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@examepl.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@examepl.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield maintainer @pytest.fixture def package(maintainer: User) -> Package: with db.begin(): - pkgbase = db.create(PackageBase, Name="test-pkg", - Packager=maintainer, Maintainer=maintainer) + pkgbase = db.create( + PackageBase, Name="test-pkg", Packager=maintainer, Maintainer=maintainer + ) package = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) yield package @@ -51,10 +54,9 @@ def test_package_link(client: TestClient, package: Package): def test_official_package_link(client: TestClient, package: Package): with db.begin(): - provider = db.create(OfficialProvider, - Name=package.Name, - Repo="core", - Provides=package.Name) + provider = db.create( + OfficialProvider, Name=package.Name, Repo="core", Provides=package.Name + ) expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}" assert util.package_link(provider) == expected @@ -63,9 +65,7 @@ def test_updated_packages(maintainer: User, package: Package): expected = { "Name": package.Name, "Version": package.Version, - "PackageBase": { - "ModifiedTS": package.PackageBase.ModifiedTS - } + "PackageBase": {"ModifiedTS": package.PackageBase.ModifiedTS}, } kill_redis() # Kill it here to ensure we're on a fake instance. @@ -77,8 +77,9 @@ def test_updated_packages(maintainer: User, package: Package): def test_query_voted(maintainer: User, package: Package): now = time.utcnow() with db.begin(): - db.create(PackageVote, User=maintainer, VoteTS=now, - PackageBase=package.PackageBase) + db.create( + PackageVote, User=maintainer, VoteTS=now, PackageBase=package.PackageBase + ) query = db.query(Package).filter(Package.ID == package.ID).all() query_voted = util.query_voted(query, maintainer) @@ -87,8 +88,7 @@ def test_query_voted(maintainer: User, package: Package): def test_query_notified(maintainer: User, package: Package): with db.begin(): - db.create(PackageNotification, User=maintainer, - PackageBase=package.PackageBase) + db.create(PackageNotification, User=maintainer, PackageBase=package.PackageBase) query = db.query(Package).filter(Package.ID == package.ID).all() query_notified = util.query_notified(query, maintainer) @@ -99,8 +99,9 @@ def test_source_uri_file(package: Package): FILE = "test_file" with db.begin(): - pkgsrc = db.create(PackageSource, Source=FILE, - Package=package, SourceArch="x86_64") + pkgsrc = db.create( + PackageSource, Source=FILE, Package=package, SourceArch="x86_64" + ) source_file_uri = config.get("options", "source_file_uri") file, uri = util.source_uri(pkgsrc) expected = source_file_uri % (pkgsrc.Source, package.PackageBase.Name) @@ -112,8 +113,9 @@ def test_source_uri_named_uri(package: Package): URL = "https://test.xyz" with db.begin(): - pkgsrc = db.create(PackageSource, Source=f"{FILE}::{URL}", - Package=package, SourceArch="x86_64") + pkgsrc = db.create( + PackageSource, Source=f"{FILE}::{URL}", Package=package, SourceArch="x86_64" + ) file, uri = util.source_uri(pkgsrc) assert (file, uri) == (FILE, URL) @@ -122,7 +124,8 @@ def test_source_uri_unnamed_uri(package: Package): URL = "https://test.xyz" with db.begin(): - pkgsrc = db.create(PackageSource, Source=f"{URL}", - Package=package, SourceArch="x86_64") + pkgsrc = db.create( + PackageSource, Source=f"{URL}", Package=package, SourceArch="x86_64" + ) file, uri = util.source_uri(pkgsrc) assert (file, uri) == (URL, URL) diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index 52241b9e..bfdb0c37 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -1,10 +1,8 @@ import re - from http import HTTPStatus from unittest import mock import pytest - from fastapi.testclient import TestClient from sqlalchemy import and_ @@ -33,30 +31,24 @@ def package_endpoint(package: Package) -> str: def create_package(pkgname: str, maintainer: User) -> Package: - pkgbase = db.create(PackageBase, - Name=pkgname, - Maintainer=maintainer) + pkgbase = db.create(PackageBase, Name=pkgname, Maintainer=maintainer) return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) -def create_package_dep(package: Package, depname: str, - dep_type_name: str = "depends") -> PackageDependency: - dep_type = db.query(DependencyType, - DependencyType.Name == dep_type_name).first() - return db.create(PackageDependency, - DependencyType=dep_type, - Package=package, - DepName=depname) +def create_package_dep( + package: Package, depname: str, dep_type_name: str = "depends" +) -> PackageDependency: + dep_type = db.query(DependencyType, DependencyType.Name == dep_type_name).first() + return db.create( + PackageDependency, DependencyType=dep_type, Package=package, DepName=depname + ) -def create_package_rel(package: Package, - relname: str) -> PackageRelation: - rel_type = db.query(RelationType, - RelationType.ID == PROVIDES_ID).first() - return db.create(PackageRelation, - RelationType=rel_type, - Package=package, - RelName=relname) +def create_package_rel(package: Package, relname: str) -> PackageRelation: + rel_type = db.query(RelationType, RelationType.ID == PROVIDES_ID).first() + return db.create( + PackageRelation, RelationType=rel_type, Package=package, RelName=relname + ) @pytest.fixture(autouse=True) @@ -66,76 +58,88 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: - """ Yield a FastAPI TestClient. """ + """Yield a FastAPI TestClient.""" yield TestClient(app=asgi.app) def create_user(username: str) -> User: with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @pytest.fixture def user() -> User: - """ Yield a user. """ + """Yield a user.""" user = create_user("test") yield user @pytest.fixture def maintainer() -> User: - """ Yield a specific User used to maintain packages. """ + """Yield a specific User used to maintain packages.""" account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@example.org", - Passwd="testPassword", - AccountType=account_type) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountType=account_type, + ) yield maintainer @pytest.fixture def comaintainer() -> User: - """ Yield a specific User used to maintain packages. """ + """Yield a specific User used to maintain packages.""" account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): - comaintainer = db.create(User, Username="test_comaintainer", - Email="test_comaintainer@example.org", - Passwd="testPassword", - AccountType=account_type) + comaintainer = db.create( + User, + Username="test_comaintainer", + Email="test_comaintainer@example.org", + Passwd="testPassword", + AccountType=account_type, + ) yield comaintainer @pytest.fixture def tu_user(): - tu_type = db.query(AccountType, - AccountType.AccountType == "Trusted User").first() + tu_type = db.query(AccountType, AccountType.AccountType == "Trusted User").first() with db.begin(): - tu_user = db.create(User, Username="test_tu", - Email="test_tu@example.org", - RealName="Test TU", Passwd="testPassword", - AccountType=tu_type) + tu_user = db.create( + User, + Username="test_tu", + Email="test_tu@example.org", + RealName="Test TU", + Passwd="testPassword", + AccountType=tu_type, + ) yield tu_user @pytest.fixture def package(maintainer: User) -> Package: - """ Yield a Package created by user. """ + """Yield a Package created by user.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, - Name="test-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name) + pkgbase = db.create( + PackageBase, + Name="test-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield package @@ -146,29 +150,34 @@ def pkgbase(package: Package) -> PackageBase: @pytest.fixture def target(maintainer: User) -> PackageBase: - """ Merge target. """ + """Merge target.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="target-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="target-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield pkgbase @pytest.fixture def pkgreq(user: User, pkgbase: PackageBase) -> PackageRequest: - """ Yield a PackageRequest related to `pkgbase`. """ + """Yield a PackageRequest related to `pkgbase`.""" with db.begin(): - pkgreq = db.create(PackageRequest, - ReqTypeID=DELETION_ID, - User=user, - PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=f"Deletion request for {pkgbase.Name}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=DELETION_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=f"Deletion request for {pkgbase.Name}", + ClosureComment=str(), + ) yield pkgreq @@ -177,31 +186,33 @@ def comment(user: User, package: Package) -> PackageComment: pkgbase = package.PackageBase now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, - User=user, - PackageBase=pkgbase, - Comments="Test comment.", - RenderedComment=str(), - CommentTS=now) + comment = db.create( + PackageComment, + User=user, + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment=str(), + CommentTS=now, + ) yield comment @pytest.fixture def packages(maintainer: User) -> list[Package]: - """ Yield 55 packages named pkg_0 .. pkg_54. """ + """Yield 55 packages named pkg_0 .. pkg_54.""" packages_ = [] now = time.utcnow() with db.begin(): for i in range(55): - pkgbase = db.create(PackageBase, - Name=f"pkg_{i}", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") packages_.append(package) yield packages_ @@ -210,18 +221,18 @@ def packages(maintainer: User) -> list[Package]: @pytest.fixture def requests(user: User, packages: list[Package]) -> list[PackageRequest]: pkgreqs = [] - deletion_type = db.query(RequestType).filter( - RequestType.ID == DELETION_ID - ).first() + deletion_type = db.query(RequestType).filter(RequestType.ID == DELETION_ID).first() with db.begin(): for i in range(55): - pkgreq = db.create(PackageRequest, - RequestType=deletion_type, - User=user, - PackageBase=packages[i].PackageBase, - PackageBaseName=packages[i].Name, - Comments=f"Deletion request for pkg_{i}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + RequestType=deletion_type, + User=user, + PackageBase=packages[i].PackageBase, + PackageBaseName=packages[i].Name, + Comments=f"Deletion request for pkg_{i}", + ClosureComment=str(), + ) pkgreqs.append(pkgreq) yield pkgreqs @@ -234,21 +245,18 @@ def test_pkgbase_not_found(client: TestClient): def test_pkgbase_redirect(client: TestClient, package: Package): with client as request: - resp = request.get(f"/pkgbase/{package.Name}", - allow_redirects=False) + resp = request.get(f"/pkgbase/{package.Name}", allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/packages/{package.Name}" def test_pkgbase(client: TestClient, package: Package): with db.begin(): - second = db.create(Package, Name="second-pkg", - PackageBase=package.PackageBase) + second = db.create(Package, Name="second-pkg", PackageBase=package.PackageBase) expected = [package.Name, second.Name] with client as request: - resp = request.get(f"/pkgbase/{package.Name}", - allow_redirects=False) + resp = request.get(f"/pkgbase/{package.Name}", allow_redirects=False) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -264,8 +272,9 @@ def test_pkgbase(client: TestClient, package: Package): assert pkgs[i].text.strip() == name -def test_pkgbase_maintainer(client: TestClient, user: User, maintainer: User, - package: Package): +def test_pkgbase_maintainer( + client: TestClient, user: User, maintainer: User, package: Package +): """ Test that the Maintainer field is beind displayed correctly. @@ -273,9 +282,9 @@ def test_pkgbase_maintainer(client: TestClient, user: User, maintainer: User, the maintainer. """ with db.begin(): - db.create(PackageComaintainer, User=user, - PackageBase=package.PackageBase, - Priority=1) + db.create( + PackageComaintainer, User=user, PackageBase=package.PackageBase, Priority=1 + ) with client as request: resp = request.get(f"/pkgbase/{package.Name}") @@ -286,7 +295,7 @@ def test_pkgbase_maintainer(client: TestClient, user: User, maintainer: User, maint = root.xpath('//table[@id="pkginfo"]/tr[@class="pkgmaint"]/td')[0] maint, comaint = maint.text.strip().split() assert maint == maintainer.Username - assert comaint == f'({user.Username})' + assert comaint == f"({user.Username})" def test_pkgbase_voters(client: TestClient, tu_user: User, package: Package): @@ -309,8 +318,7 @@ def test_pkgbase_voters(client: TestClient, tu_user: User, package: Package): assert rows[0].text.strip() == tu_user.Username -def test_pkgbase_voters_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_voters_unauthorized(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/voters" @@ -324,25 +332,30 @@ def test_pkgbase_voters_unauthorized(client: TestClient, user: User, assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" -def test_pkgbase_comment_not_found(client: TestClient, maintainer: User, - package: Package): +def test_pkgbase_comment_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # A non-existing comment. endpoint = f"/pkgbase/{package.PackageBase.Name}/comments/{comment_id}" with client as request: - resp = request.post(endpoint, data={ - "comment": "Failure" - }, cookies=cookies) + resp = request.post(endpoint, data={"comment": "Failure"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comment_form_unauthorized(client: TestClient, user: User, - maintainer: User, package: Package): +def test_pkgbase_comment_form_unauthorized( + client: TestClient, user: User, maintainer: User, package: Package +): now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, PackageBase=package.PackageBase, - User=maintainer, Comments="Test", - RenderedComment=str(), CommentTS=now) + comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=maintainer, + Comments="Test", + RenderedComment=str(), + CommentTS=now, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name @@ -352,8 +365,9 @@ def test_pkgbase_comment_form_unauthorized(client: TestClient, user: User, assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_comment_form_not_found(client: TestClient, maintainer: User, - package: Package): +def test_pkgbase_comment_form_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # A non-existing comment. pkgbasename = package.PackageBase.Name @@ -363,8 +377,9 @@ def test_pkgbase_comment_form_not_found(client: TestClient, maintainer: User, assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comments_missing_comment(client: TestClient, maintainer: User, - package: Package): +def test_pkgbase_comments_missing_comment( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/comments" with client as request: @@ -372,9 +387,10 @@ def test_pkgbase_comments_missing_comment(client: TestClient, maintainer: User, assert resp.status_code == int(HTTPStatus.BAD_REQUEST) -def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, - package: Package): - """ This test includes tests against the following routes: +def test_pkgbase_comments( + client: TestClient, maintainer: User, user: User, package: Package +): + """This test includes tests against the following routes: - POST /pkgbase/{name}/comments - GET /pkgbase/{name} (to check comments) - Tested against a comment created with the POST route @@ -383,18 +399,17 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, """ with db.begin(): user.CommentNotify = 1 - db.create(PackageNotification, - PackageBase=package.PackageBase, - User=user) + db.create(PackageNotification, PackageBase=package.PackageBase, User=user) cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments" with client as request: - resp = request.post(endpoint, data={ - "comment": "Test comment.", - "enable_notifications": True - }, cookies=cookies) + resp = request.post( + endpoint, + data={"comment": "Test comment.", "enable_notifications": True}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # user should've gotten a CommentNotification email. @@ -438,10 +453,11 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, comment_id = int(headers[0].attrib["id"].split("-")[-1]) endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}" with client as request: - resp = request.post(endpoint, data={ - "comment": "Edited comment.", - "enable_notifications": True - }, cookies=cookies) + resp = request.post( + endpoint, + data={"comment": "Edited comment.", "enable_notifications": True}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: @@ -479,27 +495,33 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, assert "form" in data -def test_pkgbase_comment_edit_unauthorized(client: TestClient, - user: User, - maintainer: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_edit_unauthorized( + client: TestClient, + user: User, + maintainer: User, + package: Package, + comment: PackageComment, +): pkgbase = package.PackageBase cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: endp = f"/pkgbase/{pkgbase.Name}/comments/{comment.ID}" - response = request.post(endp, data={ - "comment": "abcd im trying to change this comment." - }, cookies=cookies) + response = request.post( + endp, + data={"comment": "abcd im trying to change this comment."}, + cookies=cookies, + ) assert response.status_code == HTTPStatus.UNAUTHORIZED -def test_pkgbase_comment_delete(client: TestClient, - maintainer: User, - user: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_delete( + client: TestClient, + maintainer: User, + user: User, + package: Package, + comment: PackageComment, +): # Test the unauthorized case of comment deletion. cookies = {"AURSID": user.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name @@ -524,10 +546,9 @@ def test_pkgbase_comment_delete(client: TestClient, assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_pkgbase_comment_delete_unauthorized(client: TestClient, - maintainer: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_delete_unauthorized( + client: TestClient, maintainer: User, package: Package, comment: PackageComment +): # Test the unauthorized case of comment deletion. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name @@ -537,9 +558,9 @@ def test_pkgbase_comment_delete_unauthorized(client: TestClient, assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_comment_delete_not_found(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_comment_delete_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # Non-existing comment. pkgbasename = package.PackageBase.Name @@ -549,9 +570,9 @@ def test_pkgbase_comment_delete_not_found(client: TestClient, assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comment_undelete_not_found(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_comment_undelete_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # Non-existing comment. pkgbasename = package.PackageBase.Name @@ -561,13 +582,18 @@ def test_pkgbase_comment_undelete_not_found(client: TestClient, assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comment_pin_as_co(client: TestClient, package: Package, - comment: PackageComment): +def test_pkgbase_comment_pin_as_co( + client: TestClient, package: Package, comment: PackageComment +): comaint = create_user("comaint1") with db.begin(): - db.create(PackageComaintainer, PackageBase=package.PackageBase, - User=comaint, Priority=1) + db.create( + PackageComaintainer, + PackageBase=package.PackageBase, + User=comaint, + Priority=1, + ) # Pin the comment. pkgbasename = package.PackageBase.Name @@ -590,10 +616,9 @@ def test_pkgbase_comment_pin_as_co(client: TestClient, package: Package, assert comment.PinnedTS == 0 -def test_pkgbase_comment_pin(client: TestClient, - maintainer: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_pin( + client: TestClient, maintainer: User, package: Package, comment: PackageComment +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = comment.ID pkgbasename = package.PackageBase.Name @@ -617,10 +642,9 @@ def test_pkgbase_comment_pin(client: TestClient, assert comment.PinnedTS == 0 -def test_pkgbase_comment_pin_unauthorized(client: TestClient, - user: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_pin_unauthorized( + client: TestClient, user: User, package: Package, comment: PackageComment +): cookies = {"AURSID": user.login(Request(), "testPassword")} comment_id = comment.ID pkgbasename = package.PackageBase.Name @@ -630,10 +654,9 @@ def test_pkgbase_comment_pin_unauthorized(client: TestClient, assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_comment_unpin_unauthorized(client: TestClient, - user: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_unpin_unauthorized( + client: TestClient, user: User, package: Package, comment: PackageComment +): cookies = {"AURSID": user.login(Request(), "testPassword")} comment_id = comment.ID pkgbasename = package.PackageBase.Name @@ -651,8 +674,7 @@ def test_pkgbase_comaintainers_not_found(client: TestClient, maintainer: User): assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comaintainers_post_not_found(client: TestClient, - maintainer: User): +def test_pkgbase_comaintainers_post_not_found(client: TestClient, maintainer: User): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = "/pkgbase/fake/comaintainers" with client as request: @@ -660,8 +682,9 @@ def test_pkgbase_comaintainers_post_not_found(client: TestClient, assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comaintainers_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_comaintainers_unauthorized( + client: TestClient, user: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} @@ -671,9 +694,9 @@ def test_pkgbase_comaintainers_unauthorized(client: TestClient, user: User, assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" -def test_pkgbase_comaintainers_post_unauthorized(client: TestClient, - user: User, - package: Package): +def test_pkgbase_comaintainers_post_unauthorized( + client: TestClient, user: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} @@ -683,16 +706,16 @@ def test_pkgbase_comaintainers_post_unauthorized(client: TestClient, assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" -def test_pkgbase_comaintainers_post_invalid_user(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_comaintainers_post_invalid_user( + client: TestClient, maintainer: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "users": "\nfake\n" - }, cookies=cookies, allow_redirects=False) + resp = request.post( + endpoint, data={"users": "\nfake\n"}, cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -700,8 +723,9 @@ def test_pkgbase_comaintainers_post_invalid_user(client: TestClient, assert error.text.strip() == "Invalid user name: fake" -def test_pkgbase_comaintainers(client: TestClient, user: User, - maintainer: User, package: Package): +def test_pkgbase_comaintainers( + client: TestClient, user: User, maintainer: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": maintainer.login(Request(), "testPassword")} @@ -709,17 +733,23 @@ def test_pkgbase_comaintainers(client: TestClient, user: User, # Start off by adding user as a comaintainer to package. # The maintainer username given should be ignored. with client as request: - resp = request.post(endpoint, data={ - "users": f"\n{user.Username}\n{maintainer.Username}\n" - }, cookies=cookies, allow_redirects=False) + resp = request.post( + endpoint, + data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" # Do it again to exercise the last_priority bump path. with client as request: - resp = request.post(endpoint, data={ - "users": f"\n{user.Username}\n{maintainer.Username}\n" - }, cookies=cookies, allow_redirects=False) + resp = request.post( + endpoint, + data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -736,9 +766,9 @@ def test_pkgbase_comaintainers(client: TestClient, user: User, # Finish off by removing all the comaintainers. with client as request: - resp = request.post(endpoint, data={ - "users": str() - }, cookies=cookies, allow_redirects=False) + resp = request.post( + endpoint, data={"users": str()}, cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -774,15 +804,15 @@ def test_pkgbase_request(client: TestClient, user: User, package: Package): def test_pkgbase_request_post_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/pkgbase/fake/request", data={ - "type": "fake" - }, cookies=cookies) + resp = request.post( + "/pkgbase/fake/request", data={"type": "fake"}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_request_post_invalid_type(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_invalid_type( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -790,16 +820,20 @@ def test_pkgbase_request_post_invalid_type(client: TestClient, assert resp.status_code == int(HTTPStatus.BAD_REQUEST) -def test_pkgbase_request_post_no_comment_error(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_no_comment_error( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "deletion", - "comments": "" # An empty comment field causes an error. - }, cookies=cookies) + resp = request.post( + endpoint, + data={ + "type": "deletion", + "comments": "", # An empty comment field causes an error. + }, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -808,17 +842,22 @@ def test_pkgbase_request_post_no_comment_error(client: TestClient, assert error.text.strip() == expected -def test_pkgbase_request_post_merge_not_found_error(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_merge_not_found_error( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "merge", - "merge_into": "fake", # There is no PackageBase.Name "fake" - "comments": "We want to merge this." - }, cookies=cookies, allow_redirects=False) + resp = request.post( + endpoint, + data={ + "type": "merge", + "merge_into": "fake", # There is no PackageBase.Name "fake" + "comments": "We want to merge this.", + }, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -827,17 +866,22 @@ def test_pkgbase_request_post_merge_not_found_error(client: TestClient, assert error.text.strip() == expected -def test_pkgbase_request_post_merge_no_merge_into_error(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_merge_no_merge_into_error( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "merge", - "merge_into": "", # There is no PackageBase.Name "fake" - "comments": "We want to merge this." - }, cookies=cookies, allow_redirects=False) + resp = request.post( + endpoint, + data={ + "type": "merge", + "merge_into": "", # There is no PackageBase.Name "fake" + "comments": "We want to merge this.", + }, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -846,16 +890,22 @@ def test_pkgbase_request_post_merge_no_merge_into_error(client: TestClient, assert error.text.strip() == expected -def test_pkgbase_request_post_merge_self_error(client: TestClient, user: User, - package: Package): +def test_pkgbase_request_post_merge_self_error( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "merge", - "merge_into": package.PackageBase.Name, - "comments": "We want to merge this." - }, cookies=cookies, allow_redirects=False) + resp = request.post( + endpoint, + data={ + "type": "merge", + "merge_into": package.PackageBase.Name, + "comments": "We want to merge this.", + }, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -864,8 +914,9 @@ def test_pkgbase_request_post_merge_self_error(client: TestClient, user: User, assert error.text.strip() == expected -def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, - package: Package): +def test_pkgbase_flag( + client: TestClient, user: User, maintainer: User, package: Package +): pkgbase = package.PackageBase # We shouldn't have flagged the package yet; assert so. @@ -882,8 +933,9 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # Now, let's check the /pkgbase/{name}/flag-comment route. flag_comment_endpoint = f"/pkgbase/{pkgbase.Name}/flag-comment" with client as request: - resp = request.get(flag_comment_endpoint, cookies=cookies, - allow_redirects=False) + resp = request.get( + flag_comment_endpoint, cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -894,9 +946,7 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # Flag it with a valid comment. with client as request: - resp = request.post(endpoint, data={ - "comments": "Test" - }, cookies=cookies) + resp = request.post(endpoint, data={"comments": "Test"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger == user assert pkgbase.FlaggerComment == "Test" @@ -907,8 +957,9 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # Now, let's check the /pkgbase/{name}/flag-comment route. flag_comment_endpoint = f"/pkgbase/{pkgbase.Name}/flag-comment" with client as request: - resp = request.get(flag_comment_endpoint, cookies=cookies, - allow_redirects=False) + resp = request.get( + flag_comment_endpoint, cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.OK) # Now try to perform a get; we should be redirected because @@ -918,10 +969,13 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, assert resp.status_code == int(HTTPStatus.SEE_OTHER) with db.begin(): - user2 = db.create(User, Username="test2", - Email="test2@example.org", - Passwd="testPassword", - AccountType=user.AccountType) + user2 = db.create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountType=user.AccountType, + ) # Now, test that the 'user2' user can't unflag it, because they # didn't flag it to begin with. @@ -941,9 +995,9 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # Flag it again. with client as request: - resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data={ - "comments": "Test" - }, cookies=cookies) + resp = request.post( + f"/pkgbase/{pkgbase.Name}/flag", data={"comments": "Test"}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Now, unflag it for real. @@ -961,16 +1015,17 @@ def test_pkgbase_flag_vcs(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag", - cookies=cookies) + resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag", cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) - expected = ("This seems to be a VCS package. Please do " - "not flag it out-of-date if the package " - "version in the AUR does not match the most recent commit. " - "Flagging this package should only be done if the sources " - "moved or changes in the PKGBUILD are required because of " - "recent upstream changes.") + expected = ( + "This seems to be a VCS package. Please do " + "not flag it out-of-date if the package " + "version in the AUR does not match the most recent commit. " + "Flagging this package should only be done if the sources " + "moved or changes in the PKGBUILD are required because of " + "recent upstream changes." + ) assert expected in resp.text @@ -978,9 +1033,7 @@ def test_pkgbase_notify(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase # We have no notif record yet; assert that. - notif = pkgbase.notifications.filter( - PackageNotification.UserID == user.ID - ).first() + notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() assert notif is None # Enable notifications. @@ -990,9 +1043,7 @@ def test_pkgbase_notify(client: TestClient, user: User, package: Package): resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - notif = pkgbase.notifications.filter( - PackageNotification.UserID == user.ID - ).first() + notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() assert notif is not None # Disable notifications. @@ -1001,9 +1052,7 @@ def test_pkgbase_notify(client: TestClient, user: User, package: Package): resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - notif = pkgbase.notifications.filter( - PackageNotification.UserID == user.ID - ).first() + notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() assert notif is None @@ -1036,9 +1085,9 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): assert pkgbase.NumVotes == 0 -def test_pkgbase_disown_as_sole_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_disown_as_sole_maintainer( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/disown" @@ -1049,26 +1098,23 @@ def test_pkgbase_disown_as_sole_maintainer(client: TestClient, assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_pkgbase_disown_as_maint_with_comaint(client: TestClient, - user: User, - maintainer: User, - package: Package): - """ When disowning as a maintainer, the lowest priority comaintainer - is promoted to maintainer. """ +def test_pkgbase_disown_as_maint_with_comaint( + client: TestClient, user: User, maintainer: User, package: Package +): + """When disowning as a maintainer, the lowest priority comaintainer + is promoted to maintainer.""" pkgbase = package.PackageBase endp = f"/pkgbase/{pkgbase.Name}/disown" post_data = {"confirm": True} with db.begin(): - db.create(PackageComaintainer, - PackageBase=pkgbase, - User=user, - Priority=1) + db.create(PackageComaintainer, PackageBase=pkgbase, User=user, Priority=1) maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(endp, data=post_data, cookies=maint_cookies, - allow_redirects=True) + resp = request.post( + endp, data=post_data, cookies=maint_cookies, allow_redirects=True + ) assert resp.status_code == int(HTTPStatus.OK) package = db.refresh(package) @@ -1078,8 +1124,13 @@ def test_pkgbase_disown_as_maint_with_comaint(client: TestClient, assert pkgbase.comaintainers.count() == 0 -def test_pkgbase_disown(client: TestClient, user: User, maintainer: User, - comaintainer: User, package: Package): +def test_pkgbase_disown( + client: TestClient, + user: User, + maintainer: User, + comaintainer: User, + package: Package, +): maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comaint_cookies = {"AURSID": comaintainer.login(Request(), "testPassword")} user_cookies = {"AURSID": user.login(Request(), "testPassword")} @@ -1088,21 +1139,18 @@ def test_pkgbase_disown(client: TestClient, user: User, maintainer: User, endpoint = f"{pkgbase_endp}/disown" with db.begin(): - db.create(PackageComaintainer, - User=comaintainer, - PackageBase=pkgbase, - Priority=1) + db.create( + PackageComaintainer, User=comaintainer, PackageBase=pkgbase, Priority=1 + ) # GET as a normal user, which is rejected for lack of credentials. with client as request: - resp = request.get(endpoint, cookies=user_cookies, - allow_redirects=False) + resp = request.get(endpoint, cookies=user_cookies, allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # GET as a comaintainer. with client as request: - resp = request.get(endpoint, cookies=comaint_cookies, - allow_redirects=False) + resp = request.get(endpoint, cookies=comaint_cookies, allow_redirects=False) assert resp.status_code == int(HTTPStatus.OK) # Ensure that the comaintainer can see "Disown Package" link @@ -1146,8 +1194,9 @@ def test_pkgbase_disown(client: TestClient, user: User, maintainer: User, assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_pkgbase_adopt(client: TestClient, user: User, tu_user: User, - maintainer: User, package: Package): +def test_pkgbase_adopt( + client: TestClient, user: User, tu_user: User, maintainer: User, package: Package +): # Unset the maintainer as if package is orphaned. with db.begin(): package.PackageBase.Maintainer = None @@ -1165,22 +1214,19 @@ def test_pkgbase_adopt(client: TestClient, user: User, tu_user: User, # Try to adopt it when it already has a maintainer; nothing changes. user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=user_cookies, - allow_redirects=False) + resp = request.post(endpoint, cookies=user_cookies, allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == maintainer # Steal the package as a TU. tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=tu_cookies, - allow_redirects=False) + resp = request.post(endpoint, cookies=tu_cookies, allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == tu_user -def test_pkgbase_delete_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_delete_unauthorized(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/delete" @@ -1219,9 +1265,7 @@ def test_pkgbase_delete(client: TestClient, tu_user: User, package: Package): assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Let's assert that the package base record got removed. - record = db.query(PackageBase).filter( - PackageBase.Name == pkgbase.Name - ).first() + record = db.query(PackageBase).filter(PackageBase.Name == pkgbase.Name).first() assert record is None # Two emails should've been sent out; an autogenerated @@ -1234,9 +1278,9 @@ def test_pkgbase_delete(client: TestClient, tu_user: User, package: Package): assert re.match(expr, subject) -def test_pkgbase_delete_with_request(client: TestClient, tu_user: User, - pkgbase: PackageBase, - pkgreq: PackageRequest): +def test_pkgbase_delete_with_request( + client: TestClient, tu_user: User, pkgbase: PackageBase, pkgreq: PackageRequest +): # TODO: Test that a previously existing request gets Accepted when # a TU deleted the package. @@ -1257,12 +1301,15 @@ def test_pkgbase_delete_with_request(client: TestClient, tu_user: User, assert re.match(expr, email.headers.get("Subject")) -def test_packages_post_unknown_action(client: TestClient, user: User, - package: Package): +def test_packages_post_unknown_action(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "unknown"}, - cookies=cookies, allow_redirects=False) + resp = request.post( + "/packages", + data={"action": "unknown"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1274,8 +1321,12 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + resp = request.post( + "/packages", + data={"action": "stub"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1291,8 +1342,12 @@ def test_packages_post(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + resp = request.post( + "/packages", + data={"action": "stub"}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.OK) errors = get_successes(resp.text) @@ -1300,8 +1355,7 @@ def test_packages_post(client: TestClient, user: User, package: Package): assert errors[0].text.strip() == expected -def test_pkgbase_merge_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_merge_unauthorized(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: @@ -1318,8 +1372,9 @@ def test_pkgbase_merge(client: TestClient, tu_user: User, package: Package): assert not get_errors(resp.text) -def test_pkgbase_merge_post_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_merge_post_unauthorized( + client: TestClient, user: User, package: Package +): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: @@ -1327,54 +1382,62 @@ def test_pkgbase_merge_post_unauthorized(client: TestClient, user: User, assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_merge_post_unconfirmed(client: TestClient, tu_user: User, - package: Package): +def test_pkgbase_merge_post_unconfirmed( + client: TestClient, tu_user: User, package: Package +): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) - expected = ("The selected packages have not been deleted, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected -def test_pkgbase_merge_post_invalid_into(client: TestClient, tu_user: User, - package: Package): +def test_pkgbase_merge_post_invalid_into( + client: TestClient, tu_user: User, package: Package +): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, data={ - "into": "not_real", - "confirm": True - }, cookies=cookies) + resp = request.post( + endpoint, data={"into": "not_real", "confirm": True}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "Cannot find package to merge votes and comments into." assert errors[0].text.strip() == expected -def test_pkgbase_merge_post_self_invalid(client: TestClient, tu_user: User, - package: Package): +def test_pkgbase_merge_post_self_invalid( + client: TestClient, tu_user: User, package: Package +): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, data={ - "into": package.PackageBase.Name, - "confirm": True - }, cookies=cookies) + resp = request.post( + endpoint, + data={"into": package.PackageBase.Name, "confirm": True}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "Cannot merge a package base with itself." assert errors[0].text.strip() == expected -def test_pkgbase_merge_post(client: TestClient, tu_user: User, - package: Package, - pkgbase: PackageBase, - target: PackageBase, - pkgreq: PackageRequest): +def test_pkgbase_merge_post( + client: TestClient, + tu_user: User, + package: Package, + pkgbase: PackageBase, + target: PackageBase, + pkgreq: PackageRequest, +): pkgname = package.Name pkgbasename = pkgbase.Name @@ -1401,9 +1464,9 @@ def test_pkgbase_merge_post(client: TestClient, tu_user: User, # Comment on the package. endpoint = f"/pkgbase/{package.PackageBase.Name}/comments" with client as request: - resp = request.post(endpoint, data={ - "comment": "Test comment." - }, cookies=cookies) + resp = request.post( + endpoint, data={"comment": "Test comment."}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Save these relationships for later comparison. @@ -1414,10 +1477,9 @@ def test_pkgbase_merge_post(client: TestClient, tu_user: User, # Merge the package into target. endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, data={ - "into": target.Name, - "confirm": True - }, cookies=cookies) + resp = request.post( + endpoint, data={"into": target.Name, "confirm": True}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) loc = resp.headers.get("location") assert loc == f"/pkgbase/{target.Name}" @@ -1442,11 +1504,17 @@ def test_pkgbase_merge_post(client: TestClient, tu_user: User, assert pkgreq.Closer is not None # A PackageRequest is always created when merging this way. - pkgreq = db.query(PackageRequest).filter( - and_(PackageRequest.ReqTypeID == MERGE_ID, - PackageRequest.PackageBaseName == pkgbasename, - PackageRequest.MergeBaseName == target.Name) - ).first() + pkgreq = ( + db.query(PackageRequest) + .filter( + and_( + PackageRequest.ReqTypeID == MERGE_ID, + PackageRequest.PackageBaseName == pkgbasename, + PackageRequest.MergeBaseName == target.Name, + ) + ) + .first() + ) assert pkgreq is not None @@ -1464,9 +1532,9 @@ def test_pkgbase_keywords(client: TestClient, user: User, package: Package): cookies = {"AURSID": maint.login(Request(), "testPassword")} post_endpoint = f"{endpoint}/keywords" with client as request: - resp = request.post(post_endpoint, data={ - "keywords": "abc test" - }, cookies=cookies) + resp = request.post( + post_endpoint, data={"keywords": "abc test"}, cookies=cookies + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: @@ -1495,9 +1563,11 @@ def test_pkgbase_empty_keywords(client: TestClient, user: User, package: Package cookies = {"AURSID": maint.login(Request(), "testPassword")} post_endpoint = f"{endpoint}/keywords" with client as request: - resp = request.post(post_endpoint, data={ - "keywords": "abc test foo bar " - }, cookies=cookies) + resp = request.post( + post_endpoint, + data={"keywords": "abc test foo bar "}, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: @@ -1514,8 +1584,9 @@ def test_pkgbase_empty_keywords(client: TestClient, user: User, package: Package def test_unauthorized_pkgbase_keywords(client: TestClient, package: Package): with db.begin(): - user = db.create(User, Username="random_user", Email="random_user", - Passwd="testPassword") + user = db.create( + User, Username="random_user", Email="random_user", Passwd="testPassword" + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -1525,20 +1596,25 @@ def test_unauthorized_pkgbase_keywords(client: TestClient, package: Package): assert response.status_code == HTTPStatus.UNAUTHORIZED -def test_independent_user_unflag(client: TestClient, user: User, - package: Package): +def test_independent_user_unflag(client: TestClient, user: User, package: Package): with db.begin(): - flagger = db.create(User, Username="test_flagger", - Email="test_flagger@example.com", - Passwd="testPassword") + flagger = db.create( + User, + Username="test_flagger", + Email="test_flagger@example.com", + Passwd="testPassword", + ) pkgbase = package.PackageBase cookies = {"AURSID": flagger.login(Request(), "testPassword")} with client as request: endp = f"/pkgbase/{pkgbase.Name}/flag" - response = request.post(endp, data={ - "comments": "This thing needs a flag!" - }, cookies=cookies, allow_redirects=True) + response = request.post( + endp, + data={"comments": "This thing needs a flag!"}, + cookies=cookies, + allow_redirects=True, + ) assert response.status_code == HTTPStatus.OK # At this point, we've flagged it as `flagger`. diff --git a/test/test_pkgmaint.py b/test/test_pkgmaint.py index da758c22..a0fece78 100644 --- a/test/test_pkgmaint.py +++ b/test/test_pkgmaint.py @@ -14,8 +14,13 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -26,11 +31,12 @@ def packages(user: User) -> list[Package]: now = time.utcnow() with db.begin(): for i in range(5): - pkgbase = db.create(PackageBase, Name=f"pkg_{i}", - SubmittedTS=now, - ModifiedTS=now) - pkg = db.create(Package, PackageBase=pkgbase, - Name=f"pkg_{i}", Version=f"{i}.0") + pkgbase = db.create( + PackageBase, Name=f"pkg_{i}", SubmittedTS=now, ModifiedTS=now + ) + pkg = db.create( + Package, PackageBase=pkgbase, Name=f"pkg_{i}", Version=f"{i}.0" + ) output.append(pkg) yield output @@ -48,7 +54,7 @@ def test_pkgmaint(packages: list[Package]): # Modify the first package so it's out of date and gets deleted. with db.begin(): # Reduce SubmittedTS by a day + 10 seconds. - packages[0].PackageBase.SubmittedTS -= (86400 + 10) + packages[0].PackageBase.SubmittedTS -= 86400 + 10 # Run pkgmaint. pkgmaint.main() diff --git a/test/test_ratelimit.py b/test/test_ratelimit.py index 859adea9..20528847 100644 --- a/test/test_ratelimit.py +++ b/test/test_ratelimit.py @@ -1,7 +1,6 @@ from unittest import mock import pytest - from redis.client import Pipeline from aurweb import config, db, logging @@ -49,6 +48,7 @@ def mock_config_getboolean(return_value: int = 0): if section == "ratelimit" and key == "cache": return return_value return config_getboolean(section, key) + return fn @@ -60,17 +60,22 @@ def mock_config_get(return_value: str = "none"): if section == "options" and key == "cache": return return_value return config_get(section, key) + return fn @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) @mock.patch("aurweb.config.getboolean", side_effect=mock_config_getboolean(1)) @mock.patch("aurweb.config.get", side_effect=mock_config_get("none")) -def test_ratelimit_redis(get: mock.MagicMock, getboolean: mock.MagicMock, - getint: mock.MagicMock, pipeline: Pipeline): - """ This test will only cover aurweb.ratelimit's Redis +def test_ratelimit_redis( + get: mock.MagicMock, + getboolean: mock.MagicMock, + getint: mock.MagicMock, + pipeline: Pipeline, +): + """This test will only cover aurweb.ratelimit's Redis path if a real Redis server is configured. Otherwise, - it'll use the database. """ + it'll use the database.""" # We'll need a Request for everything here. request = Request() @@ -96,8 +101,12 @@ def test_ratelimit_redis(get: mock.MagicMock, getboolean: mock.MagicMock, @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) @mock.patch("aurweb.config.getboolean", side_effect=mock_config_getboolean(0)) @mock.patch("aurweb.config.get", side_effect=mock_config_get("none")) -def test_ratelimit_db(get: mock.MagicMock, getboolean: mock.MagicMock, - getint: mock.MagicMock, pipeline: Pipeline): +def test_ratelimit_db( + get: mock.MagicMock, + getboolean: mock.MagicMock, + getint: mock.MagicMock, + pipeline: Pipeline, +): # We'll need a Request for everything here. request = Request() diff --git a/test/test_redis.py b/test/test_redis.py index 82aebb57..a66cd204 100644 --- a/test/test_redis.py +++ b/test/test_redis.py @@ -3,13 +3,13 @@ from unittest import mock import pytest import aurweb.config - from aurweb.redis import redis_connection @pytest.fixture def rediss(): - """ Create a RedisStub. """ + """Create a RedisStub.""" + def mock_get(section, key): return "none" diff --git a/test/test_rendercomment.py b/test/test_rendercomment.py index bf4009fd..5b7ff5ac 100644 --- a/test/test_rendercomment.py +++ b/test/test_rendercomment.py @@ -31,8 +31,13 @@ def setup(db_test, git: GitRepository): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @@ -40,24 +45,32 @@ def user() -> User: def pkgbase(user: User) -> PackageBase: now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Packager=user, Name="pkgbase_0", - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Packager=user, + Name="pkgbase_0", + SubmittedTS=now, + ModifiedTS=now, + ) yield pkgbase @pytest.fixture def package(pkgbase: PackageBase) -> Package: with db.begin(): - package = db.create(Package, PackageBase=pkgbase, - Name=pkgbase.Name, Version="1.0") + package = db.create( + Package, PackageBase=pkgbase, Name=pkgbase.Name, Version="1.0" + ) yield package -def create_comment(user: User, pkgbase: PackageBase, comments: str, - render: bool = True): +def create_comment( + user: User, pkgbase: PackageBase, comments: str, render: bool = True +): with db.begin(): - comment = db.create(PackageComment, User=user, - PackageBase=pkgbase, Comments=comments) + comment = db.create( + PackageComment, User=user, PackageBase=pkgbase, Comments=comments + ) if render: update_comment_render(comment) return comment @@ -86,8 +99,7 @@ def test_rendercomment_main(user: User, pkgbase: PackageBase): def test_markdown_conversion(user: User, pkgbase: PackageBase): text = "*Hello* [world](https://aur.archlinux.org)!" comment = create_comment(user, pkgbase, text) - expected = ('

      Hello ' - 'world!

      ') + expected = "

      Hello " 'world!

      ' assert comment.RenderedComment == expected @@ -109,7 +121,7 @@ Visit [Arch Linux][arch]. [arch]: https://www.archlinux.org/\ """ comment = create_comment(user, pkgbase, text) - expected = '''\ + expected = """\

      Visit \ https://www.archlinux.org/#_test_. Visit https://www.archlinux.org/. @@ -117,7 +129,7 @@ Visit https://www.archlinux.org/. Visit https://www.archlinux.org/. Visit Arch Linux. Visit Arch Linux.

      \ -''' +""" assert comment.RenderedComment == expected diff --git a/test/test_requests.py b/test/test_requests.py index b7ab3835..fd831674 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -1,10 +1,8 @@ import re - from http import HTTPStatus from logging import DEBUG import pytest - from fastapi import HTTPException from fastapi.testclient import TestClient @@ -24,13 +22,13 @@ from aurweb.testing.requests import Request @pytest.fixture(autouse=True) def setup(db_test) -> None: - """ Setup the database. """ + """Setup the database.""" return @pytest.fixture def client() -> TestClient: - """ Yield a TestClient. """ + """Yield a TestClient.""" yield TestClient(app=asgi.app) @@ -43,21 +41,26 @@ def create_user(username: str, email: str) -> User: :return: User instance """ with db.begin(): - user = db.create(User, Username=username, Email=email, - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=email, + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @pytest.fixture def user() -> User: - """ Yield a User instance. """ + """Yield a User instance.""" user = create_user("test", "test@example.org") yield user @pytest.fixture def auser(user: User) -> User: - """ Yield an authenticated User instance. """ + """Yield an authenticated User instance.""" cookies = {"AURSID": user.login(Request(), "testPassword")} user.cookies = cookies yield user @@ -65,14 +68,14 @@ def auser(user: User) -> User: @pytest.fixture def user2() -> User: - """ Yield a secondary non-maintainer User instance. """ + """Yield a secondary non-maintainer User instance.""" user = create_user("test2", "test2@example.org") yield user @pytest.fixture def auser2(user2: User) -> User: - """ Yield an authenticated secondary non-maintainer User instance. """ + """Yield an authenticated secondary non-maintainer User instance.""" cookies = {"AURSID": user2.login(Request(), "testPassword")} user2.cookies = cookies yield user2 @@ -80,31 +83,34 @@ def auser2(user2: User) -> User: @pytest.fixture def maintainer() -> User: - """ Yield a specific User used to maintain packages. """ + """Yield a specific User used to maintain packages.""" with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield maintainer @pytest.fixture def packages(maintainer: User) -> list[Package]: - """ Yield 55 packages named pkg_0 .. pkg_54. """ + """Yield 55 packages named pkg_0 .. pkg_54.""" packages_ = [] now = time.utcnow() with db.begin(): for i in range(55): - pkgbase = db.create(PackageBase, - Name=f"pkg_{i}", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") packages_.append(package) yield packages_ @@ -115,20 +121,22 @@ def requests(user: User, packages: list[Package]) -> list[PackageRequest]: pkgreqs = [] with db.begin(): for i in range(55): - pkgreq = db.create(PackageRequest, - ReqTypeID=DELETION_ID, - User=user, - PackageBase=packages[i].PackageBase, - PackageBaseName=packages[i].Name, - Comments=f"Deletion request for pkg_{i}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=DELETION_ID, + User=user, + PackageBase=packages[i].PackageBase, + PackageBaseName=packages[i].Name, + Comments=f"Deletion request for pkg_{i}", + ClosureComment=str(), + ) pkgreqs.append(pkgreq) yield pkgreqs @pytest.fixture def tu_user() -> User: - """ Yield an authenticated Trusted User instance. """ + """Yield an authenticated Trusted User instance.""" user = create_user("test_tu", "test_tu@example.org") with db.begin(): user.AccountTypeID = TRUSTED_USER_ID @@ -149,31 +157,38 @@ def create_pkgbase(user: User, name: str) -> PackageBase: """ now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name=name, - Maintainer=user, Packager=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name=name, + Maintainer=user, + Packager=user, + SubmittedTS=now, + ModifiedTS=now, + ) db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) return pkgbase @pytest.fixture def pkgbase(user: User) -> PackageBase: - """ Yield a package base. """ + """Yield a package base.""" pkgbase = create_pkgbase(user, "test-package") yield pkgbase @pytest.fixture def target(user: User) -> PackageBase: - """ Yield a merge target (package base). """ + """Yield a merge target (package base).""" with db.begin(): - target = db.create(PackageBase, Name="target-package", - Maintainer=user, Packager=user) + target = db.create( + PackageBase, Name="target-package", Maintainer=user, Packager=user + ) yield target -def create_request(reqtype_id: int, user: User, pkgbase: PackageBase, - comments: str) -> PackageRequest: +def create_request( + reqtype_id: int, user: User, pkgbase: PackageBase, comments: str +) -> PackageRequest: """ Create a package request based on `reqtype_id`, `user`, `pkgbase` and `comments`. @@ -186,40 +201,43 @@ def create_request(reqtype_id: int, user: User, pkgbase: PackageBase, """ now = time.utcnow() with db.begin(): - pkgreq = db.create(PackageRequest, ReqTypeID=reqtype_id, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - RequestTS=now, - Comments=comments, - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=reqtype_id, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + RequestTS=now, + Comments=comments, + ClosureComment=str(), + ) return pkgreq @pytest.fixture def pkgreq(user: User, pkgbase: PackageBase): - """ Yield a package request. """ + """Yield a package request.""" pkgreq = create_request(DELETION_ID, user, pkgbase, "Test request.") yield pkgreq def create_notification(user: User, pkgbase: PackageBase): - """ Create a notification for a `user` on `pkgbase`. """ + """Create a notification for a `user` on `pkgbase`.""" with db.begin(): notif = db.create(PackageNotification, User=user, PackageBase=pkgbase) return notif def test_request(client: TestClient, auser: User, pkgbase: PackageBase): - """ Test the standard pkgbase request route GET method. """ + """Test the standard pkgbase request route GET method.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" with client as request: resp = request.get(endpoint, cookies=auser.cookies) assert resp.status_code == int(HTTPStatus.OK) -def test_request_post_deletion(client: TestClient, auser2: User, - pkgbase: PackageBase): - """ Test the POST route for creating a deletion request works. """ +def test_request_post_deletion(client: TestClient, auser2: User, pkgbase: PackageBase): + """Test the POST route for creating a deletion request works.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: @@ -238,9 +256,10 @@ def test_request_post_deletion(client: TestClient, auser2: User, assert re.match(expr, email.headers.get("Subject")) -def test_request_post_deletion_as_maintainer(client: TestClient, auser: User, - pkgbase: PackageBase): - """ Test the POST route for creating a deletion request as maint works. """ +def test_request_post_deletion_as_maintainer( + client: TestClient, auser: User, pkgbase: PackageBase +): + """Test the POST route for creating a deletion request as maint works.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: @@ -267,10 +286,13 @@ def test_request_post_deletion_as_maintainer(client: TestClient, auser: User, assert re.match(expr, email.headers.get("Subject")) -def test_request_post_deletion_autoaccept(client: TestClient, auser: User, - pkgbase: PackageBase, - caplog: pytest.LogCaptureFixture): - """ Test the request route for deletion as maintainer. """ +def test_request_post_deletion_autoaccept( + client: TestClient, + auser: User, + pkgbase: PackageBase, + caplog: pytest.LogCaptureFixture, +): + """Test the request route for deletion as maintainer.""" caplog.set_level(DEBUG) now = time.utcnow() @@ -284,9 +306,11 @@ def test_request_post_deletion_autoaccept(client: TestClient, auser: User, resp = request.post(endpoint, data=data, cookies=auser.cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - pkgreq = db.query(PackageRequest).filter( - PackageRequest.PackageBaseName == pkgbase.Name - ).first() + pkgreq = ( + db.query(PackageRequest) + .filter(PackageRequest.PackageBaseName == pkgbase.Name) + .first() + ) assert pkgreq is not None assert pkgreq.ReqTypeID == DELETION_ID assert pkgreq.Status == ACCEPTED_ID @@ -310,9 +334,10 @@ def test_request_post_deletion_autoaccept(client: TestClient, auser: User, assert re.search(expr, caplog.text) -def test_request_post_merge(client: TestClient, auser: User, - pkgbase: PackageBase, target: PackageBase): - """ Test the request route for merge as maintainer. """ +def test_request_post_merge( + client: TestClient, auser: User, pkgbase: PackageBase, target: PackageBase +): + """Test the request route for merge as maintainer.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = { "type": "merge", @@ -336,9 +361,8 @@ def test_request_post_merge(client: TestClient, auser: User, assert re.match(expr, email.headers.get("Subject")) -def test_request_post_orphan(client: TestClient, auser: User, - pkgbase: PackageBase): - """ Test the POST route for creating an orphan request works. """ +def test_request_post_orphan(client: TestClient, auser: User, pkgbase: PackageBase): + """Test the POST route for creating an orphan request works.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = { "type": "orphan", @@ -361,9 +385,14 @@ def test_request_post_orphan(client: TestClient, auser: User, assert re.match(expr, email.headers.get("Subject")) -def test_deletion_request(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, pkgreq: PackageRequest): - """ Test deleting a package with a preexisting request. """ +def test_deletion_request( + client: TestClient, + user: User, + tu_user: User, + pkgbase: PackageBase, + pkgreq: PackageRequest, +): + """Test deleting a package with a preexisting request.""" # `pkgreq`.ReqTypeID is already DELETION_ID. create_request(DELETION_ID, user, pkgbase, "Other request.") @@ -402,9 +431,8 @@ def test_deletion_request(client: TestClient, user: User, tu_user: User, assert body in email.body -def test_deletion_autorequest(client: TestClient, tu_user: User, - pkgbase: PackageBase): - """ Test deleting a package without a request. """ +def test_deletion_autorequest(client: TestClient, tu_user: User, pkgbase: PackageBase): + """Test deleting a package without a request.""" # `pkgreq`.ReqTypeID is already DELETION_ID. endpoint = f"/pkgbase/{pkgbase.Name}/delete" data = {"confirm": True} @@ -421,10 +449,15 @@ def test_deletion_autorequest(client: TestClient, tu_user: User, assert "[Autogenerated]" in email.body -def test_merge_request(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, target: PackageBase, - pkgreq: PackageRequest): - """ Test merging a package with a pre - existing request. """ +def test_merge_request( + client: TestClient, + user: User, + tu_user: User, + pkgbase: PackageBase, + target: PackageBase, + pkgreq: PackageRequest, +): + """Test merging a package with a pre - existing request.""" with db.begin(): pkgreq.ReqTypeID = MERGE_ID pkgreq.MergeBaseName = target.Name @@ -473,9 +506,14 @@ def test_merge_request(client: TestClient, user: User, tu_user: User, assert "[Autogenerated]" in rejected.body -def test_merge_autorequest(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, target: PackageBase): - """ Test merging a package without a request. """ +def test_merge_autorequest( + client: TestClient, + user: User, + tu_user: User, + pkgbase: PackageBase, + target: PackageBase, +): + """Test merging a package without a request.""" with db.begin(): pkgreq.ReqTypeID = MERGE_ID pkgreq.MergeBaseName = target.Name @@ -498,13 +536,17 @@ def test_merge_autorequest(client: TestClient, user: User, tu_user: User, assert "[Autogenerated]" in email.body -def test_orphan_request(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, pkgreq: PackageRequest): - """ Test the standard orphan request route. """ +def test_orphan_request( + client: TestClient, + user: User, + tu_user: User, + pkgbase: PackageBase, + pkgreq: PackageRequest, +): + """Test the standard orphan request route.""" user2 = create_user("user2", "user2@example.org") with db.begin(): - db.create(PackageComaintainer, User=user2, - PackageBase=pkgbase, Priority=1) + db.create(PackageComaintainer, User=user2, PackageBase=pkgbase, Priority=1) idle_time = config.getint("options", "request_idle_time") now = time.utcnow() @@ -537,10 +579,9 @@ def test_orphan_request(client: TestClient, user: User, tu_user: User, assert re.match(subj, email.headers.get("Subject")) -def test_request_post_orphan_autogenerated_closure(client: TestClient, - tu_user: User, - pkgbase: PackageBase, - pkgreq: PackageRequest): +def test_request_post_orphan_autogenerated_closure( + client: TestClient, tu_user: User, pkgbase: PackageBase, pkgreq: PackageRequest +): idle_time = config.getint("options", "request_idle_time") now = time.utcnow() with db.begin(): @@ -564,10 +605,13 @@ def test_request_post_orphan_autogenerated_closure(client: TestClient, assert re.search(expr, email.body) -def test_request_post_orphan_autoaccept(client: TestClient, auser: User, - pkgbase: PackageBase, - caplog: pytest.LogCaptureFixture): - """ Test the standard pkgbase request route GET method. """ +def test_request_post_orphan_autoaccept( + client: TestClient, + auser: User, + pkgbase: PackageBase, + caplog: pytest.LogCaptureFixture, +): + """Test the standard pkgbase request route GET method.""" caplog.set_level(DEBUG) now = time.utcnow() auto_orphan_age = config.getint("options", "auto_orphan_age") @@ -605,8 +649,7 @@ def test_request_post_orphan_autoaccept(client: TestClient, auser: User, assert re.search(expr, caplog.text) -def test_orphan_as_maintainer(client: TestClient, auser: User, - pkgbase: PackageBase): +def test_orphan_as_maintainer(client: TestClient, auser: User, pkgbase: PackageBase): endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: @@ -620,9 +663,10 @@ def test_orphan_as_maintainer(client: TestClient, auser: User, assert pkgbase.Maintainer is None -def test_orphan_without_requests(client: TestClient, tu_user: User, - pkgbase: PackageBase): - """ Test orphans are automatically accepted past a certain date. """ +def test_orphan_without_requests( + client: TestClient, tu_user: User, pkgbase: PackageBase +): + """Test orphans are automatically accepted past a certain date.""" endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: @@ -637,7 +681,7 @@ def test_orphan_without_requests(client: TestClient, tu_user: User, def test_closure_factory_invalid_reqtype_id(): - """ Test providing an invalid reqtype_id raises NotImplementedError. """ + """Test providing an invalid reqtype_id raises NotImplementedError.""" automated = ClosureFactory() match = r"^Unsupported '.+' value\.$" with pytest.raises(NotImplementedError, match=match): @@ -657,19 +701,25 @@ def test_requests_unauthorized(client: TestClient): assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_requests(client: TestClient, - tu_user: User, - packages: list[Package], - requests: list[PackageRequest]): +def test_requests( + client: TestClient, + tu_user: User, + packages: list[Package], + requests: list[PackageRequest], +): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.get("/requests", params={ - # Pass in url query parameters O, SeB and SB to exercise - # their paths inside of the pager_nav used in this request. - "O": 0, # Page 1 - "SeB": "nd", - "SB": "n" - }, cookies=cookies) + resp = request.get( + "/requests", + params={ + # Pass in url query parameters O, SeB and SB to exercise + # their paths inside of the pager_nav used in this request. + "O": 0, # Page 1 + "SeB": "nd", + "SB": "n", + }, + cookies=cookies, + ) assert resp.status_code == int(HTTPStatus.OK) assert "Next ›" in resp.text @@ -682,9 +732,7 @@ def test_requests(client: TestClient, # Request page 2 of the requests page. with client as request: - resp = request.get("/requests", params={ - "O": 50 # Page 2 - }, cookies=cookies) + resp = request.get("/requests", params={"O": 50}, cookies=cookies) # Page 2 assert resp.status_code == int(HTTPStatus.OK) assert "‹ Previous" in resp.text @@ -695,8 +743,9 @@ def test_requests(client: TestClient, assert len(rows) == 5 # There are five records left on the second page. -def test_requests_selfmade(client: TestClient, user: User, - requests: list[PackageRequest]): +def test_requests_selfmade( + client: TestClient, user: User, requests: list[PackageRequest] +): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: resp = request.get("/requests", cookies=cookies) @@ -710,46 +759,52 @@ def test_requests_selfmade(client: TestClient, user: User, # Our first and only link in the last row should be "Close". for row in rows: - last_row = row.xpath('./td')[-1].xpath('./a')[0] + last_row = row.xpath("./td")[-1].xpath("./a")[0] assert last_row.text.strip() == "Close" -def test_requests_close(client: TestClient, user: User, - pkgreq: PackageRequest): +def test_requests_close(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies, - allow_redirects=False) + resp = request.get( + f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.OK) -def test_requests_close_unauthorized(client: TestClient, maintainer: User, - pkgreq: PackageRequest): +def test_requests_close_unauthorized( + client: TestClient, maintainer: User, pkgreq: PackageRequest +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies, - allow_redirects=False) + resp = request.get( + f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" -def test_requests_close_post_unauthorized(client: TestClient, maintainer: User, - pkgreq: PackageRequest): +def test_requests_close_post_unauthorized( + client: TestClient, maintainer: User, pkgreq: PackageRequest +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", data={ - "reason": ACCEPTED_ID - }, cookies=cookies, allow_redirects=False) + resp = request.post( + f"/requests/{pkgreq.ID}/close", + data={"reason": ACCEPTED_ID}, + cookies=cookies, + allow_redirects=False, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" -def test_requests_close_post(client: TestClient, user: User, - pkgreq: PackageRequest): +def test_requests_close_post(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", - cookies=cookies, allow_redirects=False) + resp = request.post( + f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID @@ -757,12 +812,14 @@ def test_requests_close_post(client: TestClient, user: User, assert pkgreq.ClosureComment == str() -def test_requests_close_post_rejected(client: TestClient, user: User, - pkgreq: PackageRequest): +def test_requests_close_post_rejected( + client: TestClient, user: User, pkgreq: PackageRequest +): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", - cookies=cookies, allow_redirects=False) + resp = request.post( + f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID diff --git a/test/test_routes.py b/test/test_routes.py index 85d30c02..78b0a65b 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -1,11 +1,9 @@ import re import urllib.parse - from http import HTTPStatus import lxml.etree import pytest - from fastapi.testclient import TestClient from aurweb import db @@ -28,21 +26,26 @@ def client() -> TestClient: @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user def test_index(client: TestClient): - """ Test the index route at '/'. """ + """Test the index route at '/'.""" with client as req: response = req.get("/") assert response.status_code == int(HTTPStatus.OK) def test_index_security_headers(client: TestClient): - """ Check for the existence of CSP, XCTO, XFO and RP security headers. + """Check for the existence of CSP, XCTO, XFO and RP security headers. CSP: Content-Security-Policy XCTO: X-Content-Type-Options @@ -60,7 +63,7 @@ def test_index_security_headers(client: TestClient): def test_favicon(client: TestClient): - """ Test the favicon route at '/favicon.ico'. """ + """Test the favicon route at '/favicon.ico'.""" with client as request: response1 = request.get("/static/images/favicon.ico") response2 = request.get("/favicon.ico") @@ -69,52 +72,38 @@ def test_favicon(client: TestClient): def test_language(client: TestClient): - """ Test the language post route as a guest user. """ - post_data = { - "set_lang": "de", - "next": "/" - } + """Test the language post route as a guest user.""" + post_data = {"set_lang": "de", "next": "/"} with client as req: response = req.post("/language", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) def test_language_invalid_next(client: TestClient): - """ Test an invalid next route at '/language'. """ - post_data = { - "set_lang": "de", - "next": "https://evil.net" - } + """Test an invalid next route at '/language'.""" + post_data = {"set_lang": "de", "next": "https://evil.net"} with client as req: response = req.post("/language", data=post_data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) def test_user_language(client: TestClient, user: User): - """ Test the language post route as an authenticated user. """ - post_data = { - "set_lang": "de", - "next": "/" - } + """Test the language post route as an authenticated user.""" + post_data = {"set_lang": "de", "next": "/"} sid = user.login(Request(), "testPassword") assert sid is not None with client as req: - response = req.post("/language", data=post_data, - cookies={"AURSID": sid}) + response = req.post("/language", data=post_data, cookies={"AURSID": sid}) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert user.LangPreference == "de" def test_language_query_params(client: TestClient): - """ Test the language post route with query params. """ + """Test the language post route with query params.""" next = urllib.parse.quote_plus("/") - post_data = { - "set_lang": "de", - "next": "/", - "q": f"next={next}" - } + post_data = {"set_lang": "de", "next": "/", "q": f"next={next}"} q = post_data.get("q") with client as req: response = req.post("/language", data=post_data) @@ -154,9 +143,13 @@ def test_nonce_csp(client: TestClient): def test_id_redirect(client: TestClient): with client as request: - response = request.get("/", params={ - "id": "test", # This param will be rewritten into Location. - "key": "value", # Test that this param persists. - "key2": "value2" # And this one. - }, allow_redirects=False) + response = request.get( + "/", + params={ + "id": "test", # This param will be rewritten into Location. + "key": "value", # Test that this param persists. + "key2": "value2", # And this one. + }, + allow_redirects=False, + ) assert response.headers.get("location") == "/test?key=value&key2=value2" diff --git a/test/test_rpc.py b/test/test_rpc.py index c0861d3d..ed7e8894 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1,17 +1,14 @@ import re - from http import HTTPStatus from unittest import mock import orjson import pytest - from fastapi.testclient import TestClient from redis.client import Pipeline import aurweb.models.dependency_type as dt import aurweb.models.relation_type as rt - from aurweb import asgi, config, db, rpc, scripts, time from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID @@ -36,27 +33,42 @@ def client() -> TestClient: @pytest.fixture def user(db_test) -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User 1", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User 1", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user2() -> User: with db.begin(): - user = db.create(User, Username="user2", Email="user2@example.org", - RealName="Test User 2", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="user2", + Email="user2@example.org", + RealName="Test User 2", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user3() -> User: with db.begin(): - user = db.create(User, Username="user3", Email="user3@example.org", - RealName="Test User 3", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="user3", + Email="user3@example.org", + RealName="Test User 3", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @@ -66,39 +78,64 @@ def packages(user: User, user2: User, user3: User) -> list[Package]: # Create package records used in our tests. with db.begin(): - pkgbase = db.create(PackageBase, Name="big-chungus", - Maintainer=user, Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Bunny bunny around bunny", - URL="https://example.com/") + pkgbase = db.create( + PackageBase, Name="big-chungus", Maintainer=user, Packager=user + ) + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Bunny bunny around bunny", + URL="https://example.com/", + ) output.append(pkg) - pkgbase = db.create(PackageBase, Name="chungy-chungus", - Maintainer=user, Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Wubby wubby on wobba wuubu", - URL="https://example.com/") + pkgbase = db.create( + PackageBase, Name="chungy-chungus", Maintainer=user, Packager=user + ) + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Wubby wubby on wobba wuubu", + URL="https://example.com/", + ) output.append(pkg) - pkgbase = db.create(PackageBase, Name="gluggly-chungus", - Maintainer=user, Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="glurrba glurrba gur globba", - URL="https://example.com/") + pkgbase = db.create( + PackageBase, Name="gluggly-chungus", Maintainer=user, Packager=user + ) + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="glurrba glurrba gur globba", + URL="https://example.com/", + ) output.append(pkg) - pkgbase = db.create(PackageBase, Name="fugly-chungus", - Maintainer=user, Packager=user) + pkgbase = db.create( + PackageBase, Name="fugly-chungus", Maintainer=user, Packager=user + ) desc = "A Package belonging to a PackageBase with another name." - pkg = db.create(Package, PackageBase=pkgbase, Name="other-pkg", - Description=desc, URL="https://example.com") + pkg = db.create( + Package, + PackageBase=pkgbase, + Name="other-pkg", + Description=desc, + URL="https://example.com", + ) output.append(pkg) pkgbase = db.create(PackageBase, Name="woogly-chungus") - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="wuggla woblabeloop shemashmoop", - URL="https://example.com/") + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="wuggla woblabeloop shemashmoop", + URL="https://example.com/", + ) output.append(pkg) # Setup a few more related records on the first package: @@ -108,14 +145,15 @@ def packages(user: User, user2: User, user3: User) -> list[Package]: db.create(PackageLicense, Package=output[0], License=lic) for keyword in ["big-chungus", "smol-chungus", "sizeable-chungus"]: - db.create(PackageKeyword, - PackageBase=output[0].PackageBase, - Keyword=keyword) + db.create( + PackageKeyword, PackageBase=output[0].PackageBase, Keyword=keyword + ) now = time.utcnow() for user_ in [user, user2, user3]: - db.create(PackageVote, User=user_, - PackageBase=output[0].PackageBase, VoteTS=now) + db.create( + PackageVote, User=user_, PackageBase=output[0].PackageBase, VoteTS=now + ) scripts.popupdate.run_single(output[0].PackageBase) yield output @@ -126,35 +164,45 @@ def depends(packages: list[Package]) -> list[PackageDependency]: output = [] with db.begin(): - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.DEPENDS_ID, - DepName="chungus-depends") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.DEPENDS_ID, + DepName="chungus-depends", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[1], - DepTypeID=dt.DEPENDS_ID, - DepName="chungy-depends") + dep = db.create( + PackageDependency, + Package=packages[1], + DepTypeID=dt.DEPENDS_ID, + DepName="chungy-depends", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.OPTDEPENDS_ID, - DepName="chungus-optdepends", - DepCondition="=50") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.OPTDEPENDS_ID, + DepName="chungus-optdepends", + DepCondition="=50", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.MAKEDEPENDS_ID, - DepName="chungus-makedepends") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.MAKEDEPENDS_ID, + DepName="chungus-makedepends", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.CHECKDEPENDS_ID, - DepName="chungus-checkdepends") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.CHECKDEPENDS_ID, + DepName="chungus-checkdepends", + ) output.append(dep) yield output @@ -165,30 +213,38 @@ def relations(user: User, packages: list[Package]) -> list[PackageRelation]: output = [] with db.begin(): - rel = db.create(PackageRelation, - Package=packages[0], - RelTypeID=rt.CONFLICTS_ID, - RelName="chungus-conflicts") + rel = db.create( + PackageRelation, + Package=packages[0], + RelTypeID=rt.CONFLICTS_ID, + RelName="chungus-conflicts", + ) output.append(rel) - rel = db.create(PackageRelation, - Package=packages[1], - RelTypeID=rt.CONFLICTS_ID, - RelName="chungy-conflicts") + rel = db.create( + PackageRelation, + Package=packages[1], + RelTypeID=rt.CONFLICTS_ID, + RelName="chungy-conflicts", + ) output.append(rel) - rel = db.create(PackageRelation, - Package=packages[0], - RelTypeID=rt.PROVIDES_ID, - RelName="chungus-provides", - RelCondition="<=200") + rel = db.create( + PackageRelation, + Package=packages[0], + RelTypeID=rt.PROVIDES_ID, + RelName="chungus-provides", + RelCondition="<=200", + ) output.append(rel) - rel = db.create(PackageRelation, - Package=packages[0], - RelTypeID=rt.REPLACES_ID, - RelName="chungus-replaces", - RelCondition="<=200") + rel = db.create( + PackageRelation, + Package=packages[0], + RelTypeID=rt.REPLACES_ID, + RelName="chungus-replaces", + RelCondition="<=200", + ) output.append(rel) # Finally, yield the packages. @@ -238,51 +294,54 @@ def test_rpc_documentation_missing(): config.rehash() -def test_rpc_singular_info(client: TestClient, - user: User, - packages: list[Package], - depends: list[PackageDependency], - relations: list[PackageRelation]): +def test_rpc_singular_info( + client: TestClient, + user: User, + packages: list[Package], + depends: list[PackageDependency], + relations: list[PackageRelation], +): # Define expected response. pkg = packages[0] expected_data = { "version": 5, - "results": [{ - "Name": pkg.Name, - "Version": pkg.Version, - "Description": pkg.Description, - "URL": pkg.URL, - "PackageBase": pkg.PackageBase.Name, - "NumVotes": pkg.PackageBase.NumVotes, - "Popularity": float(pkg.PackageBase.Popularity), - "OutOfDate": None, - "Maintainer": user.Username, - "URLPath": f"/cgit/aur.git/snapshot/{pkg.Name}.tar.gz", - "Depends": ["chungus-depends"], - "OptDepends": ["chungus-optdepends=50"], - "MakeDepends": ["chungus-makedepends"], - "CheckDepends": ["chungus-checkdepends"], - "Conflicts": ["chungus-conflicts"], - "Provides": ["chungus-provides<=200"], - "Replaces": ["chungus-replaces<=200"], - "License": [pkg.package_licenses.first().License.Name], - "Keywords": [ - "big-chungus", - "sizeable-chungus", - "smol-chungus" - ] - }], + "results": [ + { + "Name": pkg.Name, + "Version": pkg.Version, + "Description": pkg.Description, + "URL": pkg.URL, + "PackageBase": pkg.PackageBase.Name, + "NumVotes": pkg.PackageBase.NumVotes, + "Popularity": float(pkg.PackageBase.Popularity), + "OutOfDate": None, + "Maintainer": user.Username, + "URLPath": f"/cgit/aur.git/snapshot/{pkg.Name}.tar.gz", + "Depends": ["chungus-depends"], + "OptDepends": ["chungus-optdepends=50"], + "MakeDepends": ["chungus-makedepends"], + "CheckDepends": ["chungus-checkdepends"], + "Conflicts": ["chungus-conflicts"], + "Provides": ["chungus-provides<=200"], + "Replaces": ["chungus-replaces<=200"], + "License": [pkg.package_licenses.first().License.Name], + "Keywords": ["big-chungus", "sizeable-chungus", "smol-chungus"], + } + ], "resultcount": 1, - "type": "multiinfo" + "type": "multiinfo", } # Make dummy request. with client as request: - resp = request.get("/rpc", params={ - "v": 5, - "type": "info", - "arg": ["chungy-chungus", "big-chungus"], - }) + resp = request.get( + "/rpc", + params={ + "v": 5, + "type": "info", + "arg": ["chungy-chungus", "big-chungus"], + }, + ) # Load request response into Python dictionary. response_data = orjson.loads(resp.text) @@ -299,19 +358,21 @@ def test_rpc_singular_info(client: TestClient, def test_rpc_split_package_urlpath(client: TestClient, user: User): with db.begin(): - pkgbase = db.create(PackageBase, Name="pkg", - Maintainer=user, Packager=user) + pkgbase = db.create(PackageBase, Name="pkg", Maintainer=user, Packager=user) pkgs = [ db.create(Package, PackageBase=pkgbase, Name="pkg_1"), db.create(Package, PackageBase=pkgbase, Name="pkg_2"), ] with client as request: - response = request.get("/rpc", params={ - "v": 5, - "type": "info", - "arg": [pkgs[0].Name], - }) + response = request.get( + "/rpc", + params={ + "v": 5, + "type": "info", + "arg": [pkgs[0].Name], + }, + ) data = orjson.loads(response.text) snapshot_uri = config.get("options", "snapshot_uri") @@ -335,9 +396,9 @@ def test_rpc_multiinfo(client: TestClient, packages: list[Package]): # Make dummy request. request_packages = ["big-chungus", "chungy-chungus"] with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "info", "arg[]": request_packages - }) + response = request.get( + "/rpc", params={"v": 5, "type": "info", "arg[]": request_packages} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -357,13 +418,15 @@ def test_rpc_mixedargs(client: TestClient, packages: list[Package]): with client as request: # Supply all of the args in the url to enforce ordering. response1 = request.get( - "/rpc?v=5&arg[]=big-chungus&arg=gluggly-chungus&type=info") + "/rpc?v=5&arg[]=big-chungus&arg=gluggly-chungus&type=info" + ) assert response1.status_code == int(HTTPStatus.OK) with client as request: response2 = request.get( "/rpc?v=5&arg=big-chungus&arg[]=gluggly-chungus" - "&type=info&arg[]=chungy-chungus") + "&type=info&arg[]=chungy-chungus" + ) assert response1.status_code == int(HTTPStatus.OK) # Load request response into Python dictionary. @@ -381,42 +444,47 @@ def test_rpc_mixedargs(client: TestClient, packages: list[Package]): assert i == [] -def test_rpc_no_dependencies_omits_key(client: TestClient, user: User, - packages: list[Package], - depends: list[PackageDependency], - relations: list[PackageRelation]): +def test_rpc_no_dependencies_omits_key( + client: TestClient, + user: User, + packages: list[Package], + depends: list[PackageDependency], + relations: list[PackageRelation], +): """ This makes sure things like 'MakeDepends' get removed from JSON strings when they don't have set values. """ pkg = packages[1] expected_response = { - 'version': 5, - 'results': [{ - 'Name': pkg.Name, - 'Version': pkg.Version, - 'Description': pkg.Description, - 'URL': pkg.URL, - 'PackageBase': pkg.PackageBase.Name, - 'NumVotes': pkg.PackageBase.NumVotes, - 'Popularity': int(pkg.PackageBase.Popularity), - 'OutOfDate': None, - 'Maintainer': user.Username, - 'URLPath': '/cgit/aur.git/snapshot/chungy-chungus.tar.gz', - 'Depends': ['chungy-depends'], - 'Conflicts': ['chungy-conflicts'], - 'License': [], - 'Keywords': [] - }], - 'resultcount': 1, - 'type': 'multiinfo' + "version": 5, + "results": [ + { + "Name": pkg.Name, + "Version": pkg.Version, + "Description": pkg.Description, + "URL": pkg.URL, + "PackageBase": pkg.PackageBase.Name, + "NumVotes": pkg.PackageBase.NumVotes, + "Popularity": int(pkg.PackageBase.Popularity), + "OutOfDate": None, + "Maintainer": user.Username, + "URLPath": "/cgit/aur.git/snapshot/chungy-chungus.tar.gz", + "Depends": ["chungy-depends"], + "Conflicts": ["chungy-conflicts"], + "License": [], + "Keywords": [], + } + ], + "resultcount": 1, + "type": "multiinfo", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "info", "arg": "chungy-chungus" - }) + response = request.get( + "/rpc", params={"v": 5, "type": "info", "arg": "chungy-chungus"} + ) response_data = orjson.loads(response.content.decode()) # Remove inconsistent keys. @@ -429,18 +497,18 @@ def test_rpc_no_dependencies_omits_key(client: TestClient, user: User, def test_rpc_bad_type(client: TestClient): # Define expected response. expected_data = { - 'version': 5, - 'results': [], - 'resultcount': 0, - 'type': 'error', - 'error': 'Incorrect request type specified.' + "version": 5, + "results": [], + "resultcount": 0, + "type": "error", + "error": "Incorrect request type specified.", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "invalid-type", "arg": "big-chungus" - }) + response = request.get( + "/rpc", params={"v": 5, "type": "invalid-type", "arg": "big-chungus"} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -452,18 +520,18 @@ def test_rpc_bad_type(client: TestClient): def test_rpc_bad_version(client: TestClient): # Define expected response. expected_data = { - 'version': 0, - 'resultcount': 0, - 'results': [], - 'type': 'error', - 'error': 'Invalid version specified.' + "version": 0, + "resultcount": 0, + "results": [], + "type": "error", + "error": "Invalid version specified.", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 0, "type": "info", "arg": "big-chungus" - }) + response = request.get( + "/rpc", params={"v": 0, "type": "info", "arg": "big-chungus"} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -475,19 +543,16 @@ def test_rpc_bad_version(client: TestClient): def test_rpc_no_version(client: TestClient): # Define expected response. expected_data = { - 'version': None, - 'resultcount': 0, - 'results': [], - 'type': 'error', - 'error': 'Please specify an API version.' + "version": None, + "resultcount": 0, + "results": [], + "type": "error", + "error": "Please specify an API version.", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "type": "info", - "arg": "big-chungus" - }) + response = request.get("/rpc", params={"type": "info", "arg": "big-chungus"}) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -499,11 +564,11 @@ def test_rpc_no_version(client: TestClient): def test_rpc_no_type(client: TestClient): # Define expected response. expected_data = { - 'version': 5, - 'results': [], - 'resultcount': 0, - 'type': 'error', - 'error': 'No request type/data specified.' + "version": 5, + "results": [], + "resultcount": 0, + "type": "error", + "error": "No request type/data specified.", } # Make dummy request. @@ -520,11 +585,11 @@ def test_rpc_no_type(client: TestClient): def test_rpc_no_args(client: TestClient): # Define expected response. expected_data = { - 'version': 5, - 'results': [], - 'resultcount': 0, - 'type': 'error', - 'error': 'No request type/data specified.' + "version": 5, + "results": [], + "resultcount": 0, + "type": "error", + "error": "No request type/data specified.", } # Make dummy request. @@ -541,9 +606,9 @@ def test_rpc_no_args(client: TestClient): def test_rpc_no_maintainer(client: TestClient, packages: list[Package]): # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "info", "arg": "woogly-chungus" - }) + response = request.get( + "/rpc", params={"v": 5, "type": "info", "arg": "woogly-chungus"} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -620,8 +685,12 @@ def mock_config_getint(section: str, key: str): @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) -def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, - pipeline: Pipeline, packages: list[Package]): +def test_rpc_ratelimit( + getint: mock.MagicMock, + client: TestClient, + pipeline: Pipeline, + packages: list[Package], +): params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} for i in range(4): @@ -685,7 +754,7 @@ def test_rpc_search(client: TestClient, packages: list[Package]): headers = {"If-None-Match": etag} response = request.get("/rpc", params=params, headers=headers) assert response.status_code == int(HTTPStatus.NOT_MODIFIED) - assert response.content == b'' + assert response.content == b"" # No args on non-m by types return an error. del params["arg"] @@ -703,12 +772,7 @@ def test_rpc_msearch(client: TestClient, user: User, packages: list[Package]): # user1 maintains 4 packages; assert that we got them all. assert data.get("resultcount") == 4 names = list(sorted(r.get("Name") for r in data.get("results"))) - expected_results = [ - "big-chungus", - "chungy-chungus", - "gluggly-chungus", - "other-pkg" - ] + expected_results = ["big-chungus", "chungy-chungus", "gluggly-chungus", "other-pkg"] assert names == expected_results # Search for a non-existent maintainer, giving us zero packages. @@ -730,11 +794,10 @@ def test_rpc_msearch(client: TestClient, user: User, packages: list[Package]): assert result.get("Name") == "big-chungus" -def test_rpc_search_depends(client: TestClient, packages: list[Package], - depends: list[PackageDependency]): - params = { - "v": 5, "type": "search", "by": "depends", "arg": "chungus-depends" - } +def test_rpc_search_depends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): + params = {"v": 5, "type": "search", "by": "depends", "arg": "chungus-depends"} with client as request: response = request.get("/rpc", params=params) data = response.json() @@ -743,13 +806,14 @@ def test_rpc_search_depends(client: TestClient, packages: list[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_makedepends(client: TestClient, packages: list[Package], - depends: list[PackageDependency]): +def test_rpc_search_makedepends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): params = { "v": 5, "type": "search", "by": "makedepends", - "arg": "chungus-makedepends" + "arg": "chungus-makedepends", } with client as request: response = request.get("/rpc", params=params) @@ -759,14 +823,10 @@ def test_rpc_search_makedepends(client: TestClient, packages: list[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_optdepends(client: TestClient, packages: list[Package], - depends: list[PackageDependency]): - params = { - "v": 5, - "type": "search", - "by": "optdepends", - "arg": "chungus-optdepends" - } +def test_rpc_search_optdepends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): + params = {"v": 5, "type": "search", "by": "optdepends", "arg": "chungus-optdepends"} with client as request: response = request.get("/rpc", params=params) data = response.json() @@ -775,13 +835,14 @@ def test_rpc_search_optdepends(client: TestClient, packages: list[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_checkdepends(client: TestClient, packages: list[Package], - depends: list[PackageDependency]): +def test_rpc_search_checkdepends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): params = { "v": 5, "type": "search", "by": "checkdepends", - "arg": "chungus-checkdepends" + "arg": "chungus-checkdepends", } with client as request: response = request.get("/rpc", params=params) @@ -799,21 +860,16 @@ def test_rpc_incorrect_by(client: TestClient): def test_rpc_jsonp_callback(client: TestClient): - """ Test the callback parameter. + """Test the callback parameter. For end-to-end verification, the `examples/jsonp.html` file can be used to submit jsonp callback requests to the RPC. """ - params = { - "v": 5, - "type": "search", - "arg": "big", - "callback": "jsonCallback" - } + params = {"v": 5, "type": "search", "arg": "big", "callback": "jsonCallback"} with client as request: response = request.get("/rpc", params=params) assert response.headers.get("content-type") == "text/javascript" - assert re.search(r'^/\*\*/jsonCallback\(.*\)$', response.text) is not None + assert re.search(r"^/\*\*/jsonCallback\(.*\)$", response.text) is not None # Test an invalid callback name; we get an application/json error. params["callback"] = "jsonCallback!" @@ -824,20 +880,14 @@ def test_rpc_jsonp_callback(client: TestClient): def test_rpc_post(client: TestClient, packages: list[Package]): - data = { - "v": 5, - "type": "info", - "arg": "big-chungus", - "arg[]": ["chungy-chungus"] - } + data = {"v": 5, "type": "info", "arg": "big-chungus", "arg[]": ["chungy-chungus"]} with client as request: resp = request.post("/rpc", data=data) assert resp.status_code == int(HTTPStatus.OK) assert resp.json().get("resultcount") == 2 -def test_rpc_too_many_search_results(client: TestClient, - packages: list[Package]): +def test_rpc_too_many_search_results(client: TestClient, packages: list[Package]): config_getint = config.getint def mock_config(section: str, key: str): @@ -858,10 +908,18 @@ def test_rpc_too_many_info_results(client: TestClient, packages: list[Package]): # regardless of the number of related records. with db.begin(): for i in range(len(packages) - 1): - db.create(PackageDependency, DepTypeID=DEPENDS_ID, - Package=packages[i], DepName=packages[i + 1].Name) - db.create(PackageRelation, RelTypeID=PROVIDES_ID, - Package=packages[i], RelName=packages[i + 1].Name) + db.create( + PackageDependency, + DepTypeID=DEPENDS_ID, + Package=packages[i], + DepName=packages[i + 1].Name, + ) + db.create( + PackageRelation, + RelTypeID=PROVIDES_ID, + Package=packages[i], + RelName=packages[i + 1].Name, + ) config_getint = config.getint diff --git a/test/test_rss.py b/test/test_rss.py index cef6a46f..8526caa1 100644 --- a/test/test_rss.py +++ b/test/test_rss.py @@ -2,7 +2,6 @@ from http import HTTPStatus import lxml.etree import pytest - from fastapi.testclient import TestClient from aurweb import db, logging, time @@ -27,13 +26,15 @@ def client(): @pytest.fixture def user(): - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() - yield db.create(User, Username="test", - Email="test@example.org", - RealName="Test User", - Passwd="testPassword", - AccountType=account_type) + account_type = db.query(AccountType, AccountType.AccountType == "User").first() + yield db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountType=account_type, + ) @pytest.fixture @@ -45,8 +46,12 @@ def packages(user): with db.begin(): for i in range(101): pkgbase = db.create( - PackageBase, Maintainer=user, Name=f"test-package-{i}", - SubmittedTS=(now + i), ModifiedTS=(now + i)) + PackageBase, + Maintainer=user, + Name=f"test-package-{i}", + SubmittedTS=(now + i), + ModifiedTS=(now + i), + ) pkg = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) pkgs.append(pkg) yield pkgs @@ -64,6 +69,7 @@ def test_rss(client, user, packages): # Test that the RSS we got is sorted by descending SubmittedTS. def key_(pkg): return pkg.PackageBase.SubmittedTS + packages = list(reversed(sorted(packages, key=key_))) # Just take the first 100. @@ -74,7 +80,7 @@ def test_rss(client, user, packages): assert len(items) == 100 for i, item in enumerate(items): - title = next(iter(item.xpath('./title'))) + title = next(iter(item.xpath("./title"))) logger.debug(f"title: '{title.text}' vs name: '{packages[i].Name}'") assert title.text == packages[i].Name @@ -87,6 +93,7 @@ def test_rss_modified(client, user, packages): # Test that the RSS we got is sorted by descending SubmittedTS. def key_(pkg): return pkg.PackageBase.ModifiedTS + packages = list(reversed(sorted(packages, key=key_))) # Just take the first 100. @@ -97,6 +104,6 @@ def test_rss_modified(client, user, packages): assert len(items) == 100 for i, item in enumerate(items): - title = next(iter(item.xpath('./title'))) + title = next(iter(item.xpath("./title"))) logger.debug(f"title: '{title.text}' vs name: '{packages[i].Name}'") assert title.text == packages[i].Name diff --git a/test/test_session.py b/test/test_session.py index edae57f9..db792b33 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -2,7 +2,6 @@ from unittest import mock import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db, time @@ -19,17 +18,23 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - ResetKey="testReset", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + ResetKey="testReset", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def session(user: User) -> Session: with db.begin(): - session = db.create(Session, User=user, SessionID="testSession", - LastUpdateTS=time.utcnow()) + session = db.create( + Session, User=user, SessionID="testSession", LastUpdateTS=time.utcnow() + ) yield session @@ -39,15 +44,21 @@ def test_session(user: User, session: Session): def test_session_cs(): - """ Test case sensitivity of the database table. """ + """Test case sensitivity of the database table.""" with db.begin(): - user2 = db.create(User, Username="test2", Email="test2@example.org", - ResetKey="testReset2", Passwd="testPassword", - AccountTypeID=USER_ID) + user2 = db.create( + User, + Username="test2", + Email="test2@example.org", + ResetKey="testReset2", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) with db.begin(): - session_cs = db.create(Session, User=user2, SessionID="TESTSESSION", - LastUpdateTS=time.utcnow()) + session_cs = db.create( + Session, User=user2, SessionID="TESTSESSION", LastUpdateTS=time.utcnow() + ) assert session_cs.SessionID == "TESTSESSION" assert session_cs.SessionID != "testSession" diff --git a/test/test_spawn.py b/test/test_spawn.py index 195eb897..be1c5e7c 100644 --- a/test/test_spawn.py +++ b/test/test_spawn.py @@ -1,6 +1,5 @@ import os import tempfile - from typing import Tuple from unittest import mock @@ -8,26 +7,21 @@ import pytest import aurweb.config import aurweb.spawn - from aurweb.exceptions import AurwebException # Some os.environ overrides we use in this suite. -TEST_ENVIRONMENT = { - "PHP_NGINX_PORT": "8001", - "FASTAPI_NGINX_PORT": "8002" -} +TEST_ENVIRONMENT = {"PHP_NGINX_PORT": "8001", "FASTAPI_NGINX_PORT": "8002"} class FakeProcess: - """ Fake a subprocess.Popen return object. """ + """Fake a subprocess.Popen return object.""" returncode = 0 - stdout = b'' - stderr = b'' + stdout = b"" + stderr = b"" def __init__(self, *args, **kwargs): - """ We need this constructor to remain compatible with Popen. """ - pass + """We need this constructor to remain compatible with Popen.""" def communicate(self) -> Tuple[bytes, bytes]: return (self.stdout, self.stderr) @@ -40,10 +34,9 @@ class FakeProcess: class MockFakeProcess: - """ FakeProcess construction helper to be used in mocks. """ + """FakeProcess construction helper to be used in mocks.""" - def __init__(self, return_code: int = 0, stdout: bytes = b'', - stderr: bytes = b''): + def __init__(self, return_code: int = 0, stdout: bytes = b"", stderr: bytes = b""): self.returncode = return_code self.stdout = stdout self.stderr = stderr @@ -101,7 +94,7 @@ def test_spawn_generate_nginx_config(): f'listen {php_host}:{TEST_ENVIRONMENT.get("PHP_NGINX_PORT")}', f"proxy_pass http://{php_address}", f'listen {fastapi_host}:{TEST_ENVIRONMENT.get("FASTAPI_NGINX_PORT")}', - f"proxy_pass http://{fastapi_address}" + f"proxy_pass http://{fastapi_address}", ] for expected in expected_content: assert expected in nginx_config diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index 93298a11..1a586800 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -27,18 +27,23 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def pubkey(user: User) -> SSHPubKey: with db.begin(): - pubkey = db.create(SSHPubKey, User=user, - Fingerprint="testFingerprint", - PubKey="testPubKey") + pubkey = db.create( + SSHPubKey, User=user, Fingerprint="testFingerprint", PubKey="testPubKey" + ) yield pubkey @@ -50,11 +55,11 @@ def test_pubkey(user: User, pubkey: SSHPubKey): def test_pubkey_cs(user: User): - """ Test case sensitivity of the database table. """ + """Test case sensitivity of the database table.""" with db.begin(): - pubkey_cs = db.create(SSHPubKey, User=user, - Fingerprint="TESTFINGERPRINT", - PubKey="TESTPUBKEY") + pubkey_cs = db.create( + SSHPubKey, User=user, Fingerprint="TESTFINGERPRINT", PubKey="TESTPUBKEY" + ) assert pubkey_cs.Fingerprint == "TESTFINGERPRINT" assert pubkey_cs.Fingerprint != "testFingerprint" diff --git a/test/test_templates.py b/test/test_templates.py index 4b138567..383f45d1 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -1,21 +1,23 @@ import re - from typing import Any import pytest import aurweb.filters # noqa: F401 - from aurweb import config, db, templates, time -from aurweb.filters import as_timezone, number_format -from aurweb.filters import timestamp_to_datetime as to_dt +from aurweb.filters import as_timezone, number_format, timestamp_to_datetime as to_dt from aurweb.models import Package, PackageBase, User from aurweb.models.account_type import USER_ID from aurweb.models.license import License from aurweb.models.package_license import PackageLicense from aurweb.models.package_relation import PackageRelation from aurweb.models.relation_type import PROVIDES_ID, REPLACES_ID -from aurweb.templates import base_template, make_context, register_filter, register_function +from aurweb.templates import ( + base_template, + make_context, + register_filter, + register_function, +) from aurweb.testing.html import parse_root from aurweb.testing.requests import Request @@ -35,19 +37,20 @@ def function(): def create_user(username: str) -> User: with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user -def create_pkgrel(package: Package, reltype_id: int, relname: str) \ - -> PackageRelation: - return db.create(PackageRelation, - Package=package, - RelTypeID=reltype_id, - RelName=relname) +def create_pkgrel(package: Package, reltype_id: int, relname: str) -> PackageRelation: + return db.create( + PackageRelation, Package=package, RelTypeID=reltype_id, RelName=relname + ) @pytest.fixture @@ -60,8 +63,13 @@ def user(db_test) -> User: def pkgbase(user: User) -> PackageBase: now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="test-pkg", + Maintainer=user, + SubmittedTS=now, + ModifiedTS=now, + ) yield pkgbase @@ -79,9 +87,10 @@ def create_license(pkg: Package, license_name: str) -> PackageLicense: def test_register_function_exists_key_error(): - """ Most instances of register_filter are tested through module - imports or template renders, so we only test failures here. """ + """Most instances of register_filter are tested through module + imports or template renders, so we only test failures here.""" with pytest.raises(KeyError): + @register_function("function") def some_func(): pass @@ -93,8 +102,9 @@ def test_commit_hash(): commit_hash = "abcdefg" long_commit_hash = commit_hash + "1234567" - def config_get_with_fallback(section: str, option: str, - fallback: str = None) -> str: + def config_get_with_fallback( + section: str, option: str, fallback: str = None + ) -> str: if section == "devel" and option == "commit_hash": return long_commit_hash return config.original_get_with_fallback(section, option, fallback) @@ -134,12 +144,12 @@ def pager_context(num_packages: int) -> dict[str, Any]: "prefix": "/packages", "total": num_packages, "O": 0, - "PP": 50 + "PP": 50, } def test_pager_no_results(): - """ Test the pager partial with no results. """ + """Test the pager partial with no results.""" num_packages = 0 context = pager_context(num_packages) body = base_template("partials/pager.html").render(context) @@ -151,7 +161,7 @@ def test_pager_no_results(): def test_pager(): - """ Test the pager partial with two pages of results. """ + """Test the pager partial with two pages of results.""" num_packages = 100 context = pager_context(num_packages) body = base_template("partials/pager.html").render(context) @@ -274,17 +284,19 @@ def check_package_details(content: str, pkg: Package) -> None: def test_package_details(user: User, package: Package): - """ Test package details with most fields populated, but not all. """ + """Test package details with most fields populated, but not all.""" request = Request(user=user, authenticated=True) context = make_context(request, "Test Details") - context.update({ - "request": request, - "git_clone_uri_anon": GIT_CLONE_URI_ANON, - "git_clone_uri_priv": GIT_CLONE_URI_PRIV, - "pkgbase": package.PackageBase, - "pkg": package, - "comaintainers": [], - }) + context.update( + { + "request": request, + "git_clone_uri_anon": GIT_CLONE_URI_ANON, + "git_clone_uri_priv": GIT_CLONE_URI_PRIV, + "pkgbase": package.PackageBase, + "pkg": package, + "comaintainers": [], + } + ) base = base_template("partials/packages/details.html") body = base.render(context, show_package_details=True) @@ -292,7 +304,7 @@ def test_package_details(user: User, package: Package): def test_package_details_filled(user: User, package: Package): - """ Test package details with all fields populated. """ + """Test package details with all fields populated.""" pkgbase = package.PackageBase with db.begin(): @@ -311,19 +323,23 @@ def test_package_details_filled(user: User, package: Package): request = Request(user=user, authenticated=True) context = make_context(request, "Test Details") - context.update({ - "request": request, - "git_clone_uri_anon": GIT_CLONE_URI_ANON, - "git_clone_uri_priv": GIT_CLONE_URI_PRIV, - "pkgbase": package.PackageBase, - "pkg": package, - "comaintainers": [], - "licenses": package.package_licenses, - "provides": package.package_relations.filter( - PackageRelation.RelTypeID == PROVIDES_ID), - "replaces": package.package_relations.filter( - PackageRelation.RelTypeID == REPLACES_ID), - }) + context.update( + { + "request": request, + "git_clone_uri_anon": GIT_CLONE_URI_ANON, + "git_clone_uri_priv": GIT_CLONE_URI_PRIV, + "pkgbase": package.PackageBase, + "pkg": package, + "comaintainers": [], + "licenses": package.package_licenses, + "provides": package.package_relations.filter( + PackageRelation.RelTypeID == PROVIDES_ID + ), + "replaces": package.package_relations.filter( + PackageRelation.RelTypeID == REPLACES_ID + ), + } + ) base = base_template("partials/packages/details.html") body = base.render(context, show_package_details=True) diff --git a/test/test_term.py b/test/test_term.py index bfa73a76..4b608a9a 100644 --- a/test/test_term.py +++ b/test/test_term.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -13,8 +12,9 @@ def setup(db_test): def test_term_creation(): with db.begin(): - term = db.create(Term, Description="Term description", - URL="https://fake_url.io") + term = db.create( + Term, Description="Term description", URL="https://fake_url.io" + ) assert bool(term.ID) assert term.Description == "Term description" assert term.URL == "https://fake_url.io" diff --git a/test/test_time.py b/test/test_time.py index 2134d217..db7b30bf 100644 --- a/test/test_time.py +++ b/test/test_time.py @@ -1,5 +1,4 @@ import aurweb.config - from aurweb.testing.requests import Request from aurweb.time import get_request_timezone, tz_offset diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index 2e7dc193..203008e3 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -1,12 +1,10 @@ import re - from http import HTTPStatus from io import StringIO from typing import Tuple import lxml.etree import pytest - from fastapi.testclient import TestClient from aurweb import config, db, filters, time @@ -16,8 +14,8 @@ from aurweb.models.tu_voteinfo import TUVoteInfo from aurweb.models.user import User from aurweb.testing.requests import Request -DATETIME_REGEX = r'^[0-9]{4}-[0-9]{2}-[0-9]{2} \(.+\)$' -PARTICIPATION_REGEX = r'^1?[0-9]{2}[%]$' # 0% - 100% +DATETIME_REGEX = r"^[0-9]{4}-[0-9]{2}-[0-9]{2} \(.+\)$" +PARTICIPATION_REGEX = r"^1?[0-9]{2}[%]$" # 0% - 100% def parse_root(html): @@ -43,11 +41,11 @@ def get_pkglist_directions(table): def get_a(node): - return node.xpath('./a')[0].text.strip() + return node.xpath("./a")[0].text.strip() def get_span(node): - return node.xpath('./span')[0].text.strip() + return node.xpath("./span")[0].text.strip() def assert_current_vote_html(row, expected): @@ -82,39 +80,51 @@ def setup(db_test): @pytest.fixture def client(): from aurweb.asgi import app + yield TestClient(app=app) @pytest.fixture def tu_user(): - tu_type = db.query(AccountType, - AccountType.AccountType == "Trusted User").first() + tu_type = db.query(AccountType, AccountType.AccountType == "Trusted User").first() with db.begin(): - tu_user = db.create(User, Username="test_tu", - Email="test_tu@example.org", - RealName="Test TU", Passwd="testPassword", - AccountType=tu_type) + tu_user = db.create( + User, + Username="test_tu", + Email="test_tu@example.org", + RealName="Test TU", + Passwd="testPassword", + AccountType=tu_type, + ) yield tu_user @pytest.fixture def tu_user2(): with db.begin(): - tu_user2 = db.create(User, Username="test_tu2", - Email="test_tu2@example.org", - RealName="Test TU 2", Passwd="testPassword", - AccountTypeID=TRUSTED_USER_ID) + tu_user2 = db.create( + User, + Username="test_tu2", + Email="test_tu2@example.org", + RealName="Test TU 2", + Passwd="testPassword", + AccountTypeID=TRUSTED_USER_ID, + ) yield tu_user2 @pytest.fixture def user(): - user_type = db.query(AccountType, - AccountType.AccountType == "User").first() + user_type = db.query(AccountType, AccountType.AccountType == "User").first() with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=user_type) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountType=user_type, + ) yield user @@ -126,10 +136,15 @@ def proposal(user, tu_user): end = ts + 1000 with db.begin(): - voteinfo = db.create(TUVoteInfo, - Agenda=agenda, Quorum=0.0, - User=user.Username, Submitter=tu_user, - Submitted=start, End=end) + voteinfo = db.create( + TUVoteInfo, + Agenda=agenda, + Quorum=0.0, + User=user.Username, + Submitter=tu_user, + Submitted=start, + End=end, + ) yield (tu_user, user, voteinfo) @@ -153,7 +168,7 @@ def test_tu_index_unauthorized(client: TestClient, user: User): def test_tu_empty_index(client, tu_user): - """ Check an empty index when we don't create any records. """ + """Check an empty index when we don't create any records.""" # Make a default get request to /tu. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} @@ -179,18 +194,23 @@ def test_tu_index(client, tu_user): # Create some test votes: (Agenda, Start, End). votes = [ ("Test agenda 1", ts - 5, ts + 1000), # Still running. - ("Test agenda 2", ts - 1000, ts - 5) # Not running anymore. + ("Test agenda 2", ts - 1000, ts - 5), # Not running anymore. ] vote_records = [] with db.begin(): for vote in votes: agenda, start, end = vote vote_records.append( - db.create(TUVoteInfo, Agenda=agenda, - User=tu_user.Username, - Submitted=start, End=end, - Quorum=0.0, - Submitter=tu_user)) + db.create( + TUVoteInfo, + Agenda=agenda, + User=tu_user.Username, + Submitted=start, + End=end, + Quorum=0.0, + Submitter=tu_user, + ) + ) with db.begin(): # Vote on an ended proposal. @@ -202,21 +222,23 @@ def test_tu_index(client, tu_user): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: # Pass an invalid cby and pby; let them default to "desc". - response = request.get("/tu", cookies=cookies, params={ - "cby": "BAD!", - "pby": "blah" - }, allow_redirects=False) + response = request.get( + "/tu", + cookies=cookies, + params={"cby": "BAD!", "pby": "blah"}, + allow_redirects=False, + ) assert response.status_code == int(HTTPStatus.OK) # Rows we expect to exist in HTML produced by /tu for current votes. expected_rows = [ ( - r'Test agenda 1', + r"Test agenda 1", DATETIME_REGEX, DATETIME_REGEX, tu_user.Username, - r'^(Yes|No)$' + r"^(Yes|No)$", ) ] @@ -239,13 +261,13 @@ def test_tu_index(client, tu_user): # Rows we expect to exist in HTML produced by /tu for past votes. expected_rows = [ ( - r'Test agenda 2', + r"Test agenda 2", DATETIME_REGEX, DATETIME_REGEX, tu_user.Username, - r'^\d+$', - r'^\d+$', - r'^(Yes|No)$' + r"^\d+$", + r"^\d+$", + r"^(Yes|No)$", ) ] @@ -315,19 +337,27 @@ def test_tu_index_table_paging(client, tu_user): with db.begin(): for i in range(25): # Create 25 current votes. - db.create(TUVoteInfo, Agenda=f"Agenda #{i}", - User=tu_user.Username, - Submitted=(ts - 5), End=(ts + 1000), - Quorum=0.0, - Submitter=tu_user) + db.create( + TUVoteInfo, + Agenda=f"Agenda #{i}", + User=tu_user.Username, + Submitted=(ts - 5), + End=(ts + 1000), + Quorum=0.0, + Submitter=tu_user, + ) for i in range(25): # Create 25 past votes. - db.create(TUVoteInfo, Agenda=f"Agenda #{25 + i}", - User=tu_user.Username, - Submitted=(ts - 1000), End=(ts - 5), - Quorum=0.0, - Submitter=tu_user) + db.create( + TUVoteInfo, + Agenda=f"Agenda #{25 + i}", + User=tu_user.Username, + Submitted=(ts - 1000), + End=(ts - 5), + Quorum=0.0, + Submitter=tu_user, + ) cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: @@ -347,7 +377,7 @@ def test_tu_index_table_paging(client, tu_user): DATETIME_REGEX, DATETIME_REGEX, tu_user.Username, - r'^(Yes|No)$' + r"^(Yes|No)$", ] for i, row in enumerate(rows): @@ -361,9 +391,9 @@ def test_tu_index_table_paging(client, tu_user): # Now, get the next page of current votes. offset = 10 # Specify coff=10 with client as request: - response = request.get("/tu", cookies=cookies, params={ - "coff": offset - }, allow_redirects=False) + response = request.get( + "/tu", cookies=cookies, params={"coff": offset}, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) old_rows = rows @@ -390,9 +420,9 @@ def test_tu_index_table_paging(client, tu_user): offset = 20 # Specify coff=10 with client as request: - response = request.get("/tu", cookies=cookies, params={ - "coff": offset - }, allow_redirects=False) + response = request.get( + "/tu", cookies=cookies, params={"coff": offset}, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) # Do it again, we only have five left. @@ -423,11 +453,15 @@ def test_tu_index_sorting(client, tu_user): with db.begin(): for i in range(2): # Create 'Agenda #1' and 'Agenda #2'. - db.create(TUVoteInfo, Agenda=f"Agenda #{i + 1}", - User=tu_user.Username, - Submitted=(ts + 5), End=(ts + 1000), - Quorum=0.0, - Submitter=tu_user) + db.create( + TUVoteInfo, + Agenda=f"Agenda #{i + 1}", + User=tu_user.Username, + Submitted=(ts + 5), + End=(ts + 1000), + Quorum=0.0, + Submitter=tu_user, + ) # Let's order each vote one day after the other. # This will allow us to test the sorting nature @@ -446,27 +480,27 @@ def test_tu_index_sorting(client, tu_user): rows = get_table_rows(table) # The latest Agenda is at the top by default. - expected = [ - "Agenda #2", - "Agenda #1" - ] + expected = ["Agenda #2", "Agenda #1"] assert len(rows) == len(expected) for i, row in enumerate(rows): - assert_current_vote_html(row, [ - expected[i], - DATETIME_REGEX, - DATETIME_REGEX, - tu_user.Username, - r'^(Yes|No)$' - ]) + assert_current_vote_html( + row, + [ + expected[i], + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r"^(Yes|No)$", + ], + ) # Make another request; one that sorts the current votes # in ascending order instead of the default descending order. with client as request: - response = request.get("/tu", cookies=cookies, params={ - "cby": "asc" - }, allow_redirects=False) + response = request.get( + "/tu", cookies=cookies, params={"cby": "asc"}, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) # Get lxml handles of the document. @@ -478,30 +512,37 @@ def test_tu_index_sorting(client, tu_user): rev_expected = list(reversed(expected)) assert len(rows) == len(rev_expected) for i, row in enumerate(rows): - assert_current_vote_html(row, [ - rev_expected[i], - DATETIME_REGEX, - DATETIME_REGEX, - tu_user.Username, - r'^(Yes|No)$' - ]) + assert_current_vote_html( + row, + [ + rev_expected[i], + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r"^(Yes|No)$", + ], + ) -def test_tu_index_last_votes(client: TestClient, tu_user: User, tu_user2: User, - user: User): +def test_tu_index_last_votes( + client: TestClient, tu_user: User, tu_user2: User, user: User +): ts = time.utcnow() with db.begin(): # Create a proposal which has ended. - voteinfo = db.create(TUVoteInfo, Agenda="Test agenda", - User=user.Username, - Submitted=(ts - 1000), - End=(ts - 5), - Yes=1, - No=1, - ActiveTUs=1, - Quorum=0.0, - Submitter=tu_user) + voteinfo = db.create( + TUVoteInfo, + Agenda="Test agenda", + User=user.Username, + Submitted=(ts - 1000), + End=(ts - 5), + Yes=1, + No=1, + ActiveTUs=1, + Quorum=0.0, + Submitter=tu_user, + ) # Create a vote on it from tu_user. db.create(TUVote, VoteInfo=voteinfo, User=tu_user) @@ -536,26 +577,27 @@ def test_tu_proposal_not_found(client, tu_user): assert response.status_code == int(HTTPStatus.NOT_FOUND) -def test_tu_proposal_unauthorized(client: TestClient, user: User, - proposal: Tuple[User, User, TUVoteInfo]): +def test_tu_proposal_unauthorized( + client: TestClient, user: User, proposal: Tuple[User, User, TUVoteInfo] +): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/tu/{proposal[2].ID}" with client as request: - response = request.get(endpoint, cookies=cookies, - allow_redirects=False) + response = request.get(endpoint, cookies=cookies, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" with client as request: - response = request.post(endpoint, cookies=cookies, - data={"decision": False}, - allow_redirects=False) + response = request.post( + endpoint, cookies=cookies, data={"decision": False}, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" -def test_tu_running_proposal(client: TestClient, - proposal: Tuple[User, User, TUVoteInfo]): +def test_tu_running_proposal( + client: TestClient, proposal: Tuple[User, User, TUVoteInfo] +): tu_user, user, voteinfo = proposal with db.begin(): voteinfo.ActiveTUs = 1 @@ -576,8 +618,7 @@ def test_tu_running_proposal(client: TestClient, assert vote_running.text.strip() == "This vote is still running." # Verify User field. - username = details.xpath( - './div[contains(@class, "user")]/strong/a/text()')[0] + username = details.xpath('./div[contains(@class, "user")]/strong/a/text()')[0] assert username.strip() == user.Username active = details.xpath('./div[contains(@class, "field")]')[1] @@ -585,10 +626,13 @@ def test_tu_running_proposal(client: TestClient, assert "Active Trusted Users assigned:" in content assert "1" in content - submitted = details.xpath( - './div[contains(@class, "submitted")]/text()')[0] - assert re.match(r'^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$', - submitted.strip()) is not None + submitted = details.xpath('./div[contains(@class, "submitted")]/text()')[0] + assert ( + re.match( + r"^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$", submitted.strip() + ) + is not None + ) submitter = details.xpath('./div[contains(@class, "submitted")]/a')[0] assert submitter.text.strip() == tu_user.Username assert submitter.attrib["href"] == f"/account/{tu_user.Username}" @@ -598,8 +642,10 @@ def test_tu_running_proposal(client: TestClient, assert end_label.strip() == "End:" end_datetime = end.xpath("./strong/text()")[0] - assert re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\)$', - end_datetime.strip()) is not None + assert ( + re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\)$", end_datetime.strip()) + is not None + ) # We have not voted yet. Assert that our voting form is shown. form = root.xpath('//form[contains(@class, "action-form")]')[0] @@ -630,8 +676,7 @@ def test_tu_running_proposal(client: TestClient, # Make another request now that we've voted. with client as request: - response = request.get( - "/tu", params={"id": voteinfo.ID}, cookies=cookies) + response = request.get("/tu", params={"id": voteinfo.ID}, cookies=cookies) assert response.status_code == int(HTTPStatus.OK) # Parse our new root. @@ -685,12 +730,13 @@ def test_tu_ended_proposal(client, proposal): def test_tu_proposal_vote_not_found(client, tu_user): - """ Test POST request to a missing vote. """ + """Test POST request to a missing vote.""" cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post("/tu/1", cookies=cookies, - data=data, allow_redirects=False) + response = request.post( + "/tu/1", cookies=cookies, data=data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -703,16 +749,14 @@ def test_tu_proposal_vote(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data) + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) assert response.status_code == int(HTTPStatus.OK) # Check that the proposal record got updated. assert voteinfo.Yes == yes + 1 # Check that the new TUVote exists. - vote = db.query(TUVote, TUVote.VoteInfo == voteinfo, - TUVote.User == tu_user).first() + vote = db.query(TUVote, TUVote.VoteInfo == voteinfo, TUVote.User == tu_user).first() assert vote is not None root = parse_root(response.text) @@ -723,7 +767,8 @@ def test_tu_proposal_vote(client, proposal): def test_tu_proposal_vote_unauthorized( - client: TestClient, proposal: Tuple[User, User, TUVoteInfo]): + client: TestClient, proposal: Tuple[User, User, TUVoteInfo] +): tu_user, user, voteinfo = proposal with db.begin(): @@ -732,8 +777,9 @@ def test_tu_proposal_vote_unauthorized( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + response = request.post( + f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.UNAUTHORIZED) root = parse_root(response.text) @@ -742,8 +788,9 @@ def test_tu_proposal_vote_unauthorized( with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + response = request.get( + f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -761,8 +808,9 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + response = request.post( + f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -771,8 +819,9 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + response = request.get( + f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -791,8 +840,9 @@ def test_tu_proposal_vote_already_voted(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + response = request.post( + f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -801,8 +851,9 @@ def test_tu_proposal_vote_already_voted(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + response = request.get( + f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -816,8 +867,7 @@ def test_tu_proposal_vote_invalid_decision(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "EVIL"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data) + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) assert response.text == "Invalid 'decision' value." @@ -829,18 +879,17 @@ def test_tu_addvote(client: TestClient, tu_user: User): assert response.status_code == int(HTTPStatus.OK) -def test_tu_addvote_unauthorized(client: TestClient, user: User, - proposal: Tuple[User, User, TUVoteInfo]): +def test_tu_addvote_unauthorized( + client: TestClient, user: User, proposal: Tuple[User, User, TUVoteInfo] +): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", cookies=cookies, - allow_redirects=False) + response = request.get("/addvote", cookies=cookies, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" with client as request: - response = request.post("/addvote", cookies=cookies, - allow_redirects=False) + response = request.post("/addvote", cookies=cookies, allow_redirects=False) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" @@ -848,8 +897,7 @@ def test_tu_addvote_unauthorized(client: TestClient, user: User, def test_tu_addvote_invalid_type(client: TestClient, tu_user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", params={"type": "faketype"}, - cookies=cookies) + response = request.get("/addvote", params={"type": "faketype"}, cookies=cookies) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -860,11 +908,7 @@ def test_tu_addvote_invalid_type(client: TestClient, tu_user: User): def test_tu_addvote_post(client: TestClient, tu_user: User, user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} - data = { - "user": user.Username, - "type": "add_tu", - "agenda": "Blah" - } + data = {"user": user.Username, "type": "add_tu", "agenda": "Blah"} with client as request: response = request.post("/addvote", cookies=cookies, data=data) @@ -874,15 +918,12 @@ def test_tu_addvote_post(client: TestClient, tu_user: User, user: User): assert voteinfo is not None -def test_tu_addvote_post_cant_duplicate_username(client: TestClient, - tu_user: User, user: User): +def test_tu_addvote_post_cant_duplicate_username( + client: TestClient, tu_user: User, user: User +): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} - data = { - "user": user.Username, - "type": "add_tu", - "agenda": "Blah" - } + data = {"user": user.Username, "type": "add_tu", "agenda": "Blah"} with client as request: response = request.post("/addvote", cookies=cookies, data=data) @@ -904,8 +945,7 @@ def test_tu_addvote_post_invalid_username(client: TestClient, tu_user: User): assert response.status_code == int(HTTPStatus.NOT_FOUND) -def test_tu_addvote_post_invalid_type(client: TestClient, tu_user: User, - user: User): +def test_tu_addvote_post_invalid_type(client: TestClient, tu_user: User, user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} data = {"user": user.Username} with client as request: @@ -913,8 +953,7 @@ def test_tu_addvote_post_invalid_type(client: TestClient, tu_user: User, assert response.status_code == int(HTTPStatus.BAD_REQUEST) -def test_tu_addvote_post_invalid_agenda(client: TestClient, - tu_user: User, user: User): +def test_tu_addvote_post_invalid_agenda(client: TestClient, tu_user: User, user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} data = {"user": user.Username, "type": "add_tu"} with client as request: diff --git a/test/test_tu_vote.py b/test/test_tu_vote.py index 91d73ecb..8c1c08de 100644 --- a/test/test_tu_vote.py +++ b/test/test_tu_vote.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db, time @@ -17,9 +16,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=TRUSTED_USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=TRUSTED_USER_ID, + ) yield user @@ -27,10 +31,15 @@ def user() -> User: def tu_voteinfo(user: User) -> TUVoteInfo: ts = time.utcnow() with db.begin(): - tu_voteinfo = db.create(TUVoteInfo, Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 5, - Quorum=0.5, Submitter=user) + tu_voteinfo = db.create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 5, + Quorum=0.5, + Submitter=user, + ) yield tu_voteinfo diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py index 17226048..34845b86 100644 --- a/test/test_tu_voteinfo.py +++ b/test/test_tu_voteinfo.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db, time @@ -17,21 +16,29 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=TRUSTED_USER_ID) + user = create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=TRUSTED_USER_ID, + ) yield user def test_tu_voteinfo_creation(user: User): ts = time.utcnow() with db.begin(): - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 5, - Quorum=0.5, - Submitter=user) + tu_voteinfo = create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 5, + Quorum=0.5, + Submitter=user, + ) assert bool(tu_voteinfo.ID) assert tu_voteinfo.Agenda == "Blah blah." assert tu_voteinfo.User == user.Username @@ -50,12 +57,15 @@ def test_tu_voteinfo_creation(user: User): def test_tu_voteinfo_is_running(user: User): ts = time.utcnow() with db.begin(): - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 1000, - Quorum=0.5, - Submitter=user) + tu_voteinfo = create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 1000, + Quorum=0.5, + Submitter=user, + ) assert tu_voteinfo.is_running() is True with db.begin(): @@ -66,12 +76,15 @@ def test_tu_voteinfo_is_running(user: User): def test_tu_voteinfo_total_votes(user: User): ts = time.utcnow() with db.begin(): - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 1000, - Quorum=0.5, - Submitter=user) + tu_voteinfo = create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 1000, + Quorum=0.5, + Submitter=user, + ) tu_voteinfo.Yes = 1 tu_voteinfo.No = 3 @@ -84,65 +97,81 @@ def test_tu_voteinfo_total_votes(user: User): def test_tu_voteinfo_null_submitter_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, End=0, - Quorum=0.50) + create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + End=0, + Quorum=0.50, + ) rollback() def test_tu_voteinfo_null_agenda_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): - create(TUVoteInfo, - User=user.Username, - Submitted=0, End=0, - Quorum=0.50, - Submitter=user) + create( + TUVoteInfo, + User=user.Username, + Submitted=0, + End=0, + Quorum=0.50, + Submitter=user, + ) rollback() def test_tu_voteinfo_null_user_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - Submitted=0, End=0, - Quorum=0.50, - Submitter=user) + create( + TUVoteInfo, + Agenda="Blah blah.", + Submitted=0, + End=0, + Quorum=0.50, + Submitter=user, + ) rollback() def test_tu_voteinfo_null_submitted_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - End=0, - Quorum=0.50, - Submitter=user) + create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + End=0, + Quorum=0.50, + Submitter=user, + ) rollback() def test_tu_voteinfo_null_end_raises(user: User): with pytest.raises(IntegrityError): with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, - Quorum=0.50, - Submitter=user) + create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + Quorum=0.50, + Submitter=user, + ) rollback() def test_tu_voteinfo_null_quorum_default(user: User): with db.begin(): - vi = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, End=0, - Submitter=user) + vi = create( + TUVoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + End=0, + Submitter=user, + ) assert vi.Quorum == 0 diff --git a/test/test_tuvotereminder.py b/test/test_tuvotereminder.py index a54c52a4..0233c8b2 100644 --- a/test/test_tuvotereminder.py +++ b/test/test_tuvotereminder.py @@ -19,8 +19,13 @@ def create_vote(user: User, voteinfo: TUVoteInfo) -> TUVote: def create_user(username: str, type_id: int): with db.begin(): - user = db.create(User, AccountTypeID=type_id, Username=username, - Email=f"{username}@example.org", Passwd=str()) + user = db.create( + User, + AccountTypeID=type_id, + Username=username, + Email=f"{username}@example.org", + Passwd=str(), + ) return user @@ -32,9 +37,11 @@ def email_pieces(voteinfo: TUVoteInfo) -> Tuple[str, str]: :return: tuple(subject, content) """ subject = f"TU Vote Reminder: Proposal {voteinfo.ID}" - content = (f"Please remember to cast your vote on proposal {voteinfo.ID} " - f"[1]. The voting period\nends in less than 48 hours.\n\n" - f"[1] {aur_location}/tu/?id={voteinfo.ID}") + content = ( + f"Please remember to cast your vote on proposal {voteinfo.ID} " + f"[1]. The voting period\nends in less than 48 hours.\n\n" + f"[1] {aur_location}/tu/?id={voteinfo.ID}" + ) return (subject, content) @@ -58,14 +65,19 @@ def voteinfo(user: User) -> TUVoteInfo: now = time.utcnow() start = config.getint("tuvotereminder", "range_start") with db.begin(): - voteinfo = db.create(TUVoteInfo, Agenda="Lorem ipsum.", - User=user.Username, End=(now + start + 1), - Quorum=0.00, Submitter=user, Submitted=0) + voteinfo = db.create( + TUVoteInfo, + Agenda="Lorem ipsum.", + User=user.Username, + End=(now + start + 1), + Quorum=0.00, + Submitter=user, + Submitted=0, + ) yield voteinfo -def test_tu_vote_reminders(user: User, user2: User, user3: User, - voteinfo: TUVoteInfo): +def test_tu_vote_reminders(user: User, user2: User, user3: User, voteinfo: TUVoteInfo): reminder.main() assert Email.count() == 3 @@ -75,7 +87,7 @@ def test_tu_vote_reminders(user: User, user2: User, user3: User, # (to, content) (user.Email, subject, content), (user2.Email, subject, content), - (user3.Email, subject, content) + (user3.Email, subject, content), ] for i, element in enumerate(expectations): email, subject, content = element @@ -84,8 +96,9 @@ def test_tu_vote_reminders(user: User, user2: User, user3: User, assert emails[i].body == content -def test_tu_vote_reminders_only_unvoted(user: User, user2: User, user3: User, - voteinfo: TUVoteInfo): +def test_tu_vote_reminders_only_unvoted( + user: User, user2: User, user3: User, voteinfo: TUVoteInfo +): # Vote with user2 and user3; leaving only user to be notified. create_vote(user2, voteinfo) create_vote(user3, voteinfo) diff --git a/test/test_user.py b/test/test_user.py index 5f25f3c9..17fd0c0e 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -1,6 +1,5 @@ import hashlib import json - from datetime import datetime, timedelta import bcrypt @@ -9,10 +8,14 @@ import pytest import aurweb.auth import aurweb.config import aurweb.models.account_type as at - from aurweb import db from aurweb.auth import creds -from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.account_type import ( + DEVELOPER_ID, + TRUSTED_USER_AND_DEV_ID, + TRUSTED_USER_ID, + USER_ID, +) from aurweb.models.ban import Ban from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -31,10 +34,14 @@ def setup(db_test): def create_user(username: str, account_type_id: int): with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - RealName=username.title(), Passwd="testPassword", - AccountTypeID=account_type_id) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + RealName=username.title(), + Passwd="testPassword", + AccountTypeID=account_type_id, + ) return user @@ -71,7 +78,7 @@ def package(user: User) -> Package: def test_user_login_logout(user: User): - """ Test creating a user and reading its columns. """ + """Test creating a user and reading its columns.""" # Assert that make_user created a valid user. assert bool(user.ID) @@ -89,8 +96,7 @@ def test_user_login_logout(user: User): assert user.is_authenticated() # Expect that User session relationships work right. - user_session = db.query(Session, - Session.UsersID == user.ID).first() + user_session = db.query(Session, Session.UsersID == user.ID).first() assert user_session == user.session assert user.session.SessionID == sid assert user.session.User == user @@ -111,8 +117,10 @@ def test_user_login_logout(user: User): assert result.is_authenticated() # Test out user string functions. - assert repr(user) == f"" + assert ( + repr(user) + == f"" + ) # Test logout. user.logout(request) @@ -145,9 +153,7 @@ def test_user_login_suspended(user: User): def test_legacy_user_authentication(user: User): with db.begin(): user.Salt = bcrypt.gensalt().decode() - user.Passwd = hashlib.md5( - f"{user.Salt}testPassword".encode() - ).hexdigest() + user.Passwd = hashlib.md5(f"{user.Salt}testPassword".encode()).hexdigest() assert not user.valid_password("badPassword") assert user.valid_password("testPassword") @@ -160,8 +166,12 @@ def test_user_login_with_outdated_sid(user: User): # Make a session with a LastUpdateTS 5 seconds ago, causing # user.login to update it with a new sid. with db.begin(): - db.create(Session, UsersID=user.ID, SessionID="stub", - LastUpdateTS=datetime.utcnow().timestamp() - 5) + db.create( + Session, + UsersID=user.ID, + SessionID="stub", + LastUpdateTS=datetime.utcnow().timestamp() - 5, + ) sid = user.login(Request(), "testPassword") assert sid and user.is_authenticated() assert sid != "stub" @@ -186,9 +196,12 @@ def test_user_ssh_pub_key(user: User): assert user.ssh_pub_keys.first() is None with db.begin(): - ssh_pub_key = db.create(SSHPubKey, UserID=user.ID, - Fingerprint="testFingerprint", - PubKey="testPubKey") + ssh_pub_key = db.create( + SSHPubKey, + UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey", + ) assert user.ssh_pub_keys.first() == ssh_pub_key @@ -283,8 +296,9 @@ def test_user_packages(user: User, package: Package): assert package in user.packages() -def test_can_edit_user(user: User, tu_user: User, dev_user: User, - tu_and_dev_user: User): +def test_can_edit_user( + user: User, tu_user: User, dev_user: User, tu_and_dev_user: User +): # User can edit. assert user.can_edit_user(user) diff --git a/test/test_usermaint.py b/test/test_usermaint.py index e572569a..7d7bd135 100644 --- a/test/test_usermaint.py +++ b/test/test_usermaint.py @@ -14,13 +14,18 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user def test_usermaint_noop(user: User): - """ Last[SSH]Login isn't expired in this test: usermaint is noop. """ + """Last[SSH]Login isn't expired in this test: usermaint is noop.""" now = time.utcnow() with db.begin(): diff --git a/test/test_util.py b/test/test_util.py index ae1de81b..686e35b4 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,10 +1,8 @@ import json - from http import HTTPStatus import fastapi import pytest - from fastapi.responses import JSONResponse from aurweb import filters, util @@ -18,7 +16,7 @@ def test_round(): def test_git_search(): - """ Test that git_search matches the full commit if necessary. """ + """Test that git_search matches the full commit if necessary.""" commit_hash = "0123456789abcdef" repo = {commit_hash} prefixlen = util.git_search(repo, commit_hash) @@ -26,7 +24,7 @@ def test_git_search(): def test_git_search_double_commit(): - """ Test that git_search matches a shorter prefix length. """ + """Test that git_search matches a shorter prefix length.""" commit_hash = "0123456789abcdef" repo = {commit_hash[:13]} # Locate the shortest prefix length that matches commit_hash. @@ -36,7 +34,6 @@ def test_git_search_double_commit(): @pytest.mark.asyncio async def test_error_or_result(): - async def route(request: fastapi.Request): raise RuntimeError("No response returned.") diff --git a/util/fix-coverage b/util/fix-coverage index 3446c4af..77cf29c1 100755 --- a/util/fix-coverage +++ b/util/fix-coverage @@ -48,9 +48,8 @@ def main(): files[i] = path for _, i in enumerate(files.keys()): - new_path = re.sub(r'^/aurweb', aurwebdir, files[i]) - cursor.execute("UPDATE file SET path = ? WHERE id = ?", ( - new_path, i)) + new_path = re.sub(r"^/aurweb", aurwebdir, files[i]) + cursor.execute("UPDATE file SET path = ? WHERE id = ?", (new_path, i)) db.commit() db.close() diff --git a/web/html/503.php b/web/html/503.php index 80eb4369..23e7014e 100644 --- a/web/html/503.php +++ b/web/html/503.php @@ -12,4 +12,3 @@ html_header( __("Service Unavailable") );
      - diff --git a/web/template/flag_comment.php b/web/template/flag_comment.php index 05eeacb2..dc285a97 100644 --- a/web/template/flag_comment.php +++ b/web/template/flag_comment.php @@ -24,4 +24,3 @@

      - diff --git a/web/template/header.php b/web/template/header.php index afe7a9b6..9631be91 100644 --- a/web/template/header.php +++ b/web/template/header.php @@ -80,4 +80,3 @@
    - diff --git a/web/template/pkgreq_close_form.php b/web/template/pkgreq_close_form.php index 6077b325..6228f6ab 100644 --- a/web/template/pkgreq_close_form.php +++ b/web/template/pkgreq_close_form.php @@ -29,4 +29,3 @@
    - diff --git a/web/template/template.phps b/web/template/template.phps index 4f8117c8..f1a0bb0d 100644 --- a/web/template/template.phps +++ b/web/template/template.phps @@ -17,4 +17,3 @@ print __("Hi, this is worth reading!")."
    \n"; html_footer(AURWEB_VERSION); - From 505eb90479df1d14c3c2e64a90a40a1ef5815765 Mon Sep 17 00:00:00 2001 From: Joakim Saario Date: Sat, 20 Aug 2022 19:29:25 +0200 Subject: [PATCH 1115/1451] chore: Add .git-blame-ignore-revs file The idea is to exclude commits that only contains formatting so that it's easier to backtrack actual code changes with `git blame`. --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..d3c9887b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# style: Run pre-commit +9c6c13b78a30cb9d800043410799e29631f803d2 From de5538a40f5d706a1f7dee7a2361be32ff2760c1 Mon Sep 17 00:00:00 2001 From: Joakim Saario Date: Sun, 21 Aug 2022 22:16:52 +0200 Subject: [PATCH 1116/1451] ci(lint): Use pre-commit --- .gitlab-ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 98f99ae3..7134673c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,18 +13,16 @@ variables: LOG_CONFIG: logging.test.conf lint: - variables: - # Space-separated list of directories that should be linted. - REQUIRES_LINT: "aurweb test migrations" stage: .pre before_script: - pacman -Sy --noconfirm --noprogressbar --cachedir .pkg-cache archlinux-keyring - pacman -Syu --noconfirm --noprogressbar --cachedir .pkg-cache - python python-isort flake8 + git python python-pre-commit script: - - bash -c 'flake8 --count $(echo "$REQUIRES_LINT" | xargs); exit $?' - - bash -c 'isort --check-only $(echo "$REQUIRES_LINT" | xargs); exit $?' + # https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763 + - export SETUPTOOLS_USE_DISTUTILS=stdlib + - pre-commit run -a test: stage: test From ce5dbf0eebb58a5f9d39736a42a1558ff0ee8b64 Mon Sep 17 00:00:00 2001 From: Joakim Saario Date: Mon, 22 Aug 2022 22:30:25 +0200 Subject: [PATCH 1117/1451] docs(contributing): Update Coding Style --- CONTRIBUTING.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52e182c7..58612a36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,21 +31,27 @@ Test patches that increase coverage in the codebase are always welcome. ### Coding Style -We use the `flake8` and `isort` tools to manage PEP-8 coherence and -import ordering in this project. +We use `autoflake`, `isort`, `black` and `flake8` to enforce coding style in a +PEP-8 compliant way. These tools run in GitLab CI using `pre-commit` to verify +that any pushed code changes comply with this. + +To enable the `pre-commit` git hook, install the `pre-commit` package either +with `pacman` or `pip` and then run `pre-commit install --install-hooks`. This +will ensure formatting is done before any code is commited to the git +repository. There are plugins for editors or IDEs which automate this process. Some example plugins: -- [tell-k/vim-autopep8](https://github.com/tell-k/vim-autopep8) +- [tenfyzhong/autoflake.vim](https://github.com/tenfyzhong/autoflake.vim) - [fisadev/vim-isort](https://github.com/fisadev/vim-isort) +- [psf/black](https://github.com/psf/black) +- [nvie/vim-flake8](https://github.com/nvie/vim-flake8) - [prabirshrestha/vim-lsp](https://github.com/prabirshrestha/vim-lsp) +- [dense-analysis/ale](https://github.com/dense-analysis/ale) -See `setup.cfg` for flake8 and isort specific rules. - -Note: We are planning on switching to [psf/black](https://github.com/psf/black). -For now, developers should ensure that flake8 and isort passes when submitting -merge requests or patch sets. +See `setup.cfg`, `pyproject.toml` and `.pre-commit-config.yaml` for tool +specific configurations. ### Development Environment From 57c040995820e08e4af9aadfdc5c946551d899ec Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 22 Aug 2022 23:44:56 -0700 Subject: [PATCH 1118/1451] style: set flake8's max-line-length=88 In accordance with black's defined style, we now expect a maximum of 88 columns for any one particular line. This change fixes remaining violations of 88 columns in the codebase (not many), and introduces the modified flake8 configuration. Signed-off-by: Kevin Morris --- schema/gendummydata.py | 13 ++++++++++--- setup.cfg | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/schema/gendummydata.py b/schema/gendummydata.py index fa59855f..dfc8eee5 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -233,7 +233,8 @@ for p in list(seen_pkgs.keys()): s = ( "INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " - "SubmitterUID, MaintainerUID, PackagerUID) VALUES (%d, '%s', '', %d, %d, %d, %s, %s);\n" + "SubmitterUID, MaintainerUID, PackagerUID) VALUES " + "(%d, '%s', '', %d, %d, %d, %s, %s);\n" ) s = s % (seen_pkgs[p], p, NOW, NOW, uuid, muid, puid) out.write(s) @@ -303,7 +304,10 @@ for p in seen_pkgs_keys: deptype = random.randrange(1, 5) if deptype == 4: dep += ": for " + random.choice(seen_pkgs_keys) - s = "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) VALUES (%d, %d, '%s');\n" + s = ( + "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) " + "VALUES (%d, %d, '%s');\n" + ) s = s % (seen_pkgs[p], deptype, dep) out.write(s) @@ -311,7 +315,10 @@ for p in seen_pkgs_keys: for i in range(0, num_deps): rel = random.choice(seen_pkgs_keys) reltype = random.randrange(1, 4) - s = "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) VALUES (%d, %d, '%s');\n" + s = ( + "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) " + "VALUES (%d, %d, '%s');\n" + ) s = s % (seen_pkgs[p], reltype, rel) out.write(s) diff --git a/setup.cfg b/setup.cfg index 3c9bf777..41978dae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [flake8] -max-line-length = 127 +max-line-length = 88 max-complexity = 10 # Ignore some unavoidable flake8 warnings; we know this is against From fbb3e052fed5a82e334bb795c58f6e0a16f55890 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 23 Aug 2022 00:07:40 -0700 Subject: [PATCH 1119/1451] ci: use cache/virtualenv for test dependencies Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 3 +++ Dockerfile | 3 ++- docker/scripts/install-deps.sh | 3 ++- docker/scripts/install-python-deps.sh | 7 +++---- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7134673c..4d082582 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,6 +4,7 @@ cache: paths: # For some reason Gitlab CI only supports storing cache/artifacts in a path relative to the build directory - .pkg-cache + - .venv variables: AUR_CONFIG: conf/config # Default MySQL config setup in before_script. @@ -31,6 +32,8 @@ test: before_script: - export PATH="$HOME/.poetry/bin:${PATH}" - ./docker/scripts/install-deps.sh + - virtualenv -p python3 .venv + - source .venv/bin/activate # Enable our virtualenv cache - ./docker/scripts/install-python-deps.sh - useradd -U -d /aurweb -c 'AUR User' aur - ./docker/mariadb-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 16e6514e..28bca0e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ VOLUME /root/.cache/pypoetry/artifacts ENV PATH="/root/.poetry/bin:${PATH}" ENV PYTHONPATH=/aurweb ENV AUR_CONFIG=conf/config +ENV COMPOSE=1 # Install system-wide dependencies. COPY ./docker/scripts/install-deps.sh /install-deps.sh @@ -27,7 +28,7 @@ RUN cp -vf conf/config.dev conf/config RUN sed -i "s;YOUR_AUR_ROOT;/aurweb;g" conf/config # Install Python dependencies. -RUN /docker/scripts/install-python-deps.sh +RUN /docker/scripts/install-python-deps.sh compose # Compile asciidocs. RUN make -C doc diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index ced18c81..82496a2b 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -17,6 +17,7 @@ pacman -Syu --noconfirm --noprogressbar \ mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ php php-fpm memcached php-memcached python-pip pyalpm \ python-srcinfo curl libeatmydata cronie python-poetry \ - python-poetry-core step-cli step-ca asciidoc + python-poetry-core step-cli step-ca asciidoc \ + python-virtualenv exec "$@" diff --git a/docker/scripts/install-python-deps.sh b/docker/scripts/install-python-deps.sh index 3d5f28f0..01a6eaa7 100755 --- a/docker/scripts/install-python-deps.sh +++ b/docker/scripts/install-python-deps.sh @@ -4,8 +4,7 @@ set -eou pipefail # Upgrade PIP; Arch Linux's version of pip is outdated for Poetry. pip install --upgrade pip -# Install the aurweb package and deps system-wide via poetry. -poetry config virtualenvs.create false +if [ ! -z "${COMPOSE+x}" ]; then + poetry config virtualenvs.create false +fi poetry install --no-interaction --no-ansi - -exec "$@" From 929bb756a8845fea4652d1b67cae515df872e98c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 23 Aug 2022 02:32:35 -0700 Subject: [PATCH 1120/1451] ci(lint): add .pre-commit cache for pre-commit Signed-off-by: Kevin Morris --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4d082582..23ed18f3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,7 @@ cache: # For some reason Gitlab CI only supports storing cache/artifacts in a path relative to the build directory - .pkg-cache - .venv + - .pre-commit variables: AUR_CONFIG: conf/config # Default MySQL config setup in before_script. @@ -23,6 +24,7 @@ lint: script: # https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763 - export SETUPTOOLS_USE_DISTUTILS=stdlib + - export XDG_CACHE_HOME=.pre-commit - pre-commit run -a test: From 8a3a7e31aca556c3a4b07f1ce717d7a0d6682f68 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Wed, 31 Aug 2022 22:01:54 -0700 Subject: [PATCH 1121/1451] upgrade: bump version to v6.1.1 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 4f97020c..ee14f61e 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.28" +AURWEB_VERSION = "v6.1.1" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 3a6dbe4d..f980ded9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.0.28" +version = "v6.1.1" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From b8a4ce4ceb085d70f7c33f7f884efb5433e65e47 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 2 Sep 2022 15:04:43 -0700 Subject: [PATCH 1122/1451] fix: include maint/comaint state in pkgbase post's error context Closes #386 Signed-off-by: Kevin Morris --- aurweb/routers/pkgbase.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 913e3955..076aec1e 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -587,6 +587,9 @@ async def pkgbase_disown_post( context = templates.make_context(request, "Disown Package") context["pkgbase"] = pkgbase + context["is_maint"] = request.user == pkgbase.Maintainer + context["is_comaint"] = request.user in comaints + if not confirm: context["errors"] = [ ( @@ -610,9 +613,7 @@ async def pkgbase_disown_post( request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST ) - if not next: - next = f"/pkgbase/{name}" - + next = next or f"/pkgbase/{name}" return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) From 6435c2b1f1f324bc717f0c12afbdc42c88e7e66b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 2 Sep 2022 15:28:02 -0700 Subject: [PATCH 1123/1451] upgrade: bump to version v6.1.2 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index ee14f61e..df129c39 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.1" +AURWEB_VERSION = "v6.1.2" _parser = None diff --git a/pyproject.toml b/pyproject.toml index f980ded9..f249c80c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.1" +version = "v6.1.2" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 7fed5742b8e2267f7ce4f4a2db15087742d781e0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 5 Sep 2022 02:33:48 -0700 Subject: [PATCH 1124/1451] fix: display requests for TUs which no longer have an associated User Closes #387 Signed-off-by: Kevin Morris --- aurweb/routers/requests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index c7935575..51be6d2c 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -7,7 +7,7 @@ from sqlalchemy import case from aurweb import db, defaults, time, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import handle_form_exceptions -from aurweb.models import PackageRequest, User +from aurweb.models import PackageRequest from aurweb.models.package_request import PENDING_ID, REJECTED_ID from aurweb.requests.util import get_pkgreq_by_id from aurweb.scripts import notify @@ -31,8 +31,8 @@ async def requests( context["O"] = O context["PP"] = PP - # A PackageRequest query, with left inner joined User and RequestType. - query = db.query(PackageRequest).join(User, User.ID == PackageRequest.UsersID) + # A PackageRequest query + query = db.query(PackageRequest) # If the request user is not elevated (TU or Dev), then # filter PackageRequests which are owned by the request user. From a629098b9299adc67a89589ee70924ee9cf4d464 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 5 Sep 2022 02:55:20 -0700 Subject: [PATCH 1125/1451] fix: conditional display on Request's 'Filed by' field Since we support requests which have no associated user, we must support the case where we are displaying such a request. Signed-off-by: Kevin Morris --- templates/requests.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/templates/requests.html b/templates/requests.html index ff265de1..ed8f31fb 100644 --- a/templates/requests.html +++ b/templates/requests.html @@ -46,9 +46,13 @@ {{ result.Comments }} {# Filed by #} - - {{ result.User.Username }} - + {# If the record has an associated User, display a link to that user. #} + {# Otherwise, display nothing (an empty column). #} + {% if result.User %} + + {{ result.User.Username }} + + {% endif %} {% set idle_time = config_getint("options", "request_idle_time") %} {% set time_delta = (utcnow - result.RequestTS) | int %} From 83ddbd220fe7b00ef66eb5a9c8269fd1e0bf322a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 5 Sep 2022 02:56:48 -0700 Subject: [PATCH 1126/1451] test: get /requests displays all requests, including those without a User Signed-off-by: Kevin Morris --- test/test_requests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test_requests.py b/test/test_requests.py index fd831674..83cdb402 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -743,6 +743,22 @@ def test_requests( assert len(rows) == 5 # There are five records left on the second page. +def test_requests_by_deleted_users( + client: TestClient, user: User, tu_user: User, pkgreq: PackageRequest +): + with db.begin(): + db.delete(user) + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/requests", cookies=cookies) + assert resp.status_code == HTTPStatus.OK + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + def test_requests_selfmade( client: TestClient, user: User, requests: list[PackageRequest] ): From 0388b12896e31bf7d4a5b0feeeb207ce6c0231dc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 5 Sep 2022 19:25:32 -0700 Subject: [PATCH 1127/1451] fix: package description on /packages/{name} view ...What in the world happened here. We were literally just populating `pkg` based on `pkgbase.packages.first()`. We should have been focusing on the package passed by the context, which is always available when `show_package_details` is true. Closes #384 Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index ca7159be..cdb62128 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -1,4 +1,3 @@ -{% set pkg = pkgbase.packages.first() %} @@ -20,13 +19,13 @@ - + - + diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index a707bbac..6e92eeff 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -304,6 +304,50 @@ def test_package(client: TestClient, package: Package): assert conflicts[0].text.strip() == ", ".join(expected) +def test_package_split_description(client: TestClient, user: User): + + with db.begin(): + pkgbase = db.create( + PackageBase, + Name="pkgbase", + Maintainer=user, + Packager=user, + ) + + pkg_a = db.create( + Package, + PackageBase=pkgbase, + Name="pkg_a", + Description="pkg_a desc", + ) + pkg_b = db.create( + Package, + PackageBase=pkgbase, + Name="pkg_b", + Description="pkg_b desc", + ) + + # Check pkg_a + with client as request: + endp = f"/packages/{pkg_a.Name}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + root = parse_root(resp.text) + row = root.xpath('//tr[@id="pkg-description"]/td')[0] + assert row.text == pkg_a.Description + + # Check pkg_b + with client as request: + endp = f"/packages/{pkg_b.Name}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + root = parse_root(resp.text) + row = root.xpath('//tr[@id="pkg-description"]/td')[0] + assert row.text == pkg_b.Description + + def paged_depends_required(client: TestClient, package: Package): maint = package.PackageBase.Maintainer new_pkgs = [] diff --git a/test/test_templates.py b/test/test_templates.py index 383f45d1..f80e68eb 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -293,7 +293,7 @@ def test_package_details(user: User, package: Package): "git_clone_uri_anon": GIT_CLONE_URI_ANON, "git_clone_uri_priv": GIT_CLONE_URI_PRIV, "pkgbase": package.PackageBase, - "pkg": package, + "package": package, "comaintainers": [], } ) @@ -329,7 +329,7 @@ def test_package_details_filled(user: User, package: Package): "git_clone_uri_anon": GIT_CLONE_URI_ANON, "git_clone_uri_priv": GIT_CLONE_URI_PRIV, "pkgbase": package.PackageBase, - "pkg": package, + "package": package, "comaintainers": [], "licenses": package.package_licenses, "provides": package.package_relations.filter( From 310c469ba8d7831495d6cc2e24dba7224a705d5f Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 5 Sep 2022 17:08:55 +0100 Subject: [PATCH 1129/1451] fix: run pre-commit checks instead of flake8 and isort Signed-off-by: Leonidas Spyropoulos --- docker/scripts/install-deps.sh | 2 +- docker/scripts/run-tests.sh | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 82496a2b..85403969 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -18,6 +18,6 @@ pacman -Syu --noconfirm --noprogressbar \ php php-fpm memcached php-memcached python-pip pyalpm \ python-srcinfo curl libeatmydata cronie python-poetry \ python-poetry-core step-cli step-ca asciidoc \ - python-virtualenv + python-virtualenv python-pre-commit exec "$@" diff --git a/docker/scripts/run-tests.sh b/docker/scripts/run-tests.sh index a726c957..5d454ecb 100755 --- a/docker/scripts/run-tests.sh +++ b/docker/scripts/run-tests.sh @@ -21,8 +21,7 @@ rm -f /data/.coverage cp -v .coverage /data/.coverage chmod 666 /data/.coverage -# Run flake8 and isort checks. +# Run pre-commit checks for dir in aurweb test migrations; do - flake8 --count $dir - isort --check-only $dir + pre-commit run -a done From a84d115fa1715c19f66540066e021ac3d4c44a3d Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 6 Sep 2022 08:24:03 +0000 Subject: [PATCH 1130/1451] chore(deps): add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} From 655402a50931693b3ac376dd5dea4b0c05d893e9 Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 6 Sep 2022 10:25:02 +0000 Subject: [PATCH 1131/1451] chore(deps): update dependency pytest-asyncio to ^0.19.0 --- poetry.lock | 124 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0395db3b..eddb0f95 100644 --- a/poetry.lock +++ b/poetry.lock @@ -34,9 +34,9 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] -test = ["uvloop (>=0.15)", "mock (>=4)", "uvloop (<0.15)", "contextlib2", "trustme", "pytest-mock (>=3.6.1)", "pytest (>=7.0)", "hypothesis (>=4.0)", "coverage[toml] (>=4.5)"] -doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "packaging"] [[package]] name = "asgiref" @@ -47,7 +47,7 @@ optional = false python-versions = ">=3.7" [package.extras] -tests = ["mypy (>=0.800)", "pytest-asyncio", "pytest"] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "atomicwrites" @@ -66,10 +66,10 @@ optional = false python-versions = ">=3.5" [package.extras] -tests_no_zope = ["cloudpickle", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] -tests = ["cloudpickle", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] -docs = ["sphinx-notfound-page", "zope.interface", "sphinx", "furo"] -dev = ["cloudpickle", "pre-commit", "sphinx-notfound-page", "sphinx", "furo", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "authlib" @@ -189,11 +189,11 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] name = "dnspython" @@ -204,12 +204,12 @@ optional = false python-versions = ">=3.6,<4.0" [package.extras] -wmi = ["wmi (>=1.5.1,<2.0.0)"] -trio = ["trio (>=0.14,<0.20)"] -curio = ["sniffio (>=1.1,<2.0)", "curio (>=1.2,<2.0)"] -doh = ["requests-toolbelt (>=0.9.1,<0.10.0)", "requests (>=2.23.0,<3.0.0)", "httpx (>=0.21.1)", "h2 (>=4.1.0)"] -idna = ["idna (>=2.1,<4.0)"] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] dnssec = ["cryptography (>=2.6,<37.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.20)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "email-validator" @@ -248,8 +248,8 @@ six = ">=1.16.0,<2.0.0" sortedcontainers = ">=2.4.0,<3.0.0" [package.extras] -lua = ["lupa (>=1.13,<2.0)"] aioredis = ["aioredis (>=2.0.1,<3.0.0)"] +lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "fastapi" @@ -264,10 +264,10 @@ pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1. starlette = "0.17.1" [package.extras] -test = ["types-dataclasses (==0.1.7)", "types-orjson (==3.6.0)", "types-ujson (==0.1.1)", "anyio[trio] (>=3.2.1,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "peewee (>=3.13.3,<4.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "email_validator (>=1.1.1,<2.0.0)", "httpx (>=0.14.0,<0.19.0)", "requests (>=2.24.0,<3.0.0)", "isort (>=5.0.6,<6.0.0)", "black (==21.9b0)", "flake8 (>=3.8.3,<4.0.0)", "mypy (==0.910)", "pytest-cov (>=2.12.0,<4.0.0)", "pytest (>=6.2.4,<7.0.0)"] -doc = ["pyyaml (>=5.3.1,<6.0.0)", "typer-cli (>=0.0.12,<0.0.13)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] -dev = ["uvicorn[standard] (>=0.12.0,<0.16.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.4.0,<2.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)"] -all = ["uvicorn[standard] (>=0.12.0,<0.16.0)", "email_validator (>=1.1.1,<2.0.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "jinja2 (>=2.11.2,<4.0.0)", "requests (>=2.24.0,<3.0.0)"] +all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,<5.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "typer-cli (>=0.0.12,<0.0.13)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==21.9b0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.1.7)", "types-orjson (==3.6.0)", "types-ujson (==0.1.1)", "ujson (>=4.0.1,<5.0.0)"] [[package]] name = "feedgen" @@ -290,8 +290,8 @@ optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"] -docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "greenlet" @@ -378,9 +378,9 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -http2 = ["h2 (>=3,<5)"] -cli = ["pygments (>=2.0.0,<3.0.0)", "rich (>=10.0.0,<11.0.0)", "click (>=8.0.0,<9.0.0)"] brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10.0.0,<11.0.0)"] +http2 = ["h2 (>=3,<5)"] [[package]] name = "hypercorn" @@ -398,10 +398,10 @@ toml = "*" wsproto = ">=0.14.0" [package.extras] -uvloop = ["uvloop"] -trio = ["trio (>=0.11.0)"] -tests = ["trio", "pytest-trio", "pytest-cov", "pytest-asyncio", "pytest", "mock", "hypothesis"] h3 = ["aioquic (>=0.9.0,<1.0)"] +tests = ["hypothesis", "mock", "pytest", "pytest-asyncio", "pytest-cov", "pytest-trio", "trio"] +trio = ["trio (>=0.11.0)"] +uvloop = ["uvloop"] [[package]] name = "hyperframe" @@ -431,9 +431,9 @@ python-versions = ">=3.7" zipp = ">=0.5" [package.extras] -testing = ["importlib-resources (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "pytest-perf (>=0.9.2)", "flufl.flake8", "pyfakefs", "packaging", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -474,10 +474,10 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" [package.extras] -source = ["Cython (>=0.29.7)"] -htmlsoup = ["beautifulsoup4"] -html5 = ["html5lib"] cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] [[package]] name = "mako" @@ -507,7 +507,7 @@ python-versions = ">=3.7" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] -testing = ["pyyaml", "coverage"] +testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" @@ -569,8 +569,8 @@ optional = false python-versions = ">=3.6" [package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "posix-ipc" @@ -655,8 +655,8 @@ python-versions = ">=3.6.1" typing-extensions = ">=3.7.4.3" [package.extras] -email = ["email-validator (>=1.0.3)"] dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] [[package]] name = "pygit2" @@ -699,21 +699,21 @@ py = ">=1.8.2" toml = "*" [package.extras] -testing = ["xmlschema", "requests", "nose", "mock", "hypothesis (>=3.56)", "argcomplete"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.16.0" -description = "Pytest support for asyncio." +version = "0.19.0" +description = "Pytest support for asyncio" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.7" [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=6.1.0" [package.extras] -testing = ["hypothesis (>=5.7.1)", "coverage"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" @@ -728,7 +728,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-forked" @@ -768,9 +768,9 @@ pytest = ">=6.2.0" pytest-forked = "*" [package.extras] -testing = ["filelock"] -setproctitle = ["setproctitle"] psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] [[package]] name = "python-dateutil" @@ -820,8 +820,8 @@ idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rfc3986" @@ -873,24 +873,24 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} [package.extras] -aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] -aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] mssql_pymssql = ["pymssql"] mssql_pyodbc = ["pyodbc"] -mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] mysql_connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] postgresql_psycopg2binary = ["psycopg2-binary"] postgresql_psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql (<1)", "pymysql"] +pymysql = ["pymysql", "pymysql (<1)"] sqlcipher = ["sqlcipher3-binary"] [[package]] @@ -916,7 +916,7 @@ python-versions = ">=3.6" anyio = ">=3.0.0,<4" [package.extras] -full = ["requests", "pyyaml", "python-multipart", "jinja2", "itsdangerous"] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] [[package]] name = "tap.py" @@ -962,9 +962,9 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -secure = ["ipaddress", "certifi", "idna (>=2.0.0)", "cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] -brotli = ["brotlipy (>=0.6.0)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] [[package]] name = "uvicorn" @@ -980,7 +980,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "PyYAML (>=5.1)", "python-dotenv (>=0.13)", "watchgod (>=0.6)", "httptools (>=0.2.0,<0.3.0)", "websockets (>=9.1)"] +standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (>=0.2.0,<0.3.0)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=9.1)"] [[package]] name = "webencodings" @@ -1024,13 +1024,13 @@ optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] -docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "7630feca99b54b3d08fd947d5c5857590ca8af8b6c3a9f0bed7eecf03385597e" +content-hash = "5326e59079df0c0520a8654e8e92e936a50df127e2e5eb6c81f465e0a3dfd339" [metadata.files] aiofiles = [ @@ -1698,8 +1698,8 @@ pytest = [ {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, - {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, + {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, + {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, diff --git a/pyproject.toml b/pyproject.toml index f249c80c..283b8101 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ srcinfo = "^0.0.8" [tool.poetry.dev-dependencies] coverage = "^6.0.2" pytest = "^6.2.5" -pytest-asyncio = "^0.16.0" +pytest-asyncio = "^0.19.0" pytest-cov = "^3.0.0" pytest-tap = "^3.2" From b38e765dfe552d68a9fdcf14116e06efcc3b4b61 Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 6 Sep 2022 22:24:52 +0000 Subject: [PATCH 1132/1451] fix(deps): update dependency aiofiles to ^0.8.0 --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index eddb0f95..61782e65 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "aiofiles" -version = "0.7.0" +version = "0.8.0" description = "File support for asyncio." category = "main" optional = false @@ -1030,12 +1030,12 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "5326e59079df0c0520a8654e8e92e936a50df127e2e5eb6c81f465e0a3dfd339" +content-hash = "cf2d693b3a53f8c1d47b46c9787d710cb39ffdba5c3285e7d5cd0c02ec191154" [metadata.files] aiofiles = [ - {file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"}, - {file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"}, + {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, + {file = "aiofiles-0.8.0.tar.gz", hash = "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"}, ] alembic = [ {file = "alembic-1.8.1-py3-none-any.whl", hash = "sha256:0a024d7f2de88d738d7395ff866997314c837be6104e90c5724350313dee4da4"}, diff --git a/pyproject.toml b/pyproject.toml index 283b8101..a1112d35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ python = ">=3.9,<3.11" # based on git tags. # General -aiofiles = "^0.7.0" +aiofiles = "^0.8.0" asgiref = "^3.4.1" bcrypt = "^3.2.0" bleach = "^4.1.0" From cdc7bd618c8ce06b52da87d2a6efe81a3dcb896e Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 6 Sep 2022 23:24:49 +0000 Subject: [PATCH 1133/1451] fix(deps): update dependency email-validator to v1.2.1 --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 61782e65..691ae494 100644 --- a/poetry.lock +++ b/poetry.lock @@ -213,8 +213,8 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "email-validator" -version = "1.1.3" -description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." +version = "1.2.1" +description = "A robust email syntax and deliverability validation library." category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" @@ -1030,7 +1030,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "cf2d693b3a53f8c1d47b46c9787d710cb39ffdba5c3285e7d5cd0c02ec191154" +content-hash = "5c5c0ec98e190669e257f4d717162794dc5841dca9168d7a941f5e72ea85f03f" [metadata.files] aiofiles = [ @@ -1240,8 +1240,8 @@ dnspython = [ {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, ] email-validator = [ - {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, - {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, + {file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"}, + {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, ] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, diff --git a/pyproject.toml b/pyproject.toml index a1112d35..27369c45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ aiofiles = "^0.8.0" asgiref = "^3.4.1" bcrypt = "^3.2.0" bleach = "^4.1.0" -email-validator = "1.1.3" +email-validator = "1.2.1" fakeredis = "^1.6.1" feedgen = "^0.9.0" httpx = "^0.20.0" From a981ae4052fd064e0ea23fb91fcd8a0d16f36c58 Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Sep 2022 00:25:32 +0000 Subject: [PATCH 1134/1451] fix(deps): update dependency httpx to ^0.23.0 --- poetry.lock | 26 ++++++++++++++------------ pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index 691ae494..1a1a8c62 100644 --- a/poetry.lock +++ b/poetry.lock @@ -348,39 +348,41 @@ python-versions = ">=3.6.1" [[package]] name = "httpcore" -version = "0.13.7" +version = "0.15.0" description = "A minimal low-level HTTP client." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] anyio = ">=3.0.0,<4.0.0" +certifi = "*" h11 = ">=0.11,<0.13" sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" -version = "0.20.0" +version = "0.23.0" description = "The next generation HTTP client." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] certifi = "*" -charset-normalizer = "*" -httpcore = ">=0.13.3,<0.14.0" +httpcore = ">=0.15.0,<0.16.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10.0.0,<11.0.0)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "hypercorn" @@ -1030,7 +1032,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "5c5c0ec98e190669e257f4d717162794dc5841dca9168d7a941f5e72ea85f03f" +content-hash = "879b45a5c84c40462afe971096ff654f7ef6981bbdcea5d5e6107e7f68355802" [metadata.files] aiofiles = [ @@ -1336,12 +1338,12 @@ hpack = [ {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, ] httpcore = [ - {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, - {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, + {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, + {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, ] httpx = [ - {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, - {file = "httpx-0.20.0.tar.gz", hash = "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b"}, + {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, + {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, ] hypercorn = [ {file = "Hypercorn-0.11.2-py3-none-any.whl", hash = "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"}, diff --git a/pyproject.toml b/pyproject.toml index 27369c45..e0c7ba00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ bleach = "^4.1.0" email-validator = "1.2.1" fakeredis = "^1.6.1" feedgen = "^0.9.0" -httpx = "^0.20.0" +httpx = "^0.23.0" itsdangerous = "^2.0.1" lxml = "^4.6.3" orjson = "^3.6.4" From a73af3e76d3fd4a89cba2cf23ee91f431ad2a990 Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Sep 2022 01:25:03 +0000 Subject: [PATCH 1135/1451] fix(deps): update dependency hypercorn to ^0.14.0 --- poetry.lock | 12 ++++++------ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1a1a8c62..801d9e95 100644 --- a/poetry.lock +++ b/poetry.lock @@ -386,8 +386,8 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "hypercorn" -version = "0.11.2" -description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn." +version = "0.14.3" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" category = "main" optional = false python-versions = ">=3.7" @@ -400,8 +400,8 @@ toml = "*" wsproto = ">=0.14.0" [package.extras] +docs = ["pydata-sphinx-theme"] h3 = ["aioquic (>=0.9.0,<1.0)"] -tests = ["hypothesis", "mock", "pytest", "pytest-asyncio", "pytest-cov", "pytest-trio", "trio"] trio = ["trio (>=0.11.0)"] uvloop = ["uvloop"] @@ -1032,7 +1032,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "879b45a5c84c40462afe971096ff654f7ef6981bbdcea5d5e6107e7f68355802" +content-hash = "0afd4b5faa1d291565d5a1a90d6d916ffc37537913d4037660a99c86bb3b3ed1" [metadata.files] aiofiles = [ @@ -1346,8 +1346,8 @@ httpx = [ {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, ] hypercorn = [ - {file = "Hypercorn-0.11.2-py3-none-any.whl", hash = "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"}, - {file = "Hypercorn-0.11.2.tar.gz", hash = "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a"}, + {file = "Hypercorn-0.14.3-py3-none-any.whl", hash = "sha256:7c491d5184f28ee960dcdc14ab45d14633ca79d72ddd13cf4fcb4cb854d679ab"}, + {file = "Hypercorn-0.14.3.tar.gz", hash = "sha256:4a87a0b7bbe9dc75fab06dbe4b301b9b90416e9866c23a377df21a969d6ab8dd"}, ] hyperframe = [ {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, diff --git a/pyproject.toml b/pyproject.toml index e0c7ba00..e6cb2c83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ SQLAlchemy = "^1.4.26" # ASGI uvicorn = "^0.15.0" gunicorn = "^20.1.0" -Hypercorn = "^0.11.2" +Hypercorn = "^0.14.0" prometheus-fastapi-instrumentator = "^5.7.1" pytest-xdist = "^2.4.0" filelock = "^3.3.2" From bb310bdf65add1559c8b459b1be71cf70864dbb1 Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Sep 2022 02:24:55 +0000 Subject: [PATCH 1136/1451] fix(deps): update dependency uvicorn to ^0.18.0 --- poetry.lock | 13 ++++++------- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 801d9e95..4d51292d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -970,19 +970,18 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.15.0" +version = "0.18.3" description = "The lightning-fast ASGI server." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] -asgiref = ">=3.4.0" click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (>=0.2.0,<0.3.0)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=9.1)"] +standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] [[package]] name = "webencodings" @@ -1032,7 +1031,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "0afd4b5faa1d291565d5a1a90d6d916ffc37537913d4037660a99c86bb3b3ed1" +content-hash = "17c8d99957aa94e4b9b0a8fa14098122d402ef52da860c13049a690e5dd18792" [metadata.files] aiofiles = [ @@ -1817,8 +1816,8 @@ urllib3 = [ {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, ] uvicorn = [ - {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, - {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, + {file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"}, + {file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, diff --git a/pyproject.toml b/pyproject.toml index e6cb2c83..4122241d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ Werkzeug = "^2.0.2" SQLAlchemy = "^1.4.26" # ASGI -uvicorn = "^0.15.0" +uvicorn = "^0.18.0" gunicorn = "^20.1.0" Hypercorn = "^0.14.0" prometheus-fastapi-instrumentator = "^5.7.1" From a39f34d695ae8f191957b38d43b2d04a7aaf1c38 Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Sep 2022 03:25:30 +0000 Subject: [PATCH 1137/1451] chore(deps): update dependency pytest to v7 --- poetry.lock | 28 ++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4d51292d..bea03647 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,14 +49,6 @@ python-versions = ">=3.7" [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "attrs" version = "22.1.0" @@ -684,24 +676,23 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "6.2.5" +version = "7.1.3" description = "pytest: simple powerful testing with Python" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" @@ -943,7 +934,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -1031,7 +1022,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "17c8d99957aa94e4b9b0a8fa14098122d402ef52da860c13049a690e5dd18792" +content-hash = "f6a259093ff2796b5a3f579fcd00cc1c6a841d769abd9c898a91a8a6a2eec76f" [metadata.files] aiofiles = [ @@ -1050,9 +1041,6 @@ asgiref = [ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, ] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, @@ -1695,8 +1683,8 @@ pyparsing = [ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, + {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, diff --git a/pyproject.toml b/pyproject.toml index 4122241d..4c0df93c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ srcinfo = "^0.0.8" [tool.poetry.dev-dependencies] coverage = "^6.0.2" -pytest = "^6.2.5" +pytest = "^7.0.0" pytest-asyncio = "^0.19.0" pytest-cov = "^3.0.0" pytest-tap = "^3.2" From 486f8bd61c458b44232a4bb7c07d08e0e15b86f8 Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Sep 2022 04:24:53 +0000 Subject: [PATCH 1138/1451] fix(deps): update dependency aiofiles to v22 --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index bea03647..a12b6aa4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,10 @@ [[package]] name = "aiofiles" -version = "0.8.0" +version = "22.1.0" description = "File support for asyncio." category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.7,<4.0" [[package]] name = "alembic" @@ -1022,12 +1022,12 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "f6a259093ff2796b5a3f579fcd00cc1c6a841d769abd9c898a91a8a6a2eec76f" +content-hash = "888f848aad23900dcff3c089e13b88547605ef760dc3714a9872a89346e150e2" [metadata.files] aiofiles = [ - {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, - {file = "aiofiles-0.8.0.tar.gz", hash = "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"}, + {file = "aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"}, + {file = "aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"}, ] alembic = [ {file = "alembic-1.8.1-py3-none-any.whl", hash = "sha256:0a024d7f2de88d738d7395ff866997314c837be6104e90c5724350313dee4da4"}, diff --git a/pyproject.toml b/pyproject.toml index 4c0df93c..78d7e73a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ python = ">=3.9,<3.11" # based on git tags. # General -aiofiles = "^0.8.0" +aiofiles = "^22.0.0" asgiref = "^3.4.1" bcrypt = "^3.2.0" bleach = "^4.1.0" From 6ab9663b7684dd25bedd5b2ee75e774ddf440fe0 Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Sep 2022 06:25:25 +0000 Subject: [PATCH 1139/1451] fix(deps): update dependency authlib to v1 --- poetry.lock | 15 ++++++--------- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index a12b6aa4..69721ec2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -65,17 +65,14 @@ tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy [[package]] name = "authlib" -version = "0.15.5" -description = "The ultimate Python library in building OAuth and OpenID Connect servers." +version = "1.0.1" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." category = "main" optional = false python-versions = "*" [package.dependencies] -cryptography = "*" - -[package.extras] -client = ["requests"] +cryptography = ">=3.2" [[package]] name = "bcrypt" @@ -1022,7 +1019,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "888f848aad23900dcff3c089e13b88547605ef760dc3714a9872a89346e150e2" +content-hash = "c2412181a05b96ad1daab6e9bddff8e1d4ce2b0b7671536ccccd69c66924c27d" [metadata.files] aiofiles = [ @@ -1046,8 +1043,8 @@ attrs = [ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] authlib = [ - {file = "Authlib-0.15.5-py2.py3-none-any.whl", hash = "sha256:ecf4a7a9f2508c0bb07e93a752dd3c495cfaffc20e864ef0ffc95e3f40d2abaf"}, - {file = "Authlib-0.15.5.tar.gz", hash = "sha256:b83cf6360c8e92b0e9df0d1f32d675790bcc4e3c03977499b1eed24dcdef4252"}, + {file = "Authlib-1.0.1-py2.py3-none-any.whl", hash = "sha256:1286e2d5ef5bfe5a11cc2d0a0d1031f0393f6ce4d61f5121cfe87fa0054e98bd"}, + {file = "Authlib-1.0.1.tar.gz", hash = "sha256:6e74a4846ac36dfc882b3cc2fbd3d9eb410a627f2f2dc11771276655345223b1"}, ] bcrypt = [ {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, diff --git a/pyproject.toml b/pyproject.toml index 78d7e73a..14182ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ paginate = "^0.5.6" # SQL alembic = "^1.7.4" mysqlclient = "^2.0.3" -Authlib = "^0.15.5" +Authlib = "^1.0.0" Jinja2 = "^3.0.2" Markdown = "^3.3.6" Werkzeug = "^2.0.2" From 7ad22d81433bc9507738e8b52f68fd1ba9c0a4b6 Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Sep 2022 14:24:55 +0000 Subject: [PATCH 1140/1451] fix(deps): update dependency bcrypt to v4 --- poetry.lock | 30 ++++++++++++++---------------- pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index 69721ec2..80104bee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -76,15 +76,12 @@ cryptography = ">=3.2" [[package]] name = "bcrypt" -version = "3.2.2" +version = "4.0.0" description = "Modern password hashing for your software and your servers" category = "main" optional = false python-versions = ">=3.6" -[package.dependencies] -cffi = ">=1.1" - [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] @@ -1019,7 +1016,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "c2412181a05b96ad1daab6e9bddff8e1d4ce2b0b7671536ccccd69c66924c27d" +content-hash = "38f6da4f493e57dbbfa462388d4b549fb54e7fd9481dc114602210e846770a9f" [metadata.files] aiofiles = [ @@ -1047,17 +1044,18 @@ authlib = [ {file = "Authlib-1.0.1.tar.gz", hash = "sha256:6e74a4846ac36dfc882b3cc2fbd3d9eb410a627f2f2dc11771276655345223b1"}, ] bcrypt = [ - {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, - {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, - {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, - {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, + {file = "bcrypt-4.0.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:845b1daf4df2dd94d2fdbc9454953ca9dd0e12970a0bfc9f3dcc6faea3fa96e4"}, + {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8780e69f9deec9d60f947b169507d2c9816e4f11548f1f7ebee2af38b9b22ae4"}, + {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c3334446fac200499e8bc04a530ce3cf0b3d7151e0e4ac5c0dddd3d95e97843"}, + {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb67f6a6c72dfb0a02f3df51550aa1862708e55128b22543e2b42c74f3620d7"}, + {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:7c7dd6c1f05bf89e65261d97ac3a6520f34c2acb369afb57e3ea4449be6ff8fd"}, + {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:594780b364fb45f2634c46ec8d3e61c1c0f1811c4f2da60e8eb15594ecbf93ed"}, + {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d0dd19aad87e4ab882ef1d12df505f4c52b28b69666ce83c528f42c07379227"}, + {file = "bcrypt-4.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bf413f2a9b0a2950fc750998899013f2e718d20fa4a58b85ca50b6df5ed1bbf9"}, + {file = "bcrypt-4.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ede0f506554571c8eda80db22b83c139303ec6b595b8f60c4c8157bdd0bdee36"}, + {file = "bcrypt-4.0.0-cp36-abi3-win32.whl", hash = "sha256:dc6ec3dc19b1c193b2f7cf279d3e32e7caf447532fbcb7af0906fe4398900c33"}, + {file = "bcrypt-4.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:0b0f0c7141622a31e9734b7f649451147c04ebb5122327ac0bd23744df84be90"}, + {file = "bcrypt-4.0.0.tar.gz", hash = "sha256:c59c170fc9225faad04dde1ba61d85b413946e8ce2e5f5f5ff30dfd67283f319"}, ] bleach = [ {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, diff --git a/pyproject.toml b/pyproject.toml index 14182ed2..52629cfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ python = ">=3.9,<3.11" # General aiofiles = "^22.0.0" asgiref = "^3.4.1" -bcrypt = "^3.2.0" +bcrypt = "^4.0.0" bleach = "^4.1.0" email-validator = "1.2.1" fakeredis = "^1.6.1" From 3de17311cfb92755f4b91e34dcf5e43f66652ea4 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 10 Sep 2022 00:25:02 +0000 Subject: [PATCH 1141/1451] fix(deps): update dependency bleach to v5 --- poetry.lock | 15 +++++++++------ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 80104bee..f13e1df2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -88,17 +88,20 @@ typecheck = ["mypy"] [[package]] name = "bleach" -version = "4.1.0" +version = "5.0.1" description = "An easy safelist-based HTML-sanitizing tool." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -packaging = "*" six = ">=1.9.0" webencodings = "*" +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.2)"] +dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] + [[package]] name = "certifi" version = "2022.6.15" @@ -1016,7 +1019,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "38f6da4f493e57dbbfa462388d4b549fb54e7fd9481dc114602210e846770a9f" +content-hash = "ac45bb4ee013a8f79016947fe222a3158ffe716008349a81086e2dbeac6b914c" [metadata.files] aiofiles = [ @@ -1058,8 +1061,8 @@ bcrypt = [ {file = "bcrypt-4.0.0.tar.gz", hash = "sha256:c59c170fc9225faad04dde1ba61d85b413946e8ce2e5f5f5ff30dfd67283f319"}, ] bleach = [ - {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, - {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, + {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, + {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, ] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, diff --git a/pyproject.toml b/pyproject.toml index 52629cfb..704e581a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ python = ">=3.9,<3.11" aiofiles = "^22.0.0" asgiref = "^3.4.1" bcrypt = "^4.0.0" -bleach = "^4.1.0" +bleach = "^5.0.0" email-validator = "1.2.1" fakeredis = "^1.6.1" feedgen = "^0.9.0" From 307d944cf1aebae5474695932a5530716794e1f7 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 10 Sep 2022 03:25:08 +0000 Subject: [PATCH 1142/1451] fix(deps): update dependency protobuf to v4 --- poetry.lock | 44 +++++++++++++++++--------------------------- pyproject.toml | 2 +- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/poetry.lock b/poetry.lock index f13e1df2..b2342cb4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -604,8 +604,8 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "3.20.1" -description = "Protocol Buffers" +version = "4.21.5" +description = "" category = "main" optional = false python-versions = ">=3.7" @@ -1019,7 +1019,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "ac45bb4ee013a8f79016947fe222a3158ffe716008349a81086e2dbeac6b914c" +content-hash = "478ba8d01d46e13dd56df2b19835750dda11e9a8bfe46ee8e7e22cb4579cf7b5" [metadata.files] aiofiles = [ @@ -1568,30 +1568,20 @@ prometheus-fastapi-instrumentator = [ {file = "prometheus_fastapi_instrumentator-5.8.2-py3-none-any.whl", hash = "sha256:5bfec239a924e1fed4ba94eb0addc73422d11821e894200b6d0e36a61c966827"}, ] protobuf = [ - {file = "protobuf-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3cc797c9d15d7689ed507b165cd05913acb992d78b379f6014e013f9ecb20996"}, - {file = "protobuf-3.20.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3"}, - {file = "protobuf-3.20.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd68be2559e2a3b84f517fb029ee611546f7812b1fdd0aa2ecc9bc6ec0e4fdde"}, - {file = "protobuf-3.20.1-cp310-cp310-win32.whl", hash = "sha256:9016d01c91e8e625141d24ec1b20fed584703e527d28512aa8c8707f105a683c"}, - {file = "protobuf-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:32ca378605b41fd180dfe4e14d3226386d8d1b002ab31c969c366549e66a2bb7"}, - {file = "protobuf-3.20.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9be73ad47579abc26c12024239d3540e6b765182a91dbc88e23658ab71767153"}, - {file = "protobuf-3.20.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:097c5d8a9808302fb0da7e20edf0b8d4703274d140fd25c5edabddcde43e081f"}, - {file = "protobuf-3.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20"}, - {file = "protobuf-3.20.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cdee09140e1cd184ba9324ec1df410e7147242b94b5f8b0c64fc89e38a8ba531"}, - {file = "protobuf-3.20.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:af0ebadc74e281a517141daad9d0f2c5d93ab78e9d455113719a45a49da9db4e"}, - {file = "protobuf-3.20.1-cp37-cp37m-win32.whl", hash = "sha256:755f3aee41354ae395e104d62119cb223339a8f3276a0cd009ffabfcdd46bb0c"}, - {file = "protobuf-3.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:62f1b5c4cd6c5402b4e2d63804ba49a327e0c386c99b1675c8a0fefda23b2067"}, - {file = "protobuf-3.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf"}, - {file = "protobuf-3.20.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:cb29edb9eab15742d791e1025dd7b6a8f6fcb53802ad2f6e3adcb102051063ab"}, - {file = "protobuf-3.20.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:69ccfdf3657ba59569c64295b7d51325f91af586f8d5793b734260dfe2e94e2c"}, - {file = "protobuf-3.20.1-cp38-cp38-win32.whl", hash = "sha256:dd5789b2948ca702c17027c84c2accb552fc30f4622a98ab5c51fcfe8c50d3e7"}, - {file = "protobuf-3.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:77053d28427a29987ca9caf7b72ccafee011257561259faba8dd308fda9a8739"}, - {file = "protobuf-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f50601512a3d23625d8a85b1638d914a0970f17920ff39cec63aaef80a93fb7"}, - {file = "protobuf-3.20.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:284f86a6207c897542d7e956eb243a36bb8f9564c1742b253462386e96c6b78f"}, - {file = "protobuf-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7403941f6d0992d40161aa8bb23e12575637008a5a02283a930addc0508982f9"}, - {file = "protobuf-3.20.1-cp39-cp39-win32.whl", hash = "sha256:db977c4ca738dd9ce508557d4fce0f5aebd105e158c725beec86feb1f6bc20d8"}, - {file = "protobuf-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:7e371f10abe57cee5021797126c93479f59fccc9693dafd6bd5633ab67808a91"}, - {file = "protobuf-3.20.1-py2.py3-none-any.whl", hash = "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388"}, - {file = "protobuf-3.20.1.tar.gz", hash = "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9"}, + {file = "protobuf-4.21.5-cp310-abi3-win32.whl", hash = "sha256:5310cbe761e87f0c1decce019d23f2101521d4dfff46034f8a12a53546036ec7"}, + {file = "protobuf-4.21.5-cp310-abi3-win_amd64.whl", hash = "sha256:e5c5a2886ae48d22a9d32fbb9b6636a089af3cd26b706750258ce1ca96cc0116"}, + {file = "protobuf-4.21.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ee04f5823ed98bb9a8c3b1dc503c49515e0172650875c3f76e225b223793a1f2"}, + {file = "protobuf-4.21.5-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:b04484d6f42f48c57dd2737a72692f4c6987529cdd148fb5b8e5f616862a2e37"}, + {file = "protobuf-4.21.5-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:5e0b272217aad8971763960238c1a1e6a65d50ef7824e23300da97569a251c55"}, + {file = "protobuf-4.21.5-cp37-cp37m-win32.whl", hash = "sha256:5eb0724615e90075f1d763983e708e1cef08e66b1891d8b8b6c33bc3b2f1a02b"}, + {file = "protobuf-4.21.5-cp37-cp37m-win_amd64.whl", hash = "sha256:011c0f267e85f5d73750b6c25f0155d5db1e9443cd3590ab669a6221dd8fcdb0"}, + {file = "protobuf-4.21.5-cp38-cp38-win32.whl", hash = "sha256:7b6f22463e2d1053d03058b7b4ceca6e4ed4c14f8c286c32824df751137bf8e7"}, + {file = "protobuf-4.21.5-cp38-cp38-win_amd64.whl", hash = "sha256:b52e7a522911a40445a5f588bd5b5e584291bfc5545e09b7060685e4b2ff814f"}, + {file = "protobuf-4.21.5-cp39-cp39-win32.whl", hash = "sha256:a7faa62b183d6a928e3daffd06af843b4287d16ef6e40f331575ecd236a7974d"}, + {file = "protobuf-4.21.5-cp39-cp39-win_amd64.whl", hash = "sha256:5e0ce02418ef03d7657a420ae8fd6fec4995ac713a3cb09164e95f694dbcf085"}, + {file = "protobuf-4.21.5-py2.py3-none-any.whl", hash = "sha256:bf711b451212dc5b0fa45ae7dada07d8e71a4b0ff0bc8e4783ee145f47ac4f82"}, + {file = "protobuf-4.21.5-py3-none-any.whl", hash = "sha256:3ec6f5b37935406bb9df9b277e79f8ed81d697146e07ef2ba8a5a272fb24b2c9"}, + {file = "protobuf-4.21.5.tar.gz", hash = "sha256:eb1106e87e095628e96884a877a51cdb90087106ee693925ec0a300468a9be3a"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, diff --git a/pyproject.toml b/pyproject.toml index 704e581a..b44291d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ httpx = "^0.23.0" itsdangerous = "^2.0.1" lxml = "^4.6.3" orjson = "^3.6.4" -protobuf = "^3.19.0" +protobuf = "^4.0.0" pygit2 = "^1.7.0" python-multipart = "^0.0.5" redis = "^3.5.3" From 69d67247498123bb0b14b731b3f662935c1867a6 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 10 Sep 2022 05:25:06 +0000 Subject: [PATCH 1143/1451] fix(deps): update dependency redis to v4 --- poetry.lock | 124 ++++++++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 2 +- 2 files changed, 118 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index b2342cb4..ef0fc1f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,6 +49,14 @@ python-versions = ">=3.7" [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "attrs" version = "22.1.0" @@ -184,6 +192,20 @@ sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest (<5)", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "pytest", "pytest-cov", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] + [[package]] name = "dnspython" version = "2.2.1" @@ -786,14 +808,20 @@ six = ">=1.4.0" [[package]] name = "redis" -version = "3.5.3" -description = "Python client for Redis key-value store" +version = "4.3.4" +description = "Python client for Redis database and key-value store" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +async-timeout = ">=4.0.2" +deprecated = ">=1.2.3" +packaging = ">=20.4" [package.extras] -hiredis = ["hiredis (>=0.1.3)"] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] [[package]] name = "requests" @@ -993,6 +1021,14 @@ MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog"] +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "wsproto" version = "1.1.0" @@ -1019,7 +1055,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "478ba8d01d46e13dd56df2b19835750dda11e9a8bfe46ee8e7e22cb4579cf7b5" +content-hash = "e084bad4236ac74fb90fcf4537c78c228b2de606e83c63ac2557a677d681e743" [metadata.files] aiofiles = [ @@ -1038,6 +1074,10 @@ asgiref = [ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, ] +async-timeout = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, @@ -1222,6 +1262,10 @@ cryptography = [ {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, ] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, @@ -1702,8 +1746,8 @@ python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] redis = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, + {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"}, + {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, @@ -1803,6 +1847,72 @@ werkzeug = [ {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, ] +wrapt = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] wsproto = [ {file = "wsproto-1.1.0-py3-none-any.whl", hash = "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b"}, {file = "wsproto-1.1.0.tar.gz", hash = "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"}, diff --git a/pyproject.toml b/pyproject.toml index b44291d7..8f9624dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ orjson = "^3.6.4" protobuf = "^4.0.0" pygit2 = "^1.7.0" python-multipart = "^0.0.5" -redis = "^3.5.3" +redis = "^4.0.0" requests = "^2.28.1" paginate = "^0.5.6" From a2d08e441ed0e769a5ec44312eba996bcd7f227c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 11 Sep 2022 17:59:45 -0700 Subject: [PATCH 1144/1451] fix(docker): run `pre-commit run -a` once Signed-off-by: Kevin Morris --- docker/scripts/run-tests.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/scripts/run-tests.sh b/docker/scripts/run-tests.sh index 5d454ecb..75e562b0 100755 --- a/docker/scripts/run-tests.sh +++ b/docker/scripts/run-tests.sh @@ -22,6 +22,4 @@ cp -v .coverage /data/.coverage chmod 666 /data/.coverage # Run pre-commit checks -for dir in aurweb test migrations; do - pre-commit run -a -done +pre-commit run -a From 03776c4663dda25195b262d483fd46b2a08dc5b9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 11 Sep 2022 18:00:11 -0700 Subject: [PATCH 1145/1451] fix(docker): cache & install pre-commit deps during image build Signed-off-by: Kevin Morris --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 28bca0e4..1f667611 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM archlinux:base-devel VOLUME /root/.cache/pypoetry/cache VOLUME /root/.cache/pypoetry/artifacts +VOLUME /root/.cache/pre-commit ENV PATH="/root/.poetry/bin:${PATH}" ENV PYTHONPATH=/aurweb @@ -41,3 +42,6 @@ RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime # Install translations. RUN make -C po all install + +# Install pre-commit repositories and run lint check. +RUN pre-commit run -a From b3853e01b82372bc0ceb41a9352e5a54a6190dda Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 11 Sep 2022 18:07:54 -0700 Subject: [PATCH 1146/1451] fix(pre-commit): include migrations in fixes/checks We want all python files related to the project to be checked, really. Some of which are still included, but migrations are a core part of FastAPI aurweb and should be included. Signed-off-by: Kevin Morris --- .pre-commit-config.yaml | 2 - ...2ce8e2ffa_utf8mb4_charset_and_collation.py | 52 +++++++++---------- .../be7adae47ac3_upgrade_voteinfo_integers.py | 6 +-- .../d64e5571bc8d_fix_pkgvote_votets.py | 7 ++- ...6e1cd_add_sso_account_id_in_table_users.py | 21 ++++---- .../versions/f47cad5d6d03_initial_revision.py | 2 +- 6 files changed, 44 insertions(+), 46 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1480d2b8..09659269 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,3 @@ -exclude: ^migrations/versions - repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 diff --git a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py index c3b79dab..5a9d5f39 100644 --- a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py +++ b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py @@ -10,41 +10,41 @@ from alembic import op import aurweb.config # revision identifiers, used by Alembic. -revision = '56e2ce8e2ffa' -down_revision = 'ef39fcd6e1cd' +revision = "56e2ce8e2ffa" +down_revision = "ef39fcd6e1cd" branch_labels = None depends_on = None # Tables affected by charset/collate change tables = [ - ('AccountTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('ApiRateLimit', 'utf8mb4', 'utf8mb4_general_ci'), - ('Bans', 'utf8mb4', 'utf8mb4_general_ci'), - ('DependencyTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('Groups', 'utf8mb4', 'utf8mb4_general_ci'), - ('Licenses', 'utf8mb4', 'utf8mb4_general_ci'), - ('OfficialProviders', 'utf8mb4', 'utf8mb4_bin'), - ('PackageBases', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageBlacklist', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageComments', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageDepends', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageKeywords', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageRelations', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageRequests', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageSources', 'utf8mb4', 'utf8mb4_general_ci'), - ('Packages', 'utf8mb4', 'utf8mb4_general_ci'), - ('RelationTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('RequestTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('SSHPubKeys', 'utf8mb4', 'utf8mb4_bin'), - ('Sessions', 'utf8mb4', 'utf8mb4_bin'), - ('TU_VoteInfo', 'utf8mb4', 'utf8mb4_general_ci'), - ('Terms', 'utf8mb4', 'utf8mb4_general_ci'), - ('Users', 'utf8mb4', 'utf8mb4_general_ci') + ("AccountTypes", "utf8mb4", "utf8mb4_general_ci"), + ("ApiRateLimit", "utf8mb4", "utf8mb4_general_ci"), + ("Bans", "utf8mb4", "utf8mb4_general_ci"), + ("DependencyTypes", "utf8mb4", "utf8mb4_general_ci"), + ("Groups", "utf8mb4", "utf8mb4_general_ci"), + ("Licenses", "utf8mb4", "utf8mb4_general_ci"), + ("OfficialProviders", "utf8mb4", "utf8mb4_bin"), + ("PackageBases", "utf8mb4", "utf8mb4_general_ci"), + ("PackageBlacklist", "utf8mb4", "utf8mb4_general_ci"), + ("PackageComments", "utf8mb4", "utf8mb4_general_ci"), + ("PackageDepends", "utf8mb4", "utf8mb4_general_ci"), + ("PackageKeywords", "utf8mb4", "utf8mb4_general_ci"), + ("PackageRelations", "utf8mb4", "utf8mb4_general_ci"), + ("PackageRequests", "utf8mb4", "utf8mb4_general_ci"), + ("PackageSources", "utf8mb4", "utf8mb4_general_ci"), + ("Packages", "utf8mb4", "utf8mb4_general_ci"), + ("RelationTypes", "utf8mb4", "utf8mb4_general_ci"), + ("RequestTypes", "utf8mb4", "utf8mb4_general_ci"), + ("SSHPubKeys", "utf8mb4", "utf8mb4_bin"), + ("Sessions", "utf8mb4", "utf8mb4_bin"), + ("TU_VoteInfo", "utf8mb4", "utf8mb4_general_ci"), + ("Terms", "utf8mb4", "utf8mb4_general_ci"), + ("Users", "utf8mb4", "utf8mb4_general_ci"), ] # Indexes affected by charset/collate change # Map of Unique Indexes key = index_name, value = [table_name, column1, column2] -indexes = {'ProviderNameProvides': ['OfficialProviders', 'Name', 'Provides']} +indexes = {"ProviderNameProvides": ["OfficialProviders", "Name", "Provides"]} # Source charset/collation, before this migration is run. src_charset = "utf8" diff --git a/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py b/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py index d910a14b..d273804f 100644 --- a/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py +++ b/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py @@ -19,8 +19,8 @@ from alembic import op from sqlalchemy.dialects.mysql import INTEGER, TINYINT # revision identifiers, used by Alembic. -revision = 'be7adae47ac3' -down_revision = '56e2ce8e2ffa' +revision = "be7adae47ac3" +down_revision = "56e2ce8e2ffa" branch_labels = None depends_on = None @@ -32,7 +32,7 @@ DOWNGRADE_T = TINYINT(3, unsigned=True) def upgrade(): - """ Upgrade 'Yes', 'No', 'Abstain' and 'ActiveTUs' to unsigned INTEGER. """ + """Upgrade 'Yes', 'No', 'Abstain' and 'ActiveTUs' to unsigned INTEGER.""" op.alter_column("TU_VoteInfo", "Yes", type_=UPGRADE_T) op.alter_column("TU_VoteInfo", "No", type_=UPGRADE_T) op.alter_column("TU_VoteInfo", "Abstain", type_=UPGRADE_T) diff --git a/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py b/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py index a89d97ef..a20b80fa 100644 --- a/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py +++ b/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py @@ -8,20 +8,19 @@ Create Date: 2022-02-18 12:47:05.322766 from datetime import datetime import sqlalchemy as sa - from alembic import op from aurweb import db from aurweb.models import PackageVote # revision identifiers, used by Alembic. -revision = 'd64e5571bc8d' -down_revision = 'be7adae47ac3' +revision = "d64e5571bc8d" +down_revision = "be7adae47ac3" branch_labels = None depends_on = None table = PackageVote.__tablename__ -column = 'VoteTS' +column = "VoteTS" epoch = datetime(1970, 1, 1) diff --git a/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py index 49bf055a..3cf369e7 100644 --- a/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py +++ b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py @@ -6,31 +6,32 @@ Create Date: 2020-06-08 10:04:13.898617 """ import sqlalchemy as sa - from alembic import op from sqlalchemy.engine.reflection import Inspector # revision identifiers, used by Alembic. -revision = 'ef39fcd6e1cd' -down_revision = 'f47cad5d6d03' +revision = "ef39fcd6e1cd" +down_revision = "f47cad5d6d03" branch_labels = None depends_on = None def table_has_column(table, column_name): for element in Inspector.from_engine(op.get_bind()).get_columns(table): - if element.get('name') == column_name: + if element.get("name") == column_name: return True return False def upgrade(): - if not table_has_column('Users', 'SSOAccountID'): - op.add_column('Users', sa.Column('SSOAccountID', sa.String(length=255), nullable=True)) - op.create_unique_constraint(None, 'Users', ['SSOAccountID']) + if not table_has_column("Users", "SSOAccountID"): + op.add_column( + "Users", sa.Column("SSOAccountID", sa.String(length=255), nullable=True) + ) + op.create_unique_constraint(None, "Users", ["SSOAccountID"]) def downgrade(): - if table_has_column('Users', 'SSOAccountID'): - op.drop_constraint('SSOAccountID', 'Users', type_='unique') - op.drop_column('Users', 'SSOAccountID') + if table_has_column("Users", "SSOAccountID"): + op.drop_constraint("SSOAccountID", "Users", type_="unique") + op.drop_column("Users", "SSOAccountID") diff --git a/migrations/versions/f47cad5d6d03_initial_revision.py b/migrations/versions/f47cad5d6d03_initial_revision.py index b214beea..7373e0fb 100644 --- a/migrations/versions/f47cad5d6d03_initial_revision.py +++ b/migrations/versions/f47cad5d6d03_initial_revision.py @@ -5,7 +5,7 @@ Create Date: 2020-02-23 13:23:32.331396 """ # revision identifiers, used by Alembic. -revision = 'f47cad5d6d03' +revision = "f47cad5d6d03" down_revision = None branch_labels = None depends_on = None From 4e0618469df308340cb3ddb2f1c74d04c470c57a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 11 Sep 2022 18:40:31 -0700 Subject: [PATCH 1147/1451] fix(test): JSONResponse() requires a content argument with fastapi 0.83.0 Signed-off-by: Kevin Morris --- test/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_util.py b/test/test_util.py index 686e35b4..2e8b2e4e 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -44,7 +44,7 @@ async def test_error_or_result(): assert data.get("error") == "No response returned." async def good_route(request: fastapi.Request): - return JSONResponse() + return JSONResponse("{}") response = await util.error_or_result(good_route, Request()) assert response.status_code == HTTPStatus.OK From bb6e602e13184b79f8d5644866ab76215b723853 Mon Sep 17 00:00:00 2001 From: renovate Date: Mon, 12 Sep 2022 01:24:39 +0000 Subject: [PATCH 1148/1451] fix(deps): update dependency fastapi to ^0.83.0 --- poetry.lock | 27 ++++++++++++++------------- pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index ef0fc1f7..ef2c70f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -264,7 +264,7 @@ lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "fastapi" -version = "0.71.0" +version = "0.83.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -272,13 +272,13 @@ python-versions = ">=3.6.1" [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.17.1" +starlette = "0.19.1" [package.extras] -all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,<5.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "typer-cli (>=0.0.12,<0.0.13)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==21.9b0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.1.7)", "types-orjson (==3.6.0)", "types-ujson (==0.1.1)", "ujson (>=4.0.1,<5.0.0)"] +all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] +dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "feedgen" @@ -924,14 +924,15 @@ parse = "*" [[package]] name = "starlette" -version = "0.17.1" +version = "0.19.1" description = "The little ASGI library that shines." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -anyio = ">=3.0.0,<4" +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] @@ -1055,7 +1056,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "e084bad4236ac74fb90fcf4537c78c228b2de606e83c63ac2557a677d681e743" +content-hash = "e1f9d796eea832af84c40c754ee3c58e633e98bd7cdb42a985b2c8657e82037e" [metadata.files] aiofiles = [ @@ -1283,8 +1284,8 @@ fakeredis = [ {file = "fakeredis-1.9.0.tar.gz", hash = "sha256:60639946e3bb1274c30416f539f01f9d73b4ea68c244c1442f5524e45f51e882"}, ] fastapi = [ - {file = "fastapi-0.71.0-py3-none-any.whl", hash = "sha256:a78eca6b084de9667f2d5f37e2ae297270e5a119cd01c2f04815795da92fc87f"}, - {file = "fastapi-0.71.0.tar.gz", hash = "sha256:2b5ac0ae89c80b40d1dd4b2ea0bb1f78d7c4affd3644d080bf050f084759fff2"}, + {file = "fastapi-0.83.0-py3-none-any.whl", hash = "sha256:694a2b6c2607a61029a4be1c6613f84d74019cb9f7a41c7a475dca8e715f9368"}, + {file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"}, ] feedgen = [ {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, @@ -1812,8 +1813,8 @@ srcinfo = [ {file = "srcinfo-0.0.8.tar.gz", hash = "sha256:5ac610cf8b15d4b0a0374bd1f7ad301675c2938f0414addf3ef7d7e3fcaf5c65"}, ] starlette = [ - {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, - {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, + {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, + {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, ] "tap.py" = [ {file = "tap.py-3.1-py3-none-any.whl", hash = "sha256:928c852f3361707b796c93730cc5402c6378660b161114461066acf53d65bf5d"}, diff --git a/pyproject.toml b/pyproject.toml index 8f9624dc..4649d74f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ pytest-xdist = "^2.4.0" filelock = "^3.3.2" posix-ipc = "^1.0.5" pyalpm = "^0.10.6" -fastapi = "^0.71.0" +fastapi = "^0.83.0" srcinfo = "^0.0.8" [tool.poetry.dev-dependencies] From df0a4a2be242a8bd5d318e71dcca0d90e0e1cc6a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 11 Sep 2022 19:04:42 -0700 Subject: [PATCH 1149/1451] feat(rpc): add /rpc/v5/{type} openapi-compatible routes We will be modeling future RPC implementations on an OpenAPI spec. While this commit does not completely cohere to OpenAPI in terms of response data, this is a good start and will allow us to cleanly document these openapi routes in the current and future. This commit brings in the new RPC routes: - GET /rpc/v5/info/{pkgname} - GET /rpc/v5/info?arg[]=pkg1&arg[]=pkg2 - POST /rpc/v5/info with JSON data `{"arg": ["pkg1", "pkg2"]}` - GET /rpc/v5/search?arg=keywords&by=valid-by-value - POST /rpc/v5/search with JSON data `{"by": "valid-by-value", "arg": "keywords"}` Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 104 ++++++++++++++++++++++++++++++++++++++++++ test/test_rpc.py | 95 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index a0cf5019..9777c0a2 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -160,3 +160,107 @@ async def rpc_post( callback: Optional[str] = Form(default=None), ): return await rpc_request(request, v, type, by, arg, args, callback) + + +@router.get("/rpc/v{version}/info/{name}") +async def rpc_openapi_info(request: Request, version: int, name: str): + return await rpc_request( + request, + version, + "info", + defaults.RPC_SEARCH_BY, + name, + [], + ) + + +@router.get("/rpc/v{version}/info") +async def rpc_openapi_multiinfo( + request: Request, + version: int, + args: Optional[list[str]] = Query(default=[], alias="arg[]"), +): + arg = args.pop(0) if args else None + return await rpc_request( + request, + version, + "info", + defaults.RPC_SEARCH_BY, + arg, + args, + ) + + +@router.post("/rpc/v{version}/info") +async def rpc_openapi_multiinfo_post( + request: Request, + version: int, +): + data = await request.json() + + args = data.get("arg", []) + if not isinstance(args, list): + rpc = RPC(version, "info") + return JSONResponse( + rpc.error("the 'arg' parameter must be of array type"), + status_code=HTTPStatus.BAD_REQUEST, + ) + + arg = args.pop(0) if args else None + return await rpc_request( + request, + version, + "info", + defaults.RPC_SEARCH_BY, + arg, + args, + ) + + +@router.get("/rpc/v{version}/search") +async def rpc_openapi_search( + request: Request, + version: int, + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), + arg: Optional[str] = Query(default=str()), +): + return await rpc_request( + request, + version, + "search", + by, + arg, + [], + ) + + +@router.post("/rpc/v{version}/search") +async def rpc_openapi_search_post( + request: Request, + version: int, +): + data = await request.json() + by = data.get("by", defaults.RPC_SEARCH_BY) + if not isinstance(by, str): + rpc = RPC(version, "search") + return JSONResponse( + rpc.error("the 'by' parameter must be of string type"), + status_code=HTTPStatus.BAD_REQUEST, + ) + + arg = data.get("arg", str()) + if not isinstance(arg, str): + rpc = RPC(version, "search") + return JSONResponse( + rpc.error("the 'arg' parameter must be of string type"), + status_code=HTTPStatus.BAD_REQUEST, + ) + + return await rpc_request( + request, + version, + "search", + by, + arg, + [], + ) diff --git a/test/test_rpc.py b/test/test_rpc.py index ed7e8894..0edd3e2e 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -933,3 +933,98 @@ def test_rpc_too_many_info_results(client: TestClient, packages: list[Package]): with client as request: resp = request.get("/rpc", params=params) assert resp.json().get("error") == "Too many package results." + + +def test_rpc_openapi_info(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = f"/rpc/v5/info/{pkgname}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_multiinfo(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/info" + resp = request.get(endp, params={"arg[]": [pkgname]}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_multiinfo_post(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/info" + resp = request.post(endp, json={"arg": [pkgname]}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_multiinfo_post_bad_request( + client: TestClient, packages: list[Package] +): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/info" + resp = request.post(endp, json={"arg": pkgname}) + assert resp.status_code == HTTPStatus.BAD_REQUEST + + data = resp.json() + expected = "the 'arg' parameter must be of array type" + assert data.get("error") == expected + + +def test_rpc_openapi_search(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/search" + resp = request.get(endp, params={"arg": pkgname}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_search_post(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/search" + resp = request.post(endp, json={"arg": pkgname}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_search_post_bad_request(client: TestClient): + # Test by parameter + with client as request: + endp = "/rpc/v5/search" + resp = request.post(endp, json={"by": 1}) + assert resp.status_code == HTTPStatus.BAD_REQUEST + data = resp.json() + expected = "the 'by' parameter must be of string type" + assert data.get("error") == expected + + # Test arg parameter + with client as request: + endp = "/rpc/v5/search" + resp = request.post(endp, json={"arg": ["a", "list"]}) + assert resp.status_code == HTTPStatus.BAD_REQUEST + data = resp.json() + expected = "the 'arg' parameter must be of string type" + assert data.get("error") == expected From 9faa7b801d54fb853bcb54c720ae0a3e297e0b10 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 11 Sep 2022 15:22:10 -0700 Subject: [PATCH 1150/1451] feat: add cdn.jsdelivr.net to script/style CSP Signed-off-by: Kevin Morris --- aurweb/asgi.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index ccca3fc5..d1703c10 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -253,10 +253,14 @@ async def add_security_headers(request: Request, call_next: typing.Callable): # Add CSP header. nonce = request.user.nonce csp = "default-src 'self'; " - script_hosts = [] + + # swagger-ui needs access to cdn.jsdelivr.net javascript + script_hosts = ["cdn.jsdelivr.net"] csp += f"script-src 'self' 'nonce-{nonce}' " + " ".join(script_hosts) - # It's fine if css is inlined. - csp += "; style-src 'self' 'unsafe-inline'" + + # swagger-ui needs access to cdn.jsdelivr.net css + css_hosts = ["cdn.jsdelivr.net"] + csp += "; style-src 'self' 'unsafe-inline' " + " ".join(css_hosts) response.headers["Content-Security-Policy"] = csp # Add XTCO header. From 5e75a00c17609dc72fb8600cb309f07a7dde41e5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 11 Sep 2022 19:59:16 -0700 Subject: [PATCH 1151/1451] upgrade: bump to version v6.1.3 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index df129c39..8b97cd0e 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.2" +AURWEB_VERSION = "v6.1.3" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 4649d74f..303b7637 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.2" +version = "v6.1.3" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 8e8b746a5b82511716b397c31e42f199a87e65d9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 12 Sep 2022 06:49:20 -0700 Subject: [PATCH 1152/1451] feat(rpc): add GET /rpc/v5/search/{arg} openapi route Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 19 ++++++++++++++++++- test/test_rpc.py | 12 ++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 9777c0a2..25574ff8 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -217,12 +217,29 @@ async def rpc_openapi_multiinfo_post( ) +@router.get("/rpc/v{version}/search/{arg}") +async def rpc_openapi_search_arg( + request: Request, + version: int, + arg: str, + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), +): + return await rpc_request( + request, + version, + "search", + by, + arg, + [], + ) + + @router.get("/rpc/v{version}/search") async def rpc_openapi_search( request: Request, version: int, - by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), arg: Optional[str] = Query(default=str()), + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), ): return await rpc_request( request, diff --git a/test/test_rpc.py b/test/test_rpc.py index 0edd3e2e..e5b37542 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -986,6 +986,18 @@ def test_rpc_openapi_multiinfo_post_bad_request( assert data.get("error") == expected +def test_rpc_openapi_search_arg(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = f"/rpc/v5/search/{pkgname}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + def test_rpc_openapi_search(client: TestClient, packages: list[Package]): pkgname = packages[0].Name From 17f2c05fd35cb105a5346671bd2e2ae178b83f02 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 12 Sep 2022 06:49:54 -0700 Subject: [PATCH 1153/1451] feat(rpc): add GET /rpc/v5/suggest/{arg} openapi route Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 12 ++++++++++++ test/test_rpc.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 25574ff8..23978f1d 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -281,3 +281,15 @@ async def rpc_openapi_search_post( arg, [], ) + + +@router.get("/rpc/v{version}/suggest/{arg}") +async def rpc_openapi_suggest(request: Request, version: int, arg: str): + return await rpc_request( + request, + version, + "suggest", + defaults.RPC_SEARCH_BY, + arg, + [], + ) diff --git a/test/test_rpc.py b/test/test_rpc.py index e5b37542..84ddd8d7 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1040,3 +1040,19 @@ def test_rpc_openapi_search_post_bad_request(client: TestClient): data = resp.json() expected = "the 'arg' parameter must be of string type" assert data.get("error") == expected + + +def test_rpc_openapi_suggest(client: TestClient, packages: list[Package]): + suggestions = { + "big": ["big-chungus"], + "chungy": ["chungy-chungus"], + } + + for term, expected in suggestions.items(): + with client as request: + endp = f"/rpc/v5/suggest/{term}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data == expected From 624954042b173c285e9ef5a87adc6319c3293685 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 12 Sep 2022 06:59:52 -0700 Subject: [PATCH 1154/1451] doc(rpc): include route doc at the top of aurweb.routers.rpc Signed-off-by: Kevin Morris --- aurweb/routers/rpc.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 23978f1d..f15b9781 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -1,3 +1,28 @@ +""" +RPC API routing module + +For legacy route documentation, see https://aur.archlinux.org/rpc + +Legacy Routes: +- GET /rpc +- POST /rpc + +Legacy example (version 5): /rpc?v=5&type=info&arg=my-package + +For OpenAPI route documentation, see https://aur.archlinux.org/docs + +OpenAPI Routes: +- GET /rpc/v{version}/info/{arg} +- GET /rpc/v{version}/info +- POST /rpc/v{version}/info +- GET /rpc/v{version}/search/{arg} +- GET /rpc/v{version}/search +- POST /rpc/v{version}/search +- GET /rpc/v{version}/suggest/{arg} + +OpenAPI example (version 5): /rpc/v5/info/my-package + +""" import hashlib import re from http import HTTPStatus From 37c7dee099841cfe368c64c93ae7432cc4364858 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 12 Sep 2022 10:36:50 -0700 Subject: [PATCH 1155/1451] fix: produce DeleteNotification a line before handle_request With this on a single line, the argument ordering and class/func execution was a bit too RNG causing exceptions to be thrown when producing a notification based off of a deleted pkgbase object. Signed-off-by: Kevin Morris --- aurweb/pkgbase/actions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 4834f8dd..9e7b0df5 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -99,9 +99,8 @@ def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: def pkgbase_delete_instance( request: Request, pkgbase: PackageBase, comments: str = str() ) -> list[notify.Notification]: - notifs = handle_request(request, DELETION_ID, pkgbase) + [ - notify.DeleteNotification(request.user.ID, pkgbase.ID) - ] + notif = notify.DeleteNotification(request.user.ID, pkgbase.ID) + notifs = handle_request(request, DELETION_ID, pkgbase) + [notif] with db.begin(): update_closure_comment(pkgbase, DELETION_ID, comments) From adc3a218636e836988105f31872b139d88c5bcc1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 12 Sep 2022 12:28:42 -0700 Subject: [PATCH 1156/1451] fix: add 'unsafe-inline' to script-src CSP swagger-ui uses inline javascript to bootstrap itself, so we need to allow unsafe inline because we can't give swagger-ui a nonce to embed. Signed-off-by: Kevin Morris --- aurweb/asgi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index d1703c10..72b47b4c 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -256,7 +256,9 @@ async def add_security_headers(request: Request, call_next: typing.Callable): # swagger-ui needs access to cdn.jsdelivr.net javascript script_hosts = ["cdn.jsdelivr.net"] - csp += f"script-src 'self' 'nonce-{nonce}' " + " ".join(script_hosts) + csp += f"script-src 'self' 'unsafe-inline' 'nonce-{nonce}' " + " ".join( + script_hosts + ) # swagger-ui needs access to cdn.jsdelivr.net css css_hosts = ["cdn.jsdelivr.net"] From f450b5dfc7e684392b85c253f44521bf097f095b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 12 Sep 2022 12:29:57 -0700 Subject: [PATCH 1157/1451] upgrade: bump to version v6.1.4 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 8b97cd0e..c1f87984 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.3" +AURWEB_VERSION = "v6.1.4" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 303b7637..f732f2e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.3" +version = "v6.1.4" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From ec3152014b05c2c6730e8363f32be01609c711b0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 13 Sep 2022 12:47:52 -0700 Subject: [PATCH 1158/1451] fix: retry transactions who fail due to deadlocks In my opinion, this kind of handling of transactions is pretty ugly. The being said, we have issues with running into deadlocks on aur.al, so this commit works against that immediate bug. An ideal solution would be to deal with retrying transactions through the `db.begin()` scope, so we wouldn't have to explicitly annotate functions as "retry functions," which is what this commit does. Closes #376 Signed-off-by: Kevin Morris --- aurweb/auth/__init__.py | 4 +- aurweb/db.py | 40 +++++++++++++++ aurweb/models/user.py | 2 +- aurweb/packages/requests.py | 23 +++++---- aurweb/packages/util.py | 20 ++++---- aurweb/pkgbase/actions.py | 91 ++++++++++++++++++++++++---------- aurweb/pkgbase/util.py | 2 + aurweb/ratelimit.py | 23 ++++++--- aurweb/routers/accounts.py | 26 +++++----- aurweb/routers/auth.py | 29 ++++++++--- aurweb/routers/html.py | 1 + aurweb/routers/pkgbase.py | 19 +++++++ aurweb/routers/requests.py | 1 + aurweb/routers/trusted_user.py | 16 +++--- aurweb/users/update.py | 6 +++ test/test_db.py | 20 ++++++++ 16 files changed, 241 insertions(+), 82 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index 0c8bba69..b8056f91 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -96,6 +96,7 @@ class AnonymousUser: class BasicAuthBackend(AuthenticationBackend): + @db.async_retry_deadlock async def authenticate(self, conn: HTTPConnection): unauthenticated = (None, AnonymousUser()) sid = conn.cookies.get("AURSID") @@ -122,8 +123,7 @@ class BasicAuthBackend(AuthenticationBackend): # At this point, we cannot have an invalid user if the record # exists, due to ForeignKey constraints in the schema upheld # by mysqlclient. - with db.begin(): - user = db.query(User).filter(User.ID == record.UsersID).first() + user = db.query(User).filter(User.ID == record.UsersID).first() user.nonce = util.make_nonce() user.authenticated = True diff --git a/aurweb/db.py b/aurweb/db.py index 7425d928..ab0f80b8 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -161,6 +161,46 @@ def begin(): return get_session().begin() +def retry_deadlock(func): + from sqlalchemy.exc import OperationalError + + def wrapper(*args, _i: int = 0, **kwargs): + # Retry 10 times, then raise the exception + # If we fail before the 10th, recurse into `wrapper` + # If we fail on the 10th, continue to throw the exception + limit = 10 + try: + return func(*args, **kwargs) + except OperationalError as exc: + if _i < limit and "Deadlock found" in str(exc): + # Retry on deadlock by recursing into `wrapper` + return wrapper(*args, _i=_i + 1, **kwargs) + # Otherwise, just raise the exception + raise exc + + return wrapper + + +def async_retry_deadlock(func): + from sqlalchemy.exc import OperationalError + + async def wrapper(*args, _i: int = 0, **kwargs): + # Retry 10 times, then raise the exception + # If we fail before the 10th, recurse into `wrapper` + # If we fail on the 10th, continue to throw the exception + limit = 10 + try: + return await func(*args, **kwargs) + except OperationalError as exc: + if _i < limit and "Deadlock found" in str(exc): + # Retry on deadlock by recursing into `wrapper` + return await wrapper(*args, _i=_i + 1, **kwargs) + # Otherwise, just raise the exception + raise exc + + return wrapper + + def get_sqlalchemy_url(): """ Build an SQLAlchemy URL for use with create_engine. diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 0404c77a..0d638677 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -151,7 +151,7 @@ class User(Base): return has_credential(self, credential, approved) - def logout(self, request: Request): + def logout(self, request: Request) -> None: self.authenticated = False if self.session: with db.begin(): diff --git a/aurweb/packages/requests.py b/aurweb/packages/requests.py index 7309a880..c09082f5 100644 --- a/aurweb/packages/requests.py +++ b/aurweb/packages/requests.py @@ -151,6 +151,7 @@ def close_pkgreq( pkgreq.ClosedTS = now +@db.retry_deadlock def handle_request( request: Request, reqtype_id: int, pkgbase: PackageBase, target: PackageBase = None ) -> list[notify.Notification]: @@ -239,15 +240,19 @@ def handle_request( to_accept.append(pkgreq) # Update requests with their new status and closures. - with db.begin(): - util.apply_all( - to_accept, - lambda p: close_pkgreq(p, request.user, pkgbase, target, ACCEPTED_ID), - ) - util.apply_all( - to_reject, - lambda p: close_pkgreq(p, request.user, pkgbase, target, REJECTED_ID), - ) + @db.retry_deadlock + def retry_closures(): + with db.begin(): + util.apply_all( + to_accept, + lambda p: close_pkgreq(p, request.user, pkgbase, target, ACCEPTED_ID), + ) + util.apply_all( + to_reject, + lambda p: close_pkgreq(p, request.user, pkgbase, target, REJECTED_ID), + ) + + retry_closures() # Create RequestCloseNotifications for all requests involved. for pkgreq in to_accept + to_reject: diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 1ae7f9fe..b6ba7e20 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -99,8 +99,7 @@ def get_pkg_or_base( :raises HTTPException: With status code 404 if record doesn't exist :return: {Package,PackageBase} instance """ - with db.begin(): - instance = db.query(cls).filter(cls.Name == name).first() + instance = db.query(cls).filter(cls.Name == name).first() if not instance: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) return instance @@ -133,16 +132,15 @@ def updated_packages(limit: int = 0, cache_ttl: int = 600) -> list[models.Packag # If we already have a cache, deserialize it and return. return orjson.loads(packages) - with db.begin(): - query = ( - db.query(models.Package) - .join(models.PackageBase) - .filter(models.PackageBase.PackagerUID.isnot(None)) - .order_by(models.PackageBase.ModifiedTS.desc()) - ) + query = ( + db.query(models.Package) + .join(models.PackageBase) + .filter(models.PackageBase.PackagerUID.isnot(None)) + .order_by(models.PackageBase.ModifiedTS.desc()) + ) - if limit: - query = query.limit(limit) + if limit: + query = query.limit(limit) packages = [] for pkg in query: diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 9e7b0df5..a453cb36 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -2,7 +2,7 @@ from fastapi import Request from aurweb import db, logging, util from aurweb.auth import creds -from aurweb.models import PackageBase +from aurweb.models import PackageBase, User from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_notification import PackageNotification from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID @@ -13,6 +13,12 @@ from aurweb.scripts import notify, popupdate logger = logging.get_logger(__name__) +@db.retry_deadlock +def _retry_notify(user: User, pkgbase: PackageBase) -> None: + with db.begin(): + db.create(PackageNotification, PackageBase=pkgbase, User=user) + + def pkgbase_notify_instance(request: Request, pkgbase: PackageBase) -> None: notif = db.query( pkgbase.notifications.filter( @@ -21,8 +27,13 @@ def pkgbase_notify_instance(request: Request, pkgbase: PackageBase) -> None: ).scalar() has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and not notif: - with db.begin(): - db.create(PackageNotification, PackageBase=pkgbase, User=request.user) + _retry_notify(request.user, pkgbase) + + +@db.retry_deadlock +def _retry_unnotify(notif: PackageNotification, pkgbase: PackageBase) -> None: + with db.begin(): + db.delete(notif) def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: @@ -31,8 +42,15 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: ).first() has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and notif: - with db.begin(): - db.delete(notif) + _retry_unnotify(notif, pkgbase) + + +@db.retry_deadlock +def _retry_unflag(pkgbase: PackageBase) -> None: + with db.begin(): + pkgbase.OutOfDateTS = None + pkgbase.Flagger = None + pkgbase.FlaggerComment = str() def pkgbase_unflag_instance(request: Request, pkgbase: PackageBase) -> None: @@ -42,20 +60,17 @@ def pkgbase_unflag_instance(request: Request, pkgbase: PackageBase) -> None: + [c.User for c in pkgbase.comaintainers], ) if has_cred: - with db.begin(): - pkgbase.OutOfDateTS = None - pkgbase.Flagger = None - pkgbase.FlaggerComment = str() + _retry_unflag(pkgbase) -def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: - disowner = request.user - notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)] +@db.retry_deadlock +def _retry_disown(request: Request, pkgbase: PackageBase): + notifs: list[notify.Notification] = [] - is_maint = disowner == pkgbase.Maintainer + is_maint = request.user == pkgbase.Maintainer comaint = pkgbase.comaintainers.filter( - PackageComaintainer.User == disowner + PackageComaintainer.User == request.user ).one_or_none() is_comaint = comaint is not None @@ -85,38 +100,48 @@ def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: pkgbase.Maintainer = None db.delete_all(pkgbase.comaintainers) + return notifs + + +def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: + disowner = request.user + notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)] + notifs += _retry_disown(request, pkgbase) util.apply_all(notifs, lambda n: n.send()) -def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: +@db.retry_deadlock +def _retry_adopt(request: Request, pkgbase: PackageBase) -> None: with db.begin(): pkgbase.Maintainer = request.user + +def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: + _retry_adopt(request, pkgbase) notif = notify.AdoptNotification(request.user.ID, pkgbase.ID) notif.send() +@db.retry_deadlock +def _retry_delete(pkgbase: PackageBase, comments: str) -> None: + with db.begin(): + update_closure_comment(pkgbase, DELETION_ID, comments) + db.delete(pkgbase) + + def pkgbase_delete_instance( request: Request, pkgbase: PackageBase, comments: str = str() ) -> list[notify.Notification]: notif = notify.DeleteNotification(request.user.ID, pkgbase.ID) notifs = handle_request(request, DELETION_ID, pkgbase) + [notif] - with db.begin(): - update_closure_comment(pkgbase, DELETION_ID, comments) - db.delete(pkgbase) + _retry_delete(pkgbase, comments) return notifs -def pkgbase_merge_instance( - request: Request, pkgbase: PackageBase, target: PackageBase, comments: str = str() -) -> None: - pkgbasename = str(pkgbase.Name) - - # Create notifications. - notifs = handle_request(request, MERGE_ID, pkgbase, target) - +@db.retry_deadlock +def _retry_merge(pkgbase: PackageBase, target: PackageBase) -> None: # Target votes and notifications sets of user IDs that are # looking to be migrated. target_votes = set(v.UsersID for v in target.package_votes) @@ -146,6 +171,20 @@ def pkgbase_merge_instance( db.delete(pkg) db.delete(pkgbase) + +def pkgbase_merge_instance( + request: Request, + pkgbase: PackageBase, + target: PackageBase, + comments: str = str(), +) -> None: + pkgbasename = str(pkgbase.Name) + + # Create notifications. + notifs = handle_request(request, MERGE_ID, pkgbase, target) + + _retry_merge(pkgbase, target) + # Log this out for accountability purposes. logger.info( f"Trusted User '{request.user.Username}' merged " diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 223c3013..968135d1 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -106,6 +106,7 @@ def remove_comaintainer( return notif +@db.retry_deadlock def remove_comaintainers(pkgbase: PackageBase, usernames: list[str]) -> None: """ Remove comaintainers from `pkgbase`. @@ -155,6 +156,7 @@ class NoopComaintainerNotification: return +@db.retry_deadlock def add_comaintainer( pkgbase: PackageBase, comaintainer: User ) -> notify.ComaintainerAddNotification: diff --git a/aurweb/ratelimit.py b/aurweb/ratelimit.py index cb08cdf5..97923a52 100644 --- a/aurweb/ratelimit.py +++ b/aurweb/ratelimit.py @@ -38,17 +38,26 @@ def _update_ratelimit_db(request: Request): now = time.utcnow() time_to_delete = now - window_length + @db.retry_deadlock + def retry_delete(records: list[ApiRateLimit]) -> None: + with db.begin(): + db.delete_all(records) + records = db.query(ApiRateLimit).filter(ApiRateLimit.WindowStart < time_to_delete) - with db.begin(): - db.delete_all(records) + retry_delete(records) + + @db.retry_deadlock + def retry_create(record: ApiRateLimit, now: int, host: str) -> ApiRateLimit: + with db.begin(): + if not record: + record = db.create(ApiRateLimit, WindowStart=now, IP=host, Requests=1) + else: + record.Requests += 1 + return record host = request.client.host record = db.query(ApiRateLimit, ApiRateLimit.IP == host).first() - with db.begin(): - if not record: - record = db.create(ApiRateLimit, WindowStart=now, IP=host, Requests=1) - else: - record.Requests += 1 + record = retry_create(record, now, host) logger.debug(record.Requests) return record diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index db05955a..3937757a 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -32,6 +32,7 @@ async def passreset(request: Request): return render_template(request, "passreset.html", context) +@db.async_retry_deadlock @router.post("/passreset", response_class=HTMLResponse) @handle_form_exceptions @requires_guest @@ -260,6 +261,7 @@ async def account_register( return render_template(request, "register.html", context) +@db.async_retry_deadlock @router.post("/register", response_class=HTMLResponse) @handle_form_exceptions @requires_guest @@ -336,18 +338,15 @@ async def account_register_post( AccountType=atype, ) - # If a PK was given and either one does not exist or the given - # PK mismatches the existing user's SSHPubKey.PubKey. - if PK: - # Get the second element in the PK, which is the actual key. - keys = util.parse_ssh_keys(PK.strip()) - for k in keys: - pk = " ".join(k) - fprint = get_fingerprint(pk) - with db.begin(): - db.create( - models.SSHPubKey, UserID=user.ID, PubKey=pk, Fingerprint=fprint - ) + # If a PK was given and either one does not exist or the given + # PK mismatches the existing user's SSHPubKey.PubKey. + if PK: + # Get the second element in the PK, which is the actual key. + keys = util.parse_ssh_keys(PK.strip()) + for k in keys: + pk = " ".join(k) + fprint = get_fingerprint(pk) + db.create(models.SSHPubKey, User=user, PubKey=pk, Fingerprint=fprint) # Send a reset key notification to the new user. WelcomeNotification(user.ID).send() @@ -458,6 +457,8 @@ async def account_edit_post( update.password, ] + # These update functions are all guarded by retry_deadlock; + # there's no need to guard this route itself. for f in updates: f(**args, request=request, user=user, context=context) @@ -633,6 +634,7 @@ async def terms_of_service(request: Request): return render_terms_of_service(request, context, accept_needed) +@db.async_retry_deadlock @router.post("/tos") @handle_form_exceptions @requires_auth diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 3f94952e..0e675559 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -28,6 +28,11 @@ async def login_get(request: Request, next: str = "/"): return await login_template(request, next) +@db.retry_deadlock +def _retry_login(request: Request, user: User, passwd: str, cookie_timeout: int) -> str: + return user.login(request, passwd, cookie_timeout) + + @router.post("/login", response_class=HTMLResponse) @handle_form_exceptions @requires_guest @@ -48,13 +53,16 @@ async def login_post( status_code=HTTPStatus.BAD_REQUEST, detail=_("Bad Referer header.") ) - with db.begin(): - user = ( - db.query(User) - .filter(or_(User.Username == user, User.Email == user)) - .first() + user = ( + db.query(User) + .filter( + or_( + User.Username == user, + User.Email == user, + ) ) - + .first() + ) if not user: return await login_template(request, next, errors=["Bad username or password."]) @@ -62,7 +70,7 @@ async def login_post( return await login_template(request, next, errors=["Account Suspended"]) cookie_timeout = cookies.timeout(remember_me) - sid = user.login(request, passwd, cookie_timeout) + sid = _retry_login(request, user, passwd, cookie_timeout) if not sid: return await login_template(request, next, errors=["Bad username or password."]) @@ -101,12 +109,17 @@ async def login_post( return response +@db.retry_deadlock +def _retry_logout(request: Request) -> None: + request.user.logout(request) + + @router.post("/logout") @handle_form_exceptions @requires_auth async def logout(request: Request, next: str = Form(default="/")): if request.user.is_authenticated(): - request.user.logout(request) + _retry_logout(request) # Use 303 since we may be handling a post request, that'll get it # to redirect to a get request. diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 2148d535..da1ffd55 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -35,6 +35,7 @@ async def favicon(request: Request): return RedirectResponse("/static/images/favicon.ico") +@db.async_retry_deadlock @router.post("/language", response_class=RedirectResponse) @handle_form_exceptions async def language( diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 076aec1e..3b1ab688 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -87,6 +87,7 @@ async def pkgbase_flag_comment(request: Request, name: str): return render_template(request, "pkgbase/flag-comment.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/keywords") @handle_form_exceptions async def pkgbase_keywords( @@ -139,6 +140,7 @@ async def pkgbase_flag_get(request: Request, name: str): return render_template(request, "pkgbase/flag.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/flag") @handle_form_exceptions @requires_auth @@ -170,6 +172,7 @@ async def pkgbase_flag_post( return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments") @handle_form_exceptions @requires_auth @@ -279,6 +282,7 @@ async def pkgbase_comment_edit( return render_template(request, "pkgbase/comments/edit.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}") @handle_form_exceptions @requires_auth @@ -324,6 +328,7 @@ async def pkgbase_comment_post( ) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/pin") @handle_form_exceptions @requires_auth @@ -362,6 +367,7 @@ async def pkgbase_comment_pin( return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/unpin") @handle_form_exceptions @requires_auth @@ -399,6 +405,7 @@ async def pkgbase_comment_unpin( return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/delete") @handle_form_exceptions @requires_auth @@ -440,6 +447,7 @@ async def pkgbase_comment_delete( return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/undelete") @handle_form_exceptions @requires_auth @@ -482,6 +490,7 @@ async def pkgbase_comment_undelete( return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/vote") @handle_form_exceptions @requires_auth @@ -501,6 +510,7 @@ async def pkgbase_vote(request: Request, name: str): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/unvote") @handle_form_exceptions @requires_auth @@ -519,6 +529,7 @@ async def pkgbase_unvote(request: Request, name: str): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/notify") @handle_form_exceptions @requires_auth @@ -528,6 +539,7 @@ async def pkgbase_notify(request: Request, name: str): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/unnotify") @handle_form_exceptions @requires_auth @@ -537,6 +549,7 @@ async def pkgbase_unnotify(request: Request, name: str): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/unflag") @handle_form_exceptions @requires_auth @@ -567,6 +580,7 @@ async def pkgbase_disown_get( return render_template(request, "pkgbase/disown.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/disown") @handle_form_exceptions @requires_auth @@ -617,6 +631,7 @@ async def pkgbase_disown_post( return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/adopt") @handle_form_exceptions @requires_auth @@ -659,6 +674,7 @@ async def pkgbase_comaintainers(request: Request, name: str) -> Response: return render_template(request, "pkgbase/comaintainers.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comaintainers") @handle_form_exceptions @requires_auth @@ -715,6 +731,7 @@ async def pkgbase_request( return render_template(request, "pkgbase/request.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/request") @handle_form_exceptions @requires_auth @@ -817,6 +834,7 @@ async def pkgbase_delete_get( return render_template(request, "pkgbase/delete.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/delete") @handle_form_exceptions @requires_auth @@ -889,6 +907,7 @@ async def pkgbase_merge_get( ) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/merge") @handle_form_exceptions @requires_auth diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index 51be6d2c..bf86bdcc 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -69,6 +69,7 @@ async def request_close(request: Request, id: int): return render_template(request, "requests/close.html", context) +@db.async_retry_deadlock @router.post("/requests/{id}/close") @handle_form_exceptions @requires_auth diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index a84bb6bd..37edb072 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -217,6 +217,7 @@ async def trusted_user_proposal(request: Request, proposal: int): return render_proposal(request, context, proposal, voteinfo, voters, vote) +@db.async_retry_deadlock @router.post("/tu/{proposal}") @handle_form_exceptions @requires_auth @@ -267,13 +268,15 @@ async def trusted_user_proposal_post( request, context, proposal, voteinfo, voters, vote, status_code=status_code ) - if decision in {"Yes", "No", "Abstain"}: - # Increment whichever decision was given to us. - setattr(voteinfo, decision, getattr(voteinfo, decision) + 1) - else: - return Response("Invalid 'decision' value.", status_code=HTTPStatus.BAD_REQUEST) - with db.begin(): + if decision in {"Yes", "No", "Abstain"}: + # Increment whichever decision was given to us. + setattr(voteinfo, decision, getattr(voteinfo, decision) + 1) + else: + return Response( + "Invalid 'decision' value.", status_code=HTTPStatus.BAD_REQUEST + ) + vote = db.create(models.TUVote, User=request.user, VoteInfo=voteinfo) context["error"] = "You've already voted for this proposal." @@ -301,6 +304,7 @@ async def trusted_user_addvote( return render_template(request, "addvote.html", context) +@db.async_retry_deadlock @router.post("/addvote") @handle_form_exceptions @requires_auth diff --git a/aurweb/users/update.py b/aurweb/users/update.py index 51f2d2e0..6bd4a295 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -8,6 +8,7 @@ from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.util import strtobool +@db.retry_deadlock def simple( U: str = str(), E: str = str(), @@ -42,6 +43,7 @@ def simple( user.OwnershipNotify = strtobool(ON) +@db.retry_deadlock def language( L: str = str(), request: Request = None, @@ -55,6 +57,7 @@ def language( context["language"] = L +@db.retry_deadlock def timezone( TZ: str = str(), request: Request = None, @@ -68,6 +71,7 @@ def timezone( context["language"] = TZ +@db.retry_deadlock def ssh_pubkey(PK: str = str(), user: models.User = None, **kwargs) -> None: if not PK: # If no pubkey is provided, wipe out any pubkeys the user @@ -101,12 +105,14 @@ def ssh_pubkey(PK: str = str(), user: models.User = None, **kwargs) -> None: ) +@db.retry_deadlock def account_type(T: int = None, user: models.User = None, **kwargs) -> None: if T is not None and (T := int(T)) != user.AccountTypeID: with db.begin(): user.AccountTypeID = T +@db.retry_deadlock def password( P: str = str(), request: Request = None, diff --git a/test/test_db.py b/test/test_db.py index 8ac5607d..22dbdd36 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -5,6 +5,7 @@ import tempfile from unittest import mock import pytest +from sqlalchemy.exc import OperationalError import aurweb.config import aurweb.initdb @@ -226,3 +227,22 @@ def test_name_without_pytest_current_test(): with mock.patch.dict("os.environ", {}, clear=True): dbname = aurweb.db.name() assert dbname == aurweb.config.get("database", "name") + + +def test_retry_deadlock(): + @db.retry_deadlock + def func(): + raise OperationalError("Deadlock found", tuple(), "") + + with pytest.raises(OperationalError): + func() + + +@pytest.mark.asyncio +async def test_async_retry_deadlock(): + @db.async_retry_deadlock + async def func(): + raise OperationalError("Deadlock found", tuple(), "") + + with pytest.raises(OperationalError): + await func() From 30e72d2db5f9b3b863ddc03efda65c37c0a4aa2c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sat, 24 Sep 2022 16:51:25 +0000 Subject: [PATCH 1159/1451] feat: archive git repository (experimental) See doc/git-archive.md for general Git archive specifications See doc/repos/metadata-repo.md for info and direction related to the new Git metadata archive --- aurweb/archives/__init__.py | 1 + aurweb/archives/spec/__init__.py | 1 + aurweb/archives/spec/base.py | 77 ++++++ aurweb/archives/spec/metadata.py | 85 ++++++ aurweb/archives/spec/pkgbases.py | 32 +++ aurweb/archives/spec/pkgnames.py | 33 +++ aurweb/archives/spec/users.py | 26 ++ aurweb/models/package_base.py | 10 + aurweb/pkgbase/util.py | 5 +- aurweb/rpc.py | 76 +++--- aurweb/schema.py | 6 + aurweb/scripts/git_archive.py | 125 +++++++++ aurweb/scripts/mkpkglists.py | 1 + aurweb/scripts/popupdate.py | 14 +- aurweb/testing/git.py | 8 +- aurweb/util.py | 8 + conf/config.defaults | 12 + conf/config.dev | 6 + doc/git-archive.md | 75 ++++++ doc/maintenance.txt | 36 ++- doc/repos/metadata-repo.md | 121 +++++++++ doc/repos/pkgbases-repo.md | 15 ++ doc/repos/pkgnames-repo.md | 15 ++ doc/repos/users-repo.md | 15 ++ doc/specs/metadata.md | 14 + doc/specs/pkgbases.md | 14 + doc/specs/pkgnames.md | 14 + doc/specs/popularity.md | 14 + doc/specs/users.md | 14 + ...70_add_popularityupdated_to_packagebase.py | 33 +++ pyproject.toml | 1 + templates/partials/packages/details.html | 2 +- test/test_git_archives.py | 241 ++++++++++++++++++ test/test_templates.py | 4 + 34 files changed, 1104 insertions(+), 50 deletions(-) create mode 100644 aurweb/archives/__init__.py create mode 100644 aurweb/archives/spec/__init__.py create mode 100644 aurweb/archives/spec/base.py create mode 100644 aurweb/archives/spec/metadata.py create mode 100644 aurweb/archives/spec/pkgbases.py create mode 100644 aurweb/archives/spec/pkgnames.py create mode 100644 aurweb/archives/spec/users.py create mode 100644 aurweb/scripts/git_archive.py create mode 100644 doc/git-archive.md create mode 100644 doc/repos/metadata-repo.md create mode 100644 doc/repos/pkgbases-repo.md create mode 100644 doc/repos/pkgnames-repo.md create mode 100644 doc/repos/users-repo.md create mode 100644 doc/specs/metadata.md create mode 100644 doc/specs/pkgbases.md create mode 100644 doc/specs/pkgnames.md create mode 100644 doc/specs/popularity.md create mode 100644 doc/specs/users.md create mode 100644 migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py create mode 100644 test/test_git_archives.py diff --git a/aurweb/archives/__init__.py b/aurweb/archives/__init__.py new file mode 100644 index 00000000..47020641 --- /dev/null +++ b/aurweb/archives/__init__.py @@ -0,0 +1 @@ +# aurweb.archives diff --git a/aurweb/archives/spec/__init__.py b/aurweb/archives/spec/__init__.py new file mode 100644 index 00000000..b6e376b4 --- /dev/null +++ b/aurweb/archives/spec/__init__.py @@ -0,0 +1 @@ +# aurweb.archives.spec diff --git a/aurweb/archives/spec/base.py b/aurweb/archives/spec/base.py new file mode 100644 index 00000000..60f734f2 --- /dev/null +++ b/aurweb/archives/spec/base.py @@ -0,0 +1,77 @@ +from pathlib import Path +from typing import Any, Dict, Iterable, List, Set + + +class GitInfo: + """Information about a Git repository.""" + + """ Path to Git repository. """ + path: str + + """ Local Git repository configuration. """ + config: Dict[str, Any] + + def __init__(self, path: str, config: Dict[str, Any] = dict()) -> "GitInfo": + self.path = Path(path) + self.config = config + + +class SpecOutput: + """Class used for git_archive.py output details.""" + + """ Filename relative to the Git repository root. """ + filename: Path + + """ Git repository information. """ + git_info: GitInfo + + """ Bytes bound for `SpecOutput.filename`. """ + data: bytes + + def __init__(self, filename: str, git_info: GitInfo, data: bytes) -> "SpecOutput": + self.filename = filename + self.git_info = git_info + self.data = data + + +class SpecBase: + """ + Base for Spec classes defined in git_archve.py --spec modules. + + All supported --spec modules must contain the following classes: + - Spec(SpecBase) + """ + + """ A list of SpecOutputs, each of which contain output file data. """ + outputs: List[SpecOutput] = list() + + """ A set of repositories to commit changes to. """ + repos: Set[str] = set() + + def generate(self) -> Iterable[SpecOutput]: + """ + "Pure virtual" output generator. + + `SpecBase.outputs` and `SpecBase.repos` should be populated within an + overridden version of this function in SpecBase derivatives. + """ + raise NotImplementedError() + + def add_output(self, filename: str, git_info: GitInfo, data: bytes) -> None: + """ + Add a SpecOutput instance to the set of outputs. + + :param filename: Filename relative to the git repository root + :param git_info: GitInfo instance + :param data: Binary data bound for `filename` + """ + if git_info.path not in self.repos: + self.repos.add(git_info.path) + + self.outputs.append( + SpecOutput( + filename, + git_info, + data, + ) + ) diff --git a/aurweb/archives/spec/metadata.py b/aurweb/archives/spec/metadata.py new file mode 100644 index 00000000..e7c8e096 --- /dev/null +++ b/aurweb/archives/spec/metadata.py @@ -0,0 +1,85 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import Package, PackageBase, User +from aurweb.rpc import RPC + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.metadata_repo = GitInfo( + config.get("git-archive", "metadata-repo"), + ) + + def generate(self) -> Iterable[SpecOutput]: + # Base query used by the RPC. + base_query = ( + db.query(Package) + .join(PackageBase) + .join(User, PackageBase.MaintainerUID == User.ID) + ) + + # Create an instance of RPC, use it to get entities from + # our query and perform a metadata subquery for all packages. + rpc = RPC(version=5, type="info") + print("performing package database query") + packages = rpc.entities(base_query).all() + print("performing package database subqueries") + rpc.subquery({pkg.ID for pkg in packages}) + + pkgbases, pkgnames = dict(), dict() + for package in packages: + # Produce RPC type=info data for `package` + data = rpc.get_info_json_data(package) + + pkgbase_name = data.get("PackageBase") + pkgbase_data = { + "ID": data.pop("PackageBaseID"), + "URLPath": data.pop("URLPath"), + "FirstSubmitted": data.pop("FirstSubmitted"), + "LastModified": data.pop("LastModified"), + "OutOfDate": data.pop("OutOfDate"), + "Maintainer": data.pop("Maintainer"), + "Keywords": data.pop("Keywords"), + "NumVotes": data.pop("NumVotes"), + "Popularity": data.pop("Popularity"), + "PopularityUpdated": package.PopularityUpdated.timestamp(), + } + + # Store the data in `pkgbases` dict. We do this so we only + # end up processing a single `pkgbase` if repeated after + # this loop + pkgbases[pkgbase_name] = pkgbase_data + + # Remove Popularity and NumVotes from package data. + # These fields change quite often which causes git data + # modification to explode. + # data.pop("NumVotes") + # data.pop("Popularity") + + # Remove the ID key from package json. + data.pop("ID") + + # Add the `package`.Name to the pkgnames set + name = data.get("Name") + pkgnames[name] = data + + # Add metadata outputs + self.add_output( + "pkgname.json", + self.metadata_repo, + orjson.dumps(pkgnames, option=ORJSON_OPTS), + ) + self.add_output( + "pkgbase.json", + self.metadata_repo, + orjson.dumps(pkgbases, option=ORJSON_OPTS), + ) + + return self.outputs diff --git a/aurweb/archives/spec/pkgbases.py b/aurweb/archives/spec/pkgbases.py new file mode 100644 index 00000000..9f02c1c6 --- /dev/null +++ b/aurweb/archives/spec/pkgbases.py @@ -0,0 +1,32 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import PackageBase + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.pkgbases_repo = GitInfo(config.get("git-archive", "pkgbases-repo")) + + def generate(self) -> Iterable[SpecOutput]: + filt = PackageBase.PackagerUID.isnot(None) + query = ( + db.query(PackageBase.Name) + .filter(filt) + .order_by(PackageBase.Name.asc()) + .all() + ) + pkgbases = [pkgbase.Name for pkgbase in query] + + self.add_output( + "pkgbase.json", + self.pkgbases_repo, + orjson.dumps(pkgbases, option=ORJSON_OPTS), + ) + return self.outputs diff --git a/aurweb/archives/spec/pkgnames.py b/aurweb/archives/spec/pkgnames.py new file mode 100644 index 00000000..c7cd9ea7 --- /dev/null +++ b/aurweb/archives/spec/pkgnames.py @@ -0,0 +1,33 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import Package, PackageBase + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.pkgnames_repo = GitInfo(config.get("git-archive", "pkgnames-repo")) + + def generate(self) -> Iterable[SpecOutput]: + filt = PackageBase.PackagerUID.isnot(None) + query = ( + db.query(Package.Name) + .join(PackageBase, PackageBase.ID == Package.PackageBaseID) + .filter(filt) + .order_by(Package.Name.asc()) + .all() + ) + pkgnames = [pkg.Name for pkg in query] + + self.add_output( + "pkgname.json", + self.pkgnames_repo, + orjson.dumps(pkgnames, option=ORJSON_OPTS), + ) + return self.outputs diff --git a/aurweb/archives/spec/users.py b/aurweb/archives/spec/users.py new file mode 100644 index 00000000..80da1641 --- /dev/null +++ b/aurweb/archives/spec/users.py @@ -0,0 +1,26 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import User + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.users_repo = GitInfo(config.get("git-archive", "users-repo")) + + def generate(self) -> Iterable[SpecOutput]: + query = db.query(User.Username).order_by(User.Username.asc()).all() + users = [user.Username for user in query] + + self.add_output( + "users.json", + self.users_repo, + orjson.dumps(users, option=ORJSON_OPTS), + ) + return self.outputs diff --git a/aurweb/models/package_base.py b/aurweb/models/package_base.py index bf80233d..26d9165f 100644 --- a/aurweb/models/package_base.py +++ b/aurweb/models/package_base.py @@ -64,3 +64,13 @@ class PackageBase(Base): if key in PackageBase.TO_FLOAT and not isinstance(attr, float): return float(attr) return attr + + +def popularity_decay(pkgbase: PackageBase, utcnow: int): + """Return the delta between now and the last time popularity was updated, in days""" + return int((utcnow - pkgbase.PopularityUpdated.timestamp()) / 86400) + + +def popularity(pkgbase: PackageBase, utcnow: int): + """Return up-to-date popularity""" + return float(pkgbase.Popularity) * (0.98 ** popularity_decay(pkgbase, utcnow)) diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 968135d1..46d6e2db 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -3,8 +3,9 @@ from typing import Any from fastapi import Request from sqlalchemy import and_ -from aurweb import config, db, defaults, l10n, util +from aurweb import config, db, defaults, l10n, time, util from aurweb.models import PackageBase, User +from aurweb.models.package_base import popularity from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comment import PackageComment from aurweb.models.package_request import PENDING_ID, PackageRequest @@ -81,6 +82,8 @@ def make_context( and_(PackageRequest.Status == PENDING_ID, PackageRequest.ClosedTS.is_(None)) ).count() + context["popularity"] = popularity(pkgbase, time.utcnow()) + return context diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 26677f80..515c6ffb 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -6,9 +6,10 @@ from fastapi.responses import HTMLResponse from sqlalchemy import and_, literal, orm import aurweb.config as config -from aurweb import db, defaults, models +from aurweb import db, defaults, models, time from aurweb.exceptions import RPCError from aurweb.filters import number_format +from aurweb.models.package_base import popularity from aurweb.packages.search import RPCSearch TYPE_MAPPING = { @@ -120,16 +121,15 @@ class RPC: if not args: raise RPCError("No request type/data specified.") - def _get_json_data(self, package: models.Package) -> dict[str, Any]: + def get_json_data(self, package: models.Package) -> dict[str, Any]: """Produce dictionary data of one Package that can be JSON-serialized. :param package: Package instance :returns: JSON-serializable dictionary """ - # Produce RPC API compatible Popularity: If zero, it's an integer - # 0, otherwise, it's formatted to the 6th decimal place. - pop = package.Popularity + # Normalize Popularity for RPC output to 6 decimal precision + pop = popularity(package, time.utcnow()) pop = 0 if not pop else float(number_format(pop, 6)) snapshot_uri = config.get("options", "snapshot_uri") @@ -151,8 +151,8 @@ class RPC: "LastModified": package.ModifiedTS, } - def _get_info_json_data(self, package: models.Package) -> dict[str, Any]: - data = self._get_json_data(package) + def get_info_json_data(self, package: models.Package) -> dict[str, Any]: + data = self.get_json_data(package) # All info results have _at least_ an empty list of # License and Keywords. @@ -176,7 +176,7 @@ class RPC: """ return [data_generator(pkg) for pkg in packages] - def _entities(self, query: orm.Query) -> orm.Query: + def entities(self, query: orm.Query) -> orm.Query: """Select specific RPC columns on `query`.""" return query.with_entities( models.Package.ID, @@ -188,38 +188,14 @@ class RPC: models.PackageBase.Name.label("PackageBaseName"), models.PackageBase.NumVotes, models.PackageBase.Popularity, + models.PackageBase.PopularityUpdated, models.PackageBase.OutOfDateTS, models.PackageBase.SubmittedTS, models.PackageBase.ModifiedTS, models.User.Username.label("Maintainer"), ).group_by(models.Package.ID) - def _handle_multiinfo_type( - self, args: list[str] = [], **kwargs - ) -> list[dict[str, Any]]: - self._enforce_args(args) - args = set(args) - - packages = ( - db.query(models.Package) - .join(models.PackageBase) - .join( - models.User, - models.User.ID == models.PackageBase.MaintainerUID, - isouter=True, - ) - .filter(models.Package.Name.in_(args)) - ) - - max_results = config.getint("options", "max_rpc_results") - packages = self._entities(packages).limit(max_results + 1) - - if packages.count() > max_results: - raise RPCError("Too many package results.") - - ids = {pkg.ID for pkg in packages} - - # Aliases for 80-width. + def subquery(self, ids: set[int]): Package = models.Package PackageKeyword = models.PackageKeyword @@ -311,7 +287,33 @@ class RPC: self.extra_info[record.ID][type_].append(name) - return self._assemble_json_data(packages, self._get_info_json_data) + def _handle_multiinfo_type( + self, args: list[str] = [], **kwargs + ) -> list[dict[str, Any]]: + self._enforce_args(args) + args = set(args) + + packages = ( + db.query(models.Package) + .join(models.PackageBase) + .join( + models.User, + models.User.ID == models.PackageBase.MaintainerUID, + isouter=True, + ) + .filter(models.Package.Name.in_(args)) + ) + + max_results = config.getint("options", "max_rpc_results") + packages = self.entities(packages).limit(max_results + 1) + + if packages.count() > max_results: + raise RPCError("Too many package results.") + + ids = {pkg.ID for pkg in packages} + self.subquery(ids) + + return self._assemble_json_data(packages, self.get_info_json_data) def _handle_search_type( self, by: str = defaults.RPC_SEARCH_BY, args: list[str] = [] @@ -330,12 +332,12 @@ class RPC: search.search_by(by, arg) max_results = config.getint("options", "max_rpc_results") - results = self._entities(search.results()).limit(max_results + 1).all() + results = self.entities(search.results()).limit(max_results + 1).all() if len(results) > max_results: raise RPCError("Too many package results.") - return self._assemble_json_data(results, self._get_json_data) + return self._assemble_json_data(results, self.get_json_data) def _handle_msearch_type( self, args: list[str] = [], **kwargs diff --git a/aurweb/schema.py b/aurweb/schema.py index b3b36195..5f998ed9 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -155,6 +155,12 @@ PackageBases = Table( nullable=False, server_default=text("0"), ), + Column( + "PopularityUpdated", + TIMESTAMP, + nullable=False, + server_default=text("'1970-01-01 00:00:01.000000'"), + ), Column("OutOfDateTS", BIGINT(unsigned=True)), Column("FlaggerComment", Text, nullable=False), Column("SubmittedTS", BIGINT(unsigned=True), nullable=False), diff --git a/aurweb/scripts/git_archive.py b/aurweb/scripts/git_archive.py new file mode 100644 index 00000000..4c909c18 --- /dev/null +++ b/aurweb/scripts/git_archive.py @@ -0,0 +1,125 @@ +import argparse +import importlib +import os +import sys +import traceback +from datetime import datetime + +import orjson +import pygit2 + +from aurweb import config + +# Constants +REF = "refs/heads/master" +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +def init_repository(git_info) -> None: + pygit2.init_repository(git_info.path) + repo = pygit2.Repository(git_info.path) + for k, v in git_info.config.items(): + repo.config[k] = v + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--spec", + type=str, + required=True, + help="name of spec module in the aurweb.archives.spec package", + ) + return parser.parse_args() + + +def update_repository(repo: pygit2.Repository): + # Use git status to determine file changes + has_changes = False + changes = repo.status() + for filepath, flags in changes.items(): + if flags != pygit2.GIT_STATUS_CURRENT: + has_changes = True + break + + if has_changes: + print("diff detected, committing") + # Add everything in the tree. + print("adding files to git tree") + + # Add the tree to staging + repo.index.read() + repo.index.add_all() + repo.index.write() + tree = repo.index.write_tree() + + # Determine base commit; if repo.head.target raises GitError, + # we have no current commits + try: + base = [repo.head.target] + except pygit2.GitError: + base = [] + + utcnow = datetime.utcnow() + author = pygit2.Signature( + config.get("git-archive", "author"), + config.get("git-archive", "author-email"), + int(utcnow.timestamp()), + 0, + ) + + # Commit the changes + timestamp = utcnow.strftime("%Y-%m-%d %H:%M:%S") + title = f"update - {timestamp}" + repo.create_commit(REF, author, author, title, tree, base) + + print("committed changes") + else: + print("no diff detected") + + +def main() -> int: + args = parse_args() + + print(f"loading '{args.spec}' spec") + spec_package = "aurweb.archives.spec" + module_path = f"{spec_package}.{args.spec}" + spec_module = importlib.import_module(module_path) + print(f"loaded '{args.spec}'") + + # Track repositories that the spec modifies. After we run + # through specs, we want to make a single commit for all + # repositories that contain changes. + repos = dict() + + print(f"running '{args.spec}' spec...") + spec = spec_module.Spec() + for output in spec.generate(): + if not os.path.exists(output.git_info.path / ".git"): + init_repository(output.git_info) + + path = output.git_info.path / output.filename + with open(path, "wb") as f: + f.write(output.data) + + if output.git_info.path not in repos: + repos[output.git_info.path] = pygit2.Repository(output.git_info.path) + + print(f"done running '{args.spec}' spec") + + print("processing repositories") + for path in spec.repos: + print(f"processing repository: {path}") + update_repository(pygit2.Repository(path)) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(0) + except Exception: + traceback.print_exc() + sys.exit(1) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 7ca171ab..bfdd12b4 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -188,6 +188,7 @@ def _main(): USERS = aurweb.config.get("mkpkglists", "userfile") bench = Benchmark() + logger.warning(f"{sys.argv[0]} is deprecated and will be soon be removed") logger.info("Started re-creating archives, wait a while...") query = ( diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index aa163be1..83506e22 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 +from datetime import datetime from sqlalchemy import and_, func from sqlalchemy.sql.functions import coalesce, sum as _sum -from aurweb import db, time +from aurweb import config, db, time from aurweb.models import PackageBase, PackageVote @@ -46,13 +47,24 @@ def run_variable(pkgbases: list[PackageBase] = []) -> None: ids = set() if pkgbases: + # If `pkgbases` were given, we should forcefully update the given + # package base records' popularities. ids = {pkgbase.ID for pkgbase in pkgbases} query = query.filter(PackageBase.ID.in_(ids)) + else: + # Otherwise, we should only update popularities which have exceeded + # the popularity interval length. + interval = config.getint("git-archive", "popularity-interval") + query = query.filter( + PackageBase.PopularityUpdated + <= datetime.fromtimestamp((now - interval)) + ) query.update( { "NumVotes": votes_subq.scalar_subquery(), "Popularity": pop_subq.scalar_subquery(), + "PopularityUpdated": datetime.fromtimestamp(now), } ) diff --git a/aurweb/testing/git.py b/aurweb/testing/git.py index 216515c8..39af87de 100644 --- a/aurweb/testing/git.py +++ b/aurweb/testing/git.py @@ -1,6 +1,4 @@ import os -import shlex -from subprocess import PIPE, Popen from typing import Tuple import py @@ -8,6 +6,7 @@ import py from aurweb.models import Package from aurweb.templates import base_template from aurweb.testing.filelock import FileLock +from aurweb.util import shell_exec class GitRepository: @@ -24,10 +23,7 @@ class GitRepository: self.file_lock.lock(on_create=self._setup) def _exec(self, cmdline: str, cwd: str) -> Tuple[int, str, str]: - args = shlex.split(cmdline) - proc = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE) - out, err = proc.communicate() - return (proc.returncode, out.decode().strip(), err.decode().strip()) + return shell_exec(cmdline, cwd) def _exec_repository(self, cmdline: str) -> Tuple[int, str, str]: return self._exec(cmdline, cwd=str(self.file_lock.path)) diff --git a/aurweb/util.py b/aurweb/util.py index 4f1bd64e..432b818a 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,6 +1,7 @@ import math import re import secrets +import shlex import string from datetime import datetime from http import HTTPStatus @@ -192,3 +193,10 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: def parse_ssh_keys(string: str) -> list[Tuple[str, str]]: """Parse a list of SSH public keys.""" return [parse_ssh_key(e) for e in string.splitlines()] + + +def shell_exec(cmdline: str, cwd: str) -> Tuple[int, str, str]: + args = shlex.split(cmdline) + proc = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + return (proc.returncode, out.decode().strip(), err.decode().strip()) diff --git a/conf/config.defaults b/conf/config.defaults index 722802cc..6cdffe65 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -131,6 +131,18 @@ packagesmetaextfile = /srv/http/aurweb/web/html/packages-meta-ext-v1.json.gz pkgbasefile = /srv/http/aurweb/web/html/pkgbase.gz userfile = /srv/http/aurweb/web/html/users.gz +[git-archive] +author = git_archive.py +author-email = no-reply@archlinux.org + +; One week worth of seconds (86400 * 7) +popularity-interval = 604800 + +metadata-repo = /srv/http/aurweb/metadata.git +users-repo = /srv/http/aurweb/users.git +pkgbases-repo = /srv/http/aurweb/pkgbases.git +pkgnames-repo = /srv/http/aurweb/pkgnames.git + [devel] ; commit_url is a format string used to produce a link to a commit hash. commit_url = https://gitlab.archlinux.org/archlinux/aurweb/-/commits/%s diff --git a/conf/config.dev b/conf/config.dev index 923c34ff..b36bfe77 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -76,5 +76,11 @@ packagesmetaextfile = /var/lib/aurweb/archives/packages-meta-ext-v1.json.gz pkgbasefile = /var/lib/aurweb/archives/pkgbase.gz userfile = /var/lib/aurweb/archives/users.gz +[git-archive] +metadata-repo = metadata.git +users-repo = users.git +pkgbases-repo = pkgbases.git +pkgnames-repo = pkgnames.git + [aurblup] db-path = YOUR_AUR_ROOT/aurblup/ diff --git a/doc/git-archive.md b/doc/git-archive.md new file mode 100644 index 00000000..cbc148b9 --- /dev/null +++ b/doc/git-archive.md @@ -0,0 +1,75 @@ +# aurweb Git Archive Specification + + + WARNING: This aurweb Git Archive implementation is + experimental and may be changed. + + +## Overview + +This git archive specification refers to the archive git repositories +created by [aurweb/scripts/git_archive.py](aurweb/scripts/git_archive.py) +using [spec modules](#spec-modules). + +## Configuration + +- `[git-archive]` + - `author` + - Git commit author + - `author-email` + - Git commit author email + +See an [official spec](#official-specs)'s documentation for spec-specific +configurations. + +## Fetch/Update Archives + +When a client has not yet fetched any initial archives, they should clone +the repository: + + $ git clone https://aur.archlinux.org/archive.git aurweb-archive + +When updating, the repository is already cloned and changes need to be pulled +from remote: + + # To update: + $ cd aurweb-archive && git pull + +For end-user production applications, see +[Minimize Disk Space](#minimize-disk-space). + +## Minimize Disk Space + +Using `git gc` on the repository will compress revisions and remove +unreachable objects which grow the repository a considerable amount +each commit. It is recommended that the following command is used +after cloning the archive or pulling updates: + + $ cd aurweb-archive && git gc --aggressive + +## Spec Modules + +Each aurweb spec module belongs to the `aurweb.archives.spec` package. For +example: a spec named "example" would be located at +`aurweb.archives.spec.example`. + +[Official spec listings](#official-specs) use the following format: + +- `spec_name` + - Spec description; what this spec produces + - `` + +### Official Specs + +- [metadata](doc/specs/metadata.md) + - Package RPC `type=info` metadata + - [metadata-repo](repos/metadata-repo.md) +- [users](doc/specs/users.md) + - List of users found in the database + - [users-repo](repos/users-repo.md) +- [pkgbases](doc/specs/pkgbases.md) + - List of package bases found in the database + - [pkgbases-repo](repos/pkgbases-repo.md) +- [pkgnames](doc/specs/pkgnames.md) + - List of package names found in the database + - [pkgnames-repo](repos/pkgnames-repo.md) diff --git a/doc/maintenance.txt b/doc/maintenance.txt index c52cf76f..56616f79 100644 --- a/doc/maintenance.txt +++ b/doc/maintenance.txt @@ -70,20 +70,48 @@ computations and clean up the database: * aurweb-pkgmaint automatically removes empty repositories that were created within the last 24 hours but never populated. -* aurweb-mkpkglists generates the package list files; it takes an optional - --extended flag, which additionally produces multiinfo metadata. It also - generates {archive.gz}.sha256 files that should be located within +* [Deprecated] aurweb-mkpkglists generates the package list files; it takes + an optional --extended flag, which additionally produces multiinfo metadata. + It also generates {archive.gz}.sha256 files that should be located within mkpkglists.archivedir which contain a SHA-256 hash of their matching .gz counterpart. * aurweb-usermaint removes the last login IP address of all users that did not login within the past seven days. +* aurweb-git-archive generates Git repository archives based on a --spec. + This script is a new generation of aurweb-mkpkglists, which creates and + maintains Git repository versions of the archives produced by + aurweb-mkpkglists. See doc/git-archive.md for detailed documentation. + These scripts can be installed by running `poetry install` and are usually scheduled using Cron. The current setup is: ---- -*/5 * * * * poetry run aurweb-mkpkglists [--extended] +# Run aurweb-git-archive --spec metadata directly after +# aurweb-mkpkglists so that they are executed sequentially, since +# both scripts are quite heavy. `aurweb-mkpkglists` should be removed +# from here once its deprecation period has ended. +*/5 * * * * poetry run aurweb-mkpkglists [--extended] && poetry run aurweb-git-archive --spec metadata + +# Update popularity once an hour. This is done to reduce the amount +# of changes caused by popularity data. Even if a package is otherwise +# unchanged, popularity is recalculated every 5 minutes via aurweb-popupdate, +# which causes changes for a large chunk of packages. +# +# At this interval, clients can still take advantage of popularity +# data, but its updates are guarded behind hour-long intervals. +*/60 * * * * poetry run aurweb-git-archive --spec popularity + +# Usernames +*/5 * * * * poetry run aurweb-git-archive --spec users + +# Package base names +*/5 * * * * poetry run aurweb-git-archive --spec pkgbases + +# Package names +*/5 * * * * poetry run aurweb-git-archive --spec pkgnames + 1 */2 * * * poetry run aurweb-popupdate 2 */2 * * * poetry run aurweb-aurblup 3 */2 * * * poetry run aurweb-pkgmaint diff --git a/doc/repos/metadata-repo.md b/doc/repos/metadata-repo.md new file mode 100644 index 00000000..cc678f40 --- /dev/null +++ b/doc/repos/metadata-repo.md @@ -0,0 +1,121 @@ +# Repository: metadata-repo + +## Overview + +The resulting repository contains RPC `type=info` JSON data for packages, +split into two different files: + +- `pkgbase.json` contains details about each package base in the AUR +- `pkgname.json` contains details about each package in the AUR + +See [Data](#data) for a breakdown of how data is presented in this +repository based off of a RPC `type=info` base. + +See [File Layout](#file-layout) for a detailed summary of the layout +of these files and the data contained within. + +**NOTE: `Popularity` now requires a client-side calculation, see [Popularity Calculation](#popularity-calculation).** + +## Data + +This repository contains RPC `type=info` data for all packages found +in AUR's database, reorganized to be suitable for Git repository +changes. + +- `pkgname.json` holds Package-specific metadata + - Some fields have been removed from `pkgname.json` objects + - `ID` + - `PackageBaseID -> ID` (moved to `pkgbase.json`) + - `NumVotes` (moved to `pkgbase.json`) + - `Popularity` (moved to `pkgbase.json`) +- `pkgbase.json` holds PackageBase-specific metadata + - Package Base fields from `pkgname.json` have been moved over to + `pkgbase.json` + - `ID` + - `Keywords` + - `FirstSubmitted` + - `LastModified` + - `OutOfDate` + - `Maintainer` + - `URLPath` + - `NumVotes` + - `Popularity` + - `PopularityUpdated` + +## Popularity Calculation + +Clients intending to use popularity data from this archive **must** +perform a decay calculation on their end to reflect a close approximation +of up-to-date popularity. + +Putting this step onto the client allows the server to maintain +less popularity record updates, dramatically improving archiving +of popularity data. The same calculation is done on the server-side +when producing outputs for RPC `type=info` and package pages. + +``` +Let T = Current UTC timestamp in seconds +Let PU = PopularityUpdated timestamp in seconds + +# The delta between now and PU in days +Let D = (T - PU) / 86400 + +# Calculate up-to-date popularity: +P = Popularity * (0.98^D) +``` + +We can see that the resulting up-to-date popularity value decays as +the exponent is increased: +- `1.0 * (0.98^1) = 0.98` +- `1.0 * (0.98^2) = 0.96039999` +- ... + +This decay calculation is essentially pushing back the date found for +votes by the exponent, which takes into account the time-factor. However, +since this calculation is based off of decimals and exponents, it +eventually becomes imprecise. The AUR updates these records on a forced +interval and whenever a vote is added to or removed from a particular package +to avoid imprecision from being an issue for clients + +## File Layout + +#### pkgbase.json: + + { + "pkgbase1": { + "FirstSubmitted": 123456, + "ID": 1, + "LastModified": 123456, + "Maintainer": "kevr", + "OutOfDate": null, + "URLPath": "/cgit/aur.git/snapshot/pkgbase1.tar.gz", + "NumVotes": 1, + "Popularity": 1.0, + "PopularityUpdated": 12345567753.0 + }, + ... + } + +#### pkgname.json: + + { + "pkg1": { + "CheckDepends": [], # Only included if a check dependency exists + "Conflicts": [], # Only included if a conflict exists + "Depends": [], # Only included if a dependency exists + "Description": "some description", + "Groups": [], # Only included if a group exists + "ID": 1, + "Keywords": [], + "License": [], + "MakeDepends": [], # Only included if a make dependency exists + "Name": "pkg1", + "OptDepends": [], # Only included if an opt dependency exists + "PackageBase": "pkgbase1", + "Provides": [], # Only included if `provides` is defined + "Replaces": [], # Only included if `replaces` is defined + "URL": "https://some_url.com", + "Version": "1.0-1" + }, + ... + } diff --git a/doc/repos/pkgbases-repo.md b/doc/repos/pkgbases-repo.md new file mode 100644 index 00000000..f4cb896f --- /dev/null +++ b/doc/repos/pkgbases-repo.md @@ -0,0 +1,15 @@ +# Repository: pkgbases-repo + +## Overview + +- `pkgbase.json` contains a list of package base names + +## File Layout + +### pkgbase.json: + + [ + "pkgbase1", + "pkgbase2", + ... + ] diff --git a/doc/repos/pkgnames-repo.md b/doc/repos/pkgnames-repo.md new file mode 100644 index 00000000..ae6fb4ed --- /dev/null +++ b/doc/repos/pkgnames-repo.md @@ -0,0 +1,15 @@ +# Repository: pkgnames-repo + +## Overview + +- `pkgname.json` contains a list of package names + +## File Layout + +### pkgname.json: + + [ + "pkgname1", + "pkgname2", + ... + ] diff --git a/doc/repos/users-repo.md b/doc/repos/users-repo.md new file mode 100644 index 00000000..23db9cfb --- /dev/null +++ b/doc/repos/users-repo.md @@ -0,0 +1,15 @@ +# Repository: users-repo + +## Overview + +- `users.json` contains a list of usernames + +## File Layout + +### users.json: + + [ + "user1", + "user2", + ... + ] diff --git a/doc/specs/metadata.md b/doc/specs/metadata.md new file mode 100644 index 00000000..282c0dd5 --- /dev/null +++ b/doc/specs/metadata.md @@ -0,0 +1,14 @@ +# Git Archive Spec: metadata + +## Configuration + +- `[git-archive]` + - `metadata-repo` + - Path to package metadata git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [metadata-repo](doc/repos/metadata-repo.md) diff --git a/doc/specs/pkgbases.md b/doc/specs/pkgbases.md new file mode 100644 index 00000000..80279070 --- /dev/null +++ b/doc/specs/pkgbases.md @@ -0,0 +1,14 @@ +# Git Archive Spec: pkgbases + +## Configuration + +- `[git-archive]` + - `pkgbases-repo` + - Path to pkgbases git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [pkgbases-repo](doc/repos/pkgbases-repo.md) diff --git a/doc/specs/pkgnames.md b/doc/specs/pkgnames.md new file mode 100644 index 00000000..0a4a907d --- /dev/null +++ b/doc/specs/pkgnames.md @@ -0,0 +1,14 @@ +# Git Archive Spec: pkgnames + +## Configuration + +- `[git-archive]` + - `pkgnames-repo` + - Path to pkgnames git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [pkgnames-repo](doc/repos/pkgnames-repo.md) diff --git a/doc/specs/popularity.md b/doc/specs/popularity.md new file mode 100644 index 00000000..3084f458 --- /dev/null +++ b/doc/specs/popularity.md @@ -0,0 +1,14 @@ +# Git Archive Spec: popularity + +## Configuration + +- `[git-archive]` + - `popularity-repo` + - Path to popularity git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [popularity-repo](doc/repos/popularity-repo.md) diff --git a/doc/specs/users.md b/doc/specs/users.md new file mode 100644 index 00000000..25396154 --- /dev/null +++ b/doc/specs/users.md @@ -0,0 +1,14 @@ +# Git Archive Spec: users + +## Configuration + +- `[git-archive]` + - `users-repo` + - Path to users git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [users-repo](doc/repos/users-repo.md) diff --git a/migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py b/migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py new file mode 100644 index 00000000..afa87687 --- /dev/null +++ b/migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py @@ -0,0 +1,33 @@ +"""add PopularityUpdated to PackageBase + +Revision ID: 6441d3b65270 +Revises: d64e5571bc8d +Create Date: 2022-09-22 18:08:03.280664 + +""" +from alembic import op +from sqlalchemy.exc import OperationalError + +from aurweb.models.package_base import PackageBase +from aurweb.scripts import popupdate + +# revision identifiers, used by Alembic. +revision = "6441d3b65270" +down_revision = "d64e5571bc8d" +branch_labels = None +depends_on = None + +table = PackageBase.__table__ + + +def upgrade(): + try: + op.add_column(table.name, table.c.PopularityUpdated) + except OperationalError: + print(f"table '{table.name}' already exists, skipping migration") + + popupdate.run_variable() + + +def downgrade(): + op.drop_column(table.name, "PopularityUpdated") diff --git a/pyproject.toml b/pyproject.toml index f732f2e7..775ece09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,3 +117,4 @@ aurweb-tuvotereminder = "aurweb.scripts.tuvotereminder:main" aurweb-usermaint = "aurweb.scripts.usermaint:main" aurweb-config = "aurweb.scripts.config:main" aurweb-adduser = "aurweb.scripts.adduser:main" +aurweb-git-archive = "aurweb.scripts.git_archive:main" diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 86bc1de5..8ecf9bd8 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -149,7 +149,7 @@ - + diff --git a/test/test_git_archives.py b/test/test_git_archives.py new file mode 100644 index 00000000..8ee4c2ba --- /dev/null +++ b/test/test_git_archives.py @@ -0,0 +1,241 @@ +from http import HTTPStatus +from typing import Tuple +from unittest import mock + +import py +import pygit2 +import pytest +from fastapi.testclient import TestClient + +from aurweb import asgi, config, db +from aurweb.archives.spec.base import GitInfo, SpecBase +from aurweb.models import Package, PackageBase, User +from aurweb.scripts import git_archive +from aurweb.testing.requests import Request + + +@pytest.fixture +def mock_metadata_archive( + tmp_path: py.path.local, +) -> Tuple[py.path.local, py.path.local]: + metadata_path = tmp_path / "metadata.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "metadata-repo": + return str(metadata_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield metadata_path + + +@pytest.fixture +def mock_users_archive(tmp_path: py.path.local) -> py.path.local: + users_path = tmp_path / "users.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "users-repo": + return str(users_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield users_path + + +@pytest.fixture +def mock_pkgbases_archive(tmp_path: py.path.local) -> py.path.local: + pkgbases_path = tmp_path / "pkgbases.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "pkgbases-repo": + return str(pkgbases_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield pkgbases_path + + +@pytest.fixture +def mock_pkgnames_archive(tmp_path: py.path.local) -> py.path.local: + pkgnames_path = tmp_path / "pkgnames.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "pkgnames-repo": + return str(pkgnames_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield pkgnames_path + + +@pytest.fixture +def metadata(mock_metadata_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "metadata"] + with mock.patch("sys.argv", args): + yield mock_metadata_archive + + +@pytest.fixture +def users(mock_users_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "users"] + with mock.patch("sys.argv", args): + yield mock_users_archive + + +@pytest.fixture +def pkgbases(mock_pkgbases_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "pkgbases"] + with mock.patch("sys.argv", args): + yield mock_pkgbases_archive + + +@pytest.fixture +def pkgnames(mock_pkgnames_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "pkgnames"] + with mock.patch("sys.argv", args): + yield mock_pkgnames_archive + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) + + +@pytest.fixture +def user(db_test: None) -> User: + with db.begin(): + user_ = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + ) + + yield user_ + + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase_ = db.create( + PackageBase, + Name="test", + Maintainer=user, + Packager=user, + ) + + pkg_ = db.create( + Package, + PackageBase=pkgbase_, + Name="test", + ) + + yield pkg_ + + +def commit_count(repo: pygit2.Repository) -> int: + commits = 0 + for _ in repo.walk(repo.head.target): + commits += 1 + return commits + + +def test_specbase_raises_notimplementederror(): + spec = SpecBase() + with pytest.raises(NotImplementedError): + spec.generate() + + +def test_gitinfo_config(tmpdir: py.path.local): + path = tmpdir / "test.git" + git_info = GitInfo(path, {"user.name": "Test Person"}) + git_archive.init_repository(git_info) + + repo = pygit2.Repository(path) + assert repo.config["user.name"] == "Test Person" + + +def test_metadata(metadata: py.path.local, package: Package): + # Run main(), which creates mock_metadata_archive and commits current + # package data to it, exercising the "diff detected, committing" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + # Run main() again to exercise the "no diff detected" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + +def test_metadata_change( + client: TestClient, metadata: py.path.local, user: User, package: Package +): + """Test that metadata changes via aurweb cause git_archive to produce diffs.""" + # Run main(), which creates mock_metadata_archive and commits current + # package data to it, exercising the "diff detected, committing" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + # Now, we modify `package`-related metadata via aurweb POST. + pkgbasename = package.PackageBase.Name + cookies = {"AURSID": user.login(Request(), "testPassword")} + + with client as request: + endp = f"/pkgbase/{pkgbasename}/keywords" + post_data = {"keywords": "abc def"} + resp = request.post(endp, data=post_data, cookies=cookies, allow_redirects=True) + assert resp.status_code == HTTPStatus.OK + + # Run main() again, which should now produce a new commit with the + # keyword changes we just made + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 2 + + +def test_metadata_delete(client: TestClient, metadata: py.path.local, package: Package): + # Run main(), which creates mock_metadata_archive and commits current + # package data to it, exercising the "diff detected, committing" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + with db.begin(): + db.delete(package) + + # The deletion here should have caused a diff to be produced in git + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 2 + + +def test_users(users: py.path.local, user: User): + assert git_archive.main() == 0 + repo = pygit2.Repository(users) + assert commit_count(repo) == 1 + + +def test_pkgbases(pkgbases: py.path.local, package: Package): + assert git_archive.main() == 0 + repo = pygit2.Repository(pkgbases) + assert commit_count(repo) == 1 + + +def test_pkgnames(pkgnames: py.path.local, package: Package): + assert git_archive.main() == 0 + repo = pygit2.Repository(pkgnames) + assert commit_count(repo) == 1 diff --git a/test/test_templates.py b/test/test_templates.py index f80e68eb..2ff31fc9 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -9,6 +9,7 @@ from aurweb.filters import as_timezone, number_format, timestamp_to_datetime as from aurweb.models import Package, PackageBase, User from aurweb.models.account_type import USER_ID from aurweb.models.license import License +from aurweb.models.package_base import popularity from aurweb.models.package_license import PackageLicense from aurweb.models.package_relation import PackageRelation from aurweb.models.relation_type import PROVIDES_ID, REPLACES_ID @@ -287,12 +288,14 @@ def test_package_details(user: User, package: Package): """Test package details with most fields populated, but not all.""" request = Request(user=user, authenticated=True) context = make_context(request, "Test Details") + context.update( { "request": request, "git_clone_uri_anon": GIT_CLONE_URI_ANON, "git_clone_uri_priv": GIT_CLONE_URI_PRIV, "pkgbase": package.PackageBase, + "popularity": popularity(package.PackageBase, time.utcnow()), "package": package, "comaintainers": [], } @@ -329,6 +332,7 @@ def test_package_details_filled(user: User, package: Package): "git_clone_uri_anon": GIT_CLONE_URI_ANON, "git_clone_uri_priv": GIT_CLONE_URI_PRIV, "pkgbase": package.PackageBase, + "popularity": popularity(package.PackageBase, time.utcnow()), "package": package, "comaintainers": [], "licenses": package.package_licenses, From 137644e9192c8421b0a78a1b955910eed09e9276 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 25 Sep 2022 10:03:05 +0200 Subject: [PATCH 1160/1451] docs: suggest shallow clone in git-archive.md we should be suggesting to make a shallow clone to reduce the amount of data that is being transferred initially Signed-off-by: moson-mo --- doc/git-archive.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/git-archive.md b/doc/git-archive.md index cbc148b9..d7c80f76 100644 --- a/doc/git-archive.md +++ b/doc/git-archive.md @@ -24,10 +24,10 @@ configurations. ## Fetch/Update Archives -When a client has not yet fetched any initial archives, they should clone -the repository: +When a client has not yet fetched any initial archives, they should +shallow-clone the repository: - $ git clone https://aur.archlinux.org/archive.git aurweb-archive + $ git clone --depth=1 https://aur.archlinux.org/archive.git aurweb-archive When updating, the repository is already cloned and changes need to be pulled from remote: From 0dddaeeb98ea13dfa10a0462af178dc50481333f Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Fri, 23 Sep 2022 13:31:50 +0100 Subject: [PATCH 1161/1451] fix: remove sessions of suspended users Fixes: #394 Signed-off-by: Leonidas Spyropoulos --- aurweb/routers/accounts.py | 2 ++ aurweb/users/update.py | 16 +++++++++ test/test_accounts_routes.py | 70 +++++++++++++++++++++++++++++++----- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 3937757a..524ef814 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -412,6 +412,7 @@ async def account_edit_post( TZ: str = Form(aurweb.config.get("options", "default_timezone")), P: str = Form(default=str()), # New Password C: str = Form(default=None), # Password Confirm + S: bool = Form(default=False), # Suspended PK: str = Form(default=None), # PubKey CN: bool = Form(default=False), # Comment Notify UN: bool = Form(default=False), # Update Notify @@ -455,6 +456,7 @@ async def account_edit_post( update.ssh_pubkey, update.account_type, update.password, + update.suspend, ] # These update functions are all guarded by retry_deadlock; diff --git a/aurweb/users/update.py b/aurweb/users/update.py index 6bd4a295..df41f843 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -134,3 +134,19 @@ def password( # If the target user is the request user, login with # the updated password to update the Session record. user.login(request, P, cookies.timeout(remember_me)) + + +@db.retry_deadlock +def suspend( + S: bool = False, + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: + if S and user.session: + context["S"] = None + with db.begin(): + db.delete_all( + db.query(models.Session).filter(models.Session.UsersID == user.ID) + ) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index eab8fa4f..b6dce19e 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -9,6 +9,7 @@ import lxml.html import pytest from fastapi.testclient import TestClient +import aurweb.config import aurweb.models.account_type as at from aurweb import captcha, db, logging, time from aurweb.asgi import app @@ -35,6 +36,9 @@ logger = logging.get_logger(__name__) # Some test global constants. TEST_USERNAME = "test" TEST_EMAIL = "test@example.org" +TEST_REFERER = { + "referer": aurweb.config.get("options", "aur_location") + "/login", +} def make_ssh_pubkey(): @@ -61,7 +65,12 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: - yield TestClient(app=app) + client = TestClient(app=app) + + # Necessary for forged login CSRF protection on the login route. Set here + # instead of only on the necessary requests for convenience. + client.headers.update(TEST_REFERER) + yield client def create_user(username: str) -> User: @@ -1003,13 +1012,8 @@ def test_post_account_edit_suspended(client: TestClient, user: User): # Make sure the user record got updated correctly. assert user.Suspended - - post_data.update({"S": False}) - with client as request: - resp = request.post(endpoint, data=post_data, cookies=cookies) - assert resp.status_code == int(HTTPStatus.OK) - - assert not user.Suspended + # Let's make sure the DB got updated properly. + assert user.session is None def test_post_account_edit_error_unauthorized(client: TestClient, user: User): @@ -1262,6 +1266,56 @@ def test_post_account_edit_other_user_type_as_tu( assert expected in caplog.text +def test_post_account_edit_other_user_suspend_as_tu(client: TestClient, tu_user: User): + with db.begin(): + user = create_user("test3") + # Create a session for user + sid = user.login(Request(), "testPassword") + assert sid is not None + + # `user` needs its own TestClient, to keep its AURSID cookies + # apart from `tu_user`s during our testing. + user_client = TestClient(app=app) + user_client.headers.update(TEST_REFERER) + + # Test that `user` can view their account edit page while logged in. + user_cookies = {"AURSID": sid} + with client as request: + endpoint = f"/account/{user.Username}/edit" + resp = request.get(endpoint, cookies=user_cookies, allow_redirects=False) + assert resp.status_code == HTTPStatus.OK + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + assert cookies is not None # This is useless, we create the dict here ^ + # As a TU, we can see the Account for other users. + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + # As a TU, we can modify other user's account types. + data = { + "U": user.Username, + "E": user.Email, + "S": True, + "passwd": "testPassword", + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # Test that `user` no longer has a session. + with user_client as request: + resp = request.get(endpoint, cookies=user_cookies, allow_redirects=False) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Since user is now suspended, they should not be able to login. + data = {"user": user.Username, "passwd": "testPassword", "next": "/"} + with user_client as request: + resp = request.post("/login", data=data) + assert resp.status_code == HTTPStatus.OK + errors = get_errors(resp.text) + assert errors[0].text.strip() == "Account Suspended" + + def test_post_account_edit_other_user_type_as_tu_invalid_type( client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture ): From e00b0059f75cb467d4eeab7fb8f8332bbc67288d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 26 Sep 2022 01:27:37 -0700 Subject: [PATCH 1162/1451] doc: remove --spec popularity from cron recommendations Signed-off-by: Kevin Morris --- doc/maintenance.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/doc/maintenance.txt b/doc/maintenance.txt index 56616f79..dacf2b60 100644 --- a/doc/maintenance.txt +++ b/doc/maintenance.txt @@ -94,15 +94,6 @@ usually scheduled using Cron. The current setup is: # from here once its deprecation period has ended. */5 * * * * poetry run aurweb-mkpkglists [--extended] && poetry run aurweb-git-archive --spec metadata -# Update popularity once an hour. This is done to reduce the amount -# of changes caused by popularity data. Even if a package is otherwise -# unchanged, popularity is recalculated every 5 minutes via aurweb-popupdate, -# which causes changes for a large chunk of packages. -# -# At this interval, clients can still take advantage of popularity -# data, but its updates are guarded behind hour-long intervals. -*/60 * * * * poetry run aurweb-git-archive --spec popularity - # Usernames */5 * * * * poetry run aurweb-git-archive --spec users From eb0c5605e491d51ee1ade8431934bad78f7b141e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 26 Sep 2022 01:28:38 -0700 Subject: [PATCH 1163/1451] upgrade: bump version to v6.1.5 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index c1f87984..83b965e3 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.4" +AURWEB_VERSION = "v6.1.5" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 775ece09..46d8806f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.4" +version = "v6.1.5" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 8657fd336e4c47dce3eaf78988944658f85bd64e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Thu, 29 Sep 2022 17:43:26 -0700 Subject: [PATCH 1164/1451] feat: GET|POST /account/{name}/delete Closes #348 Signed-off-by: Kevin Morris --- aurweb/models/package_vote.py | 2 +- aurweb/models/session.py | 2 +- aurweb/routers/accounts.py | 76 ++++++++++++++++++++++++- po/aurweb.pot | 4 ++ templates/account/delete.html | 43 ++++++++++++++ test/test_accounts_routes.py | 103 ++++++++++++++++++++++++++++++++++ 6 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 templates/account/delete.html diff --git a/aurweb/models/package_vote.py b/aurweb/models/package_vote.py index fa769bb6..b9e233d9 100644 --- a/aurweb/models/package_vote.py +++ b/aurweb/models/package_vote.py @@ -14,7 +14,7 @@ class PackageVote(Base): User = relationship( _User, - backref=backref("package_votes", lazy="dynamic"), + backref=backref("package_votes", lazy="dynamic", cascade="all, delete"), foreign_keys=[__table__.c.UsersID], ) diff --git a/aurweb/models/session.py b/aurweb/models/session.py index d3d69f8c..ff97f017 100644 --- a/aurweb/models/session.py +++ b/aurweb/models/session.py @@ -13,7 +13,7 @@ class Session(Base): User = relationship( _User, - backref=backref("session", uselist=False), + backref=backref("session", cascade="all, delete", uselist=False), foreign_keys=[__table__.c.UsersID], ) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 524ef814..12e59b30 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -3,13 +3,13 @@ import typing from http import HTTPStatus from typing import Any -from fastapi import APIRouter, Form, Request +from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, or_ import aurweb.config from aurweb import cookies, db, l10n, logging, models, util -from aurweb.auth import account_type_required, requires_auth, requires_guest +from aurweb.auth import account_type_required, creds, requires_auth, requires_guest from aurweb.captcha import get_captcha_salts from aurweb.exceptions import ValidationError, handle_form_exceptions from aurweb.l10n import get_translator_for_request @@ -598,6 +598,78 @@ async def accounts_post( return render_template(request, "account/index.html", context) +@router.get("/account/{name}/delete") +@requires_auth +async def account_delete(request: Request, name: str): + user = db.query(models.User).filter(models.User.Username == name).first() + if not user: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + has_cred = request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user]) + if not has_cred: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + detail=_("You do not have permission to edit this account."), + status_code=HTTPStatus.UNAUTHORIZED, + ) + + context = make_context(request, "Accounts") + context["name"] = name + return render_template(request, "account/delete.html", context) + + +@db.async_retry_deadlock +@router.post("/account/{name}/delete") +@handle_form_exceptions +@requires_auth +async def account_delete_post( + request: Request, + name: str, + passwd: str = Form(default=str()), + confirm: bool = Form(default=False), +): + user = db.query(models.User).filter(models.User.Username == name).first() + if not user: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + has_cred = request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user]) + if not has_cred: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + detail=_("You do not have permission to edit this account."), + status_code=HTTPStatus.UNAUTHORIZED, + ) + + context = make_context(request, "Accounts") + context["name"] = name + + confirm = util.strtobool(confirm) + if not confirm: + context["errors"] = [ + "The account has not been deleted, check the confirmation checkbox." + ] + return render_template( + request, + "account/delete.html", + context, + status_code=HTTPStatus.BAD_REQUEST, + ) + + if not request.user.valid_password(passwd): + context["errors"] = ["Invalid password."] + return render_template( + request, + "account/delete.html", + context, + status_code=HTTPStatus.BAD_REQUEST, + ) + + with db.begin(): + db.delete(user) + + return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) + + def render_terms_of_service(request: Request, context: dict, terms: typing.Iterable): if not terms: return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) diff --git a/po/aurweb.pot b/po/aurweb.pot index bc4bab84..1838fae5 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -2346,3 +2346,7 @@ msgstr "" #: templates/partials/packages/package_metadata.html msgid "dependencies" msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" diff --git a/templates/account/delete.html b/templates/account/delete.html new file mode 100644 index 00000000..625d3c2d --- /dev/null +++ b/templates/account/delete.html @@ -0,0 +1,43 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {{ "Accounts" | tr }}

    + + {% include "partials/error.html" %} + +

    + {{ + "You can use this form to permanently delete the AUR account %s%s%s." + | tr | format("", name, "") | safe + }} +

    + +

    + {{ + "%sWARNING%s: This action cannot be undone." + | tr | format("", "") | safe + }} +

    + + +
    +
    +

    + + +

    +

    + +

    +

    + +

    +
    + + +
    +{% endblock %} diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index b6dce19e..f4034a9a 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -1949,3 +1949,106 @@ def test_accounts_unauthorized(client: TestClient, user: User): resp = request.get("/accounts", cookies=cookies, allow_redirects=False) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" + + +def test_account_delete_self_unauthorized(client: TestClient, tu_user: User): + with db.begin(): + user = create_user("some_user") + user2 = create_user("user2") + + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/delete" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == HTTPStatus.UNAUTHORIZED + + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == HTTPStatus.UNAUTHORIZED + + # But a TU does have access + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with TestClient(app=app) as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == HTTPStatus.OK + + +def test_account_delete_self_not_found(client: TestClient, user: User): + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = "/account/non-existent-user/delete" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == HTTPStatus.NOT_FOUND + + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == HTTPStatus.NOT_FOUND + + +def test_account_delete_self(client: TestClient, user: User): + username = user.Username + + # Confirm that we can view our own account deletion page + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{username}/delete" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == HTTPStatus.OK + + # The checkbox must be checked + with client as request: + resp = request.post( + endpoint, + data={"passwd": "fakePassword", "confirm": False}, + cookies=cookies, + ) + assert resp.status_code == HTTPStatus.BAD_REQUEST + errors = get_errors(resp.text) + assert ( + errors[0].text.strip() + == "The account has not been deleted, check the confirmation checkbox." + ) + + # The correct password must be supplied + with client as request: + resp = request.post( + endpoint, + data={"passwd": "fakePassword", "confirm": True}, + cookies=cookies, + ) + assert resp.status_code == HTTPStatus.BAD_REQUEST + errors = get_errors(resp.text) + assert errors[0].text.strip() == "Invalid password." + + # Supply everything correctly and delete ourselves + with client as request: + resp = request.post( + endpoint, + data={"passwd": "testPassword", "confirm": True}, + cookies=cookies, + ) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Check that our User record no longer exists in the database + record = db.query(User).filter(User.Username == username).first() + assert record is None + + +def test_account_delete_as_tu(client: TestClient, tu_user: User): + with db.begin(): + user = create_user("user2") + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + username = user.Username + endpoint = f"/account/{username}/delete" + + # Delete the user + with client as request: + resp = request.post( + endpoint, + data={"passwd": "testPassword", "confirm": True}, + cookies=cookies, + ) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Check that our User record no longer exists in the database + record = db.query(User).filter(User.Username == username).first() + assert record is None From 3ae6323a7ccf9d2637255c522e0ff8371f7ace20 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Fri, 30 Sep 2022 05:19:58 -0700 Subject: [PATCH 1165/1451] upgrade: bump to v6.1.6 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 83b965e3..c9f36e51 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.5" +AURWEB_VERSION = "v6.1.6" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 46d8806f..77d136db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.5" +version = "v6.1.6" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 18f5e142b9180648763c5513e2f123dbcfde67b4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 11 Oct 2022 14:50:09 -0700 Subject: [PATCH 1166/1451] fix: include orphaned packages in metadata output Signed-off-by: Kevin Morris --- aurweb/archives/spec/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/archives/spec/metadata.py b/aurweb/archives/spec/metadata.py index e7c8e096..ce7c6f30 100644 --- a/aurweb/archives/spec/metadata.py +++ b/aurweb/archives/spec/metadata.py @@ -22,7 +22,7 @@ class Spec(SpecBase): base_query = ( db.query(Package) .join(PackageBase) - .join(User, PackageBase.MaintainerUID == User.ID) + .join(User, PackageBase.MaintainerUID == User.ID, isouter=True) ) # Create an instance of RPC, use it to get entities from From da5a646a731eab817d6bc2b2ebf54bb1dec58e23 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 11 Oct 2022 15:04:25 -0700 Subject: [PATCH 1167/1451] upgrade: bump to v6.1.7 Signed-off-by: Kevin Morris --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index c9f36e51..e8ca70d9 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.6" +AURWEB_VERSION = "v6.1.7" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 77d136db..fea2f922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.6" +version = "v6.1.7" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From b757e66997579b1d5e5c25a444894a6ac246577d Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 12 Jul 2022 15:12:38 +0100 Subject: [PATCH 1168/1451] feature: add filters and stats for requests Signed-off-by: Leonidas Spyropoulos --- aurweb/routers/requests.py | 46 ++++++++++++++++++++++++++--- templates/requests.html | 59 ++++++++++++++++++++++++++++++++++++++ test/test_requests.py | 16 ++++++++++- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index bf86bdcc..ca5fae73 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -8,7 +8,12 @@ from aurweb import db, defaults, time, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import handle_form_exceptions from aurweb.models import PackageRequest -from aurweb.models.package_request import PENDING_ID, REJECTED_ID +from aurweb.models.package_request import ( + ACCEPTED_ID, + CLOSED_ID, + PENDING_ID, + REJECTED_ID, +) from aurweb.requests.util import get_pkgreq_by_id from aurweb.scripts import notify from aurweb.templates import make_context, render_template @@ -22,26 +27,59 @@ async def requests( request: Request, O: int = Query(default=defaults.O), PP: int = Query(default=defaults.PP), + filter_pending: bool = False, + filter_closed: bool = False, + filter_accepted: bool = False, + filter_rejected: bool = False, ): context = make_context(request, "Requests") context["q"] = dict(request.query_params) + if len(dict(request.query_params)) == 0: + filter_pending = True + O, PP = util.sanitize_params(O, PP) context["O"] = O context["PP"] = PP + context["filter_pending"] = filter_pending + context["filter_closed"] = filter_closed + context["filter_accepted"] = filter_accepted + context["filter_rejected"] = filter_rejected # A PackageRequest query query = db.query(PackageRequest) + # Requests statistics + context["total_requests"] = query.count() + pending_count = 0 + query.filter(PackageRequest.Status == PENDING_ID).count() + context["pending_requests"] = pending_count + closed_count = 0 + query.filter(PackageRequest.Status == CLOSED_ID).count() + context["closed_requests"] = closed_count + accepted_count = 0 + query.filter(PackageRequest.Status == ACCEPTED_ID).count() + context["accepted_requests"] = accepted_count + rejected_count = 0 + query.filter(PackageRequest.Status == REJECTED_ID).count() + context["rejected_requests"] = rejected_count + + # Apply filters + in_filters = [] + if filter_pending: + in_filters.append(PENDING_ID) + if filter_closed: + in_filters.append(CLOSED_ID) + if filter_accepted: + in_filters.append(ACCEPTED_ID) + if filter_rejected: + in_filters.append(REJECTED_ID) + filtered = query.filter(PackageRequest.Status.in_(in_filters)) # If the request user is not elevated (TU or Dev), then # filter PackageRequests which are owned by the request user. if not request.user.is_elevated(): - query = query.filter(PackageRequest.UsersID == request.user.ID) + filtered = filtered.filter(PackageRequest.UsersID == request.user.ID) - context["total"] = query.count() + context["total"] = filtered.count() context["results"] = ( - query.order_by( + filtered.order_by( # Order primarily by the Status column being PENDING_ID, # and secondarily by RequestTS; both in descending order. case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), diff --git a/templates/requests.html b/templates/requests.html index ed8f31fb..9037855c 100644 --- a/templates/requests.html +++ b/templates/requests.html @@ -4,6 +4,65 @@ {% set plural = "%d package requests found." %} {% block pageContent %} +
    +

    {{ "Requests" | tr }}

    +

    {{ "Total Statistics" | tr }}

    +
    {{ "Git Clone URL" | tr }}:
    {{ "Description" | tr }}:{{ pkg.Description }}{{ package.Description }}
    {{ "Upstream URL" | tr }}: - {% if pkg.URL %} - {{ pkg.URL }} + {% if package.URL %} + {{ package.URL }} {% else %} {{ "None" | tr }} {% endif %} From 25e05830a670b0ca99d007eabf2d8c127a13ce9a Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Mon, 5 Sep 2022 19:50:41 -0700 Subject: [PATCH 1128/1451] test: test that /packages/{name} produces the package's description This commit fixes two of our tests in test_templates.py to go along with our new template modifications, as well as a new test in test_packages_routes.py which constructs two packages belonging to the same package base, then tests that viewing their pages produces their independent descriptions. Signed-off-by: Kevin Morris --- templates/partials/packages/details.html | 2 +- test/test_packages_routes.py | 44 ++++++++++++++++++++++++ test/test_templates.py | 4 +-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index cdb62128..86bc1de5 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -17,7 +17,7 @@
    {{ "Description" | tr }}: {{ package.Description }}
    {{ "Popularity" | tr }}:{{ pkgbase.Popularity | number_format(6 if pkgbase.Popularity <= 0.2 else 2) }}{{ popularity | number_format(6 if popularity <= 0.2 else 2) }}
    {{ "First Submitted" | tr }}:
    + + + + + + + + + + + + + + + + + + + + + + +
    {{ "Total" | tr }}:{{ total_requests }}
    {{ "Pending" | tr }}:{{ pending_requests }}
    {{ "Closed" | tr }}:{{ closed_requests }}
    {{ "Accepted" | tr }}:{{ accepted_requests }}
    {{ "Rejected" | tr }}:{{ rejected_requests }}
    +

    {{ "Filters" | tr }}

    +
    +
    +
    + {{ "Select filter criteria" | tr }} +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    {% if not total %}

    {{ "No requests matched your search criteria." | tr }}

    diff --git a/test/test_requests.py b/test/test_requests.py index 83cdb402..344b9edc 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -717,6 +717,10 @@ def test_requests( "O": 0, # Page 1 "SeB": "nd", "SB": "n", + "filter_pending": True, + "filter_closed": True, + "filter_accepted": True, + "filter_rejected": True, }, cookies=cookies, ) @@ -732,7 +736,17 @@ def test_requests( # Request page 2 of the requests page. with client as request: - resp = request.get("/requests", params={"O": 50}, cookies=cookies) # Page 2 + resp = request.get( + "/requests", + params={ + "O": 50, + "filter_pending": True, + "filter_closed": True, + "filter_accepted": True, + "filter_rejected": True, + }, + cookies=cookies, + ) # Page 2 assert resp.status_code == int(HTTPStatus.OK) assert "‹ Previous" in resp.text From 9c0f8f053ecaa2a34473dcf4b6b45c2d6812df96 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Fri, 6 May 2022 20:22:07 +0100 Subject: [PATCH 1169/1451] chore: rename logging.py and redis.py to avoid circular imports Signed-off-by: Leonidas Spyropoulos --- aurweb/asgi.py | 7 +++---- aurweb/{logging.py => aur_logging.py} | 0 aurweb/{redis.py => aur_redis.py} | 4 ++-- aurweb/initdb.py | 2 +- aurweb/models/user.py | 4 ++-- aurweb/packages/util.py | 2 +- aurweb/pkgbase/actions.py | 4 ++-- aurweb/prometheus.py | 4 ++-- aurweb/ratelimit.py | 6 +++--- aurweb/routers/accounts.py | 4 ++-- aurweb/routers/html.py | 6 +++--- aurweb/routers/packages.py | 4 ++-- aurweb/routers/pkgbase.py | 4 ++-- aurweb/routers/trusted_user.py | 4 ++-- aurweb/scripts/mkpkglists.py | 4 ++-- aurweb/scripts/notify.py | 4 ++-- aurweb/scripts/rendercomment.py | 4 ++-- aurweb/testing/alpm.py | 4 ++-- aurweb/testing/filelock.py | 4 ++-- aurweb/users/validate.py | 4 ++-- aurweb/util.py | 4 ++-- test/conftest.py | 4 ++-- test/test_accounts_routes.py | 4 ++-- test/test_asgi.py | 6 +++--- test/test_homepage.py | 2 +- test/test_logging.py | 4 ++-- test/test_packages_util.py | 2 +- test/test_ratelimit.py | 6 +++--- test/test_redis.py | 24 ++++++++++++------------ test/test_rendercomment.py | 4 ++-- test/test_rpc.py | 2 +- test/test_rss.py | 4 ++-- 32 files changed, 72 insertions(+), 73 deletions(-) rename aurweb/{logging.py => aur_logging.py} (100%) rename aurweb/{redis.py => aur_redis.py} (95%) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 72b47b4c..b172626f 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -22,19 +22,18 @@ from starlette.middleware.sessions import SessionMiddleware import aurweb.captcha # noqa: F401 import aurweb.config import aurweb.filters # noqa: F401 -import aurweb.logging import aurweb.pkgbase.util as pkgbaseutil -from aurweb import logging, prometheus, util +from aurweb import aur_logging, prometheus, util +from aurweb.aur_redis import redis_connection from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine, query from aurweb.models import AcceptedTerm, Term from aurweb.packages.util import get_pkg_or_base from aurweb.prometheus import instrumentator -from aurweb.redis import redis_connection from aurweb.routers import APP_ROUTES from aurweb.templates import make_context, render_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) # Setup the FastAPI app. app = FastAPI() diff --git a/aurweb/logging.py b/aurweb/aur_logging.py similarity index 100% rename from aurweb/logging.py rename to aurweb/aur_logging.py diff --git a/aurweb/redis.py b/aurweb/aur_redis.py similarity index 95% rename from aurweb/redis.py rename to aurweb/aur_redis.py index af179b9b..ec66df19 100644 --- a/aurweb/redis.py +++ b/aurweb/aur_redis.py @@ -2,9 +2,9 @@ import fakeredis from redis import ConnectionPool, Redis import aurweb.config -from aurweb import logging +from aurweb import aur_logging -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) pool = None diff --git a/aurweb/initdb.py b/aurweb/initdb.py index ded4330d..ee59212c 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -3,8 +3,8 @@ import argparse import alembic.command import alembic.config +import aurweb.aur_logging import aurweb.db -import aurweb.logging import aurweb.schema diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 0d638677..9846d996 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -10,12 +10,12 @@ from sqlalchemy.orm import backref, relationship import aurweb.config import aurweb.models.account_type import aurweb.schema -from aurweb import db, logging, schema, time, util +from aurweb import aur_logging, db, schema, time, util from aurweb.models.account_type import AccountType as _AccountType from aurweb.models.ban import is_banned from aurweb.models.declarative import Base -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) SALT_ROUNDS_DEFAULT = 12 diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index b6ba7e20..cddec0ac 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -7,11 +7,11 @@ from fastapi import HTTPException from sqlalchemy import orm from aurweb import config, db, models +from aurweb.aur_redis import redis_connection from aurweb.models import Package from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_relation import PackageRelation -from aurweb.redis import redis_connection from aurweb.templates import register_filter Providers = list[Union[PackageRelation, OfficialProvider]] diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index a453cb36..56ba738d 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -1,6 +1,6 @@ from fastapi import Request -from aurweb import db, logging, util +from aurweb import aur_logging, db, util from aurweb.auth import creds from aurweb.models import PackageBase, User from aurweb.models.package_comaintainer import PackageComaintainer @@ -10,7 +10,7 @@ from aurweb.packages.requests import handle_request, update_closure_comment from aurweb.pkgbase import util as pkgbaseutil from aurweb.scripts import notify, popupdate -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) @db.retry_deadlock diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index 0bbea4be..b8b7984f 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -5,9 +5,9 @@ from prometheus_fastapi_instrumentator import Instrumentator from prometheus_fastapi_instrumentator.metrics import Info from starlette.routing import Match, Route -from aurweb import logging +from aurweb import aur_logging -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) _instrumentator = Instrumentator() diff --git a/aurweb/ratelimit.py b/aurweb/ratelimit.py index 97923a52..ea191972 100644 --- a/aurweb/ratelimit.py +++ b/aurweb/ratelimit.py @@ -1,11 +1,11 @@ from fastapi import Request from redis.client import Pipeline -from aurweb import config, db, logging, time +from aurweb import aur_logging, config, db, time +from aurweb.aur_redis import redis_connection from aurweb.models import ApiRateLimit -from aurweb.redis import redis_connection -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def _update_ratelimit_redis(request: Request, pipeline: Pipeline): diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 12e59b30..24aacdf7 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -8,7 +8,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, or_ import aurweb.config -from aurweb import cookies, db, l10n, logging, models, util +from aurweb import aur_logging, cookies, db, l10n, models, util from aurweb.auth import account_type_required, creds, requires_auth, requires_guest from aurweb.captcha import get_captcha_salts from aurweb.exceptions import ValidationError, handle_form_exceptions @@ -22,7 +22,7 @@ from aurweb.users import update, validate from aurweb.users.util import get_user_by_name router = APIRouter() -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) @router.get("/passreset", response_class=HTMLResponse) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index da1ffd55..f5e6657f 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -16,7 +16,7 @@ from sqlalchemy import and_, case, or_ import aurweb.config import aurweb.models.package_request -from aurweb import cookies, db, logging, models, time, util +from aurweb import aur_logging, cookies, db, models, time, util from aurweb.cache import db_count_cache from aurweb.exceptions import handle_form_exceptions from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID @@ -24,7 +24,7 @@ from aurweb.models.package_request import PENDING_ID from aurweb.packages.util import query_notified, query_voted, updated_packages from aurweb.templates import make_context, render_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) router = APIRouter() @@ -80,7 +80,7 @@ async def index(request: Request): bases = db.query(models.PackageBase) - redis = aurweb.redis.redis_connection() + redis = aurweb.aur_redis.redis_connection() cache_expire = 300 # Five minutes. # Package statistics. diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 55d2abf5..0d482521 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -5,7 +5,7 @@ from typing import Any from fastapi import APIRouter, Form, Query, Request, Response import aurweb.filters # noqa: F401 -from aurweb import config, db, defaults, logging, models, util +from aurweb import aur_logging, config, db, defaults, models, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import InvariantError, handle_form_exceptions from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID @@ -15,7 +15,7 @@ from aurweb.packages.util import get_pkg_or_base from aurweb.pkgbase import actions as pkgbase_actions, util as pkgbaseutil from aurweb.templates import make_context, make_variable_context, render_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) router = APIRouter() diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 3b1ab688..9dab76f8 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Form, HTTPException, Query, Request, Response from fastapi.responses import JSONResponse, RedirectResponse from sqlalchemy import and_ -from aurweb import config, db, l10n, logging, templates, time, util +from aurweb import aur_logging, config, db, l10n, templates, time, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import InvariantError, ValidationError, handle_form_exceptions from aurweb.models import PackageBase @@ -21,7 +21,7 @@ from aurweb.scripts import notify, popupdate from aurweb.scripts.rendercomment import update_comment_render_fastapi from aurweb.templates import make_variable_context, render_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) router = APIRouter() diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 37edb072..4248347d 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import RedirectResponse, Response from sqlalchemy import and_, func, or_ -from aurweb import db, l10n, logging, models, time +from aurweb import aur_logging, db, l10n, models, time from aurweb.auth import creds, requires_auth from aurweb.exceptions import handle_form_exceptions from aurweb.models import User @@ -15,7 +15,7 @@ from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID from aurweb.templates import make_context, make_variable_context, render_template router = APIRouter() -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) # Some TU route specific constants. ITEMS_PER_PAGE = 10 # Paged table size. diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index bfdd12b4..e74bbf25 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -32,11 +32,11 @@ import orjson from sqlalchemy import literal, orm import aurweb.config -from aurweb import db, filters, logging, models, util +from aurweb import aur_logging, db, filters, models, util from aurweb.benchmark import Benchmark from aurweb.models import Package, PackageBase, User -logger = logging.get_logger("aurweb.scripts.mkpkglists") +logger = aur_logging.get_logger("aurweb.scripts.mkpkglists") TYPE_MAP = { diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index f19438bb..93108cd3 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -13,7 +13,7 @@ import aurweb.config import aurweb.db import aurweb.filters import aurweb.l10n -from aurweb import db, logging +from aurweb import aur_logging, db from aurweb.models import PackageBase, User from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comment import PackageComment @@ -22,7 +22,7 @@ from aurweb.models.package_request import PackageRequest from aurweb.models.request_type import RequestType from aurweb.models.tu_vote import TUVote -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) aur_location = aurweb.config.get("options", "aur_location") diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index ff6fe09c..4a2c84bd 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -9,10 +9,10 @@ import markdown import pygit2 import aurweb.config -from aurweb import db, logging, util +from aurweb import aur_logging, db, util from aurweb.models import PackageComment -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) class LinkifyExtension(markdown.extensions.Extension): diff --git a/aurweb/testing/alpm.py b/aurweb/testing/alpm.py index ddafb710..61a9315f 100644 --- a/aurweb/testing/alpm.py +++ b/aurweb/testing/alpm.py @@ -4,10 +4,10 @@ import re import shutil import subprocess -from aurweb import logging, util +from aurweb import aur_logging, util from aurweb.templates import base_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) class AlpmDatabase: diff --git a/aurweb/testing/filelock.py b/aurweb/testing/filelock.py index 33b42cb3..d582f0bf 100644 --- a/aurweb/testing/filelock.py +++ b/aurweb/testing/filelock.py @@ -4,9 +4,9 @@ from typing import Callable from posix_ipc import O_CREAT, Semaphore -from aurweb import logging +from aurweb import aur_logging -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def default_on_create(path): diff --git a/aurweb/users/validate.py b/aurweb/users/validate.py index 6c27a0b7..8fc68864 100644 --- a/aurweb/users/validate.py +++ b/aurweb/users/validate.py @@ -9,7 +9,7 @@ when encountering invalid criteria and return silently otherwise. from fastapi import Request from sqlalchemy import and_ -from aurweb import config, db, l10n, logging, models, time, util +from aurweb import aur_logging, config, db, l10n, models, time, util from aurweb.auth import creds from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token from aurweb.exceptions import ValidationError @@ -17,7 +17,7 @@ from aurweb.models.account_type import ACCOUNT_TYPE_NAME from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.util import strtobool -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def invalid_fields(E: str = str(), U: str = str(), **kwargs) -> None: diff --git a/aurweb/util.py b/aurweb/util.py index 432b818a..cda12af1 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -15,9 +15,9 @@ from email_validator import EmailSyntaxError, validate_email from fastapi.responses import JSONResponse import aurweb.config -from aurweb import defaults, logging +from aurweb import aur_logging, defaults -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def make_random_string(length: int) -> str: diff --git a/test/conftest.py b/test/conftest.py index aac221f7..15a982aa 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -52,12 +52,12 @@ from sqlalchemy.orm import scoped_session import aurweb.config import aurweb.db -from aurweb import initdb, logging, testing +from aurweb import aur_logging, initdb, testing from aurweb.testing.email import Email from aurweb.testing.filelock import FileLock from aurweb.testing.git import GitRepository -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) # Synchronization lock for database setup. setup_lock = Lock() diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index f4034a9a..33baa0ea 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -11,7 +11,7 @@ from fastapi.testclient import TestClient import aurweb.config import aurweb.models.account_type as at -from aurweb import captcha, db, logging, time +from aurweb import aur_logging, captcha, db, time from aurweb.asgi import app from aurweb.db import create, query from aurweb.models.accepted_term import AcceptedTerm @@ -31,7 +31,7 @@ from aurweb.models.user import User from aurweb.testing.html import get_errors from aurweb.testing.requests import Request -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) # Some test global constants. TEST_USERNAME = "test" diff --git a/test/test_asgi.py b/test/test_asgi.py index 6ff80fa3..3b794c76 100644 --- a/test/test_asgi.py +++ b/test/test_asgi.py @@ -10,17 +10,17 @@ from fastapi import HTTPException from fastapi.testclient import TestClient import aurweb.asgi +import aurweb.aur_redis import aurweb.config -import aurweb.redis from aurweb.exceptions import handle_form_exceptions from aurweb.testing.requests import Request @pytest.fixture def setup(db_test, email_test): - aurweb.redis.redis_connection().flushall() + aurweb.aur_redis.redis_connection().flushall() yield - aurweb.redis.redis_connection().flushall() + aurweb.aur_redis.redis_connection().flushall() @pytest.fixture diff --git a/test/test_homepage.py b/test/test_homepage.py index 5490a244..521f71c4 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -7,6 +7,7 @@ from fastapi.testclient import TestClient from aurweb import db, time from aurweb.asgi import app +from aurweb.aur_redis import redis_connection from aurweb.models.account_type import USER_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -14,7 +15,6 @@ from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_request import PackageRequest from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User -from aurweb.redis import redis_connection from aurweb.testing.html import parse_root from aurweb.testing.requests import Request diff --git a/test/test_logging.py b/test/test_logging.py index 63092d07..90d13c93 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -1,6 +1,6 @@ -from aurweb import logging +from aurweb import aur_logging -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def test_logging(caplog): diff --git a/test/test_packages_util.py b/test/test_packages_util.py index 0042cd71..a5273b68 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -2,6 +2,7 @@ import pytest from fastapi.testclient import TestClient from aurweb import asgi, config, db, time +from aurweb.aur_redis import kill_redis from aurweb.models.account_type import USER_ID from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.package import Package @@ -11,7 +12,6 @@ from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote from aurweb.models.user import User from aurweb.packages import util -from aurweb.redis import kill_redis @pytest.fixture(autouse=True) diff --git a/test/test_ratelimit.py b/test/test_ratelimit.py index 20528847..b7cd7e7d 100644 --- a/test/test_ratelimit.py +++ b/test/test_ratelimit.py @@ -3,13 +3,13 @@ from unittest import mock import pytest from redis.client import Pipeline -from aurweb import config, db, logging +from aurweb import aur_logging, config, db +from aurweb.aur_redis import redis_connection from aurweb.models import ApiRateLimit from aurweb.ratelimit import check_ratelimit -from aurweb.redis import redis_connection from aurweb.testing.requests import Request -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) @pytest.fixture(autouse=True) diff --git a/test/test_redis.py b/test/test_redis.py index a66cd204..6f9bdb40 100644 --- a/test/test_redis.py +++ b/test/test_redis.py @@ -3,11 +3,11 @@ from unittest import mock import pytest import aurweb.config -from aurweb.redis import redis_connection +from aurweb.aur_redis import redis_connection @pytest.fixture -def rediss(): +def redis(): """Create a RedisStub.""" def mock_get(section, key): @@ -21,20 +21,20 @@ def rediss(): yield redis -def test_redis_stub(rediss): +def test_redis_stub(redis): # We don't yet have a test key set. - assert rediss.get("test") is None + assert redis.get("test") is None # Set the test key to abc. - rediss.set("test", "abc") - assert rediss.get("test").decode() == "abc" + redis.set("test", "abc") + assert redis.get("test").decode() == "abc" # Test expire. - rediss.expire("test", 0) - assert rediss.get("test") is None + redis.expire("test", 0) + assert redis.get("test") is None # Now, set the test key again and use delete() on it. - rediss.set("test", "abc") - assert rediss.get("test").decode() == "abc" - rediss.delete("test") - assert rediss.get("test") is None + redis.set("test", "abc") + assert redis.get("test").decode() == "abc" + redis.delete("test") + assert redis.get("test") is None diff --git a/test/test_rendercomment.py b/test/test_rendercomment.py index 5b7ff5ac..59eb7191 100644 --- a/test/test_rendercomment.py +++ b/test/test_rendercomment.py @@ -2,14 +2,14 @@ from unittest import mock import pytest -from aurweb import config, db, logging, time +from aurweb import aur_logging, config, db, time from aurweb.models import Package, PackageBase, PackageComment, User from aurweb.models.account_type import USER_ID from aurweb.scripts import rendercomment from aurweb.scripts.rendercomment import update_comment_render from aurweb.testing.git import GitRepository -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) aur_location = config.get("options", "aur_location") diff --git a/test/test_rpc.py b/test/test_rpc.py index 84ddd8d7..f417d379 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -10,6 +10,7 @@ from redis.client import Pipeline import aurweb.models.dependency_type as dt import aurweb.models.relation_type as rt from aurweb import asgi, config, db, rpc, scripts, time +from aurweb.aur_redis import redis_connection from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID from aurweb.models.license import License @@ -22,7 +23,6 @@ from aurweb.models.package_relation import PackageRelation from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import PROVIDES_ID from aurweb.models.user import User -from aurweb.redis import redis_connection @pytest.fixture diff --git a/test/test_rss.py b/test/test_rss.py index 8526caa1..d227a183 100644 --- a/test/test_rss.py +++ b/test/test_rss.py @@ -4,14 +4,14 @@ import lxml.etree import pytest from fastapi.testclient import TestClient -from aurweb import db, logging, time +from aurweb import aur_logging, db, time from aurweb.asgi import app from aurweb.models.account_type import AccountType from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.user import User -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) @pytest.fixture(autouse=True) From 8555e232aeb331d3104fba5ec6b71341f979628b Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 22 Oct 2022 20:15:46 +0100 Subject: [PATCH 1170/1451] docs: fix mailing list after migration to mailman3 Closes: #396 Signed-off-by: Leonidas Spyropoulos --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58612a36..c8d4f90d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Before sending patches, you are recommended to run `flake8` and `isort`. You can add a git hook to do this by installing `python-pre-commit` and running `pre-commit install`. -[1]: https://lists.archlinux.org/listinfo/aur-dev +[1]: https://lists.archlinux.org/mailman3/lists/aur-dev.lists.archlinux.org/ [2]: https://gitlab.archlinux.org/archlinux/aurweb ### Coding Guidelines From 0417603499f890a475eb7890bad3ba63c44637ca Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 22 Oct 2022 21:48:40 +0100 Subject: [PATCH 1171/1451] housekeep: bump renovate dependencies email-validator: 1.2.1 -> ^1.3.0 uvicorn: ^0.18.0 -> ^0.19.0 fastapi: ^0.83.0 -> ^0.85.0 pytest-asyncio: ^0.19.0 -> ^0.20.1 pytest-cov ^3.0.0 -> ^4.0.0 Signed-off-by: Leonidas Spyropoulos --- poetry.lock | 869 ++++++++++++++++++++++++++----------------------- pyproject.toml | 10 +- 2 files changed, 466 insertions(+), 413 deletions(-) diff --git a/poetry.lock b/poetry.lock index ef2c70f9..9cf24f9a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,7 +23,7 @@ tz = ["python-dateutil"] [[package]] name = "anyio" -version = "3.6.1" +version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false @@ -36,7 +36,7 @@ sniffio = ">=1.1" [package.extras] doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +trio = ["trio (>=0.16,<0.22)"] [[package]] name = "asgiref" @@ -69,11 +69,11 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "authlib" -version = "1.0.1" +version = "1.1.0" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." category = "main" optional = false @@ -84,7 +84,7 @@ cryptography = ">=3.2" [[package]] name = "bcrypt" -version = "4.0.0" +version = "4.0.1" description = "Modern password hashing for your software and your servers" category = "main" optional = false @@ -112,7 +112,7 @@ dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0 [[package]] name = "certifi" -version = "2022.6.15" +version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -138,7 +138,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" @@ -161,7 +161,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.4" +version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -175,7 +175,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "37.0.4" +version = "38.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -188,7 +188,7 @@ cffi = ">=1.12" docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] @@ -204,7 +204,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest (<5)", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "pytest", "pytest-cov", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] +dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] [[package]] name = "dnspython" @@ -224,8 +224,8 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "email-validator" -version = "1.2.1" -description = "A robust email syntax and deliverability validation library." +version = "1.3.0" +description = "A robust email address syntax and deliverability validation library." category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" @@ -247,7 +247,7 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.9.0" +version = "1.9.4" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false @@ -255,7 +255,6 @@ python-versions = ">=3.7,<4.0" [package.dependencies] redis = "<4.4" -six = ">=1.16.0,<2.0.0" sortedcontainers = ">=2.4.0,<3.0.0" [package.extras] @@ -264,21 +263,21 @@ lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "fastapi" -version = "0.83.0" +version = "0.85.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.19.1" +starlette = "0.20.4" [package.extras] -all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] -dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +all = ["email-validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] +dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.7.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-orjson (==3.6.2)", "types-ujson (==5.4.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "feedgen" @@ -306,14 +305,14 @@ testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pyt [[package]] name = "greenlet" -version = "1.1.2" +version = "1.1.3.post0" description = "Lightweight in-process concurrent programming" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] -docs = ["sphinx"] +docs = ["Sphinx"] [[package]] name = "gunicorn" @@ -323,6 +322,9 @@ category = "main" optional = false python-versions = ">=3.5" +[package.dependencies] +setuptools = ">=3.0" + [package.extras] eventlet = ["eventlet (>=0.24.1)"] gevent = ["gevent (>=1.4.0)"] @@ -411,7 +413,7 @@ toml = "*" wsproto = ">=0.14.0" [package.extras] -docs = ["pydata-sphinx-theme"] +docs = ["pydata_sphinx_theme"] h3 = ["aioquic (>=0.9.0,<1.0)"] trio = ["trio (>=0.11.0)"] uvloop = ["uvloop"] @@ -426,7 +428,7 @@ python-versions = ">=3.6.1" [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false @@ -434,7 +436,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.12.0" +version = "5.0.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -444,9 +446,9 @@ python-versions = ">=3.7" zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -489,12 +491,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] -htmlsoup = ["beautifulsoup4"] +htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=0.29.7)"] [[package]] name = "mako" -version = "1.2.1" +version = "1.2.3" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "main" optional = false @@ -504,7 +506,7 @@ python-versions = ">=3.7" MarkupSafe = ">=0.9.2" [package.extras] -babel = ["babel"] +babel = ["Babel"] lingua = ["lingua"] testing = ["pytest"] @@ -540,7 +542,7 @@ python-versions = ">=3.5" [[package]] name = "orjson" -version = "3.7.12" +version = "3.8.0" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false @@ -603,7 +605,7 @@ python-versions = ">=3.6.1" [[package]] name = "prometheus-client" -version = "0.14.1" +version = "0.15.0" description = "Python client for the Prometheus monitoring system." category = "main" optional = false @@ -614,7 +616,7 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "5.8.2" +version = "5.9.1" description = "Instrument your FastAPI with Prometheus metrics" category = "main" optional = false @@ -626,7 +628,7 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "4.21.5" +version = "4.21.8" description = "" category = "main" optional = false @@ -658,14 +660,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" -version = "1.9.2" +version = "1.10.2" description = "Data validation and settings management using python type hints" category = "main" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] -typing-extensions = ">=3.7.4.3" +typing-extensions = ">=4.1.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] @@ -673,7 +675,7 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygit2" -version = "1.10.0" +version = "1.10.1" description = "Python bindings for libgit2." category = "main" optional = false @@ -715,7 +717,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. [[package]] name = "pytest-asyncio" -version = "0.19.0" +version = "0.20.1" description = "Pytest support for asyncio" category = "dev" optional = false @@ -729,7 +731,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy [[package]] name = "pytest-cov" -version = "3.0.0" +version = "4.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false @@ -839,7 +841,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rfc3986" @@ -855,6 +857,19 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] +[[package]] +name = "setuptools" +version = "65.5.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -865,11 +880,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "sniffio" -version = "1.2.0" +version = "1.3.0" description = "Sniff out which async library your code is running under" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [[package]] name = "sortedcontainers" @@ -881,7 +896,7 @@ python-versions = "*" [[package]] name = "sqlalchemy" -version = "1.4.40" +version = "1.4.42" description = "Database Abstraction Library" category = "main" optional = false @@ -895,21 +910,21 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] -mssql_pymssql = ["pymssql"] -mssql_pyodbc = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] -mysql_connector = ["mysql-connector-python"] +mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] -postgresql_psycopg2binary = ["psycopg2-binary"] -postgresql_psycopg2cffi = ["psycopg2cffi"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql", "pymysql (<1)"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "srcinfo" @@ -924,11 +939,11 @@ parse = "*" [[package]] name = "starlette" -version = "0.19.1" +version = "0.20.4" description = "The little ASGI library that shines." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] anyio = ">=3.4.0,<5" @@ -938,7 +953,7 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\"" full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] [[package]] -name = "tap.py" +name = "tap-py" version = "3.1" description = "Test Anything Protocol (TAP) tools" category = "dev" @@ -966,7 +981,7 @@ python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -974,7 +989,7 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.11" +version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -982,12 +997,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.18.3" +version = "0.19.0" description = "The lightning-fast ASGI server." category = "main" optional = false @@ -998,7 +1013,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] [[package]] name = "webencodings" @@ -1032,7 +1047,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "wsproto" -version = "1.1.0" +version = "1.2.0" description = "WebSockets state-machine based protocol implementation" category = "main" optional = false @@ -1043,20 +1058,20 @@ h11 = ">=0.9.0,<1" [[package]] name = "zipp" -version = "3.8.1" +version = "3.9.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "e1f9d796eea832af84c40c754ee3c58e633e98bd7cdb42a985b2c8657e82037e" +content-hash = "de9f0dc1d7e3f149a83629ad30d161da38aa1498b81aaa8bdfd2ebed50f232ab" [metadata.files] aiofiles = [ @@ -1068,8 +1083,8 @@ alembic = [ {file = "alembic-1.8.1.tar.gz", hash = "sha256:cd0b5e45b14b706426b833f06369b9a6d5ee03f826ec3238723ce8caaf6e5ffa"}, ] anyio = [ - {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, - {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, ] asgiref = [ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, @@ -1084,30 +1099,39 @@ attrs = [ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] authlib = [ - {file = "Authlib-1.0.1-py2.py3-none-any.whl", hash = "sha256:1286e2d5ef5bfe5a11cc2d0a0d1031f0393f6ce4d61f5121cfe87fa0054e98bd"}, - {file = "Authlib-1.0.1.tar.gz", hash = "sha256:6e74a4846ac36dfc882b3cc2fbd3d9eb410a627f2f2dc11771276655345223b1"}, + {file = "Authlib-1.1.0-py2.py3-none-any.whl", hash = "sha256:be4b6a1dea51122336c210a6945b27a105b9ac572baffd15b07bcff4376c1523"}, + {file = "Authlib-1.1.0.tar.gz", hash = "sha256:0a270c91409fc2b7b0fbee6996e09f2ee3187358762111a9a4225c874b94e891"}, ] bcrypt = [ - {file = "bcrypt-4.0.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:845b1daf4df2dd94d2fdbc9454953ca9dd0e12970a0bfc9f3dcc6faea3fa96e4"}, - {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8780e69f9deec9d60f947b169507d2c9816e4f11548f1f7ebee2af38b9b22ae4"}, - {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c3334446fac200499e8bc04a530ce3cf0b3d7151e0e4ac5c0dddd3d95e97843"}, - {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb67f6a6c72dfb0a02f3df51550aa1862708e55128b22543e2b42c74f3620d7"}, - {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:7c7dd6c1f05bf89e65261d97ac3a6520f34c2acb369afb57e3ea4449be6ff8fd"}, - {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:594780b364fb45f2634c46ec8d3e61c1c0f1811c4f2da60e8eb15594ecbf93ed"}, - {file = "bcrypt-4.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d0dd19aad87e4ab882ef1d12df505f4c52b28b69666ce83c528f42c07379227"}, - {file = "bcrypt-4.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bf413f2a9b0a2950fc750998899013f2e718d20fa4a58b85ca50b6df5ed1bbf9"}, - {file = "bcrypt-4.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ede0f506554571c8eda80db22b83c139303ec6b595b8f60c4c8157bdd0bdee36"}, - {file = "bcrypt-4.0.0-cp36-abi3-win32.whl", hash = "sha256:dc6ec3dc19b1c193b2f7cf279d3e32e7caf447532fbcb7af0906fe4398900c33"}, - {file = "bcrypt-4.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:0b0f0c7141622a31e9734b7f649451147c04ebb5122327ac0bd23744df84be90"}, - {file = "bcrypt-4.0.0.tar.gz", hash = "sha256:c59c170fc9225faad04dde1ba61d85b413946e8ce2e5f5f5ff30dfd67283f319"}, + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, ] bleach = [ {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, ] certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, @@ -1188,80 +1212,84 @@ colorama = [ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] coverage = [ - {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, - {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, - {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, - {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, - {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, - {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, - {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, - {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, - {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, - {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, - {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, - {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, - {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, - {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, - {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, - {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] cryptography = [ - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, - {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, - {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, - {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, + {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, + {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, + {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, ] deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, @@ -1272,20 +1300,20 @@ dnspython = [ {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, ] email-validator = [ - {file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"}, - {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, + {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"}, + {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, ] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] fakeredis = [ - {file = "fakeredis-1.9.0-py3-none-any.whl", hash = "sha256:868467ff399520fc77e37ff002c60d1b2a1674742982e27338adaeebcc537648"}, - {file = "fakeredis-1.9.0.tar.gz", hash = "sha256:60639946e3bb1274c30416f539f01f9d73b4ea68c244c1442f5524e45f51e882"}, + {file = "fakeredis-1.9.4-py3-none-any.whl", hash = "sha256:61afe14095aad3e7413a0a6fe63041da1b4bc3e41d5228a33b60bd03fabf22d8"}, + {file = "fakeredis-1.9.4.tar.gz", hash = "sha256:17415645d11994061f5394f3f1c76ba4531f3f8b63f9c55a8fd2120bebcbfae9"}, ] fastapi = [ - {file = "fastapi-0.83.0-py3-none-any.whl", hash = "sha256:694a2b6c2607a61029a4be1c6613f84d74019cb9f7a41c7a475dca8e715f9368"}, - {file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"}, + {file = "fastapi-0.85.1-py3-none-any.whl", hash = "sha256:de3166b6b1163dc22da4dc4ebdc3192fcbac7700dd1870a1afa44de636a636b5"}, + {file = "fastapi-0.85.1.tar.gz", hash = "sha256:1facd097189682a4ff11cbd01334a992e51b56be663b2bd50c2c09523624f144"}, ] feedgen = [ {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, @@ -1295,61 +1323,72 @@ filelock = [ {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] greenlet = [ - {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, - {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, - {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, - {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, - {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, - {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, - {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, - {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, - {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, - {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, - {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, - {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, - {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, - {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, - {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, - {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, - {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, - {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, - {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, - {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, - {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, - {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, - {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, - {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, - {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, - {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, - {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, - {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, - {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, - {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, - {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, - {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9649891ab4153f217f319914455ccf0b86986b55fc0573ce803eb998ad7d6854"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-win32.whl", hash = "sha256:11fc7692d95cc7a6a8447bb160d98671ab291e0a8ea90572d582d57361360f05"}, + {file = "greenlet-1.1.3.post0-cp27-cp27m-win_amd64.whl", hash = "sha256:05ae7383f968bba4211b1fbfc90158f8e3da86804878442b4fb6c16ccbcaa519"}, + {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ccbe7129a282ec5797df0451ca1802f11578be018a32979131065565da89b392"}, + {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8b58232f5b72973350c2b917ea3df0bebd07c3c82a0a0e34775fc2c1f857e9"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f6661b58412879a2aa099abb26d3c93e91dedaba55a6394d1fb1512a77e85de9"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c6e942ca9835c0b97814d14f78da453241837419e0d26f7403058e8db3e38f8"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a812df7282a8fc717eafd487fccc5ba40ea83bb5b13eb3c90c446d88dbdfd2be"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a7a6560df073ec9de2b7cb685b199dfd12519bc0020c62db9d1bb522f989fa"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17a69967561269b691747e7f436d75a4def47e5efcbc3c573180fc828e176d80"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:60839ab4ea7de6139a3be35b77e22e0398c270020050458b3d25db4c7c394df5"}, + {file = "greenlet-1.1.3.post0-cp310-cp310-win_amd64.whl", hash = "sha256:8926a78192b8b73c936f3e87929931455a6a6c6c385448a07b9f7d1072c19ff3"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c6f90234e4438062d6d09f7d667f79edcc7c5e354ba3a145ff98176f974b8132"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814f26b864ed2230d3a7efe0336f5766ad012f94aad6ba43a7c54ca88dd77cba"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fda1139d87ce5f7bd80e80e54f9f2c6fe2f47983f1a6f128c47bf310197deb6"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0643250dd0756f4960633f5359884f609a234d4066686754e834073d84e9b51"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cb863057bed786f6622982fb8b2c122c68e6e9eddccaa9fa98fd937e45ee6c4f"}, + {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c0581077cf2734569f3e500fab09c0ff6a2ab99b1afcacbad09b3c2843ae743"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:695d0d8b5ae42c800f1763c9fce9d7b94ae3b878919379150ee5ba458a460d57"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5662492df0588a51d5690f6578f3bbbd803e7f8d99a99f3bf6128a401be9c269"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:bffba15cff4802ff493d6edcf20d7f94ab1c2aee7cfc1e1c7627c05f1102eee8"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-win32.whl", hash = "sha256:7afa706510ab079fd6d039cc6e369d4535a48e202d042c32e2097f030a16450f"}, + {file = "greenlet-1.1.3.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:3a24f3213579dc8459e485e333330a921f579543a5214dbc935bc0763474ece3"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:64e10f303ea354500c927da5b59c3802196a07468332d292aef9ddaca08d03dd"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:eb6ac495dccb1520667cfea50d89e26f9ffb49fa28496dea2b95720d8b45eb54"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:88720794390002b0c8fa29e9602b395093a9a766b229a847e8d88349e418b28a"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39464518a2abe9c505a727af7c0b4efff2cf242aa168be5f0daa47649f4d7ca8"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0914f02fcaa8f84f13b2df4a81645d9e82de21ed95633765dd5cc4d3af9d7403"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96656c5f7c95fc02c36d4f6ef32f4e94bb0b6b36e6a002c21c39785a4eec5f5d"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4f74aa0092602da2069df0bc6553919a15169d77bcdab52a21f8c5242898f519"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3aeac044c324c1a4027dca0cde550bd83a0c0fbff7ef2c98df9e718a5086c194"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-win32.whl", hash = "sha256:fe7c51f8a2ab616cb34bc33d810c887e89117771028e1e3d3b77ca25ddeace04"}, + {file = "greenlet-1.1.3.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:70048d7b2c07c5eadf8393e6398595591df5f59a2f26abc2f81abca09610492f"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:66aa4e9a726b70bcbfcc446b7ba89c8cec40f405e51422c39f42dfa206a96a05"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:025b8de2273d2809f027d347aa2541651d2e15d593bbce0d5f502ca438c54136"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:82a38d7d2077128a017094aff334e67e26194f46bd709f9dcdacbf3835d47ef5"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7d20c3267385236b4ce54575cc8e9f43e7673fc761b069c820097092e318e3b"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8ece5d1a99a2adcb38f69af2f07d96fb615415d32820108cd340361f590d128"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2794eef1b04b5ba8948c72cc606aab62ac4b0c538b14806d9c0d88afd0576d6b"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a8d24eb5cb67996fb84633fdc96dbc04f2d8b12bfcb20ab3222d6be271616b67"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0120a879aa2b1ac5118bce959ea2492ba18783f65ea15821680a256dfad04754"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-win32.whl", hash = "sha256:bef49c07fcb411c942da6ee7d7ea37430f830c482bf6e4b72d92fd506dd3a427"}, + {file = "greenlet-1.1.3.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:62723e7eb85fa52e536e516ee2ac91433c7bb60d51099293671815ff49ed1c21"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d25cdedd72aa2271b984af54294e9527306966ec18963fd032cc851a725ddc1b"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:924df1e7e5db27d19b1359dc7d052a917529c95ba5b8b62f4af611176da7c8ad"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ec615d2912b9ad807afd3be80bf32711c0ff9c2b00aa004a45fd5d5dde7853d9"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0971d37ae0eaf42344e8610d340aa0ad3d06cd2eee381891a10fe771879791f9"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325f272eb997916b4a3fc1fea7313a8adb760934c2140ce13a2117e1b0a8095d"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75afcbb214d429dacdf75e03a1d6d6c5bd1fa9c35e360df8ea5b6270fb2211c"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c2d21c2b768d8c86ad935e404cc78c30d53dea009609c3ef3a9d49970c864b5"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:467b73ce5dcd89e381292fb4314aede9b12906c18fab903f995b86034d96d5c8"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-win32.whl", hash = "sha256:8149a6865b14c33be7ae760bcdb73548bb01e8e47ae15e013bf7ef9290ca309a"}, + {file = "greenlet-1.1.3.post0-cp38-cp38-win_amd64.whl", hash = "sha256:104f29dd822be678ef6b16bf0035dcd43206a8a48668a6cae4d2fe9c7a7abdeb"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c8c9301e3274276d3d20ab6335aa7c5d9e5da2009cccb01127bddb5c951f8870"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8415239c68b2ec9de10a5adf1130ee9cb0ebd3e19573c55ba160ff0ca809e012"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:3c22998bfef3fcc1b15694818fc9b1b87c6cc8398198b96b6d355a7bcb8c934e"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa1845944e62f358d63fcc911ad3b415f585612946b8edc824825929b40e59e"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:890f633dc8cb307761ec566bc0b4e350a93ddd77dc172839be122be12bae3e10"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf37343e43404699d58808e51f347f57efd3010cc7cee134cdb9141bd1ad9ea"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5edf75e7fcfa9725064ae0d8407c849456553a181ebefedb7606bac19aa1478b"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a954002064ee919b444b19c1185e8cce307a1f20600f47d6f4b6d336972c809"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-win32.whl", hash = "sha256:2ccdc818cc106cc238ff7eba0d71b9c77be868fdca31d6c3b1347a54c9b187b2"}, + {file = "greenlet-1.1.3.post0-cp39-cp39-win_amd64.whl", hash = "sha256:91a84faf718e6f8b888ca63d0b2d6d185c8e2a198d2a7322d75c303e7097c8b7"}, + {file = "greenlet-1.1.3.post0.tar.gz", hash = "sha256:f5e09dc5c6e1796969fd4b775ea1417d70e49a5df29aaa8e5d10675d9e11872c"}, ] gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, @@ -1384,12 +1423,12 @@ hyperframe = [ {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, ] idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, - {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, + {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, + {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1476,8 +1515,8 @@ lxml = [ {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, ] mako = [ - {file = "Mako-1.2.1-py3-none-any.whl", hash = "sha256:df3921c3081b013c8a2d5ff03c18375651684921ae83fd12e64800b7da923257"}, - {file = "Mako-1.2.1.tar.gz", hash = "sha256:f054a5ff4743492f1aa9ecc47172cb33b42b9d993cffcc146c9de17e717b0307"}, + {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"}, + {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"}, ] markdown = [ {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"}, @@ -1535,48 +1574,48 @@ mysqlclient = [ {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, ] orjson = [ - {file = "orjson-3.7.12-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:5fbf5ec736c952e150a4399862bdd0043c1597e4d9e64adebe750855e72e2f65"}, - {file = "orjson-3.7.12-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c09ed2e953447472c497ec682f4f40727744ed72672600e2e105ed5c373a82b1"}, - {file = "orjson-3.7.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdbbf6f8a23c66fa67661966891fd62341c5b7265e77fd6ecd7195aac26e76c0"}, - {file = "orjson-3.7.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a04df90f09e9c64c082d5e9af50e3e4c8cdc151b681f9d4928bb6bb17ef45c7b"}, - {file = "orjson-3.7.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:946d769d6e57e31838c8486e3f440540214690aaecca3bd2a57e31a227d27031"}, - {file = "orjson-3.7.12-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fff4760d3c04edcc99be0c9040b4cbb3f6c4ae5b4c4fc1ec1f70c3fe47a9ea5a"}, - {file = "orjson-3.7.12-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a7a57ab51d92235604044da31e1481e53b44b6df4688929dd8c176ff09381516"}, - {file = "orjson-3.7.12-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0966b2f6db800ed40138df80040b84ba6a180f50af9b9a4ed5f7231114f6beb8"}, - {file = "orjson-3.7.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ec3f644f1a1e3b642050ee1428311eaec2b959ffb6122ebc216143e67a939b64"}, - {file = "orjson-3.7.12-cp310-none-win_amd64.whl", hash = "sha256:75a7d1b61300e76b06767dc60ff3f38af4a6634cb8169bc8e9db2b4124c27e6d"}, - {file = "orjson-3.7.12-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8c618af13ae16e050342018a9d019365c6f7d1cba04f42fd8d8ca1d1a604a54c"}, - {file = "orjson-3.7.12-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9ef5f5c5fd1d0086f9323dafacfa902c2f4f120f319e689457ee2a66aebfc889"}, - {file = "orjson-3.7.12-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:eec6d61468ee0f251ac33d8738942390fda4e1e36f2d9c365ac271a87e78004b"}, - {file = "orjson-3.7.12-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:277ac2591570d88d5501cbf5855fc4a421cc51f3075b3be1b50ef2f8e8d2d014"}, - {file = "orjson-3.7.12-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae14ccad9b912abfee0e598a9fb57b6888ec3d2121983b757d9135702d1ab035"}, - {file = "orjson-3.7.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53b8ac02e683286e1979f1c57c026503c2433a26525adb1671142b0b13d52a7c"}, - {file = "orjson-3.7.12-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:4d76fc5708cf1a7a394b42c1c697a8635fbce73730455870127815b8d7229bcf"}, - {file = "orjson-3.7.12-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:39717add544688a3a938fcbc4122cf1b31030ba8ea1145d12fc6ee29d0eabe27"}, - {file = "orjson-3.7.12-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93beb800fc35402db6c7d435fcf8b3e45822eb668d112c2def3e2851b3557bb1"}, - {file = "orjson-3.7.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:148b33d2a9f7e464e0a292f13fa11e226baf11b61495ad536977e800bc9ca845"}, - {file = "orjson-3.7.12-cp37-none-win_amd64.whl", hash = "sha256:2baefa5fb5133448f06d24b2523dfb3eda562a93bb69c33f539c7bbb8b0d61ed"}, - {file = "orjson-3.7.12-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:e6ae6d14062be5a210909f8816936e0b9b9747b8416d99ec927ab4b8d73bdce6"}, - {file = "orjson-3.7.12-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:22738105f3e926ef22702b14a9b79652f18f8dd45b798a126ee9644e0ac683d8"}, - {file = "orjson-3.7.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7e5aa0bf79f475c67d22eb4c085416ebb05042ce3c98abdbcfe11c1674d096d"}, - {file = "orjson-3.7.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8e7698b66ed751d9b887a27f5e02fb8405f06edafc47ac4542b2e10b2927f9e1"}, - {file = "orjson-3.7.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9accc4ba1cb83b70ac89f9de465b12e96bc6713158d27b655106413ed07944a"}, - {file = "orjson-3.7.12-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:5a9cc4f2231756b939f3aaa997024e748e06ac9bc5619343aa0e88b2833a567f"}, - {file = "orjson-3.7.12-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:e1082f82cfc2fd9ee42b3716900da8b13a2efd627a105438c5d98f2476ddcd54"}, - {file = "orjson-3.7.12-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a80722ed6545069d4f8fe16e02f5e9a67e09b6872c4c7501fa095d57471d96a6"}, - {file = "orjson-3.7.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b37eba028ef4f55587ac4eb6dffc5207a884cb506f79e4104f2d5587f163a676"}, - {file = "orjson-3.7.12-cp38-none-win_amd64.whl", hash = "sha256:94cc18a7d20b1fc36f6a60ad98027a27e1462fb815cf0245728285df0ea6b5cf"}, - {file = "orjson-3.7.12-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:71975ed815c929e14351cfde6d74ea892e850f74b02eaa57d2b96cc8c3fbed7b"}, - {file = "orjson-3.7.12-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:5a45baa048b462774b3b777725416006b7eec4b70b1bfc40d895cfa65c5b5eac"}, - {file = "orjson-3.7.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bffc45cd04480be9f18b790f28d716dde117de43b02e0f702935b584fada1de"}, - {file = "orjson-3.7.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a7122f702fe62e79ff3e8a6f975b5559440345ace5618ee1d97c49230f2839b6"}, - {file = "orjson-3.7.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6d1fd006691ea9e500ebba753dea471daef8972260e8ef48b4f356daa2fb3d1"}, - {file = "orjson-3.7.12-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:4b5851c0acc2a35173ba5fa854e15bf6f18757fafe1f7cce0fbc7fc24af3ec8a"}, - {file = "orjson-3.7.12-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:0f2ddd043450579ba35bbcf34e9217ee4de0fc52716ae3eb6cfff5e24fcc0ba3"}, - {file = "orjson-3.7.12-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ff6006857688991e800e9d2d992195451e25353c47b313f0db859016ceb811b3"}, - {file = "orjson-3.7.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:818405b65fa9d9d37330e57d87f91b40c10d2469d16914c7a819d0d494af482c"}, - {file = "orjson-3.7.12-cp39-none-win_amd64.whl", hash = "sha256:c1e4297b5dee3e14e068cc35505b3e1a626dd3fb8d357842902616564d2f713f"}, - {file = "orjson-3.7.12.tar.gz", hash = "sha256:05f20fa1a368207d16ecdf16072c3be58f85c4954cd2ed6c9704463963b9791a"}, + {file = "orjson-3.8.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:9a93850a1bdc300177b111b4b35b35299f046148ba23020f91d6efd7bf6b9d20"}, + {file = "orjson-3.8.0-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:7536a2a0b41672f824912aeab545c2467a9ff5ca73a066ff04fb81043a0a177a"}, + {file = "orjson-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66c19399bb3b058e3236af7910b57b19a4fc221459d722ed72a7dc90370ca090"}, + {file = "orjson-3.8.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b391d5c2ddc2f302d22909676b306cb6521022c3ee306c861a6935670291b2c"}, + {file = "orjson-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb1042970ca5f544a047d6c235a7eb4acdb69df75441dd1dfcbc406377ab37"}, + {file = "orjson-3.8.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d189e2acb510e374700cb98cf11b54f0179916ee40f8453b836157ae293efa79"}, + {file = "orjson-3.8.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6a23b40c98889e9abac084ce5a1fb251664b41da9f6bdb40a4729e2288ed2ed4"}, + {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b68a42a31f8429728183c21fb440c21de1b62e5378d0d73f280e2d894ef8942e"}, + {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff13410ddbdda5d4197a4a4c09969cb78c722a67550f0a63c02c07aadc624833"}, + {file = "orjson-3.8.0-cp310-none-win_amd64.whl", hash = "sha256:2d81e6e56bbea44be0222fb53f7b255b4e7426290516771592738ca01dbd053b"}, + {file = "orjson-3.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e2defd9527651ad39ec20ae03c812adf47ef7662bdd6bc07dabb10888d70dc62"}, + {file = "orjson-3.8.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9e6ac22cec72d5b39035b566e4b86c74b84866f12b5b0b6541506a080fb67d6d"}, + {file = "orjson-3.8.0-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e2f4a5542f50e3d336a18cb224fc757245ca66b1fd0b70b5dd4471b8ff5f2b0e"}, + {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1418feeb8b698b9224b1f024555895169d481604d5d884498c1838d7412794c"}, + {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6e3da2e4bd27c3b796519ca74132c7b9e5348fb6746315e0f6c1592bc5cf1caf"}, + {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896a21a07f1998648d9998e881ab2b6b80d5daac4c31188535e9d50460edfcf7"}, + {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:4065906ce3ad6195ac4d1bddde862fe811a42d7be237a1ff762666c3a4bb2151"}, + {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:5f856279872a4449fc629924e6a083b9821e366cf98b14c63c308269336f7c14"}, + {file = "orjson-3.8.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1b1cd25acfa77935bb2e791b75211cec0cfc21227fe29387e553c545c3ff87e1"}, + {file = "orjson-3.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3e2459d441ab8fd8b161aa305a73d5269b3cda13b5a2a39eba58b4dd3e394f49"}, + {file = "orjson-3.8.0-cp37-none-win_amd64.whl", hash = "sha256:d2b5dafbe68237a792143137cba413447f60dd5df428e05d73dcba10c1ea6fcf"}, + {file = "orjson-3.8.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5b072ef8520cfe7bd4db4e3c9972d94336763c2253f7c4718a49e8733bada7b8"}, + {file = "orjson-3.8.0-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e68c699471ea3e2dd1b35bfd71c6a0a0e4885b64abbe2d98fce1ef11e0afaff3"}, + {file = "orjson-3.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7225e8b08996d1a0c804d3a641a53e796685e8c9a9fd52bd428980032cad9a"}, + {file = "orjson-3.8.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f687776a03c19f40b982fb5c414221b7f3d19097841571be2223d1569a59877"}, + {file = "orjson-3.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7990a9caf3b34016ac30be5e6cfc4e7efd76aa85614a1215b0eae4f0c7e3db59"}, + {file = "orjson-3.8.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:02d638d43951ba346a80f0abd5942a872cc87db443e073f6f6fc530fee81e19b"}, + {file = "orjson-3.8.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f4b46dbdda2f0bd6480c39db90b21340a19c3b0fcf34bc4c6e465332930ca539"}, + {file = "orjson-3.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:655d7387a1634a9a477c545eea92a1ee902ab28626d701c6de4914e2ed0fecd2"}, + {file = "orjson-3.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5edb93cdd3eb32977633fa7aaa6a34b8ab54d9c49cdcc6b0d42c247a29091b22"}, + {file = "orjson-3.8.0-cp38-none-win_amd64.whl", hash = "sha256:03ed95814140ff09f550b3a42e6821f855d981c94d25b9cc83e8cca431525d70"}, + {file = "orjson-3.8.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7b0e72974a5d3b101226899f111368ec2c9824d3e9804af0e5b31567f53ad98a"}, + {file = "orjson-3.8.0-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6ea5fe20ef97545e14dd4d0263e4c5c3bc3d2248d39b4b0aed4b84d528dfc0af"}, + {file = "orjson-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6433c956f4a18112342a18281e0bec67fcd8b90be3a5271556c09226e045d805"}, + {file = "orjson-3.8.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87462791dd57de2e3e53068bf4b7169c125c50960f1bdda08ed30c797cb42a56"}, + {file = "orjson-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be02f6acee33bb63862eeff80548cd6b8a62e2d60ad2d8dfd5a8824cc43d8887"}, + {file = "orjson-3.8.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:a709c2249c1f2955dbf879506fd43fa08c31fdb79add9aeb891e3338b648bf60"}, + {file = "orjson-3.8.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2065b6d280dc58f131ffd93393737961ff68ae7eb6884b68879394074cc03c13"}, + {file = "orjson-3.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fd6cac83136e06e538a4d17117eaeabec848c1e86f5742d4811656ad7ee475f"}, + {file = "orjson-3.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25b5e48fbb9f0b428a5e44cf740675c9281dd67816149fc33659803399adbbe8"}, + {file = "orjson-3.8.0-cp39-none-win_amd64.whl", hash = "sha256:2058653cc12b90e482beacb5c2d52dc3d7606f9e9f5a52c1c10ef49371e76f52"}, + {file = "orjson-3.8.0.tar.gz", hash = "sha256:fb42f7cf57d5804a9daa6b624e3490ec9e2631e042415f3aebe9f35a8492ba6c"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -1605,28 +1644,28 @@ priority = [ {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, ] prometheus-client = [ - {file = "prometheus_client-0.14.1-py3-none-any.whl", hash = "sha256:522fded625282822a89e2773452f42df14b5a8e84a86433e3f8a189c1d54dc01"}, - {file = "prometheus_client-0.14.1.tar.gz", hash = "sha256:5459c427624961076277fdc6dc50540e2bacb98eebde99886e59ec55ed92093a"}, + {file = "prometheus_client-0.15.0-py3-none-any.whl", hash = "sha256:db7c05cbd13a0f79975592d112320f2605a325969b270a94b71dcabc47b931d2"}, + {file = "prometheus_client-0.15.0.tar.gz", hash = "sha256:be26aa452490cfcf6da953f9436e95a9f2b4d578ca80094b4458930e5f584ab1"}, ] prometheus-fastapi-instrumentator = [ - {file = "prometheus-fastapi-instrumentator-5.8.2.tar.gz", hash = "sha256:f1fa362043b974d138f5245acc973c32d1fa798bd2bd98ef2754befbf385a566"}, - {file = "prometheus_fastapi_instrumentator-5.8.2-py3-none-any.whl", hash = "sha256:5bfec239a924e1fed4ba94eb0addc73422d11821e894200b6d0e36a61c966827"}, + {file = "prometheus-fastapi-instrumentator-5.9.1.tar.gz", hash = "sha256:3651a72f73359a28e8afb0d370ebe3774147323ee2285e21236b229ce79172fc"}, + {file = "prometheus_fastapi_instrumentator-5.9.1-py3-none-any.whl", hash = "sha256:b5206ea9aa6975a0b07f3bf7376932b8a1b2983164b5abb04878e75ba336d9ed"}, ] protobuf = [ - {file = "protobuf-4.21.5-cp310-abi3-win32.whl", hash = "sha256:5310cbe761e87f0c1decce019d23f2101521d4dfff46034f8a12a53546036ec7"}, - {file = "protobuf-4.21.5-cp310-abi3-win_amd64.whl", hash = "sha256:e5c5a2886ae48d22a9d32fbb9b6636a089af3cd26b706750258ce1ca96cc0116"}, - {file = "protobuf-4.21.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ee04f5823ed98bb9a8c3b1dc503c49515e0172650875c3f76e225b223793a1f2"}, - {file = "protobuf-4.21.5-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:b04484d6f42f48c57dd2737a72692f4c6987529cdd148fb5b8e5f616862a2e37"}, - {file = "protobuf-4.21.5-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:5e0b272217aad8971763960238c1a1e6a65d50ef7824e23300da97569a251c55"}, - {file = "protobuf-4.21.5-cp37-cp37m-win32.whl", hash = "sha256:5eb0724615e90075f1d763983e708e1cef08e66b1891d8b8b6c33bc3b2f1a02b"}, - {file = "protobuf-4.21.5-cp37-cp37m-win_amd64.whl", hash = "sha256:011c0f267e85f5d73750b6c25f0155d5db1e9443cd3590ab669a6221dd8fcdb0"}, - {file = "protobuf-4.21.5-cp38-cp38-win32.whl", hash = "sha256:7b6f22463e2d1053d03058b7b4ceca6e4ed4c14f8c286c32824df751137bf8e7"}, - {file = "protobuf-4.21.5-cp38-cp38-win_amd64.whl", hash = "sha256:b52e7a522911a40445a5f588bd5b5e584291bfc5545e09b7060685e4b2ff814f"}, - {file = "protobuf-4.21.5-cp39-cp39-win32.whl", hash = "sha256:a7faa62b183d6a928e3daffd06af843b4287d16ef6e40f331575ecd236a7974d"}, - {file = "protobuf-4.21.5-cp39-cp39-win_amd64.whl", hash = "sha256:5e0ce02418ef03d7657a420ae8fd6fec4995ac713a3cb09164e95f694dbcf085"}, - {file = "protobuf-4.21.5-py2.py3-none-any.whl", hash = "sha256:bf711b451212dc5b0fa45ae7dada07d8e71a4b0ff0bc8e4783ee145f47ac4f82"}, - {file = "protobuf-4.21.5-py3-none-any.whl", hash = "sha256:3ec6f5b37935406bb9df9b277e79f8ed81d697146e07ef2ba8a5a272fb24b2c9"}, - {file = "protobuf-4.21.5.tar.gz", hash = "sha256:eb1106e87e095628e96884a877a51cdb90087106ee693925ec0a300468a9be3a"}, + {file = "protobuf-4.21.8-cp310-abi3-win32.whl", hash = "sha256:c252c55ee15175aa1b21b7b9896e6add5162d066d5202e75c39f96136f08cce3"}, + {file = "protobuf-4.21.8-cp310-abi3-win_amd64.whl", hash = "sha256:809ca0b225d3df42655a12f311dd0f4148a943c51f1ad63c38343e457492b689"}, + {file = "protobuf-4.21.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bbececaf3cfea9ea65ebb7974e6242d310d2a7772a6f015477e0d79993af4511"}, + {file = "protobuf-4.21.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:b02eabb9ebb1a089ed20626a90ad7a69cee6bcd62c227692466054b19c38dd1f"}, + {file = "protobuf-4.21.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:4761201b93e024bb70ee3a6a6425d61f3152ca851f403ba946fb0cde88872661"}, + {file = "protobuf-4.21.8-cp37-cp37m-win32.whl", hash = "sha256:f2d55ff22ec300c4d954d3b0d1eeb185681ec8ad4fbecff8a5aee6a1cdd345ba"}, + {file = "protobuf-4.21.8-cp37-cp37m-win_amd64.whl", hash = "sha256:c5f94911dd8feb3cd3786fc90f7565c9aba7ce45d0f254afd625b9628f578c3f"}, + {file = "protobuf-4.21.8-cp38-cp38-win32.whl", hash = "sha256:b37b76efe84d539f16cba55ee0036a11ad91300333abd213849cbbbb284b878e"}, + {file = "protobuf-4.21.8-cp38-cp38-win_amd64.whl", hash = "sha256:2c92a7bfcf4ae76a8ac72e545e99a7407e96ffe52934d690eb29a8809ee44d7b"}, + {file = "protobuf-4.21.8-cp39-cp39-win32.whl", hash = "sha256:89d641be4b5061823fa0e463c50a2607a97833e9f8cfb36c2f91ef5ccfcc3861"}, + {file = "protobuf-4.21.8-cp39-cp39-win_amd64.whl", hash = "sha256:bc471cf70a0f53892fdd62f8cd4215f0af8b3f132eeee002c34302dff9edd9b6"}, + {file = "protobuf-4.21.8-py2.py3-none-any.whl", hash = "sha256:a55545ce9eec4030cf100fcb93e861c622d927ef94070c1a3c01922902464278"}, + {file = "protobuf-4.21.8-py3-none-any.whl", hash = "sha256:0f236ce5016becd989bf39bd20761593e6d8298eccd2d878eda33012645dc369"}, + {file = "protobuf-4.21.8.tar.gz", hash = "sha256:427426593b55ff106c84e4a88cac855175330cb6eb7e889e85aaa7b5652b686d"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -1640,76 +1679,81 @@ pycparser = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydantic = [ - {file = "pydantic-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e"}, - {file = "pydantic-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08"}, - {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c"}, - {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131"}, - {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76"}, - {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567"}, - {file = "pydantic-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044"}, - {file = "pydantic-1.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555"}, - {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84"}, - {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f"}, - {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb"}, - {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b"}, - {file = "pydantic-1.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001"}, - {file = "pydantic-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56"}, - {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4"}, - {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f"}, - {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979"}, - {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d"}, - {file = "pydantic-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7"}, - {file = "pydantic-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3"}, - {file = "pydantic-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa"}, - {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3"}, - {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0"}, - {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8"}, - {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8"}, - {file = "pydantic-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326"}, - {file = "pydantic-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801"}, - {file = "pydantic-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44"}, - {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747"}, - {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453"}, - {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb"}, - {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15"}, - {file = "pydantic-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55"}, - {file = "pydantic-1.9.2-py3-none-any.whl", hash = "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e"}, - {file = "pydantic-1.9.2.tar.gz", hash = "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d"}, + {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, + {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, + {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, + {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, + {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, + {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, + {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, + {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, + {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, + {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, + {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, + {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, + {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, + {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, + {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, + {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, + {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, + {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, + {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, + {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, + {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, + {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, + {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, + {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, + {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, + {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, + {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, + {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, + {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, + {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, + {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, + {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, + {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, + {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, + {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, + {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] pygit2 = [ - {file = "pygit2-1.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:493a0ce9cbc580855942cdcb2bf3b674f3295c26233e990bfa98058c321313f1"}, - {file = "pygit2-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:73a5fd0304252c84f5f9f1b5b0eadfa3641a04d11f96d89fbd77ffade52adc37"}, - {file = "pygit2-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f812ec8ea10e83b05a770f4f95808f729bc821e0548af69fd0a80e17876003"}, - {file = "pygit2-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a6e0866bb033b1dcfc62fedfe44b12dee92f619c6cfed7ca1de6867fba31f9"}, - {file = "pygit2-1.10.0-cp310-cp310-win32.whl", hash = "sha256:9b3b328ad53420a16908a5bba4923d3b26eef27a570802e68c5ed5afb0eca0f3"}, - {file = "pygit2-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:21021a48aa2151e5f0504d56099a194cffc0fede688703f8d0764edf186d802b"}, - {file = "pygit2-1.10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5717cd2fd1a0d23a2bbdf8ce4271aa72e1d283d258c88b341d9d9c4673707e73"}, - {file = "pygit2-1.10.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c4506581e816e2357adf4e9b642de8b386778cbf09bd870a9843ef9c9a5379"}, - {file = "pygit2-1.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c514d6d9b051f2f5f0d8e277ccefda3380bfbf38047e12c92f8e3f110d27314"}, - {file = "pygit2-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:2a23e157251a77f2cfd944ae119a730ef5fa66132eb15119b01b016650a1dbae"}, - {file = "pygit2-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c380f0a8c669aeaf71d9d73f8de16502dc050a6022f0571c77bd5efecf88492c"}, - {file = "pygit2-1.10.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a0f30d90e52a664a8b1a6ae30067e503a576fc53d40c6a1bc533dc67a70b1410"}, - {file = "pygit2-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9308e78f83e46c95db59128161a5dfe5f6a1652342238224142474d41a0d7011"}, - {file = "pygit2-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e64db8ec4ee0aaf6e726fa4655ea9cbde7a7f2cf34f134f25f6faa52a27d618"}, - {file = "pygit2-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f702aacea8ace3422e02ac20161a4f1afcf13bd0d20edd18726ff386165bbb"}, - {file = "pygit2-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e86bc4c74c40fb46156158c1dd774c1f0e50ee3a860af4131ce2ac1dfff4fc34"}, - {file = "pygit2-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:f1785ba78690b06581694b2e898b68cd1bc344417475c0b994d574b0d2010160"}, - {file = "pygit2-1.10.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e9d7db4fe6ddf8f7ab29c6a24a8a9bd0af92a214ad0e812b49eb7c411cddf3e"}, - {file = "pygit2-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56d44a3437a6d642c98a33830d8e3d2556e608abba412e451fec514702fa9a76"}, - {file = "pygit2-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8eb687e4bf7b46ca545f50eb25c5e9c41a86be59ae51e83ce42f7793658b560"}, - {file = "pygit2-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71e084dd1c075c0ca3f4b8015ce3cc2dd73fc1c0ead52b6a79990ec5ab7f67d9"}, - {file = "pygit2-1.10.0-cp39-cp39-win32.whl", hash = "sha256:e1f4d7e981c9240912cd587e9b5f1d00a03b79248fcf15add5e3944d11d21884"}, - {file = "pygit2-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2ee457af2d6ca47838d5ddd0c558af829e7db8d1402f61e4695024d3ce54301d"}, - {file = "pygit2-1.10.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55f05d86b5d635f98816183b1eadbc0349dc26451a58b1920051b2f7593b9d0a"}, - {file = "pygit2-1.10.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd17ebbbcf5c7f10e2dfbd2b7b6abdf5686069a0a1a84c72c1c6bf17c26c72dd"}, - {file = "pygit2-1.10.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2098086b479b6b744d5fb2822fd2d01d14d05ac84c34d911c46b609bd5435c18"}, - {file = "pygit2-1.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:01b66a9de0a753ccd2e835b8598f119bccb587bcde9f78adc24a73ead456b083"}, - {file = "pygit2-1.10.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ca00acb3d117d736d9dc1144092fdb899f95e1b1aba6d2c3b6df58b80b24dfb"}, - {file = "pygit2-1.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61db929b4bc52796f22516199ae697a594bbc205e97275f61365c0225ac1130"}, - {file = "pygit2-1.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:33cf14c6188e6494231547e581790e73e66114b5d5e6ef8617487b8a5e13e987"}, - {file = "pygit2-1.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23de3170eb76bcaa207fe23caea939bdfefeabdefc3f09191acf0d0b461ce87b"}, - {file = "pygit2-1.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a70e219feee75b18cfc78bd2cee755760be8ec4bedd40aa10d6cd257567a44a"}, - {file = "pygit2-1.10.0.tar.gz", hash = "sha256:7c751eee88c731b922e4e487ee287e2e40906b2bd32d0bfd2105947f63e867de"}, + {file = "pygit2-1.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3f60e47c6a7a87f18a112753eb98848f4c5333986bec1940558ce09cdaf53bf"}, + {file = "pygit2-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f0f69ea42231bebf08006c85cd5aa233c9c047c5a88b7fcfb4b639476b70e31b"}, + {file = "pygit2-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0097b6631ef05c837c4800fad559d0865a90c55475a18f38c6f2f5a12750e914"}, + {file = "pygit2-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b5bdcdfae205d9cc0c80bc53fad222a5ba67e66fd336ef223f86b0ac5835"}, + {file = "pygit2-1.10.1-cp310-cp310-win32.whl", hash = "sha256:3efd2a2ab2bb443e1b758525546d74a5a12fe27006194d3c02b3e6ecc1e101e6"}, + {file = "pygit2-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:11225811194ae6b9dbb34c2e8900e0eba6eacc180d82766e3dbddcbd2c6e6454"}, + {file = "pygit2-1.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:73e251d0b73f1010ad28c20bcdcf33e312fb363f10b7268ad2bcfa09770f9ac2"}, + {file = "pygit2-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cb73f7967207a9ac485722ef0e517e5ca482f3c1308a0ac934707cb267b0ac7a"}, + {file = "pygit2-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b115bef251af4daf18f2f967287b56da2eae2941d5389dc1666bd0160892d769"}, + {file = "pygit2-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd55a6cf7ad6276fb5772e5c60c51fca2d9a5e68ea3e7237847421c10080a68"}, + {file = "pygit2-1.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:33138c256ad0ff084f5d8a82ab7d280f9ed6706ebb000ac82e3d133e2d82b366"}, + {file = "pygit2-1.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f4f507e5cd775f6d5d95ec65761af4cdb33b2f859af15bf10a06d11efd0d3b2"}, + {file = "pygit2-1.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:752f844d5379081fae5ef78e3bf6f0f35ae9b11aafc37e5e03e1c3607b196806"}, + {file = "pygit2-1.10.1-cp37-cp37m-win32.whl", hash = "sha256:b31ffdbc87629613ae03a533e01eee79112a12f66faf375fa08934074044a664"}, + {file = "pygit2-1.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e09386b71ad474f2c2c02b6b251fa904b1145dabfe9095955ab30a789aaf84c0"}, + {file = "pygit2-1.10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:564e832e750f889aea3bb3e82674e1c860c9b89a141404530271e1341723a258"}, + {file = "pygit2-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43bb910272866eb822e930dbd0feecc340e0c24934143aab651fa180cc5ebfb0"}, + {file = "pygit2-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e4905cbb87db598b1cb38800ff995c0ba1f58745e2f52af4d54dbc93b9bda8"}, + {file = "pygit2-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f4689ce38cd62a7000d38602ba4d704df5cec708e5d98dadaffcf510f3317"}, + {file = "pygit2-1.10.1-cp38-cp38-win32.whl", hash = "sha256:b67ef30f3c022be1d6da9ef0188f60fc2d20639bff44693ef5653818e887001b"}, + {file = "pygit2-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:dcd849c44bd743d829dbd9dc9d7e13c14cf31a47c22e2e3f9e98fa845a8b8b28"}, + {file = "pygit2-1.10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e8bb9002924975271d64e8869b44ea97f068e85b5edd03e802e4917b770aaf2d"}, + {file = "pygit2-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:889ca83528c0649afd970da700cc6ed47dc340481f146a39ba5bfbeca1ddd6f8"}, + {file = "pygit2-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5465db21c6fd481ec29aa7afcca9a85b1fdb19b2f2d09a31b4bdba2f1bd0e75"}, + {file = "pygit2-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ceecd5d30583f9db56aadcd7238bb3c76a2934d8a932de47aed77fe3c188e7"}, + {file = "pygit2-1.10.1-cp39-cp39-win32.whl", hash = "sha256:9d6e1270b91e7bf70185bb4c3686e04cca87a385c8a2d5c74eec8770091531be"}, + {file = "pygit2-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:d4251830276018c2346ddccad4b4ce06ed1d983b002a633c4d894b13669052d0"}, + {file = "pygit2-1.10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7eb2cee54a1cb468b5502493ee4f3ec2f1f82db9c46fab7dacaa37afc4fcde8e"}, + {file = "pygit2-1.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411dc8af5f25c30a0c3d79ee1e22fb892d6fd6ccb54d4c1fb7746e6274e36426"}, + {file = "pygit2-1.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe41da630f4e7cb290dc7e97edf30a59d634426af52a89d4ab5c0fb1ea9ccfe4"}, + {file = "pygit2-1.10.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9da53c6f5c08308450059d7dfb3067d59c45f14bee99743e536c5f9d9823f154"}, + {file = "pygit2-1.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb49f9469a893f75f105cdf2c79254859aaf2fdce1078c38514ca12fe185a759"}, + {file = "pygit2-1.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff838665d6410b5a605f53c1ccd2d2f87ca30de59e89773e7cb5e10211426f90"}, + {file = "pygit2-1.10.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9d23bb613f5692da78c09a79ae40d6ced57b772ae9153aed23a9aa1889a16c85"}, + {file = "pygit2-1.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a3cc867fa6907bfc78d7d1322f3dabd4107b16238205df7e2dec9ee265f0c0"}, + {file = "pygit2-1.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb3eb2f1d437db6e115d5f56d122f2f3737fa2e6063aa42e4d856ca76d785ce6"}, + {file = "pygit2-1.10.1.tar.gz", hash = "sha256:354651bf062c02d1f08041d6fbf1a9b4bf7a93afce65979bdc08bdc65653aa2e"}, ] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, @@ -1720,12 +1764,12 @@ pytest = [ {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, - {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, + {file = "pytest-asyncio-0.20.1.tar.gz", hash = "sha256:626699de2a747611f3eeb64168b3575f70439b06c3d0206e6ceaeeb956e65519"}, + {file = "pytest_asyncio-0.20.1-py3-none-any.whl", hash = "sha256:2c85a835df33fda40fe3973b451e0c194ca11bc2c007eabff90bb3d156fc172b"}, ] pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] pytest-forked = [ {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, @@ -1758,65 +1802,74 @@ rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] +setuptools = [ + {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, + {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] sniffio = [ - {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, - {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.4.40-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:b07fc38e6392a65935dc8b486229679142b2ea33c94059366b4d8b56f1e35a97"}, - {file = "SQLAlchemy-1.4.40-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fb4edb6c354eac0fcc07cb91797e142f702532dbb16c1d62839d6eec35f814cf"}, - {file = "SQLAlchemy-1.4.40-cp27-cp27m-win32.whl", hash = "sha256:2026632051a93997cf8f6fda14360f99230be1725b7ab2ef15be205a4b8a5430"}, - {file = "SQLAlchemy-1.4.40-cp27-cp27m-win_amd64.whl", hash = "sha256:f2aa85aebc0ef6b342d5d3542f969caa8c6a63c8d36cf5098769158a9fa2123c"}, - {file = "SQLAlchemy-1.4.40-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0b9e3d81f86ba04007f0349e373a5b8c81ec2047aadb8d669caf8c54a092461"}, - {file = "SQLAlchemy-1.4.40-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1ab08141d93de83559f6a7d9a962830f918623a885b3759ec2b9d1a531ff28fe"}, - {file = "SQLAlchemy-1.4.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00dd998b43b282c71de46b061627b5edb9332510eb1edfc5017b9e4356ed44ea"}, - {file = "SQLAlchemy-1.4.40-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb342c0e25cc8f78a0e7c692da3b984f072666b316fbbec2a0e371cb4dfef5f0"}, - {file = "SQLAlchemy-1.4.40-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23b693876ac7963b6bc7b1a5f3a2642f38d2624af834faad5933913928089d1b"}, - {file = "SQLAlchemy-1.4.40-cp310-cp310-win32.whl", hash = "sha256:2cf50611ef4221ad587fb7a1708e61ff72966f84330c6317642e08d6db4138fd"}, - {file = "SQLAlchemy-1.4.40-cp310-cp310-win_amd64.whl", hash = "sha256:26ee4dbac5dd7abf18bf3cd8f04e51f72c339caf702f68172d308888cd26c6c9"}, - {file = "SQLAlchemy-1.4.40-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b41b87b929118838bafc4bb18cf3c5cd1b3be4b61cd9042e75174df79e8ac7a2"}, - {file = "SQLAlchemy-1.4.40-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:885e11638946472b4a0a7db8e6df604b2cf64d23dc40eedc3806d869fcb18fae"}, - {file = "SQLAlchemy-1.4.40-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b7ff0a8bf0aec1908b92b8dfa1246128bf4f94adbdd3da6730e9c542e112542d"}, - {file = "SQLAlchemy-1.4.40-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfa8ab4ba0c97ab6bcae1f0948497d14c11b6c6ecd1b32b8a79546a0823d8211"}, - {file = "SQLAlchemy-1.4.40-cp36-cp36m-win32.whl", hash = "sha256:d259fa08e4b3ed952c01711268bcf6cd2442b0c54866d64aece122f83da77c6d"}, - {file = "SQLAlchemy-1.4.40-cp36-cp36m-win_amd64.whl", hash = "sha256:c8d974c991eef0cd29418a5957ae544559dc326685a6f26b3a914c87759bf2f4"}, - {file = "SQLAlchemy-1.4.40-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:28b1791a30d62fc104070965f1a2866699c45bbf5adc0be0cf5f22935edcac58"}, - {file = "SQLAlchemy-1.4.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7ccdca6cd167611f4a62a8c2c0c4285c2535640d77108f782ce3f3cccb70f3a"}, - {file = "SQLAlchemy-1.4.40-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:69deec3a94de10062080d91e1ba69595efeafeafe68b996426dec9720031fb25"}, - {file = "SQLAlchemy-1.4.40-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ad778f4e80913fb171247e4fa82123d0068615ae1d51a9791fc4284cb81748"}, - {file = "SQLAlchemy-1.4.40-cp37-cp37m-win32.whl", hash = "sha256:9ced2450c9fd016f9232d976661623e54c450679eeefc7aa48a3d29924a63189"}, - {file = "SQLAlchemy-1.4.40-cp37-cp37m-win_amd64.whl", hash = "sha256:cdee4d475e35684d210dc6b430ff8ca2ed0636378ac19b457e2f6f350d1f5acc"}, - {file = "SQLAlchemy-1.4.40-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:08b47c971327e733ffd6bae2d4f50a7b761793efe69d41067fcba86282819eea"}, - {file = "SQLAlchemy-1.4.40-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf03d37819dc17a388d313919daf32058d19ba1e592efdf14ce8cbd997e6023"}, - {file = "SQLAlchemy-1.4.40-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a62c0ecbb9976550f26f7bf75569f425e661e7249349487f1483115e5fc893a6"}, - {file = "SQLAlchemy-1.4.40-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec440990ab00650d0c7ea2c75bc225087afdd7ddcb248e3d934def4dff62762"}, - {file = "SQLAlchemy-1.4.40-cp38-cp38-win32.whl", hash = "sha256:2b64955850a14b9d481c17becf0d3f62fb1bb31ac2c45c2caf5ad06d9e811187"}, - {file = "SQLAlchemy-1.4.40-cp38-cp38-win_amd64.whl", hash = "sha256:959bf4390766a8696aa01285016c766b4eb676f712878aac5fce956dd49695d9"}, - {file = "SQLAlchemy-1.4.40-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:0992f3cc640ec0f88f721e426da884c34ff0a60eb73d3d64172e23dfadfc8a0b"}, - {file = "SQLAlchemy-1.4.40-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa9e0d7832b7511b3b3fd0e67fac85ff11fd752834c143ca2364c9b778c0485a"}, - {file = "SQLAlchemy-1.4.40-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9d0f1a9538cc5e75f2ea0cb6c3d70155a1b7f18092c052e0d84105622a41b63"}, - {file = "SQLAlchemy-1.4.40-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c956a5d1adb49a35d78ef0fae26717afc48a36262359bb5b0cbd7a3a247c26f"}, - {file = "SQLAlchemy-1.4.40-cp39-cp39-win32.whl", hash = "sha256:6b70d02bbe1adbbf715d2249cacf9ac17c6f8d22dfcb3f1a4fbc5bf64364da8a"}, - {file = "SQLAlchemy-1.4.40-cp39-cp39-win_amd64.whl", hash = "sha256:bf073c619b5a7f7cd731507d0fdc7329bee14b247a63b0419929e4acd24afea8"}, - {file = "SQLAlchemy-1.4.40.tar.gz", hash = "sha256:44a660506080cc975e1dfa5776fe5f6315ddc626a77b50bf0eee18b0389ea265"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-win32.whl", hash = "sha256:1d0c23ecf7b3bc81e29459c34a3f4c68ca538de01254e24718a7926810dc39a6"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27m-win_amd64.whl", hash = "sha256:6c9d004eb78c71dd4d3ce625b80c96a827d2e67af9c0d32b1c1e75992a7916cc"}, + {file = "SQLAlchemy-1.4.42-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9e3a65ce9ed250b2f096f7b559fe3ee92e6605fab3099b661f0397a9ac7c8d95"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:2e56dfed0cc3e57b2f5c35719d64f4682ef26836b81067ee6cfad062290fd9e2"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42c59ffd2d625b28cdb2ae4cde8488543d428cba17ff672a543062f7caee525"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22459fc1718785d8a86171bbe7f01b5c9d7297301ac150f508d06e62a2b4e8d2"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df76e9c60879fdc785a34a82bf1e8691716ffac32e7790d31a98d7dec6e81545"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-win32.whl", hash = "sha256:e7e740453f0149437c101ea4fdc7eea2689938c5760d7dcc436c863a12f1f565"}, + {file = "SQLAlchemy-1.4.42-cp310-cp310-win_amd64.whl", hash = "sha256:effc89e606165ca55f04f3f24b86d3e1c605e534bf1a96e4e077ce1b027d0b71"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:97ff50cd85bb907c2a14afb50157d0d5486a4b4639976b4a3346f34b6d1b5272"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12c6949bae10f1012ab5c0ea52ab8db99adcb8c7b717938252137cdf694c775"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11b2ec26c5d2eefbc3e6dca4ec3d3d95028be62320b96d687b6e740424f83b7d"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-win32.whl", hash = "sha256:6045b3089195bc008aee5c273ec3ba9a93f6a55bc1b288841bd4cfac729b6516"}, + {file = "SQLAlchemy-1.4.42-cp311-cp311-win_amd64.whl", hash = "sha256:0501f74dd2745ec38f44c3a3900fb38b9db1ce21586b691482a19134062bf049"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6e39e97102f8e26c6c8550cb368c724028c575ec8bc71afbbf8faaffe2b2092a"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d878929c30e41fb3d757a5853b680a561974a0168cd33a750be4ab93181628"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa5b7eb2051e857bf83bade0641628efe5a88de189390725d3e6033a1fff4257"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1c5f8182b4f89628d782a183d44db51b5af84abd6ce17ebb9804355c88a7b5"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-win32.whl", hash = "sha256:a7dd5b7b34a8ba8d181402d824b87c5cee8963cb2e23aa03dbfe8b1f1e417cde"}, + {file = "SQLAlchemy-1.4.42-cp36-cp36m-win_amd64.whl", hash = "sha256:5ede1495174e69e273fad68ad45b6d25c135c1ce67723e40f6cf536cb515e20b"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:9256563506e040daddccaa948d055e006e971771768df3bb01feeb4386c242b0"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4948b6c5f4e56693bbeff52f574279e4ff972ea3353f45967a14c30fb7ae2beb"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1811a0b19a08af7750c0b69e38dec3d46e47c4ec1d74b6184d69f12e1c99a5e0"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b01d9cd2f9096f688c71a3d0f33f3cd0af8549014e66a7a7dee6fc214a7277d"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-win32.whl", hash = "sha256:bd448b262544b47a2766c34c0364de830f7fb0772d9959c1c42ad61d91ab6565"}, + {file = "SQLAlchemy-1.4.42-cp37-cp37m-win_amd64.whl", hash = "sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ab7c158f98de6cb4f1faab2d12973b330c2878d0c6b689a8ca424c02d66e1b3"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee377eb5c878f7cefd633ab23c09e99d97c449dd999df639600f49b74725b80"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:934472bb7d8666727746a75670a1f8d91a9cae8c464bba79da30a0f6faccd9e1"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb94a3d1ba77ff2ef11912192c066f01e68416f554c194d769391638c8ad09a"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-win32.whl", hash = "sha256:f0f574465b78f29f533976c06b913e54ab4980b9931b69aa9d306afff13a9471"}, + {file = "SQLAlchemy-1.4.42-cp38-cp38-win_amd64.whl", hash = "sha256:a85723c00a636eed863adb11f1e8aaa36ad1c10089537823b4540948a8429798"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5ce6929417d5dce5ad1d3f147db81735a4a0573b8fb36e3f95500a06eaddd93e"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723e3b9374c1ce1b53564c863d1a6b2f1dc4e97b1c178d9b643b191d8b1be738"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:876eb185911c8b95342b50a8c4435e1c625944b698a5b4a978ad2ffe74502908"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd49af453e590884d9cdad3586415922a8e9bb669d874ee1dc55d2bc425aacd"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-win32.whl", hash = "sha256:e4ef8cb3c5b326f839bfeb6af5f406ba02ad69a78c7aac0fbeeba994ad9bb48a"}, + {file = "SQLAlchemy-1.4.42-cp39-cp39-win_amd64.whl", hash = "sha256:5f966b64c852592469a7eb759615bbd351571340b8b344f1d3fa2478b5a4c934"}, + {file = "SQLAlchemy-1.4.42.tar.gz", hash = "sha256:177e41914c476ed1e1b77fd05966ea88c094053e17a85303c4ce007f88eff363"}, ] srcinfo = [ {file = "srcinfo-0.0.8-py3-none-any.whl", hash = "sha256:0922ee4302b927d7ddea74c47e539b226a0a7738dc89f95b66404a28d07f3f6b"}, {file = "srcinfo-0.0.8.tar.gz", hash = "sha256:5ac610cf8b15d4b0a0374bd1f7ad301675c2938f0414addf3ef7d7e3fcaf5c65"}, ] starlette = [ - {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, - {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, + {file = "starlette-0.20.4-py3-none-any.whl", hash = "sha256:c0414d5a56297d37f3db96a84034d61ce29889b9eaccf65eb98a0b39441fcaa3"}, + {file = "starlette-0.20.4.tar.gz", hash = "sha256:42fcf3122f998fefce3e2c5ad7e5edbf0f02cf685d646a83a08d404726af5084"}, ] -"tap.py" = [ +tap-py = [ {file = "tap.py-3.1-py3-none-any.whl", hash = "sha256:928c852f3361707b796c93730cc5402c6378660b161114461066acf53d65bf5d"}, {file = "tap.py-3.1.tar.gz", hash = "sha256:3c0cd45212ad5a25b35445964e2517efa000a118a1bfc3437dae828892eaf1e1"}, ] @@ -1829,16 +1882,16 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] urllib3 = [ - {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, - {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] uvicorn = [ - {file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"}, - {file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"}, + {file = "uvicorn-0.19.0-py3-none-any.whl", hash = "sha256:cc277f7e73435748e69e075a721841f7c4a95dba06d12a72fe9874acced16f6f"}, + {file = "uvicorn-0.19.0.tar.gz", hash = "sha256:cf538f3018536edb1f4a826311137ab4944ed741d52aeb98846f52215de57f25"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, @@ -1915,10 +1968,10 @@ wrapt = [ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] wsproto = [ - {file = "wsproto-1.1.0-py3-none-any.whl", hash = "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b"}, - {file = "wsproto-1.1.0.tar.gz", hash = "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"}, + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, ] zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, + {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, + {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, ] diff --git a/pyproject.toml b/pyproject.toml index fea2f922..3b615c73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ aiofiles = "^22.0.0" asgiref = "^3.4.1" bcrypt = "^4.0.0" bleach = "^5.0.0" -email-validator = "1.2.1" +email-validator = "^1.3.0" fakeredis = "^1.6.1" feedgen = "^0.9.0" httpx = "^0.23.0" @@ -85,7 +85,7 @@ Werkzeug = "^2.0.2" SQLAlchemy = "^1.4.26" # ASGI -uvicorn = "^0.18.0" +uvicorn = "^0.19.0" gunicorn = "^20.1.0" Hypercorn = "^0.14.0" prometheus-fastapi-instrumentator = "^5.7.1" @@ -93,14 +93,14 @@ pytest-xdist = "^2.4.0" filelock = "^3.3.2" posix-ipc = "^1.0.5" pyalpm = "^0.10.6" -fastapi = "^0.83.0" +fastapi = "^0.85.1" srcinfo = "^0.0.8" [tool.poetry.dev-dependencies] coverage = "^6.0.2" pytest = "^7.0.0" -pytest-asyncio = "^0.19.0" -pytest-cov = "^3.0.0" +pytest-asyncio = "^0.20.1" +pytest-cov = "^4.0.0" pytest-tap = "^3.2" [tool.poetry.scripts] From 524334409a1744e8caf6fb4b2f0d42ec189bca27 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 22 Oct 2022 21:58:30 +0100 Subject: [PATCH 1172/1451] fix: add production logging.prod.conf to be less verbose Signed-off-by: Leonidas Spyropoulos --- logging.prod.conf | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 logging.prod.conf diff --git a/logging.prod.conf b/logging.prod.conf new file mode 100644 index 00000000..63692a28 --- /dev/null +++ b/logging.prod.conf @@ -0,0 +1,58 @@ +[loggers] +keys=root,aurweb,uvicorn,hypercorn,alembic + +[handlers] +keys=simpleHandler,detailedHandler + +[formatters] +keys=simpleFormatter,detailedFormatter + +[logger_root] +level=INFO +; We add NullHandler programmatically. +handlers= +propogate=0 + +[logger_aurweb] +level=INFO +handlers=simpleHandler +qualname=aurweb +propagate=1 + +[logger_uvicorn] +level=WARN +handlers=simpleHandler +qualname=uvicorn +propagate=0 + +[logger_hypercorn] +level=WARN +handlers=simpleHandler +qualname=hypercorn +propagate=0 + +[logger_alembic] +level=WARN +handlers=simpleHandler +qualname=alembic +propagate=0 + +[handler_simpleHandler] +class=StreamHandler +level=INFO +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_detailedHandler] +class=StreamHandler +level=DEBUG +formatter=detailedFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s %(levelname)-8s | %(name)s @ (%(filename)s:%(lineno)d): %(message)s +datefmt=%H:%M:%S + +[formatter_detailedFormatter] +format=%(asctime)s %(levelname)-8s | [%(name)s.%(funcName)s() @ %(filename)s:%(lineno)d]: %(message)s +datefmt=%H:%M:%S From 3dcbee5a4f035777b5b65d124bc3c46240b661c8 Mon Sep 17 00:00:00 2001 From: Mario Oenning Date: Fri, 28 Oct 2022 12:42:50 +0000 Subject: [PATCH 1173/1451] fix: make overwriting of archive files atomic --- aurweb/scripts/mkpkglists.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index e74bbf25..67cc7fab 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -24,7 +24,6 @@ import io import os import shutil import sys -import tempfile from collections import defaultdict from typing import Any @@ -219,10 +218,9 @@ def _main(): output = list() snapshot_uri = aurweb.config.get("options", "snapshot_uri") - tmpdir = tempfile.mkdtemp() - tmp_packages = os.path.join(tmpdir, os.path.basename(PACKAGES)) - tmp_meta = os.path.join(tmpdir, os.path.basename(META)) - tmp_metaext = os.path.join(tmpdir, os.path.basename(META_EXT)) + tmp_packages = f"{PACKAGES}.tmp" + tmp_meta = f"{META}.tmp" + tmp_metaext = f"{META_EXT}.tmp" gzips = { "packages": gzip.open(tmp_packages, "wt"), "meta": gzip.open(tmp_meta, "wb"), @@ -276,13 +274,13 @@ def _main(): # Produce pkgbase.gz query = db.query(PackageBase.Name).filter(PackageBase.PackagerUID.isnot(None)).all() - tmp_pkgbase = os.path.join(tmpdir, os.path.basename(PKGBASE)) + tmp_pkgbase = f"{PKGBASE}.tmp" with gzip.open(tmp_pkgbase, "wt") as f: f.writelines([f"{base.Name}\n" for i, base in enumerate(query)]) # Produce users.gz query = db.query(User.Username).all() - tmp_users = os.path.join(tmpdir, os.path.basename(USERS)) + tmp_users = f"{USERS}.tmp" with gzip.open(tmp_users, "wt") as f: f.writelines([f"{user.Username}\n" for i, user in enumerate(query)]) @@ -297,7 +295,7 @@ def _main(): for src, dst in files: checksum = sha256sum(src) - base = os.path.basename(src) + base = os.path.basename(dst) checksum_formatted = f"SHA256 ({base}) = {checksum}" checksum_file = f"{dst}.sha256" @@ -307,7 +305,6 @@ def _main(): # Move the new archive into its rightful place. shutil.move(src, dst) - os.removedirs(tmpdir) seconds = filters.number_format(bench.end(), 4) logger.info(f"Completed in {seconds} seconds.") From d793193fdfc9d8369a89a932b5dc719ab1153985 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 27 Oct 2022 15:11:37 +0100 Subject: [PATCH 1174/1451] style: make logging easier to read Signed-off-by: Leonidas Spyropoulos --- logging.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logging.conf b/logging.conf index 7dfd30f0..d27b0153 100644 --- a/logging.conf +++ b/logging.conf @@ -50,9 +50,9 @@ formatter=detailedFormatter args=(sys.stdout,) [formatter_simpleFormatter] -format=%(asctime)s %(levelname)-5s | %(name)s: %(message)s +format=%(asctime)s %(levelname)-8s | %(name)s @ (%(filename)s:%(lineno)d): %(message)s datefmt=%H:%M:%S [formatter_detailedFormatter] -format=%(asctime)s %(levelname)-5s | %(name)s.%(funcName)s() @ L%(lineno)d: %(message)s +format=%(asctime)s %(levelname)-8s | [%(name)s.%(funcName)s() @ %(filename)s:%(lineno)d]: %(message)s datefmt=%H:%M:%S From 7e06823e580942cc11b8164a559386e766d94470 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 27 Oct 2022 15:34:52 +0100 Subject: [PATCH 1175/1451] refactor: remove redundand parenthesis when return tuple Signed-off-by: Leonidas Spyropoulos --- aurweb/auth/__init__.py | 2 +- aurweb/git/update.py | 2 +- aurweb/packages/util.py | 6 +++--- aurweb/routers/accounts.py | 4 ++-- aurweb/routers/packages.py | 30 +++++++++++++++--------------- aurweb/scripts/rendercomment.py | 6 +++--- aurweb/util.py | 6 +++--- test/test_packages_routes.py | 4 ++-- test/test_pkgbase_routes.py | 4 ++-- test/test_spawn.py | 2 +- test/test_tuvotereminder.py | 2 +- 11 files changed, 34 insertions(+), 34 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index b8056f91..5a1fc8d0 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -127,7 +127,7 @@ class BasicAuthBackend(AuthenticationBackend): user.nonce = util.make_nonce() user.authenticated = True - return (AuthCredentials(["authenticated"]), user) + return AuthCredentials(["authenticated"]), user def _auth_required(auth_goal: bool = True): diff --git a/aurweb/git/update.py b/aurweb/git/update.py index 94a8d623..b1256fdb 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -52,7 +52,7 @@ def parse_dep(depstring): depname = re.sub(r"(<|=|>).*", "", dep) depcond = dep[len(depname) :] - return (depname, desc, depcond) + return depname, desc, depcond def create_pkgbase(conn, pkgbase, user): diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index cddec0ac..25671028 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -239,12 +239,12 @@ def source_uri(pkgsrc: models.PackageSource) -> Tuple[str, str]: the package base name. :param pkgsrc: PackageSource instance - :return (text, uri) tuple + :return text, uri)tuple """ if "::" in pkgsrc.Source: return pkgsrc.Source.split("::", 1) elif "://" in pkgsrc.Source: - return (pkgsrc.Source, pkgsrc.Source) + return pkgsrc.Source, pkgsrc.Source path = config.get("options", "source_file_uri") pkgbasename = pkgsrc.Package.PackageBase.Name - return (pkgsrc.Source, path % (pkgsrc.Source, pkgbasename)) + return pkgsrc.Source, path % (pkgsrc.Source, pkgbasename) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 24aacdf7..07962c37 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -160,9 +160,9 @@ def process_account_form(request: Request, user: models.User, args: dict[str, An for check in checks: check(**args, request=request, user=user, _=_) except ValidationError as exc: - return (False, exc.data) + return False, exc.data - return (True, []) + return True, [] def make_account_form_context( diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 0d482521..a4aac496 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -213,7 +213,7 @@ async def package( async def packages_unflag(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: - return (False, ["You did not select any packages to unflag."]) + return False, ["You did not select any packages to unflag."] # Holds the set of package bases we're looking to unflag. # Constructed below via looping through the packages query. @@ -226,14 +226,14 @@ async def packages_unflag(request: Request, package_ids: list[int] = [], **kwarg creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger] ) if not has_cred: - return (False, ["You did not select any packages to unflag."]) + return False, ["You did not select any packages to unflag."] if pkg.PackageBase not in bases: bases.update({pkg.PackageBase}) for pkgbase in bases: pkgbase_actions.pkgbase_unflag_instance(request, pkgbase) - return (True, ["The selected packages have been unflagged."]) + return True, ["The selected packages have been unflagged."] async def packages_notify(request: Request, package_ids: list[int] = [], **kwargs): @@ -271,13 +271,13 @@ async def packages_notify(request: Request, package_ids: list[int] = [], **kwarg pkgbase_actions.pkgbase_notify_instance(request, pkgbase) # TODO: This message does not yet have a translation. - return (True, ["The selected packages' notifications have been enabled."]) + return True, ["The selected packages' notifications have been enabled."] async def packages_unnotify(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: # TODO: This error does not yet have a translation. - return (False, ["You did not select any packages for notification removal."]) + return False, ["You did not select any packages for notification removal."] # TODO: This error does not yet have a translation. error_tuple = ( @@ -307,14 +307,14 @@ async def packages_unnotify(request: Request, package_ids: list[int] = [], **kwa pkgbase_actions.pkgbase_unnotify_instance(request, pkgbase) # TODO: This message does not yet have a translation. - return (True, ["The selected packages' notifications have been removed."]) + return True, ["The selected packages' notifications have been removed."] async def packages_adopt( request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs ): if not package_ids: - return (False, ["You did not select any packages to adopt."]) + return False, ["You did not select any packages to adopt."] if not confirm: return ( @@ -347,7 +347,7 @@ async def packages_adopt( for pkgbase in bases: pkgbase_actions.pkgbase_adopt_instance(request, pkgbase) - return (True, ["The selected packages have been adopted."]) + return True, ["The selected packages have been adopted."] def disown_all(request: Request, pkgbases: list[models.PackageBase]) -> list[str]: @@ -364,7 +364,7 @@ async def packages_disown( request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs ): if not package_ids: - return (False, ["You did not select any packages to disown."]) + return False, ["You did not select any packages to disown."] if not confirm: return ( @@ -397,9 +397,9 @@ async def packages_disown( # Now, disown all the bases if we can. if errors := disown_all(request, bases): - return (False, errors) + return False, errors - return (True, ["The selected packages have been disowned."]) + return True, ["The selected packages have been disowned."] async def packages_delete( @@ -410,7 +410,7 @@ async def packages_delete( **kwargs, ): if not package_ids: - return (False, ["You did not select any packages to delete."]) + return False, ["You did not select any packages to delete."] if not confirm: return ( @@ -422,7 +422,7 @@ async def packages_delete( ) if not request.user.has_credential(creds.PKGBASE_DELETE): - return (False, ["You do not have permission to delete packages."]) + return False, ["You do not have permission to delete packages."] # set-ify package_ids and query the database for related records. package_ids = set(package_ids) @@ -432,7 +432,7 @@ async def packages_delete( # Let the user know there was an issue with their input: they have # provided at least one package_id which does not exist in the DB. # TODO: This error has not yet been translated. - return (False, ["One of the packages you selected does not exist."]) + return False, ["One of the packages you selected does not exist."] # Make a set out of all package bases related to `packages`. bases = {pkg.PackageBase for pkg in packages} @@ -448,7 +448,7 @@ async def packages_delete( ) util.apply_all(notifs, lambda n: n.send()) - return (True, ["The selected packages have been deleted."]) + return True, ["The selected packages have been deleted."] # A mapping of action string -> callback functions used within the diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 4a2c84bd..643b0370 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -46,7 +46,7 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): el = Element("a") el.set("href", f"https://bugs.archlinux.org/task/{m.group(1)}") el.text = markdown.util.AtomicString(m.group(0)) - return (el, m.start(0), m.end(0)) + return el, m.start(0), m.end(0) class FlysprayLinksExtension(markdown.extensions.Extension): @@ -74,7 +74,7 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): oid = m.group(1) if oid not in self._repo: # Unknown OID; preserve the orginal text. - return (None, None, None) + return None, None, None el = Element("a") commit_uri = aurweb.config.get("options", "commit_uri") @@ -83,7 +83,7 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): "href", commit_uri % (quote_plus(self._head), quote_plus(oid[:prefixlen])) ) el.text = markdown.util.AtomicString(oid[:prefixlen]) - return (el, m.start(0), m.end(0)) + return el, m.start(0), m.end(0) class GitCommitsExtension(markdown.extensions.Extension): diff --git a/aurweb/util.py b/aurweb/util.py index cda12af1..0a39cd3d 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -107,7 +107,7 @@ def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: except ValueError: per_page = defaults.PP - return (offset, per_page) + return offset, per_page def strtobool(value: Union[str, bool]) -> bool: @@ -187,7 +187,7 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: if proc.returncode: raise invalid_exc - return (prefix, key) + return prefix, key def parse_ssh_keys(string: str) -> list[Tuple[str, str]]: @@ -199,4 +199,4 @@ def shell_exec(cmdline: str, cwd: str) -> Tuple[int, str, str]: args = shlex.split(cmdline) proc = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE) out, err = proc.communicate() - return (proc.returncode, out.decode().strip(), err.decode().strip()) + return proc.returncode, out.decode().strip(), err.decode().strip() diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 6e92eeff..3b717783 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1149,7 +1149,7 @@ def test_packages_post_unknown_action(client: TestClient, user: User, package: P def test_packages_post_error(client: TestClient, user: User, package: Package): async def stub_action(request: Request, **kwargs): - return (False, ["Some error."]) + return False, ["Some error."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): @@ -1170,7 +1170,7 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): def test_packages_post(client: TestClient, user: User, package: Package): async def stub_action(request: Request, **kwargs): - return (True, ["Some success."]) + return True, ["Some success."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index bfdb0c37..18c11626 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -1315,7 +1315,7 @@ def test_packages_post_unknown_action(client: TestClient, user: User, package: P def test_packages_post_error(client: TestClient, user: User, package: Package): async def stub_action(request: Request, **kwargs): - return (False, ["Some error."]) + return False, ["Some error."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): @@ -1336,7 +1336,7 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): def test_packages_post(client: TestClient, user: User, package: Package): async def stub_action(request: Request, **kwargs): - return (True, ["Some success."]) + return True, ["Some success."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): diff --git a/test/test_spawn.py b/test/test_spawn.py index be1c5e7c..25b9ebfc 100644 --- a/test/test_spawn.py +++ b/test/test_spawn.py @@ -24,7 +24,7 @@ class FakeProcess: """We need this constructor to remain compatible with Popen.""" def communicate(self) -> Tuple[bytes, bytes]: - return (self.stdout, self.stderr) + return self.stdout, self.stderr def terminate(self) -> None: raise Exception("Fake termination.") diff --git a/test/test_tuvotereminder.py b/test/test_tuvotereminder.py index 0233c8b2..5f2ae3a1 100644 --- a/test/test_tuvotereminder.py +++ b/test/test_tuvotereminder.py @@ -42,7 +42,7 @@ def email_pieces(voteinfo: TUVoteInfo) -> Tuple[str, str]: f"[1]. The voting period\nends in less than 48 hours.\n\n" f"[1] {aur_location}/tu/?id={voteinfo.ID}" ) - return (subject, content) + return subject, content @pytest.fixture From 48e5dc6763b664fb307d2894cedab0a9aaf09630 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 27 Oct 2022 15:49:48 +0100 Subject: [PATCH 1176/1451] feat: remove empty lines from ssh_keys text area, and show helpful message Signed-off-by: Leonidas Spyropoulos --- aurweb/util.py | 2 +- po/aurweb.pot | 4 ++++ templates/partials/account_form.html | 7 +++++++ test/test_util.py | 29 +++++++++++++++++++++++++--- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/aurweb/util.py b/aurweb/util.py index 0a39cd3d..7b997609 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -192,7 +192,7 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: def parse_ssh_keys(string: str) -> list[Tuple[str, str]]: """Parse a list of SSH public keys.""" - return [parse_ssh_key(e) for e in string.splitlines()] + return [parse_ssh_key(e) for e in string.strip().splitlines(True) if e.strip()] def shell_exec(cmdline: str, cwd: str) -> Tuple[int, str, str]: diff --git a/po/aurweb.pot b/po/aurweb.pot index 1838fae5..ff1bde8b 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -1398,6 +1398,10 @@ msgid "" "the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 007fb389..a433a57d 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -264,6 +264,13 @@

    +

    + + {{ + "Specify multiple SSH Keys separated by new line, empty lines are ignored." | tr + }} + +

    diff --git a/test/test_util.py b/test/test_util.py index 2e8b2e4e..fd7d8655 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -96,14 +96,37 @@ YbxDwGimZZslg0OZu9UzoAT6xEGyiZsqJkTMbRp1ZYIOv9jHCJxRuxxuN3fzxyT3xE69+vhq2/NJX\ vTNJCD6JtMClxbIXW9q74nNqG+2SD/VQNMUz/505TK1PbY/4uyFfq5HquHJXQVCBll03FRerNHH2N\ schFne6BFHpa48PCoZNH45wLjFXwUyrGU1HrNqh6ZPdRfBTrTOkgs+BKBxGNeV45aYUPu/cFBSPcB\ fRSo6OFcejKc=""" + assert_multiple_keys(pks) + + +def test_parse_ssh_keys_with_extra_lines(): + pks = """ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyN\ +TYAAABBBEURnkiY6JoLyqDE8Li1XuAW+LHmkmLDMW/GL5wY7k4/A+Ta7bjA3MOKrF9j4EuUTvCuNX\ +ULxvpfSqheTFWZc+g= + + + + +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDmqEapFMh/ajPHnm1dBweYPeLOUjC0Ydp6uw7rB\ +S5KCggUVQR8WfIm+sRYTj2+smGsK6zHMBjFnbzvV11vnMqcnY+Sa4LhIAdwkbt/b8HlGaLj1hCWSh\ +a5b5/noeK7L+CECGHdvfJhpxBbhq38YEdFnCGbslk/4NriNcUp/DO81CXb1RzJ9GBFH8ivPW1mbe9\ +YbxDwGimZZslg0OZu9UzoAT6xEGyiZsqJkTMbRp1ZYIOv9jHCJxRuxxuN3fzxyT3xE69+vhq2/NJX\ +8aRsxGPL9G/XKcaYGS6y6LW4quIBCz/XsTZfx1GmkQeZPYHH8FeE+XC/+toXL/kamxdOQKFYEEpWK\ +vTNJCD6JtMClxbIXW9q74nNqG+2SD/VQNMUz/505TK1PbY/4uyFfq5HquHJXQVCBll03FRerNHH2N\ +schFne6BFHpa48PCoZNH45wLjFXwUyrGU1HrNqh6ZPdRfBTrTOkgs+BKBxGNeV45aYUPu/cFBSPcB\ +fRSo6OFcejKc= + + +""" + assert_multiple_keys(pks) + + +def assert_multiple_keys(pks): keys = util.parse_ssh_keys(pks) assert len(keys) == 2 - pfx1, key1, pfx2, key2 = pks.split() k1, k2 = keys - assert pfx1 == k1[0] assert key1 == k1[1] - assert pfx2 == k2[0] assert key2 == k2[1] From 333051ab1f65d28fce7ecbae8ada50a75564303d Mon Sep 17 00:00:00 2001 From: Mario Oenning Date: Fri, 28 Oct 2022 16:55:16 +0000 Subject: [PATCH 1177/1451] feat: add field "Submitter" to metadata-archives --- aurweb/scripts/mkpkglists.py | 5 +++++ test/test_mkpkglists.py | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 67cc7fab..1ff6fbb2 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -163,6 +163,7 @@ def as_dict(package: Package) -> dict[str, Any]: "Popularity": float(package.Popularity), "OutOfDate": package.OutOfDate, "Maintainer": package.Maintainer, + "Submitter": package.Submitter, "FirstSubmitted": package.FirstSubmitted, "LastModified": package.LastModified, } @@ -190,10 +191,13 @@ def _main(): logger.warning(f"{sys.argv[0]} is deprecated and will be soon be removed") logger.info("Started re-creating archives, wait a while...") + Submitter = orm.aliased(User) + query = ( db.query(Package) .join(PackageBase, PackageBase.ID == Package.PackageBaseID) .join(User, PackageBase.MaintainerUID == User.ID, isouter=True) + .join(Submitter, PackageBase.SubmitterUID == Submitter.ID, isouter=True) .filter(PackageBase.PackagerUID.isnot(None)) .with_entities( Package.ID, @@ -207,6 +211,7 @@ def _main(): PackageBase.Popularity, PackageBase.OutOfDateTS.label("OutOfDate"), User.Username.label("Maintainer"), + Submitter.Username.label("Submitter"), PackageBase.SubmittedTS.label("FirstSubmitted"), PackageBase.ModifiedTS.label("LastModified"), ) diff --git a/test/test_mkpkglists.py b/test/test_mkpkglists.py index 3c105817..e7800ffe 100644 --- a/test/test_mkpkglists.py +++ b/test/test_mkpkglists.py @@ -30,6 +30,7 @@ META_KEYS = [ "Popularity", "OutOfDate", "Maintainer", + "Submitter", "FirstSubmitted", "LastModified", "URLPath", @@ -61,7 +62,12 @@ def packages(user: User) -> list[Package]: lic = db.create(License, Name="GPL") for i in range(5): # Create the package. - pkgbase = db.create(PackageBase, Name=f"pkgbase_{i}", Packager=user) + pkgbase = db.create( + PackageBase, + Name=f"pkgbase_{i}", + Packager=user, + Submitter=user, + ) pkg = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") # Create some related records. From 6ee34ab3cb14ada09d991141779ae9c5f9f50698 Mon Sep 17 00:00:00 2001 From: Mario Oenning Date: Mon, 31 Oct 2022 09:42:56 +0000 Subject: [PATCH 1178/1451] feat: add field "CoMaintainers" to metadata-archives --- aurweb/scripts/mkpkglists.py | 15 +++++++++++++++ test/test_mkpkglists.py | 3 +++ 2 files changed, 18 insertions(+) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 1ff6fbb2..903d96ae 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -142,6 +142,21 @@ def get_extended_fields(): ) .distinct() .order_by("Name"), + # Co-Maintainer + db.query(models.PackageComaintainer) + .join(models.User, models.User.ID == models.PackageComaintainer.UsersID) + .join( + models.Package, + models.Package.PackageBaseID == models.PackageComaintainer.PackageBaseID, + ) + .with_entities( + models.Package.ID, + literal("CoMaintainers").label("Type"), + models.User.Username.label("Name"), + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), ] query = subqueries[0].union_all(*subqueries[1:]) return get_extended_dict(query) diff --git a/test/test_mkpkglists.py b/test/test_mkpkglists.py index e7800ffe..8edbcd81 100644 --- a/test/test_mkpkglists.py +++ b/test/test_mkpkglists.py @@ -11,6 +11,7 @@ from aurweb.models import ( License, Package, PackageBase, + PackageComaintainer, PackageDependency, PackageLicense, User, @@ -79,6 +80,7 @@ def packages(user: User) -> list[Package]: DepName=f"dep_{i}", DepCondition=">=1.0", ) + db.create(PackageComaintainer, User=user, PackageBase=pkgbase, Priority=1) # Add the package to our output list. output.append(pkg) @@ -229,6 +231,7 @@ def test_mkpkglists_extended(config_mock: None, user: User, packages: list[Packa assert key in pkg, f"{pkg=} record does not have {key=}" assert isinstance(pkg["Depends"], list) assert isinstance(pkg["License"], list) + assert isinstance(pkg["CoMaintainers"], list) for file in (PACKAGES, PKGBASE, USERS, META, META_EXT): with open(f"{file}.sha256") as f: From 286834bab1e184d2f92b6c03440e8dc85c2b8d0c Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 31 Oct 2022 14:43:31 +0000 Subject: [PATCH 1179/1451] fix: regression on gzipped filenames from 3dcbee5a With the 3dcbee5a the filenames inside the .gz archives contained .tmp at the end. This fixes those by using Gzip Class constructor instead of the gzip.open method. Signed-off-by: Leonidas Spyropoulos --- aurweb/scripts/mkpkglists.py | 55 +++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 903d96ae..7ff2690b 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -242,8 +242,10 @@ def _main(): tmp_meta = f"{META}.tmp" tmp_metaext = f"{META_EXT}.tmp" gzips = { - "packages": gzip.open(tmp_packages, "wt"), - "meta": gzip.open(tmp_meta, "wb"), + "packages": gzip.GzipFile( + filename=PACKAGES, mode="wb", fileobj=open(tmp_packages, "wb") + ), + "meta": gzip.GzipFile(filename=META, mode="wb", fileobj=open(tmp_meta, "wb")), } # Append list opening to the metafile. @@ -252,7 +254,9 @@ def _main(): # Produce packages.gz + packages-meta-ext-v1.json.gz extended = False if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: - gzips["meta_ext"] = gzip.open(tmp_metaext, "wb") + gzips["meta_ext"] = gzip.GzipFile( + filename=META_EXT, mode="wb", fileobj=open(tmp_metaext, "wb") + ) # Append list opening to the meta_ext file. gzips.get("meta_ext").write(b"[\n") f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) @@ -261,28 +265,29 @@ def _main(): results = query.all() n = len(results) - 1 - for i, result in enumerate(results): - # Append to packages.gz. - gzips.get("packages").write(f"{result.Name}\n") + with io.TextIOWrapper(gzips.get("packages")) as p: + for i, result in enumerate(results): + # Append to packages.gz. + p.write(f"{result.Name}\n") - # Construct our result JSON dictionary. - item = as_dict(result) - item["URLPath"] = snapshot_uri % result.Name + # Construct our result JSON dictionary. + item = as_dict(result) + item["URLPath"] = snapshot_uri % result.Name - # We stream out package json objects line per line, so - # we also need to include the ',' character at the end - # of package lines (excluding the last package). - suffix = b",\n" if i < n else b"\n" + # We stream out package json objects line per line, so + # we also need to include the ',' character at the end + # of package lines (excluding the last package). + suffix = b",\n" if i < n else b"\n" - # Write out to packagesmetafile - output.append(item) - gzips.get("meta").write(orjson.dumps(output[-1]) + suffix) + # Write out to packagesmetafile + output.append(item) + gzips.get("meta").write(orjson.dumps(output[-1]) + suffix) - if extended: - # Write out to packagesmetaextfile. - data_ = data.get(result.ID, {}) - output[-1].update(data_) - gzips.get("meta_ext").write(orjson.dumps(output[-1]) + suffix) + if extended: + # Write out to packagesmetaextfile. + data_ = data.get(result.ID, {}) + output[-1].update(data_) + gzips.get("meta_ext").write(orjson.dumps(output[-1]) + suffix) # Append the list closing to meta/meta_ext. gzips.get("meta").write(b"]") @@ -295,13 +300,17 @@ def _main(): # Produce pkgbase.gz query = db.query(PackageBase.Name).filter(PackageBase.PackagerUID.isnot(None)).all() tmp_pkgbase = f"{PKGBASE}.tmp" - with gzip.open(tmp_pkgbase, "wt") as f: + pkgbase_gzip = gzip.GzipFile( + filename=PKGBASE, mode="wb", fileobj=open(tmp_pkgbase, "wb") + ) + with io.TextIOWrapper(pkgbase_gzip) as f: f.writelines([f"{base.Name}\n" for i, base in enumerate(query)]) # Produce users.gz query = db.query(User.Username).all() tmp_users = f"{USERS}.tmp" - with gzip.open(tmp_users, "wt") as f: + users_gzip = gzip.GzipFile(filename=USERS, mode="wb", fileobj=open(tmp_users, "wb")) + with io.TextIOWrapper(users_gzip) as f: f.writelines([f"{user.Username}\n" for i, user in enumerate(query)]) files = [ From 5669821b299427081f32de7d9d6712dff8b793dc Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 31 Oct 2022 18:00:39 +0100 Subject: [PATCH 1180/1451] perf: tweak some queries in mkpkglists We can omit the "distinct" from some queries because constraints in the DB ensure uniqueness: * Groups sub-query PackageGroup: Primary key makes "PackageID" + "GroupID" unique Groups: Unique index on "Name" column -> Technically we can't have a package with the same group-name twice * Licenses sub-query: PackageLicense -> Primary key makes "PackageID" + "LicenseID" unique Licenses -> Unique index on "Name" column -> Technically we can't have a package with the same license-name twice * Keywords sub-query: PackageKeywords -> Primary key makes "PackageBaseID" + "KeywordID" unique (And a Package can only have one PackageBase) Keywords -> Unique index on "Name" column -> Technically we can't have a package with the same Keyword twice * Packages main-query: We join PackageBases and Users on their primary key columns (which are guaranteed to be unique) -> There is no way we could end up with more than one record for a Package Signed-off-by: moson-mo --- aurweb/scripts/mkpkglists.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 7ff2690b..d2d11c5e 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -94,7 +94,7 @@ def get_extended_fields(): models.PackageDependency.DepName.label("Name"), models.PackageDependency.DepCondition.label("Cond"), ) - .distinct() + .distinct() # A package could have the same dependency multiple times .order_by("Name"), # PackageRelation db.query(models.PackageRelation) @@ -105,7 +105,7 @@ def get_extended_fields(): models.PackageRelation.RelName.label("Name"), models.PackageRelation.RelCondition.label("Cond"), ) - .distinct() + .distinct() # A package could have the same relation multiple times .order_by("Name"), # Groups db.query(models.PackageGroup) @@ -116,7 +116,6 @@ def get_extended_fields(): models.Group.Name.label("Name"), literal(str()).label("Cond"), ) - .distinct() .order_by("Name"), # Licenses db.query(models.PackageLicense) @@ -127,7 +126,6 @@ def get_extended_fields(): models.License.Name.label("Name"), literal(str()).label("Cond"), ) - .distinct() .order_by("Name"), # Keywords db.query(models.PackageKeyword) @@ -140,7 +138,6 @@ def get_extended_fields(): models.PackageKeyword.Keyword.label("Name"), literal(str()).label("Cond"), ) - .distinct() .order_by("Name"), # Co-Maintainer db.query(models.PackageComaintainer) @@ -155,7 +152,7 @@ def get_extended_fields(): models.User.Username.label("Name"), literal(str()).label("Cond"), ) - .distinct() + .distinct() # A package could have the same co-maintainer multiple times .order_by("Name"), ] query = subqueries[0].union_all(*subqueries[1:]) @@ -230,7 +227,6 @@ def _main(): PackageBase.SubmittedTS.label("FirstSubmitted"), PackageBase.ModifiedTS.label("LastModified"), ) - .distinct() .order_by("Name") ) From f10c1a0505d446dfd0f78fc3a03f842d62be82f7 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sun, 23 Oct 2022 10:28:30 +0100 Subject: [PATCH 1181/1451] perf: add PackageKeywords.PackageBaseID index This is used on the export for package-meta.v1.gz generation Signed-off-by: Leonidas Spyropoulos --- aurweb/schema.py | 1 + ...57fd7_add_packagekeyword_packagebaseuid.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py diff --git a/aurweb/schema.py b/aurweb/schema.py index 5f998ed9..0ba3e9c2 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -201,6 +201,7 @@ PackageKeywords = Table( nullable=False, server_default=text("''"), ), + Index("KeywordsPackageBaseID", "PackageBaseID"), mysql_engine="InnoDB", mysql_charset="utf8mb4", mysql_collate="utf8mb4_general_ci", diff --git a/migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py b/migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py new file mode 100644 index 00000000..03291152 --- /dev/null +++ b/migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py @@ -0,0 +1,24 @@ +"""add PackageKeyword.PackageBaseUID index + +Revision ID: 9e3158957fd7 +Revises: 6441d3b65270 +Create Date: 2022-10-17 11:11:46.203322 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "9e3158957fd7" +down_revision = "6441d3b65270" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_index( + "KeywordsPackageBaseID", "PackageKeywords", ["PackageBaseID"], unique=False + ) + + +def downgrade(): + op.drop_index("KeywordsPackageBaseID", table_name="PackageKeywords") From d00371f444aa3465c0adc2ea9118c5eb0633e1be Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 1 Nov 2022 17:17:34 +0000 Subject: [PATCH 1182/1451] housekeep: bump renovate dependencies Signed-off-by: Leonidas Spyropoulos --- poetry.lock | 341 +++++++++++++++++++++++-------------------------- pyproject.toml | 10 +- 2 files changed, 167 insertions(+), 184 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9cf24f9a..f6b79a30 100644 --- a/poetry.lock +++ b/poetry.lock @@ -153,11 +153,11 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" @@ -234,6 +234,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" dnspython = ">=1.15.0" idna = ">=2.0.0" +[[package]] +name = "exceptiongroup" +version = "1.0.0" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "execnet" version = "1.9.0" @@ -247,7 +258,7 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.9.4" +version = "1.10.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false @@ -263,7 +274,7 @@ lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "fastapi" -version = "0.85.1" +version = "0.85.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -276,8 +287,8 @@ starlette = "0.20.4" [package.extras] all = ["email-validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.7.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-orjson (==3.6.2)", "types-ujson (==5.4.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.7.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "pytest-cov (>=2.12.0,<5.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<=1.4.41)", "types-orjson (==3.6.2)", "types-ujson (==5.5.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "feedgen" @@ -305,14 +316,15 @@ testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pyt [[package]] name = "greenlet" -version = "1.1.3.post0" +version = "2.0.0" description = "Lightweight in-process concurrent programming" category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] -docs = ["Sphinx"] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["faulthandler", "objgraph"] [[package]] name = "gunicorn" @@ -542,7 +554,7 @@ python-versions = ">=3.5" [[package]] name = "orjson" -version = "3.8.0" +version = "3.8.1" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false @@ -628,20 +640,12 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "4.21.8" +version = "4.21.9" description = "" category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pyalpm" version = "0.10.6" @@ -697,7 +701,7 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "main" optional = false @@ -706,11 +710,11 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -744,18 +748,6 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "pytest-forked" -version = "1.4.0" -description = "run tests in isolated forked subprocesses" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -py = "*" -pytest = ">=3.10" - [[package]] name = "pytest-tap" version = "3.3" @@ -770,7 +762,7 @@ pytest = ">=3.0" [[package]] name = "pytest-xdist" -version = "2.5.0" +version = "3.0.2" description = "pytest xdist plugin for distributed testing and loop-on-failing modes" category = "main" optional = false @@ -779,7 +771,6 @@ python-versions = ">=3.6" [package.dependencies] execnet = ">=1.1" pytest = ">=6.2.0" -pytest-forked = "*" [package.extras] psutil = ["psutil (>=3.0)"] @@ -1058,7 +1049,7 @@ h11 = ">=0.9.0,<1" [[package]] name = "zipp" -version = "3.9.0" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1071,7 +1062,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "de9f0dc1d7e3f149a83629ad30d161da38aa1498b81aaa8bdfd2ebed50f232ab" +content-hash = "84f0bae9789174cbdc5aa672b9e72f0ef91763f63ed73e8cafb45f26efd9bb47" [metadata.files] aiofiles = [ @@ -1208,8 +1199,8 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, @@ -1303,17 +1294,21 @@ email-validator = [ {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"}, {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, ] +exceptiongroup = [ + {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, + {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, +] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] fakeredis = [ - {file = "fakeredis-1.9.4-py3-none-any.whl", hash = "sha256:61afe14095aad3e7413a0a6fe63041da1b4bc3e41d5228a33b60bd03fabf22d8"}, - {file = "fakeredis-1.9.4.tar.gz", hash = "sha256:17415645d11994061f5394f3f1c76ba4531f3f8b63f9c55a8fd2120bebcbfae9"}, + {file = "fakeredis-1.10.0-py3-none-any.whl", hash = "sha256:0be420a79fabda234963a2730c4ce609a6d44a598e8dd253ce97785bef944285"}, + {file = "fakeredis-1.10.0.tar.gz", hash = "sha256:2b02370118535893d832bcd3c099ef282de3f13b29ae3922432e2225794ec334"}, ] fastapi = [ - {file = "fastapi-0.85.1-py3-none-any.whl", hash = "sha256:de3166b6b1163dc22da4dc4ebdc3192fcbac7700dd1870a1afa44de636a636b5"}, - {file = "fastapi-0.85.1.tar.gz", hash = "sha256:1facd097189682a4ff11cbd01334a992e51b56be663b2bd50c2c09523624f144"}, + {file = "fastapi-0.85.2-py3-none-any.whl", hash = "sha256:6292db0edd4a11f0d938d6033ccec5f706e9d476958bf33b119e8ddb4e524bde"}, + {file = "fastapi-0.85.2.tar.gz", hash = "sha256:3e10ea0992c700e0b17b6de8c2092d7b9cd763ce92c49ee8d4be10fee3b2f367"}, ] feedgen = [ {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, @@ -1323,72 +1318,61 @@ filelock = [ {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] greenlet = [ - {file = "greenlet-1.1.3.post0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9649891ab4153f217f319914455ccf0b86986b55fc0573ce803eb998ad7d6854"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-win32.whl", hash = "sha256:11fc7692d95cc7a6a8447bb160d98671ab291e0a8ea90572d582d57361360f05"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-win_amd64.whl", hash = "sha256:05ae7383f968bba4211b1fbfc90158f8e3da86804878442b4fb6c16ccbcaa519"}, - {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ccbe7129a282ec5797df0451ca1802f11578be018a32979131065565da89b392"}, - {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8b58232f5b72973350c2b917ea3df0bebd07c3c82a0a0e34775fc2c1f857e9"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f6661b58412879a2aa099abb26d3c93e91dedaba55a6394d1fb1512a77e85de9"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c6e942ca9835c0b97814d14f78da453241837419e0d26f7403058e8db3e38f8"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a812df7282a8fc717eafd487fccc5ba40ea83bb5b13eb3c90c446d88dbdfd2be"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a7a6560df073ec9de2b7cb685b199dfd12519bc0020c62db9d1bb522f989fa"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17a69967561269b691747e7f436d75a4def47e5efcbc3c573180fc828e176d80"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:60839ab4ea7de6139a3be35b77e22e0398c270020050458b3d25db4c7c394df5"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-win_amd64.whl", hash = "sha256:8926a78192b8b73c936f3e87929931455a6a6c6c385448a07b9f7d1072c19ff3"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c6f90234e4438062d6d09f7d667f79edcc7c5e354ba3a145ff98176f974b8132"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814f26b864ed2230d3a7efe0336f5766ad012f94aad6ba43a7c54ca88dd77cba"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fda1139d87ce5f7bd80e80e54f9f2c6fe2f47983f1a6f128c47bf310197deb6"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0643250dd0756f4960633f5359884f609a234d4066686754e834073d84e9b51"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cb863057bed786f6622982fb8b2c122c68e6e9eddccaa9fa98fd937e45ee6c4f"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c0581077cf2734569f3e500fab09c0ff6a2ab99b1afcacbad09b3c2843ae743"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:695d0d8b5ae42c800f1763c9fce9d7b94ae3b878919379150ee5ba458a460d57"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5662492df0588a51d5690f6578f3bbbd803e7f8d99a99f3bf6128a401be9c269"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:bffba15cff4802ff493d6edcf20d7f94ab1c2aee7cfc1e1c7627c05f1102eee8"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-win32.whl", hash = "sha256:7afa706510ab079fd6d039cc6e369d4535a48e202d042c32e2097f030a16450f"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:3a24f3213579dc8459e485e333330a921f579543a5214dbc935bc0763474ece3"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:64e10f303ea354500c927da5b59c3802196a07468332d292aef9ddaca08d03dd"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:eb6ac495dccb1520667cfea50d89e26f9ffb49fa28496dea2b95720d8b45eb54"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:88720794390002b0c8fa29e9602b395093a9a766b229a847e8d88349e418b28a"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39464518a2abe9c505a727af7c0b4efff2cf242aa168be5f0daa47649f4d7ca8"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0914f02fcaa8f84f13b2df4a81645d9e82de21ed95633765dd5cc4d3af9d7403"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96656c5f7c95fc02c36d4f6ef32f4e94bb0b6b36e6a002c21c39785a4eec5f5d"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4f74aa0092602da2069df0bc6553919a15169d77bcdab52a21f8c5242898f519"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3aeac044c324c1a4027dca0cde550bd83a0c0fbff7ef2c98df9e718a5086c194"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-win32.whl", hash = "sha256:fe7c51f8a2ab616cb34bc33d810c887e89117771028e1e3d3b77ca25ddeace04"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:70048d7b2c07c5eadf8393e6398595591df5f59a2f26abc2f81abca09610492f"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:66aa4e9a726b70bcbfcc446b7ba89c8cec40f405e51422c39f42dfa206a96a05"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:025b8de2273d2809f027d347aa2541651d2e15d593bbce0d5f502ca438c54136"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:82a38d7d2077128a017094aff334e67e26194f46bd709f9dcdacbf3835d47ef5"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7d20c3267385236b4ce54575cc8e9f43e7673fc761b069c820097092e318e3b"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8ece5d1a99a2adcb38f69af2f07d96fb615415d32820108cd340361f590d128"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2794eef1b04b5ba8948c72cc606aab62ac4b0c538b14806d9c0d88afd0576d6b"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a8d24eb5cb67996fb84633fdc96dbc04f2d8b12bfcb20ab3222d6be271616b67"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0120a879aa2b1ac5118bce959ea2492ba18783f65ea15821680a256dfad04754"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-win32.whl", hash = "sha256:bef49c07fcb411c942da6ee7d7ea37430f830c482bf6e4b72d92fd506dd3a427"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:62723e7eb85fa52e536e516ee2ac91433c7bb60d51099293671815ff49ed1c21"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d25cdedd72aa2271b984af54294e9527306966ec18963fd032cc851a725ddc1b"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:924df1e7e5db27d19b1359dc7d052a917529c95ba5b8b62f4af611176da7c8ad"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ec615d2912b9ad807afd3be80bf32711c0ff9c2b00aa004a45fd5d5dde7853d9"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0971d37ae0eaf42344e8610d340aa0ad3d06cd2eee381891a10fe771879791f9"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325f272eb997916b4a3fc1fea7313a8adb760934c2140ce13a2117e1b0a8095d"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75afcbb214d429dacdf75e03a1d6d6c5bd1fa9c35e360df8ea5b6270fb2211c"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c2d21c2b768d8c86ad935e404cc78c30d53dea009609c3ef3a9d49970c864b5"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:467b73ce5dcd89e381292fb4314aede9b12906c18fab903f995b86034d96d5c8"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-win32.whl", hash = "sha256:8149a6865b14c33be7ae760bcdb73548bb01e8e47ae15e013bf7ef9290ca309a"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-win_amd64.whl", hash = "sha256:104f29dd822be678ef6b16bf0035dcd43206a8a48668a6cae4d2fe9c7a7abdeb"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c8c9301e3274276d3d20ab6335aa7c5d9e5da2009cccb01127bddb5c951f8870"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8415239c68b2ec9de10a5adf1130ee9cb0ebd3e19573c55ba160ff0ca809e012"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:3c22998bfef3fcc1b15694818fc9b1b87c6cc8398198b96b6d355a7bcb8c934e"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa1845944e62f358d63fcc911ad3b415f585612946b8edc824825929b40e59e"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:890f633dc8cb307761ec566bc0b4e350a93ddd77dc172839be122be12bae3e10"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf37343e43404699d58808e51f347f57efd3010cc7cee134cdb9141bd1ad9ea"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5edf75e7fcfa9725064ae0d8407c849456553a181ebefedb7606bac19aa1478b"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a954002064ee919b444b19c1185e8cce307a1f20600f47d6f4b6d336972c809"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-win32.whl", hash = "sha256:2ccdc818cc106cc238ff7eba0d71b9c77be868fdca31d6c3b1347a54c9b187b2"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-win_amd64.whl", hash = "sha256:91a84faf718e6f8b888ca63d0b2d6d185c8e2a198d2a7322d75c303e7097c8b7"}, - {file = "greenlet-1.1.3.post0.tar.gz", hash = "sha256:f5e09dc5c6e1796969fd4b775ea1417d70e49a5df29aaa8e5d10675d9e11872c"}, + {file = "greenlet-2.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4be4dedbd2fa9b7c35627f322d6d3139cb125bc18d5ef2f40237990850ea446f"}, + {file = "greenlet-2.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:75c022803de010294366f3608d4bba3e346693b1b7427b79d57e3d924ed03838"}, + {file = "greenlet-2.0.0-cp27-cp27m-win32.whl", hash = "sha256:4a1953465b7651073cffde74ed7d121e602ef9a9740d09ee137b01879ac15a2f"}, + {file = "greenlet-2.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:a65205e6778142528978b4acca76888e7e7f0be261e395664e49a5c21baa2141"}, + {file = "greenlet-2.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d71feebf5c8041c80dfda76427e14e3ca00bca042481bd3e9612a9d57b2cbbf7"}, + {file = "greenlet-2.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f7edbd2957f72aea357241fe42ffc712a8e9b8c2c42f24e2ef5d97b255f66172"}, + {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79687c48e7f564be40c46b3afea6d141b8d66ffc2bc6147e026d491c6827954a"}, + {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a245898ec5e9ca0bc87a63e4e222cc633dc4d1f1a0769c34a625ad67edb9f9de"}, + {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adcf45221f253b3a681c99da46fa6ac33596fa94c2f30c54368f7ee1c4563a39"}, + {file = "greenlet-2.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3dc294afebf2acfd029373dbf3d01d36fd8d6888a03f5a006e2d690f66b153d9"}, + {file = "greenlet-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1cfeae4dda32eb5c64df05d347c4496abfa57ad16a90082798a2bba143c6c854"}, + {file = "greenlet-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:d58d4b4dc82e2d21ebb7dd7d3a6d370693b2236a1407fe3988dc1d4ea07575f9"}, + {file = "greenlet-2.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0d7efab8418c1fb3ea00c4abb89e7b0179a952d0d53ad5fcff798ca7440f8e8"}, + {file = "greenlet-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:f8a10e14238407be3978fa6d190eb3724f9d766655fefc0134fd5482f1fb0108"}, + {file = "greenlet-2.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:98b848a0b75e76b446dc71fdbac712d9078d96bb1c1607f049562dde1f8801e1"}, + {file = "greenlet-2.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:8e8dbad9b4f4c3e37898914cfccb7c4f00dbe3146333cfe52a1a3103cc2ff97c"}, + {file = "greenlet-2.0.0-cp35-cp35m-win32.whl", hash = "sha256:069a8a557541a04518dc3beb9a78637e4e6b286814849a2ecfac529eaa78562b"}, + {file = "greenlet-2.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:cc211c2ff5d3b2ba8d557a71e3b4f0f0a2020067515143a9516ea43884271192"}, + {file = "greenlet-2.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:d4e7642366e638f45d70c5111590a56fbd0ffb7f474af20c6c67c01270bcf5cf"}, + {file = "greenlet-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e7a0dca752b4e3395890ab4085c3ec3838d73714261914c01b53ed7ea23b5867"}, + {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8c67ecda450ad4eac7837057f5deb96effa836dacaf04747710ccf8eeb73092"}, + {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cc1abaf47cfcfdc9ac0bdff173cebab22cd54e9e3490135a4a9302d0ff3b163"}, + {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efdbbbf7b6c8d5be52977afa65b9bb7b658bab570543280e76c0fabc647175ed"}, + {file = "greenlet-2.0.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7acaa51355d5b9549d474dc71be6846ee9a8f2cb82f4936e5efa7a50bbeb94ad"}, + {file = "greenlet-2.0.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2be628bca0395610da08921f9376dd14317f37256d41078f5c618358467681e1"}, + {file = "greenlet-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:eca9c0473de053dcc92156dd62c38c3578628b536c7f0cd66e655e211c14ac32"}, + {file = "greenlet-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9a4a9fea68fd98814999d91ea585e49ed68d7e199a70bef13a857439f60a4609"}, + {file = "greenlet-2.0.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:6b28420ae290bfbf5d827f976abccc2f74f0a3f5e4fb69b66acf98f1cbe95e7e"}, + {file = "greenlet-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:2b8e1c939b363292ecc93999fb1ad53ffc5d0aac8e933e4362b62365241edda5"}, + {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c5ddadfe40e903c6217ed2b95a79f49e942bb98527547cc339fc7e43a424aad"}, + {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e5ead803b11b60b347e08e0f37234d9a595f44a6420026e47bcaf94190c3cd6"}, + {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b89b78ffb516c2921aa180c2794082666e26680eef05996b91f46127da24d964"}, + {file = "greenlet-2.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:939963d0137ec92540d95b68b7f795e8dbadce0a1fca53e3e7ef8ddc18ee47cb"}, + {file = "greenlet-2.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c1e93ef863810fba75faf418f0861dbf59bfe01a7b5d0a91d39603df58d3d3fa"}, + {file = "greenlet-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:6fd342126d825b76bf5b49717a7c682e31ed1114906cdec7f5a0c2ff1bc737a7"}, + {file = "greenlet-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5392ddb893e7fba237b988f846c4a80576557cc08664d56dc1a69c5c02bdc80c"}, + {file = "greenlet-2.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b4fd73b62c1038e7ee938b1de328eaa918f76aa69c812beda3aff8a165494201"}, + {file = "greenlet-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:0ba0f2e5c4a8f141952411e356dba05d6fe0c38325ee0e4f2d0c6f4c2c3263d5"}, + {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8bacecee0c9348ab7c95df810e12585e9e8c331dfc1e22da4ed0bd635a5f483"}, + {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:341053e0a96d512315c27c34fad4672c4573caf9eb98310c39e7747645c88d8b"}, + {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fcdd8ae391ffabb3b672397b58a9737aaff6b8cae0836e8db8ff386fcea802"}, + {file = "greenlet-2.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c3aa7d3bc545162a6676445709b24a2a375284dc5e2f2432d58b80827c2bd91c"}, + {file = "greenlet-2.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d8dca31a39dd9f25641559b8cdf9066168c682dfcfbe0f797f03e4c9718a63a"}, + {file = "greenlet-2.0.0-cp38-cp38-win32.whl", hash = "sha256:aa2b371c3633e694d043d6cec7376cb0031c6f67029f37eef40bda105fd58753"}, + {file = "greenlet-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:0fa2a66fdf0d09929e79f786ad61529d4e752f452466f7ddaa5d03caf77a603d"}, + {file = "greenlet-2.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:e7ec3f2465ba9b7d25895307abe1c1c101a257c54b9ea1522bbcbe8ca8793735"}, + {file = "greenlet-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:99e9851e40150504474915605649edcde259a4cd9bce2fcdeb4cf33ad0b5c293"}, + {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20bf68672ae14ef2e2e6d3ac1f308834db1d0b920b3b0674eef48b2dce0498dd"}, + {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30198bccd774f9b6b1ba7564a0d02a79dd1fe926cfeb4107856fe16c9dfb441c"}, + {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d65d7d1ff64fb300127d2ffd27db909de4d21712a5dde59a3ad241fb65ee83d7"}, + {file = "greenlet-2.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5d396a5457458460b0c28f738fc8ab2738ee61b00c3f845c7047a333acd96c"}, + {file = "greenlet-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09f00f9938eb5ae1fe203558b56081feb0ca34a2895f8374cd01129ddf4d111c"}, + {file = "greenlet-2.0.0-cp39-cp39-win32.whl", hash = "sha256:089e123d80dbc6f61fff1ff0eae547b02c343d50968832716a7b0a33bea5f792"}, + {file = "greenlet-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc283f99a4815ef70cad537110e3e03abcef56ab7d005ba9a8c6ec33054ce9c0"}, + {file = "greenlet-2.0.0.tar.gz", hash = "sha256:6c66f0da8049ee3c126b762768179820d4c0ae0ca46ae489039e4da2fae39a52"}, ] gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, @@ -1574,48 +1558,55 @@ mysqlclient = [ {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, ] orjson = [ - {file = "orjson-3.8.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:9a93850a1bdc300177b111b4b35b35299f046148ba23020f91d6efd7bf6b9d20"}, - {file = "orjson-3.8.0-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:7536a2a0b41672f824912aeab545c2467a9ff5ca73a066ff04fb81043a0a177a"}, - {file = "orjson-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66c19399bb3b058e3236af7910b57b19a4fc221459d722ed72a7dc90370ca090"}, - {file = "orjson-3.8.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b391d5c2ddc2f302d22909676b306cb6521022c3ee306c861a6935670291b2c"}, - {file = "orjson-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb1042970ca5f544a047d6c235a7eb4acdb69df75441dd1dfcbc406377ab37"}, - {file = "orjson-3.8.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d189e2acb510e374700cb98cf11b54f0179916ee40f8453b836157ae293efa79"}, - {file = "orjson-3.8.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6a23b40c98889e9abac084ce5a1fb251664b41da9f6bdb40a4729e2288ed2ed4"}, - {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b68a42a31f8429728183c21fb440c21de1b62e5378d0d73f280e2d894ef8942e"}, - {file = "orjson-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff13410ddbdda5d4197a4a4c09969cb78c722a67550f0a63c02c07aadc624833"}, - {file = "orjson-3.8.0-cp310-none-win_amd64.whl", hash = "sha256:2d81e6e56bbea44be0222fb53f7b255b4e7426290516771592738ca01dbd053b"}, - {file = "orjson-3.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e2defd9527651ad39ec20ae03c812adf47ef7662bdd6bc07dabb10888d70dc62"}, - {file = "orjson-3.8.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9e6ac22cec72d5b39035b566e4b86c74b84866f12b5b0b6541506a080fb67d6d"}, - {file = "orjson-3.8.0-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e2f4a5542f50e3d336a18cb224fc757245ca66b1fd0b70b5dd4471b8ff5f2b0e"}, - {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1418feeb8b698b9224b1f024555895169d481604d5d884498c1838d7412794c"}, - {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6e3da2e4bd27c3b796519ca74132c7b9e5348fb6746315e0f6c1592bc5cf1caf"}, - {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896a21a07f1998648d9998e881ab2b6b80d5daac4c31188535e9d50460edfcf7"}, - {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:4065906ce3ad6195ac4d1bddde862fe811a42d7be237a1ff762666c3a4bb2151"}, - {file = "orjson-3.8.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:5f856279872a4449fc629924e6a083b9821e366cf98b14c63c308269336f7c14"}, - {file = "orjson-3.8.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1b1cd25acfa77935bb2e791b75211cec0cfc21227fe29387e553c545c3ff87e1"}, - {file = "orjson-3.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3e2459d441ab8fd8b161aa305a73d5269b3cda13b5a2a39eba58b4dd3e394f49"}, - {file = "orjson-3.8.0-cp37-none-win_amd64.whl", hash = "sha256:d2b5dafbe68237a792143137cba413447f60dd5df428e05d73dcba10c1ea6fcf"}, - {file = "orjson-3.8.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5b072ef8520cfe7bd4db4e3c9972d94336763c2253f7c4718a49e8733bada7b8"}, - {file = "orjson-3.8.0-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e68c699471ea3e2dd1b35bfd71c6a0a0e4885b64abbe2d98fce1ef11e0afaff3"}, - {file = "orjson-3.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7225e8b08996d1a0c804d3a641a53e796685e8c9a9fd52bd428980032cad9a"}, - {file = "orjson-3.8.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f687776a03c19f40b982fb5c414221b7f3d19097841571be2223d1569a59877"}, - {file = "orjson-3.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7990a9caf3b34016ac30be5e6cfc4e7efd76aa85614a1215b0eae4f0c7e3db59"}, - {file = "orjson-3.8.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:02d638d43951ba346a80f0abd5942a872cc87db443e073f6f6fc530fee81e19b"}, - {file = "orjson-3.8.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f4b46dbdda2f0bd6480c39db90b21340a19c3b0fcf34bc4c6e465332930ca539"}, - {file = "orjson-3.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:655d7387a1634a9a477c545eea92a1ee902ab28626d701c6de4914e2ed0fecd2"}, - {file = "orjson-3.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5edb93cdd3eb32977633fa7aaa6a34b8ab54d9c49cdcc6b0d42c247a29091b22"}, - {file = "orjson-3.8.0-cp38-none-win_amd64.whl", hash = "sha256:03ed95814140ff09f550b3a42e6821f855d981c94d25b9cc83e8cca431525d70"}, - {file = "orjson-3.8.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7b0e72974a5d3b101226899f111368ec2c9824d3e9804af0e5b31567f53ad98a"}, - {file = "orjson-3.8.0-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6ea5fe20ef97545e14dd4d0263e4c5c3bc3d2248d39b4b0aed4b84d528dfc0af"}, - {file = "orjson-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6433c956f4a18112342a18281e0bec67fcd8b90be3a5271556c09226e045d805"}, - {file = "orjson-3.8.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87462791dd57de2e3e53068bf4b7169c125c50960f1bdda08ed30c797cb42a56"}, - {file = "orjson-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be02f6acee33bb63862eeff80548cd6b8a62e2d60ad2d8dfd5a8824cc43d8887"}, - {file = "orjson-3.8.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:a709c2249c1f2955dbf879506fd43fa08c31fdb79add9aeb891e3338b648bf60"}, - {file = "orjson-3.8.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2065b6d280dc58f131ffd93393737961ff68ae7eb6884b68879394074cc03c13"}, - {file = "orjson-3.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fd6cac83136e06e538a4d17117eaeabec848c1e86f5742d4811656ad7ee475f"}, - {file = "orjson-3.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25b5e48fbb9f0b428a5e44cf740675c9281dd67816149fc33659803399adbbe8"}, - {file = "orjson-3.8.0-cp39-none-win_amd64.whl", hash = "sha256:2058653cc12b90e482beacb5c2d52dc3d7606f9e9f5a52c1c10ef49371e76f52"}, - {file = "orjson-3.8.0.tar.gz", hash = "sha256:fb42f7cf57d5804a9daa6b624e3490ec9e2631e042415f3aebe9f35a8492ba6c"}, + {file = "orjson-3.8.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:a70aaa2e56356e58c6e1b49f7b7f069df5b15e55db002a74db3ff3f7af67c7ff"}, + {file = "orjson-3.8.1-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d45db052d01d0ab7579470141d5c3592f4402d43cfacb67f023bc1210a67b7bc"}, + {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2aae92398c0023ac26a6cd026375f765ef5afe127eccabf563c78af7b572d59"}, + {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0bd5b4e539db8a9635776bdf9a25c3db84e37165e65d45c8ca90437adc46d6d8"}, + {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21efb87b168066201a120b0f54a2381f6f51ff3727e07b3908993732412b314a"}, + {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:e073338e422f518c1d4d80efc713cd17f3ed6d37c8c7459af04a95459f3206d1"}, + {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8f672f3987f6424f60ab2e86ea7ed76dd2806b8e9b506a373fc8499aed85ddb5"}, + {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:231c30958ed99c23128a21993c5ac0a70e1e568e6a898a47f70d5d37461ca47c"}, + {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59b4baf71c9f39125d7e535974b146cc180926462969f6d8821b4c5e975e11b3"}, + {file = "orjson-3.8.1-cp310-none-win_amd64.whl", hash = "sha256:fe25f50dc3d45364428baa0dbe3f613a5171c64eb0286eb775136b74e61ba58a"}, + {file = "orjson-3.8.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6802edf98f6918e89df355f56be6e7db369b31eed64ff2496324febb8b0aa43b"}, + {file = "orjson-3.8.1-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a4244f4199a160717f0027e434abb886e322093ceadb2f790ff0c73ed3e17662"}, + {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6956cf7a1ac97523e96f75b11534ff851df99a6474a561ad836b6e82004acbb8"}, + {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b4e3857dd2416b479f700e9bdf4fcec8c690d2716622397d2b7e848f9833e50"}, + {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8873e490dea0f9cd975d66f84618b6fb57b1ba45ecb218313707a71173d764f"}, + {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:124207d2cd04e845eaf2a6171933cde40aebcb8c2d7d3b081e01be066d3014b6"}, + {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d8ed77098c2e22181fce971f49a34204c38b79ca91c01d515d07015339ae8165"}, + {file = "orjson-3.8.1-cp311-none-win_amd64.whl", hash = "sha256:8623ac25fa0850a44ac845e9333c4da9ae5707b7cec8ac87cbe9d4e41137180f"}, + {file = "orjson-3.8.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:d67a0bd0283a3b17ac43c5ab8e4a7e9d3aa758d6ec5d51c232343c408825a5ad"}, + {file = "orjson-3.8.1-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d89ef8a4444d83e0a5171d14f2ab4895936ab1773165b020f97d29cf289a2d88"}, + {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97839a6abbebb06099294e6057d5b3061721ada08b76ae792e7041b6cb54c97f"}, + {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6071bcf51f0ae4d53b9d3e9164f7138164df4291c484a7b14562075aaa7a2b7b"}, + {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15e7d691cee75b5192fc1fa8487bf541d463246dc25c926b9b40f5b6ab56770"}, + {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:b9abc49c014def1b832fcd53bdc670474b6fe41f373d16f40409882c0d0eccba"}, + {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:3fd5472020042482d7da4c26a0ee65dbd931f691e1c838c6cf4232823179ecc1"}, + {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e399ed1b0d6f8089b9b6ff2cb3e71ba63a56d8ea88e1d95467949795cc74adfd"}, + {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5e3db6496463c3000d15b7a712da5a9601c6c43682f23f81862fe1d2a338f295"}, + {file = "orjson-3.8.1-cp37-none-win_amd64.whl", hash = "sha256:0f21eed14697083c01f7e00a87e21056fc8fb5851e8a7bca98345189abcdb4d4"}, + {file = "orjson-3.8.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5a9e324213220578d324e0858baeab47808a13d3c3fbc6ba55a3f4f069d757cf"}, + {file = "orjson-3.8.1-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69097c50c3ccbcc61292192b045927f1688ca57ce80525dc5d120e0b91e19bb0"}, + {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7822cba140f7ca48ed0256229f422dbae69e3a3475176185db0c0538cfadb57"}, + {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03389e3750c521a7f3d4837de23cfd21a7f24574b4b3985c9498f440d21adb03"}, + {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f9d9b5c6692097de07dd0b2d5ff20fd135bacd1b2fb7ea383ee717a4150c93"}, + {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:c2c9ef10b6344465fd5ac002be2d34f818211274dd79b44c75b2c14a979f84f3"}, + {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7adaac93678ac61f5dc070f615b18639d16ee66f6a946d5221dbf315e8b74bec"}, + {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0c1750f73658906b82cabbf4be2f74300644c17cb037fbc8b48d746c3b90c76"}, + {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:da6306e1f03e7085fe0db61d4a3377f70c6fd865118d0afe17f80ae9a8f6f124"}, + {file = "orjson-3.8.1-cp38-none-win_amd64.whl", hash = "sha256:f532c2cbe8c140faffaebcfb34d43c9946599ea8138971f181a399bec7d6b123"}, + {file = "orjson-3.8.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:6a7b76d4b44bca418f7797b1e157907b56b7d31caa9091db4e99ebee51c16933"}, + {file = "orjson-3.8.1-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f850489d89ea12be486492e68f0fd63e402fa28e426d4f0b5fc1eec0595e6109"}, + {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4449e70b98f3ad3e43958360e4be1189c549865c0a128e8629ec96ce92d251c3"}, + {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:45357eea9114bd41ef19280066591e9069bb4f6f5bffd533e9bfc12a439d735f"}, + {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5a9bc5bc4d730153529cb0584c63ff286d50663ccd48c9435423660b1bb12d"}, + {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:a806aca6b80fa1d996aa16593e4995a71126a085ee1a59fff19ccad29a4e47fd"}, + {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:395d02fd6be45f960da014372e7ecefc9e5f8df57a0558b7111a5fa8423c0669"}, + {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:caff3c1e964cfee044a03a46244ecf6373f3c56142ad16458a1446ac6d69824a"}, + {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ded261268d5dfd307078fe3370295e5eb15bdde838bbb882acf8538e061c451"}, + {file = "orjson-3.8.1-cp39-none-win_amd64.whl", hash = "sha256:45c1914795ffedb2970bfcd3ed83daf49124c7c37943ed0a7368971c6ea5e278"}, + {file = "orjson-3.8.1.tar.gz", hash = "sha256:07c42de52dfef56cdcaf2278f58e837b26f5b5af5f1fd133a68c4af203851fc7"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -1652,24 +1643,20 @@ prometheus-fastapi-instrumentator = [ {file = "prometheus_fastapi_instrumentator-5.9.1-py3-none-any.whl", hash = "sha256:b5206ea9aa6975a0b07f3bf7376932b8a1b2983164b5abb04878e75ba336d9ed"}, ] protobuf = [ - {file = "protobuf-4.21.8-cp310-abi3-win32.whl", hash = "sha256:c252c55ee15175aa1b21b7b9896e6add5162d066d5202e75c39f96136f08cce3"}, - {file = "protobuf-4.21.8-cp310-abi3-win_amd64.whl", hash = "sha256:809ca0b225d3df42655a12f311dd0f4148a943c51f1ad63c38343e457492b689"}, - {file = "protobuf-4.21.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bbececaf3cfea9ea65ebb7974e6242d310d2a7772a6f015477e0d79993af4511"}, - {file = "protobuf-4.21.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:b02eabb9ebb1a089ed20626a90ad7a69cee6bcd62c227692466054b19c38dd1f"}, - {file = "protobuf-4.21.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:4761201b93e024bb70ee3a6a6425d61f3152ca851f403ba946fb0cde88872661"}, - {file = "protobuf-4.21.8-cp37-cp37m-win32.whl", hash = "sha256:f2d55ff22ec300c4d954d3b0d1eeb185681ec8ad4fbecff8a5aee6a1cdd345ba"}, - {file = "protobuf-4.21.8-cp37-cp37m-win_amd64.whl", hash = "sha256:c5f94911dd8feb3cd3786fc90f7565c9aba7ce45d0f254afd625b9628f578c3f"}, - {file = "protobuf-4.21.8-cp38-cp38-win32.whl", hash = "sha256:b37b76efe84d539f16cba55ee0036a11ad91300333abd213849cbbbb284b878e"}, - {file = "protobuf-4.21.8-cp38-cp38-win_amd64.whl", hash = "sha256:2c92a7bfcf4ae76a8ac72e545e99a7407e96ffe52934d690eb29a8809ee44d7b"}, - {file = "protobuf-4.21.8-cp39-cp39-win32.whl", hash = "sha256:89d641be4b5061823fa0e463c50a2607a97833e9f8cfb36c2f91ef5ccfcc3861"}, - {file = "protobuf-4.21.8-cp39-cp39-win_amd64.whl", hash = "sha256:bc471cf70a0f53892fdd62f8cd4215f0af8b3f132eeee002c34302dff9edd9b6"}, - {file = "protobuf-4.21.8-py2.py3-none-any.whl", hash = "sha256:a55545ce9eec4030cf100fcb93e861c622d927ef94070c1a3c01922902464278"}, - {file = "protobuf-4.21.8-py3-none-any.whl", hash = "sha256:0f236ce5016becd989bf39bd20761593e6d8298eccd2d878eda33012645dc369"}, - {file = "protobuf-4.21.8.tar.gz", hash = "sha256:427426593b55ff106c84e4a88cac855175330cb6eb7e889e85aaa7b5652b686d"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, + {file = "protobuf-4.21.9-cp310-abi3-win32.whl", hash = "sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392"}, + {file = "protobuf-4.21.9-cp310-abi3-win_amd64.whl", hash = "sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf"}, + {file = "protobuf-4.21.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1"}, + {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719"}, + {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740"}, + {file = "protobuf-4.21.9-cp37-cp37m-win32.whl", hash = "sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c"}, + {file = "protobuf-4.21.9-cp37-cp37m-win_amd64.whl", hash = "sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536"}, + {file = "protobuf-4.21.9-cp38-cp38-win32.whl", hash = "sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce"}, + {file = "protobuf-4.21.9-cp38-cp38-win_amd64.whl", hash = "sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444"}, + {file = "protobuf-4.21.9-cp39-cp39-win32.whl", hash = "sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa"}, + {file = "protobuf-4.21.9-cp39-cp39-win_amd64.whl", hash = "sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b"}, + {file = "protobuf-4.21.9-py2.py3-none-any.whl", hash = "sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965"}, + {file = "protobuf-4.21.9-py3-none-any.whl", hash = "sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b"}, + {file = "protobuf-4.21.9.tar.gz", hash = "sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99"}, ] pyalpm = [ {file = "pyalpm-0.10.6.tar.gz", hash = "sha256:99e6ec73b8c46bb12466013f228f831ee0d18e8ab664b91a01c2a3c40de07c7f"}, @@ -1760,8 +1747,8 @@ pyparsing = [ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.20.1.tar.gz", hash = "sha256:626699de2a747611f3eeb64168b3575f70439b06c3d0206e6ceaeeb956e65519"}, @@ -1771,17 +1758,13 @@ pytest-cov = [ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] pytest-tap = [ {file = "pytest-tap-3.3.tar.gz", hash = "sha256:5f0919a147cf0396b2f10d64d365a0bf8062e06543e93c675c9d37f5605e983c"}, {file = "pytest_tap-3.3-py3-none-any.whl", hash = "sha256:4fbbc0e090c2e94f6199bee4e4f68ab3c5e176b37a72a589ad84e0f72a2fce55"}, ] pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, + {file = "pytest-xdist-3.0.2.tar.gz", hash = "sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291"}, + {file = "pytest_xdist-3.0.2-py3-none-any.whl", hash = "sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -1972,6 +1955,6 @@ wsproto = [ {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, ] zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/pyproject.toml b/pyproject.toml index 3b615c73..0bf1bdf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,13 +62,13 @@ asgiref = "^3.4.1" bcrypt = "^4.0.0" bleach = "^5.0.0" email-validator = "^1.3.0" -fakeredis = "^1.6.1" +fakeredis = "^1.10.0" feedgen = "^0.9.0" httpx = "^0.23.0" itsdangerous = "^2.0.1" lxml = "^4.6.3" -orjson = "^3.6.4" -protobuf = "^4.0.0" +orjson = "^3.8.1" +protobuf = "^4.21.9" pygit2 = "^1.7.0" python-multipart = "^0.0.5" redis = "^4.0.0" @@ -89,7 +89,7 @@ uvicorn = "^0.19.0" gunicorn = "^20.1.0" Hypercorn = "^0.14.0" prometheus-fastapi-instrumentator = "^5.7.1" -pytest-xdist = "^2.4.0" +pytest-xdist = "^3.0.2" filelock = "^3.3.2" posix-ipc = "^1.0.5" pyalpm = "^0.10.6" @@ -98,7 +98,7 @@ srcinfo = "^0.0.8" [tool.poetry.dev-dependencies] coverage = "^6.0.2" -pytest = "^7.0.0" +pytest = "^7.2.0" pytest-asyncio = "^0.20.1" pytest-cov = "^4.0.0" pytest-tap = "^3.2" From c0e806072e705652f0eb6d22dff1f64ab8735dcd Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 1 Nov 2022 18:31:37 +0000 Subject: [PATCH 1183/1451] chore: bump to v6.1.8 Signed-off-by: Leonidas Spyropoulos --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index e8ca70d9..49806738 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.7" +AURWEB_VERSION = "v6.1.8" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 0bf1bdf8..7fc0db47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.7" +version = "v6.1.8" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 4f56a0166208b781adc13da53bc001e376379b57 Mon Sep 17 00:00:00 2001 From: Lex Black Date: Fri, 4 Nov 2022 08:47:03 +0100 Subject: [PATCH 1184/1451] chore: fix mailing-lists urls Those changed after the migration to mailman3 Signed-off-by: Leonidas Spyropoulos --- templates/home.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/home.html b/templates/home.html index 6a5fca69..3a7bc76d 100644 --- a/templates/home.html +++ b/templates/home.html @@ -42,7 +42,7 @@

    {{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests." | tr - | format('', "") + | format('', "") | safe }}

    @@ -72,8 +72,8 @@

    {{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." | tr - | format('', "", - '', "") + | format('', "", + '', "") | safe }}

    From c248a74f80d5c72bd6a01f5dfc7ee1c05b2bc6a5 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 7 Nov 2022 14:36:34 +0100 Subject: [PATCH 1185/1451] chore: fix mailing-list URL on passreset page small addition to the patch provided in #404 Signed-off-by: moson-mo --- templates/passreset.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/passreset.html b/templates/passreset.html index d2c3c2ee..6a31109f 100644 --- a/templates/passreset.html +++ b/templates/passreset.html @@ -47,7 +47,7 @@ {% else %} - {% set url = "https://mailman.archlinux.org/mailman/listinfo/aur-general" %} + {% set url = "https://lists.archlinux.org/mailman3/lists/aur-general.lists.archlinux.org/" %} {{ "If you have forgotten the user name and the primary e-mail " "address you used to register, please send a message to the " "%saur-general%s mailing list." From 73f0bddf0b52bc79ef29b5eaf20f2af8d305528b Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 8 Nov 2022 13:14:42 +0000 Subject: [PATCH 1186/1451] fix: handle default requests when using pages The default page shows the pending requests which were working OK if one used the Filters button. This fixes the case when someone submits by using the pager (Next, Last etc). Closes: #405 Signed-off-by: Leonidas Spyropoulos --- aurweb/routers/requests.py | 10 +++++++-- test/test_requests.py | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index ca5fae73..d1f1b830 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -18,6 +18,13 @@ from aurweb.requests.util import get_pkgreq_by_id from aurweb.scripts import notify from aurweb.templates import make_context, render_template +FILTER_PARAMS = { + "filter_pending", + "filter_closed", + "filter_accepted", + "filter_rejected", +} + router = APIRouter() @@ -36,7 +43,7 @@ async def requests( context["q"] = dict(request.query_params) - if len(dict(request.query_params)) == 0: + if not dict(request.query_params).keys() & FILTER_PARAMS: filter_pending = True O, PP = util.sanitize_params(O, PP) @@ -89,7 +96,6 @@ async def requests( .offset(O) .all() ) - return render_template(request, "requests.html", context) diff --git a/test/test_requests.py b/test/test_requests.py index 344b9edc..7dfcf5e5 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -734,6 +734,52 @@ def test_requests( rows = root.xpath('//table[@class="results"]/tbody/tr') assert len(rows) == defaults.PP + # Request page 2 of the requests page. + with client as request: + resp = request.get("/requests", params={"O": 50}, cookies=cookies) # Page 2 + assert resp.status_code == int(HTTPStatus.OK) + + assert "‹ Previous" in resp.text + assert "« First" in resp.text + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 5 # There are five records left on the second page. + + +def test_requests_with_filters( + client: TestClient, + tu_user: User, + packages: list[Package], + requests: list[PackageRequest], +): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.get( + "/requests", + params={ + # Pass in url query parameters O, SeB and SB to exercise + # their paths inside of the pager_nav used in this request. + "O": 0, # Page 1 + "SeB": "nd", + "SB": "n", + "filter_pending": True, + "filter_closed": True, + "filter_accepted": True, + "filter_rejected": True, + }, + cookies=cookies, + ) + assert resp.status_code == int(HTTPStatus.OK) + + assert "Next ›" in resp.text + assert "Last »" in resp.text + + root = parse_root(resp.text) + # We have 55 requests, our defaults.PP is 50, so expect we have 50 rows. + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == defaults.PP + # Request page 2 of the requests page. with client as request: resp = request.get( From 50287cb066c02de5337f87508e51852d4a1e5ccb Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 7 Nov 2022 14:19:38 +0100 Subject: [PATCH 1187/1451] feat(rpc): add "by" parameters - package relations This adds new "by" search-parameters: provides, conflicts and replaces Signed-off-by: moson-mo --- aurweb/packages/search.py | 34 ++++++++++++++++++++++++++++++++++ aurweb/rpc.py | 3 +++ test/test_rpc.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 224212d1..7e767bde 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -14,6 +14,7 @@ from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_notification import PackageNotification from aurweb.models.package_vote import PackageVote +from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID class PackageSearch: @@ -286,6 +287,9 @@ class RPCSearch(PackageSearch): "makedepends": self._search_by_makedepends, "optdepends": self._search_by_optdepends, "checkdepends": self._search_by_checkdepends, + "provides": self._search_by_provides, + "conflicts": self._search_by_conflicts, + "replaces": self._search_by_replaces, } ) @@ -304,6 +308,18 @@ class RPCSearch(PackageSearch): ) return self.query + def _join_relations(self, rel_type_id: int) -> orm.Query: + """Join Package with PackageRelation and filter results + based on `rel_type_id`. + + :param rel_type_id: RelationType ID + :returns: PackageRelation-joined orm.Query + """ + self.query = self.query.join(models.PackageRelation).filter( + models.PackageRelation.RelTypeID == rel_type_id + ) + return self.query + def _search_by_depends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(DEPENDS_ID).filter( models.PackageDependency.DepName == keywords @@ -328,6 +344,24 @@ class RPCSearch(PackageSearch): ) return self + def _search_by_provides(self, keywords: str) -> "RPCSearch": + self.query = self._join_relations(PROVIDES_ID).filter( + models.PackageRelation.RelName == keywords + ) + return self + + def _search_by_conflicts(self, keywords: str) -> "RPCSearch": + self.query = self._join_relations(CONFLICTS_ID).filter( + models.PackageRelation.RelName == keywords + ) + return self + + def _search_by_replaces(self, keywords: str) -> "RPCSearch": + self.query = self._join_relations(REPLACES_ID).filter( + models.PackageRelation.RelName == keywords + ) + return self + def search_by(self, by: str, keywords: str) -> "RPCSearch": """Override inherited search_by. In this override, we reduce the scope of what we handle within this function. We do not set `by` diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 515c6ffb..9004a51f 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -83,6 +83,9 @@ class RPC: "makedepends", "optdepends", "checkdepends", + "provides", + "conflicts", + "replaces", } # A mapping of by aliases. diff --git a/test/test_rpc.py b/test/test_rpc.py index f417d379..c5004f07 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -852,6 +852,42 @@ def test_rpc_search_checkdepends( assert result.get("Name") == packages[0].Name +def test_rpc_search_provides( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "provides", "arg": "chungus-provides"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_conflicts( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "conflicts", "arg": "chungus-conflicts"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_replaces( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "replaces", "arg": "chungus-replaces"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + def test_rpc_incorrect_by(client: TestClient): params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} with client as request: From 0583f30a53880b8908dd3746bf81b8f560bc09b2 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 7 Nov 2022 21:41:42 +0100 Subject: [PATCH 1188/1451] feat(rpc): add "by" parameter - groups Adding "by" parameter to search by "groups" Signed-off-by: moson-mo --- aurweb/packages/search.py | 17 ++++++++++++++++- aurweb/rpc.py | 1 + test/test_rpc.py | 25 ++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 7e767bde..60e9f0fc 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -3,7 +3,7 @@ from typing import Set from sqlalchemy import and_, case, or_, orm from aurweb import db, models -from aurweb.models import Package, PackageBase, User +from aurweb.models import Group, Package, PackageBase, User from aurweb.models.dependency_type import ( CHECKDEPENDS_ID, DEPENDS_ID, @@ -11,6 +11,7 @@ from aurweb.models.dependency_type import ( OPTDEPENDS_ID, ) from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_group import PackageGroup from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_notification import PackageNotification from aurweb.models.package_vote import PackageVote @@ -290,6 +291,7 @@ class RPCSearch(PackageSearch): "provides": self._search_by_provides, "conflicts": self._search_by_conflicts, "replaces": self._search_by_replaces, + "groups": self._search_by_groups, } ) @@ -320,6 +322,14 @@ class RPCSearch(PackageSearch): ) return self.query + def _join_groups(self) -> orm.Query: + """Join Package with PackageGroup and Group. + + :returns: PackageGroup/Group-joined orm.Query + """ + self.query = self.query.join(PackageGroup).join(Group) + return self.query + def _search_by_depends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(DEPENDS_ID).filter( models.PackageDependency.DepName == keywords @@ -362,6 +372,11 @@ class RPCSearch(PackageSearch): ) return self + def _search_by_groups(self, keywords: str) -> orm.Query: + self._join_groups() + self.query = self.query.filter(Group.Name == keywords) + return self + def search_by(self, by: str, keywords: str) -> "RPCSearch": """Override inherited search_by. In this override, we reduce the scope of what we handle within this function. We do not set `by` diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 9004a51f..5cdf675d 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -86,6 +86,7 @@ class RPC: "provides", "conflicts", "replaces", + "groups", } # A mapping of by aliases. diff --git a/test/test_rpc.py b/test/test_rpc.py index c5004f07..bbd74588 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -13,10 +13,12 @@ from aurweb import asgi, config, db, rpc, scripts, time from aurweb.aur_redis import redis_connection from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID +from aurweb.models.group import Group from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_dependency import PackageDependency +from aurweb.models.package_group import PackageGroup from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_license import PackageLicense from aurweb.models.package_relation import PackageRelation @@ -139,11 +141,14 @@ def packages(user: User, user2: User, user3: User) -> list[Package]: output.append(pkg) # Setup a few more related records on the first package: - # a license, some keywords and some votes. + # a license, group, some keywords and some votes. with db.begin(): lic = db.create(License, Name="GPL") db.create(PackageLicense, Package=output[0], License=lic) + grp = db.create(Group, Name="testgroup") + db.create(PackageGroup, Package=output[0], Group=grp) + for keyword in ["big-chungus", "smol-chungus", "sizeable-chungus"]: db.create( PackageKeyword, PackageBase=output[0].PackageBase, Keyword=keyword @@ -326,6 +331,7 @@ def test_rpc_singular_info( "Replaces": ["chungus-replaces<=200"], "License": [pkg.package_licenses.first().License.Name], "Keywords": ["big-chungus", "sizeable-chungus", "smol-chungus"], + "Groups": ["testgroup"], } ], "resultcount": 1, @@ -888,6 +894,23 @@ def test_rpc_search_replaces( assert result.get("Name") == packages[0].Name +def test_rpc_search_groups( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): + params = { + "v": 5, + "type": "search", + "by": "groups", + "arg": "testgroup", + } + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + def test_rpc_incorrect_by(client: TestClient): params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} with client as request: From 5484e68b42392c95a90a3425841419a5782c412a Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 7 Nov 2022 22:46:24 +0100 Subject: [PATCH 1189/1451] feat(rpc): add "by" parameter - submitter Add "by" parameter: submitter Signed-off-by: moson-mo --- aurweb/packages/search.py | 2 +- aurweb/rpc.py | 3 ++- test/test_rpc.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 60e9f0fc..51d97d8e 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -269,7 +269,7 @@ class RPCSearch(PackageSearch): sanitization done for the PackageSearch `by` argument. """ - keys_removed = ("b", "N", "B", "k", "c", "M", "s") + keys_removed = ("b", "N", "B", "k", "c", "M") def __init__(self) -> "RPCSearch": super().__init__() diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 5cdf675d..fa36486e 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -87,10 +87,11 @@ class RPC: "conflicts", "replaces", "groups", + "submitter", } # A mapping of by aliases. - BY_ALIASES = {"name-desc": "nd", "name": "n", "maintainer": "m"} + BY_ALIASES = {"name-desc": "nd", "name": "n", "maintainer": "m", "submitter": "s"} def __init__(self, version: int = 0, type: str = None) -> "RPC": self.version = version diff --git a/test/test_rpc.py b/test/test_rpc.py index bbd74588..5d59d16b 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -81,7 +81,11 @@ def packages(user: User, user2: User, user3: User) -> list[Package]: # Create package records used in our tests. with db.begin(): pkgbase = db.create( - PackageBase, Name="big-chungus", Maintainer=user, Packager=user + PackageBase, + Name="big-chungus", + Maintainer=user, + Packager=user, + Submitter=user2, ) pkg = db.create( Package, @@ -93,7 +97,11 @@ def packages(user: User, user2: User, user3: User) -> list[Package]: output.append(pkg) pkgbase = db.create( - PackageBase, Name="chungy-chungus", Maintainer=user, Packager=user + PackageBase, + Name="chungy-chungus", + Maintainer=user, + Packager=user, + Submitter=user2, ) pkg = db.create( Package, @@ -911,6 +919,25 @@ def test_rpc_search_groups( assert result.get("Name") == packages[0].Name +def test_rpc_search_submitter(client: TestClient, user2: User, packages: list[Package]): + params = {"v": 5, "type": "search", "by": "submitter", "arg": user2.Username} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + + # user2 submitted 2 packages + assert data.get("resultcount") == 2 + names = list(sorted(r.get("Name") for r in data.get("results"))) + expected_results = ["big-chungus", "chungy-chungus"] + assert names == expected_results + + # Search for a non-existent submitter, giving us zero packages. + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 0 + + def test_rpc_incorrect_by(client: TestClient): params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} with client as request: From efd20ed2c740910996e9f1aa7e24a2337be4db11 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Tue, 8 Nov 2022 15:26:27 +0100 Subject: [PATCH 1190/1451] feat(rpc): add "by" parameter - keywords Add "by" parameter: keywords Signed-off-by: moson-mo --- aurweb/packages/search.py | 9 +++++++-- aurweb/rpc.py | 9 ++++++++- test/test_rpc.py | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 51d97d8e..37a5b6ff 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -269,7 +269,7 @@ class RPCSearch(PackageSearch): sanitization done for the PackageSearch `by` argument. """ - keys_removed = ("b", "N", "B", "k", "c", "M") + keys_removed = ("b", "N", "B", "c", "M") def __init__(self) -> "RPCSearch": super().__init__() @@ -372,11 +372,16 @@ class RPCSearch(PackageSearch): ) return self - def _search_by_groups(self, keywords: str) -> orm.Query: + def _search_by_groups(self, keywords: str) -> "RPCSearch": self._join_groups() self.query = self.query.filter(Group.Name == keywords) return self + def _search_by_keywords(self, keywords: str) -> "RPCSearch": + self._join_keywords() + self.query = self.query.filter(PackageKeyword.Keyword == keywords) + return self + def search_by(self, by: str, keywords: str) -> "RPCSearch": """Override inherited search_by. In this override, we reduce the scope of what we handle within this function. We do not set `by` diff --git a/aurweb/rpc.py b/aurweb/rpc.py index fa36486e..2a07f6c7 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -88,10 +88,17 @@ class RPC: "replaces", "groups", "submitter", + "keywords", } # A mapping of by aliases. - BY_ALIASES = {"name-desc": "nd", "name": "n", "maintainer": "m", "submitter": "s"} + BY_ALIASES = { + "name-desc": "nd", + "name": "n", + "maintainer": "m", + "submitter": "s", + "keywords": "k", + } def __init__(self, version: int = 0, type: str = None) -> "RPC": self.version = version diff --git a/test/test_rpc.py b/test/test_rpc.py index 5d59d16b..9c3ca883 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -938,6 +938,25 @@ def test_rpc_search_submitter(client: TestClient, user2: User, packages: list[Pa assert data.get("resultcount") == 0 +def test_rpc_search_keywords(client: TestClient, packages: list[Package]): + params = {"v": 5, "type": "search", "by": "keywords", "arg": "big-chungus"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + + # should get 2 packages + assert data.get("resultcount") == 1 + names = list(sorted(r.get("Name") for r in data.get("results"))) + expected_results = ["big-chungus"] + assert names == expected_results + + # non-existent search + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 0 + + def test_rpc_incorrect_by(client: TestClient): params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} with client as request: From bcd808ddc11c570d9259a93a69d165403e48230e Mon Sep 17 00:00:00 2001 From: moson-mo Date: Tue, 8 Nov 2022 16:44:59 +0100 Subject: [PATCH 1191/1451] feat(rpc): add "by" parameter - comaintainers Add "by" parameter: comaintainers Signed-off-by: moson-mo --- aurweb/packages/search.py | 2 +- aurweb/rpc.py | 2 ++ test/test_rpc.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 37a5b6ff..c0740cda 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -269,7 +269,7 @@ class RPCSearch(PackageSearch): sanitization done for the PackageSearch `by` argument. """ - keys_removed = ("b", "N", "B", "c", "M") + keys_removed = ("b", "N", "B", "M") def __init__(self) -> "RPCSearch": super().__init__() diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 2a07f6c7..34caf756 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -89,6 +89,7 @@ class RPC: "groups", "submitter", "keywords", + "comaintainers", } # A mapping of by aliases. @@ -98,6 +99,7 @@ class RPC: "maintainer": "m", "submitter": "s", "keywords": "k", + "comaintainers": "c", } def __init__(self, version: int = 0, type: str = None) -> "RPC": diff --git a/test/test_rpc.py b/test/test_rpc.py index 9c3ca883..4768a2da 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -17,6 +17,7 @@ from aurweb.models.group import Group from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_group import PackageGroup from aurweb.models.package_keyword import PackageKeyword @@ -149,7 +150,7 @@ def packages(user: User, user2: User, user3: User) -> list[Package]: output.append(pkg) # Setup a few more related records on the first package: - # a license, group, some keywords and some votes. + # a license, group, some keywords, comaintainer and some votes. with db.begin(): lic = db.create(License, Name="GPL") db.create(PackageLicense, Package=output[0], License=lic) @@ -157,6 +158,13 @@ def packages(user: User, user2: User, user3: User) -> list[Package]: grp = db.create(Group, Name="testgroup") db.create(PackageGroup, Package=output[0], Group=grp) + db.create( + PackageComaintainer, + PackageBase=output[0].PackageBase, + User=user2, + Priority=1, + ) + for keyword in ["big-chungus", "smol-chungus", "sizeable-chungus"]: db.create( PackageKeyword, PackageBase=output[0].PackageBase, Keyword=keyword @@ -957,6 +965,27 @@ def test_rpc_search_keywords(client: TestClient, packages: list[Package]): assert data.get("resultcount") == 0 +def test_rpc_search_comaintainers( + client: TestClient, user2: User, packages: list[Package] +): + params = {"v": 5, "type": "search", "by": "comaintainers", "arg": user2.Username} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + + # should get 1 package + assert data.get("resultcount") == 1 + names = list(sorted(r.get("Name") for r in data.get("results"))) + expected_results = ["big-chungus"] + assert names == expected_results + + # non-existent search + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 0 + + def test_rpc_incorrect_by(client: TestClient): params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} with client as request: From 500d6b403b827e51e602818bb17e4ecbcd2b5842 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Fri, 4 Nov 2022 14:09:09 +0000 Subject: [PATCH 1192/1451] feat: add co-maintainers to RPC Signed-off-by: Leonidas Spyropoulos --- aurweb/rpc.py | 16 ++++++++++++++++ test/test_rpc.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 34caf756..af31d2de 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -284,6 +284,22 @@ class RPC: ) .distinct() .order_by("Name"), + # Co-Maintainer + db.query(models.PackageComaintainer) + .join(models.User, models.User.ID == models.PackageComaintainer.UsersID) + .join( + models.Package, + models.Package.PackageBaseID + == models.PackageComaintainer.PackageBaseID, + ) + .with_entities( + models.Package.ID, + literal("CoMaintainers").label("Type"), + models.User.Username.label("Name"), + literal(str()).label("Cond"), + ) + .distinct() # A package could have the same co-maintainer multiple times + .order_by("Name"), ] # Union all subqueries together. diff --git a/test/test_rpc.py b/test/test_rpc.py index 4768a2da..424352db 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -272,6 +272,33 @@ def relations(user: User, packages: list[Package]) -> list[PackageRelation]: yield output +@pytest.fixture +def comaintainer( + user2: User, user3: User, packages: list[Package] +) -> list[PackageComaintainer]: + output = [] + + with db.begin(): + comaintainer = db.create( + PackageComaintainer, + User=user2, + PackageBase=packages[0].PackageBase, + Priority=1, + ) + output.append(comaintainer) + + comaintainer = db.create( + PackageComaintainer, + User=user3, + PackageBase=packages[0].PackageBase, + Priority=1, + ) + output.append(comaintainer) + + # Finally, yield the packages. + yield output + + @pytest.fixture(autouse=True) def setup(db_test): # Create some extra package relationships. @@ -321,6 +348,7 @@ def test_rpc_singular_info( packages: list[Package], depends: list[PackageDependency], relations: list[PackageRelation], + comaintainer: list[PackageComaintainer], ): # Define expected response. pkg = packages[0] @@ -343,6 +371,7 @@ def test_rpc_singular_info( "MakeDepends": ["chungus-makedepends"], "CheckDepends": ["chungus-checkdepends"], "Conflicts": ["chungus-conflicts"], + "CoMaintainers": ["user2", "user3"], "Provides": ["chungus-provides<=200"], "Replaces": ["chungus-replaces<=200"], "License": [pkg.package_licenses.first().License.Name], From bce5b81acd2b2dfcdfaf46ae962241e7dbe61ef9 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 10 Nov 2022 21:28:16 +0000 Subject: [PATCH 1193/1451] feat: allow filtering requests from maintainers These are usually easy to handle from TUs so allow to filter for them Signed-off-by: Leonidas Spyropoulos --- aurweb/routers/requests.py | 19 +++++++++++--- pytest.ini | 11 +++----- templates/requests.html | 5 ++++ test/test_requests.py | 53 ++++++++++++++++++++++++++++++++------ 4 files changed, 69 insertions(+), 19 deletions(-) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index d1f1b830..6880abd9 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -2,12 +2,12 @@ from http import HTTPStatus from fastapi import APIRouter, Form, Query, Request from fastapi.responses import RedirectResponse -from sqlalchemy import case +from sqlalchemy import case, orm from aurweb import db, defaults, time, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import handle_form_exceptions -from aurweb.models import PackageRequest +from aurweb.models import PackageBase, PackageRequest, User from aurweb.models.package_request import ( ACCEPTED_ID, CLOSED_ID, @@ -23,6 +23,7 @@ FILTER_PARAMS = { "filter_closed", "filter_accepted", "filter_rejected", + "filter_maintainers_requests", } router = APIRouter() @@ -38,6 +39,7 @@ async def requests( filter_closed: bool = False, filter_accepted: bool = False, filter_rejected: bool = False, + filter_maintainer_requests: bool = False, ): context = make_context(request, "Requests") @@ -53,9 +55,17 @@ async def requests( context["filter_closed"] = filter_closed context["filter_accepted"] = filter_accepted context["filter_rejected"] = filter_rejected + context["filter_maintainer_requests"] = filter_maintainer_requests + Maintainer = orm.aliased(User) # A PackageRequest query - query = db.query(PackageRequest) + query = ( + db.query(PackageRequest) + .join(PackageBase) + .join(User, PackageRequest.UsersID == User.ID, isouter=True) + .join(Maintainer, PackageBase.MaintainerUID == Maintainer.ID, isouter=True) + ) + # query = db.query(PackageRequest).join(User) # Requests statistics context["total_requests"] = query.count() @@ -79,6 +89,9 @@ async def requests( if filter_rejected: in_filters.append(REJECTED_ID) filtered = query.filter(PackageRequest.Status.in_(in_filters)) + # Additionally filter for requests made from package maintainer + if filter_maintainer_requests: + filtered = filtered.filter(PackageRequest.UsersID == PackageBase.MaintainerUID) # If the request user is not elevated (TU or Dev), then # filter PackageRequests which are owned by the request user. if not request.user.is_elevated(): diff --git a/pytest.ini b/pytest.ini index 9f70a2bd..62d1922a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,13 +1,8 @@ [pytest] -# Ignore the following DeprecationWarning(s): -# - asyncio.base_events -# - DeprecationWarning speaking about internal asyncio -# using the loop= argument being deprecated starting -# with python 3.8, before python 3.10. -# - Note: This is a bug in upstream filed at -# https://bugs.python.org/issue45097 filterwarnings = - ignore::DeprecationWarning:asyncio.base_events + # This is coming from https://github.com/pytest-dev/pytest-xdist/issues/825 and it's caused from pytest-cov + # Remove once fixed: https://github.com/pytest-dev/pytest-cov/issues/557 + ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning # Build in coverage and pytest-xdist multiproc testing. addopts = --cov=aurweb --cov-append --dist load --dist loadfile -n auto diff --git a/templates/requests.html b/templates/requests.html index 9037855c..669b46b0 100644 --- a/templates/requests.html +++ b/templates/requests.html @@ -56,6 +56,11 @@ +

    + + +
    diff --git a/test/test_requests.py b/test/test_requests.py index 7dfcf5e5..6475fae6 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -96,7 +96,21 @@ def maintainer() -> User: @pytest.fixture -def packages(maintainer: User) -> list[Package]: +def maintainer2() -> User: + """Yield a specific User used to maintain packages.""" + with db.begin(): + maintainer = db.create( + User, + Username="test_maintainer2", + Email="test_maintainer2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield maintainer + + +@pytest.fixture +def packages(maintainer: User, maintainer2: User) -> list[Package]: """Yield 55 packages named pkg_0 .. pkg_54.""" packages_ = [] now = time.utcnow() @@ -105,7 +119,7 @@ def packages(maintainer: User) -> list[Package]: pkgbase = db.create( PackageBase, Name=f"pkg_{i}", - Maintainer=maintainer, + Maintainer=maintainer2 if i > 52 else maintainer, Packager=maintainer, Submitter=maintainer, ModifiedTS=now, @@ -117,14 +131,18 @@ def packages(maintainer: User) -> list[Package]: @pytest.fixture -def requests(user: User, packages: list[Package]) -> list[PackageRequest]: +def requests( + user: User, maintainer2: User, packages: list[Package] +) -> list[PackageRequest]: pkgreqs = [] with db.begin(): for i in range(55): pkgreq = db.create( PackageRequest, ReqTypeID=DELETION_ID, - User=user, + User=maintainer2 + if packages[i].PackageBase.Maintainer.Username == "test_maintainer2" + else user, PackageBase=packages[i].PackageBase, PackageBaseName=packages[i].Name, Comments=f"Deletion request for pkg_{i}", @@ -717,10 +735,6 @@ def test_requests( "O": 0, # Page 1 "SeB": "nd", "SB": "n", - "filter_pending": True, - "filter_closed": True, - "filter_accepted": True, - "filter_rejected": True, }, cookies=cookies, ) @@ -767,6 +781,7 @@ def test_requests_with_filters( "filter_closed": True, "filter_accepted": True, "filter_rejected": True, + "filter_maintainer_requests": False, }, cookies=cookies, ) @@ -790,6 +805,7 @@ def test_requests_with_filters( "filter_closed": True, "filter_accepted": True, "filter_rejected": True, + "filter_maintainer_requests": False, }, cookies=cookies, ) # Page 2 @@ -803,6 +819,27 @@ def test_requests_with_filters( assert len(rows) == 5 # There are five records left on the second page. +def test_requests_for_maintainer_requests( + client: TestClient, + tu_user: User, + packages: list[Package], + requests: list[PackageRequest], +): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.get( + "/requests", + params={"filter_maintainer_requests": True}, + cookies=cookies, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We only expect 2 requests since we are looking for requests from the maintainers + assert len(rows) == 2 + + def test_requests_by_deleted_users( client: TestClient, user: User, tu_user: User, pkgreq: PackageRequest ): From ff92e95f7a36bd51afa7f5108c9f3ff758d43cba Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Mon, 21 Nov 2022 13:39:43 +0000 Subject: [PATCH 1194/1451] fix: delete associated ssh public keys with account deletion Signed-off-by: Leonidas Spyropoulos --- aurweb/models/ssh_pub_key.py | 2 +- test/test_accounts_routes.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/aurweb/models/ssh_pub_key.py b/aurweb/models/ssh_pub_key.py index a2af34f4..c0b59445 100644 --- a/aurweb/models/ssh_pub_key.py +++ b/aurweb/models/ssh_pub_key.py @@ -13,7 +13,7 @@ class SSHPubKey(Base): User = relationship( "User", - backref=backref("ssh_pub_keys", lazy="dynamic"), + backref=backref("ssh_pub_keys", lazy="dynamic", cascade="all, delete"), foreign_keys=[__table__.c.UserID], ) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 33baa0ea..f44fd44e 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -2032,6 +2032,37 @@ def test_account_delete_self(client: TestClient, user: User): assert record is None +def test_account_delete_self_with_ssh_public_key(client: TestClient, user: User): + username = user.Username + + with db.begin(): + db.create( + SSHPubKey, User=user, Fingerprint="testFingerprint", PubKey="testPubKey" + ) + + # Confirm that we can view our own account deletion page + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{username}/delete" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == HTTPStatus.OK + + # Supply everything correctly and delete ourselves + with client as request: + resp = request.post( + endpoint, + data={"passwd": "testPassword", "confirm": True}, + cookies=cookies, + ) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Check that our User record no longer exists in the database + user_record = db.query(User).filter(User.Username == username).first() + assert user_record is None + sshpubkey_record = db.query(SSHPubKey).filter(SSHPubKey.User == user).first() + assert sshpubkey_record is None + + def test_account_delete_as_tu(client: TestClient, tu_user: User): with db.begin(): user = create_user("user2") From d5e102e3f4622b4c55edd75bb086ae9f764a71c9 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Tue, 22 Nov 2022 18:39:15 +0100 Subject: [PATCH 1195/1451] feat: add "Submitter" field to /rpc info request Signed-off-by: moson-mo --- aurweb/rpc.py | 54 +++++++++++++++++++++++++++++++++--------------- test/test_rpc.py | 5 +++++ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index af31d2de..2aa27500 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -154,6 +154,7 @@ class RPC: "PackageBase": package.PackageBaseName, # Maintainer should be set following this update if one exists. "Maintainer": package.Maintainer, + "Submitter": package.Submitter, "Version": package.Version, "Description": package.Description, "URL": package.URL, @@ -192,22 +193,35 @@ class RPC: def entities(self, query: orm.Query) -> orm.Query: """Select specific RPC columns on `query`.""" - return query.with_entities( - models.Package.ID, - models.Package.Name, - models.Package.Version, - models.Package.Description, - models.Package.URL, - models.Package.PackageBaseID, - models.PackageBase.Name.label("PackageBaseName"), - models.PackageBase.NumVotes, - models.PackageBase.Popularity, - models.PackageBase.PopularityUpdated, - models.PackageBase.OutOfDateTS, - models.PackageBase.SubmittedTS, - models.PackageBase.ModifiedTS, - models.User.Username.label("Maintainer"), - ).group_by(models.Package.ID) + Submitter = orm.aliased(models.User) + + query = ( + query.join( + Submitter, + Submitter.ID == models.PackageBase.SubmitterUID, + isouter=True, + ) + .with_entities( + models.Package.ID, + models.Package.Name, + models.Package.Version, + models.Package.Description, + models.Package.URL, + models.Package.PackageBaseID, + models.PackageBase.Name.label("PackageBaseName"), + models.PackageBase.NumVotes, + models.PackageBase.Popularity, + models.PackageBase.PopularityUpdated, + models.PackageBase.OutOfDateTS, + models.PackageBase.SubmittedTS, + models.PackageBase.ModifiedTS, + models.User.Username.label("Maintainer"), + Submitter.Username.label("Submitter"), + ) + .group_by(models.Package.ID) + ) + + return query def subquery(self, ids: set[int]): Package = models.Package @@ -367,7 +381,13 @@ class RPC: if len(results) > max_results: raise RPCError("Too many package results.") - return self._assemble_json_data(results, self.get_json_data) + data = self._assemble_json_data(results, self.get_json_data) + + # remove Submitter for search results + for pkg in data: + pkg.pop("Submitter") + + return data def _handle_msearch_type( self, args: list[str] = [], **kwargs diff --git a/test/test_rpc.py b/test/test_rpc.py index 424352db..04efd38f 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -345,6 +345,7 @@ def test_rpc_documentation_missing(): def test_rpc_singular_info( client: TestClient, user: User, + user2: User, packages: list[Package], depends: list[PackageDependency], relations: list[PackageRelation], @@ -365,6 +366,7 @@ def test_rpc_singular_info( "Popularity": float(pkg.PackageBase.Popularity), "OutOfDate": None, "Maintainer": user.Username, + "Submitter": user2.Username, "URLPath": f"/cgit/aur.git/snapshot/{pkg.Name}.tar.gz", "Depends": ["chungus-depends"], "OptDepends": ["chungus-optdepends=50"], @@ -498,6 +500,7 @@ def test_rpc_mixedargs(client: TestClient, packages: list[Package]): def test_rpc_no_dependencies_omits_key( client: TestClient, user: User, + user2: User, packages: list[Package], depends: list[PackageDependency], relations: list[PackageRelation], @@ -520,6 +523,7 @@ def test_rpc_no_dependencies_omits_key( "Popularity": int(pkg.PackageBase.Popularity), "OutOfDate": None, "Maintainer": user.Username, + "Submitter": user2.Username, "URLPath": "/cgit/aur.git/snapshot/chungy-chungus.tar.gz", "Depends": ["chungy-depends"], "Conflicts": ["chungy-conflicts"], @@ -799,6 +803,7 @@ def test_rpc_search(client: TestClient, packages: list[Package]): result = data.get("results")[0] assert result.get("Name") == packages[0].Name + assert result.get("Submitter") is None # Test the If-None-Match headers. etag = response.headers.get("ETag").strip('"') From 6b0978b9a518bebb9197b9e71ff0d53f24f77bc9 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Tue, 22 Nov 2022 21:51:15 +0000 Subject: [PATCH 1196/1451] fix(deps): update dependencies from renovate Signed-off-by: Leonidas Spyropoulos --- .pre-commit-config.yaml | 4 +- poetry.lock | 598 +++++++++++++++++----------------------- pyproject.toml | 14 +- 3 files changed, 261 insertions(+), 355 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09659269..ab4240c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: debug-statements - repo: https://github.com/myint/autoflake - rev: v1.4 + rev: v1.7.7 hooks: - id: autoflake args: @@ -25,7 +25,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.10.0 hooks: - id: black diff --git a/poetry.lock b/poetry.lock index f6b79a30..22cbd3fd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -175,7 +175,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "38.0.1" +version = "38.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -192,20 +192,6 @@ sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] - [[package]] name = "dnspython" version = "2.2.1" @@ -236,7 +222,7 @@ idna = ">=2.0.0" [[package]] name = "exceptiongroup" -version = "1.0.0" +version = "1.0.4" description = "Backport of PEP 654 (exception groups)" category = "main" optional = false @@ -258,14 +244,14 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.10.0" +version = "2.0.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false python-versions = ">=3.7,<4.0" [package.dependencies] -redis = "<4.4" +redis = "<4.5" sortedcontainers = ">=2.4.0,<3.0.0" [package.extras] @@ -316,7 +302,7 @@ testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pyt [[package]] name = "greenlet" -version = "2.0.0" +version = "2.0.1" description = "Lightweight in-process concurrent programming" category = "main" optional = false @@ -324,7 +310,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] docs = ["Sphinx", "docutils (<0.18)"] -test = ["faulthandler", "objgraph"] +test = ["faulthandler", "objgraph", "psutil"] [[package]] name = "gunicorn" @@ -345,11 +331,11 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "h11" -version = "0.12.0" +version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "h2" @@ -373,16 +359,16 @@ python-versions = ">=3.6.1" [[package]] name = "httpcore" -version = "0.15.0" +version = "0.16.1" description = "A minimal low-level HTTP client." category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -anyio = ">=3.0.0,<4.0.0" +anyio = ">=3.0,<5.0" certifi = "*" -h11 = ">=0.11,<0.13" +h11 = ">=0.13,<0.15" sniffio = ">=1.0.0,<2.0.0" [package.extras] @@ -391,7 +377,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" -version = "0.23.0" +version = "0.23.1" description = "The next generation HTTP client." category = "main" optional = false @@ -399,7 +385,7 @@ python-versions = ">=3.7" [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.16.0" +httpcore = ">=0.15.0,<0.17.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -508,7 +494,7 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "mako" -version = "1.2.3" +version = "1.2.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "main" optional = false @@ -554,7 +540,7 @@ python-versions = ">=3.5" [[package]] name = "orjson" -version = "3.8.1" +version = "3.8.2" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false @@ -679,11 +665,11 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygit2" -version = "1.10.1" +version = "1.11.1" description = "Python bindings for libgit2." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.dependencies] cffi = ">=1.9.1" @@ -721,7 +707,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. [[package]] name = "pytest-asyncio" -version = "0.20.1" +version = "0.20.2" description = "Pytest support for asyncio" category = "dev" optional = false @@ -801,7 +787,7 @@ six = ">=1.4.0" [[package]] name = "redis" -version = "4.3.4" +version = "4.3.5" description = "Python client for Redis database and key-value store" category = "main" optional = false @@ -809,7 +795,6 @@ python-versions = ">=3.6" [package.dependencies] async-timeout = ">=4.0.2" -deprecated = ">=1.2.3" packaging = ">=20.4" [package.extras] @@ -850,7 +835,7 @@ idna2008 = ["idna"] [[package]] name = "setuptools" -version = "65.5.0" +version = "65.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false @@ -858,7 +843,7 @@ python-versions = ">=3.7" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -887,7 +872,7 @@ python-versions = "*" [[package]] name = "sqlalchemy" -version = "1.4.42" +version = "1.4.44" description = "Database Abstraction Library" category = "main" optional = false @@ -993,7 +978,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.19.0" +version = "0.20.0" description = "The lightning-fast ASGI server." category = "main" optional = false @@ -1004,7 +989,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "webencodings" @@ -1028,14 +1013,6 @@ MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog"] -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - [[package]] name = "wsproto" version = "1.2.0" @@ -1062,7 +1039,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "84f0bae9789174cbdc5aa672b9e72f0ef91763f63ed73e8cafb45f26efd9bb47" +content-hash = "b178f1fcbba93d9cbc8dd23193b25afd5e1ba971196757abf098a1dfa2666cba" [metadata.files] aiofiles = [ @@ -1255,36 +1232,32 @@ coverage = [ {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] cryptography = [ - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, - {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, - {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, - {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, -] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, + {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"}, + {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"}, + {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"}, + {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"}, + {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"}, + {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"}, + {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"}, + {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"}, + {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"}, + {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"}, + {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"}, ] dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, @@ -1295,16 +1268,16 @@ email-validator = [ {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, ] exceptiongroup = [ - {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, - {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, + {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, + {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, ] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] fakeredis = [ - {file = "fakeredis-1.10.0-py3-none-any.whl", hash = "sha256:0be420a79fabda234963a2730c4ce609a6d44a598e8dd253ce97785bef944285"}, - {file = "fakeredis-1.10.0.tar.gz", hash = "sha256:2b02370118535893d832bcd3c099ef282de3f13b29ae3922432e2225794ec334"}, + {file = "fakeredis-2.0.0-py3-none-any.whl", hash = "sha256:fb3186cbbe4c549f922b0f08eb84b09c0e51ecf8efbed3572d20544254f93a97"}, + {file = "fakeredis-2.0.0.tar.gz", hash = "sha256:6d1dc2417921b7ce56a80877afa390d6335a3154146f201a86e3a14417bdc79e"}, ] fastapi = [ {file = "fastapi-0.85.2-py3-none-any.whl", hash = "sha256:6292db0edd4a11f0d938d6033ccec5f706e9d476958bf33b119e8ddb4e524bde"}, @@ -1318,69 +1291,74 @@ filelock = [ {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] greenlet = [ - {file = "greenlet-2.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4be4dedbd2fa9b7c35627f322d6d3139cb125bc18d5ef2f40237990850ea446f"}, - {file = "greenlet-2.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:75c022803de010294366f3608d4bba3e346693b1b7427b79d57e3d924ed03838"}, - {file = "greenlet-2.0.0-cp27-cp27m-win32.whl", hash = "sha256:4a1953465b7651073cffde74ed7d121e602ef9a9740d09ee137b01879ac15a2f"}, - {file = "greenlet-2.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:a65205e6778142528978b4acca76888e7e7f0be261e395664e49a5c21baa2141"}, - {file = "greenlet-2.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d71feebf5c8041c80dfda76427e14e3ca00bca042481bd3e9612a9d57b2cbbf7"}, - {file = "greenlet-2.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f7edbd2957f72aea357241fe42ffc712a8e9b8c2c42f24e2ef5d97b255f66172"}, - {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79687c48e7f564be40c46b3afea6d141b8d66ffc2bc6147e026d491c6827954a"}, - {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a245898ec5e9ca0bc87a63e4e222cc633dc4d1f1a0769c34a625ad67edb9f9de"}, - {file = "greenlet-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adcf45221f253b3a681c99da46fa6ac33596fa94c2f30c54368f7ee1c4563a39"}, - {file = "greenlet-2.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3dc294afebf2acfd029373dbf3d01d36fd8d6888a03f5a006e2d690f66b153d9"}, - {file = "greenlet-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1cfeae4dda32eb5c64df05d347c4496abfa57ad16a90082798a2bba143c6c854"}, - {file = "greenlet-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:d58d4b4dc82e2d21ebb7dd7d3a6d370693b2236a1407fe3988dc1d4ea07575f9"}, - {file = "greenlet-2.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0d7efab8418c1fb3ea00c4abb89e7b0179a952d0d53ad5fcff798ca7440f8e8"}, - {file = "greenlet-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:f8a10e14238407be3978fa6d190eb3724f9d766655fefc0134fd5482f1fb0108"}, - {file = "greenlet-2.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:98b848a0b75e76b446dc71fdbac712d9078d96bb1c1607f049562dde1f8801e1"}, - {file = "greenlet-2.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:8e8dbad9b4f4c3e37898914cfccb7c4f00dbe3146333cfe52a1a3103cc2ff97c"}, - {file = "greenlet-2.0.0-cp35-cp35m-win32.whl", hash = "sha256:069a8a557541a04518dc3beb9a78637e4e6b286814849a2ecfac529eaa78562b"}, - {file = "greenlet-2.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:cc211c2ff5d3b2ba8d557a71e3b4f0f0a2020067515143a9516ea43884271192"}, - {file = "greenlet-2.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:d4e7642366e638f45d70c5111590a56fbd0ffb7f474af20c6c67c01270bcf5cf"}, - {file = "greenlet-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e7a0dca752b4e3395890ab4085c3ec3838d73714261914c01b53ed7ea23b5867"}, - {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8c67ecda450ad4eac7837057f5deb96effa836dacaf04747710ccf8eeb73092"}, - {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cc1abaf47cfcfdc9ac0bdff173cebab22cd54e9e3490135a4a9302d0ff3b163"}, - {file = "greenlet-2.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efdbbbf7b6c8d5be52977afa65b9bb7b658bab570543280e76c0fabc647175ed"}, - {file = "greenlet-2.0.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7acaa51355d5b9549d474dc71be6846ee9a8f2cb82f4936e5efa7a50bbeb94ad"}, - {file = "greenlet-2.0.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2be628bca0395610da08921f9376dd14317f37256d41078f5c618358467681e1"}, - {file = "greenlet-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:eca9c0473de053dcc92156dd62c38c3578628b536c7f0cd66e655e211c14ac32"}, - {file = "greenlet-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9a4a9fea68fd98814999d91ea585e49ed68d7e199a70bef13a857439f60a4609"}, - {file = "greenlet-2.0.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:6b28420ae290bfbf5d827f976abccc2f74f0a3f5e4fb69b66acf98f1cbe95e7e"}, - {file = "greenlet-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:2b8e1c939b363292ecc93999fb1ad53ffc5d0aac8e933e4362b62365241edda5"}, - {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c5ddadfe40e903c6217ed2b95a79f49e942bb98527547cc339fc7e43a424aad"}, - {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e5ead803b11b60b347e08e0f37234d9a595f44a6420026e47bcaf94190c3cd6"}, - {file = "greenlet-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b89b78ffb516c2921aa180c2794082666e26680eef05996b91f46127da24d964"}, - {file = "greenlet-2.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:939963d0137ec92540d95b68b7f795e8dbadce0a1fca53e3e7ef8ddc18ee47cb"}, - {file = "greenlet-2.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c1e93ef863810fba75faf418f0861dbf59bfe01a7b5d0a91d39603df58d3d3fa"}, - {file = "greenlet-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:6fd342126d825b76bf5b49717a7c682e31ed1114906cdec7f5a0c2ff1bc737a7"}, - {file = "greenlet-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5392ddb893e7fba237b988f846c4a80576557cc08664d56dc1a69c5c02bdc80c"}, - {file = "greenlet-2.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b4fd73b62c1038e7ee938b1de328eaa918f76aa69c812beda3aff8a165494201"}, - {file = "greenlet-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:0ba0f2e5c4a8f141952411e356dba05d6fe0c38325ee0e4f2d0c6f4c2c3263d5"}, - {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8bacecee0c9348ab7c95df810e12585e9e8c331dfc1e22da4ed0bd635a5f483"}, - {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:341053e0a96d512315c27c34fad4672c4573caf9eb98310c39e7747645c88d8b"}, - {file = "greenlet-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fcdd8ae391ffabb3b672397b58a9737aaff6b8cae0836e8db8ff386fcea802"}, - {file = "greenlet-2.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c3aa7d3bc545162a6676445709b24a2a375284dc5e2f2432d58b80827c2bd91c"}, - {file = "greenlet-2.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d8dca31a39dd9f25641559b8cdf9066168c682dfcfbe0f797f03e4c9718a63a"}, - {file = "greenlet-2.0.0-cp38-cp38-win32.whl", hash = "sha256:aa2b371c3633e694d043d6cec7376cb0031c6f67029f37eef40bda105fd58753"}, - {file = "greenlet-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:0fa2a66fdf0d09929e79f786ad61529d4e752f452466f7ddaa5d03caf77a603d"}, - {file = "greenlet-2.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:e7ec3f2465ba9b7d25895307abe1c1c101a257c54b9ea1522bbcbe8ca8793735"}, - {file = "greenlet-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:99e9851e40150504474915605649edcde259a4cd9bce2fcdeb4cf33ad0b5c293"}, - {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20bf68672ae14ef2e2e6d3ac1f308834db1d0b920b3b0674eef48b2dce0498dd"}, - {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30198bccd774f9b6b1ba7564a0d02a79dd1fe926cfeb4107856fe16c9dfb441c"}, - {file = "greenlet-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d65d7d1ff64fb300127d2ffd27db909de4d21712a5dde59a3ad241fb65ee83d7"}, - {file = "greenlet-2.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5d396a5457458460b0c28f738fc8ab2738ee61b00c3f845c7047a333acd96c"}, - {file = "greenlet-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09f00f9938eb5ae1fe203558b56081feb0ca34a2895f8374cd01129ddf4d111c"}, - {file = "greenlet-2.0.0-cp39-cp39-win32.whl", hash = "sha256:089e123d80dbc6f61fff1ff0eae547b02c343d50968832716a7b0a33bea5f792"}, - {file = "greenlet-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc283f99a4815ef70cad537110e3e03abcef56ab7d005ba9a8c6ec33054ce9c0"}, - {file = "greenlet-2.0.0.tar.gz", hash = "sha256:6c66f0da8049ee3c126b762768179820d4c0ae0ca46ae489039e4da2fae39a52"}, + {file = "greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"}, + {file = "greenlet-2.0.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515"}, + {file = "greenlet-2.0.1-cp27-cp27m-win32.whl", hash = "sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a"}, + {file = "greenlet-2.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524"}, + {file = "greenlet-2.0.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243"}, + {file = "greenlet-2.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45"}, + {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da"}, + {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d"}, + {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd"}, + {file = "greenlet-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617"}, + {file = "greenlet-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a"}, + {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce"}, + {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72"}, + {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82"}, + {file = "greenlet-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd"}, + {file = "greenlet-2.0.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f"}, + {file = "greenlet-2.0.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f"}, + {file = "greenlet-2.0.1-cp35-cp35m-win32.whl", hash = "sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955"}, + {file = "greenlet-2.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77"}, + {file = "greenlet-2.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148"}, + {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2"}, + {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39"}, + {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92"}, + {file = "greenlet-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928"}, + {file = "greenlet-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd"}, + {file = "greenlet-2.0.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, + {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, + {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"}, + {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, + {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, + {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, + {file = "greenlet-2.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, + {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, + {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"}, + {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, + {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, + {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, + {file = "greenlet-2.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, + {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, + {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"}, + {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, + {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, + {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, + {file = "greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"}, ] gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] h11 = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] h2 = [ {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, @@ -1391,12 +1369,12 @@ hpack = [ {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, ] httpcore = [ - {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, - {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, + {file = "httpcore-0.16.1-py3-none-any.whl", hash = "sha256:8d393db683cc8e35cc6ecb02577c5e1abfedde52b38316d038932a84b4875ecb"}, + {file = "httpcore-0.16.1.tar.gz", hash = "sha256:3d3143ff5e1656a5740ea2f0c167e8e9d48c5a9bbd7f00ad1f8cff5711b08543"}, ] httpx = [ - {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, - {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, + {file = "httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"}, + {file = "httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"}, ] hypercorn = [ {file = "Hypercorn-0.14.3-py3-none-any.whl", hash = "sha256:7c491d5184f28ee960dcdc14ab45d14633ca79d72ddd13cf4fcb4cb854d679ab"}, @@ -1499,8 +1477,8 @@ lxml = [ {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, ] mako = [ - {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"}, - {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"}, + {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, + {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, ] markdown = [ {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"}, @@ -1558,55 +1536,55 @@ mysqlclient = [ {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, ] orjson = [ - {file = "orjson-3.8.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:a70aaa2e56356e58c6e1b49f7b7f069df5b15e55db002a74db3ff3f7af67c7ff"}, - {file = "orjson-3.8.1-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d45db052d01d0ab7579470141d5c3592f4402d43cfacb67f023bc1210a67b7bc"}, - {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2aae92398c0023ac26a6cd026375f765ef5afe127eccabf563c78af7b572d59"}, - {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0bd5b4e539db8a9635776bdf9a25c3db84e37165e65d45c8ca90437adc46d6d8"}, - {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21efb87b168066201a120b0f54a2381f6f51ff3727e07b3908993732412b314a"}, - {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:e073338e422f518c1d4d80efc713cd17f3ed6d37c8c7459af04a95459f3206d1"}, - {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8f672f3987f6424f60ab2e86ea7ed76dd2806b8e9b506a373fc8499aed85ddb5"}, - {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:231c30958ed99c23128a21993c5ac0a70e1e568e6a898a47f70d5d37461ca47c"}, - {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59b4baf71c9f39125d7e535974b146cc180926462969f6d8821b4c5e975e11b3"}, - {file = "orjson-3.8.1-cp310-none-win_amd64.whl", hash = "sha256:fe25f50dc3d45364428baa0dbe3f613a5171c64eb0286eb775136b74e61ba58a"}, - {file = "orjson-3.8.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6802edf98f6918e89df355f56be6e7db369b31eed64ff2496324febb8b0aa43b"}, - {file = "orjson-3.8.1-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a4244f4199a160717f0027e434abb886e322093ceadb2f790ff0c73ed3e17662"}, - {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6956cf7a1ac97523e96f75b11534ff851df99a6474a561ad836b6e82004acbb8"}, - {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b4e3857dd2416b479f700e9bdf4fcec8c690d2716622397d2b7e848f9833e50"}, - {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8873e490dea0f9cd975d66f84618b6fb57b1ba45ecb218313707a71173d764f"}, - {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:124207d2cd04e845eaf2a6171933cde40aebcb8c2d7d3b081e01be066d3014b6"}, - {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d8ed77098c2e22181fce971f49a34204c38b79ca91c01d515d07015339ae8165"}, - {file = "orjson-3.8.1-cp311-none-win_amd64.whl", hash = "sha256:8623ac25fa0850a44ac845e9333c4da9ae5707b7cec8ac87cbe9d4e41137180f"}, - {file = "orjson-3.8.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:d67a0bd0283a3b17ac43c5ab8e4a7e9d3aa758d6ec5d51c232343c408825a5ad"}, - {file = "orjson-3.8.1-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d89ef8a4444d83e0a5171d14f2ab4895936ab1773165b020f97d29cf289a2d88"}, - {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97839a6abbebb06099294e6057d5b3061721ada08b76ae792e7041b6cb54c97f"}, - {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6071bcf51f0ae4d53b9d3e9164f7138164df4291c484a7b14562075aaa7a2b7b"}, - {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15e7d691cee75b5192fc1fa8487bf541d463246dc25c926b9b40f5b6ab56770"}, - {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:b9abc49c014def1b832fcd53bdc670474b6fe41f373d16f40409882c0d0eccba"}, - {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:3fd5472020042482d7da4c26a0ee65dbd931f691e1c838c6cf4232823179ecc1"}, - {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e399ed1b0d6f8089b9b6ff2cb3e71ba63a56d8ea88e1d95467949795cc74adfd"}, - {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5e3db6496463c3000d15b7a712da5a9601c6c43682f23f81862fe1d2a338f295"}, - {file = "orjson-3.8.1-cp37-none-win_amd64.whl", hash = "sha256:0f21eed14697083c01f7e00a87e21056fc8fb5851e8a7bca98345189abcdb4d4"}, - {file = "orjson-3.8.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5a9e324213220578d324e0858baeab47808a13d3c3fbc6ba55a3f4f069d757cf"}, - {file = "orjson-3.8.1-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69097c50c3ccbcc61292192b045927f1688ca57ce80525dc5d120e0b91e19bb0"}, - {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7822cba140f7ca48ed0256229f422dbae69e3a3475176185db0c0538cfadb57"}, - {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03389e3750c521a7f3d4837de23cfd21a7f24574b4b3985c9498f440d21adb03"}, - {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f9d9b5c6692097de07dd0b2d5ff20fd135bacd1b2fb7ea383ee717a4150c93"}, - {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:c2c9ef10b6344465fd5ac002be2d34f818211274dd79b44c75b2c14a979f84f3"}, - {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7adaac93678ac61f5dc070f615b18639d16ee66f6a946d5221dbf315e8b74bec"}, - {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0c1750f73658906b82cabbf4be2f74300644c17cb037fbc8b48d746c3b90c76"}, - {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:da6306e1f03e7085fe0db61d4a3377f70c6fd865118d0afe17f80ae9a8f6f124"}, - {file = "orjson-3.8.1-cp38-none-win_amd64.whl", hash = "sha256:f532c2cbe8c140faffaebcfb34d43c9946599ea8138971f181a399bec7d6b123"}, - {file = "orjson-3.8.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:6a7b76d4b44bca418f7797b1e157907b56b7d31caa9091db4e99ebee51c16933"}, - {file = "orjson-3.8.1-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f850489d89ea12be486492e68f0fd63e402fa28e426d4f0b5fc1eec0595e6109"}, - {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4449e70b98f3ad3e43958360e4be1189c549865c0a128e8629ec96ce92d251c3"}, - {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:45357eea9114bd41ef19280066591e9069bb4f6f5bffd533e9bfc12a439d735f"}, - {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5a9bc5bc4d730153529cb0584c63ff286d50663ccd48c9435423660b1bb12d"}, - {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:a806aca6b80fa1d996aa16593e4995a71126a085ee1a59fff19ccad29a4e47fd"}, - {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:395d02fd6be45f960da014372e7ecefc9e5f8df57a0558b7111a5fa8423c0669"}, - {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:caff3c1e964cfee044a03a46244ecf6373f3c56142ad16458a1446ac6d69824a"}, - {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ded261268d5dfd307078fe3370295e5eb15bdde838bbb882acf8538e061c451"}, - {file = "orjson-3.8.1-cp39-none-win_amd64.whl", hash = "sha256:45c1914795ffedb2970bfcd3ed83daf49124c7c37943ed0a7368971c6ea5e278"}, - {file = "orjson-3.8.1.tar.gz", hash = "sha256:07c42de52dfef56cdcaf2278f58e837b26f5b5af5f1fd133a68c4af203851fc7"}, + {file = "orjson-3.8.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:43e69b360c2851b45c7dbab3b95f7fa8469df73fab325a683f7389c4db63aa71"}, + {file = "orjson-3.8.2-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:64c5da5c9679ef3d85e9bbcbb62f4ccdc1f1975780caa20f2ec1e37b4da6bd36"}, + {file = "orjson-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c632a2157fa9ec098d655287e9e44809615af99837c49f53d96bfbca453c5bd"}, + {file = "orjson-3.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f63da6309c282a2b58d4a846f0717f6440356b4872838b9871dc843ed1fe2b38"}, + {file = "orjson-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c9be25c313ba2d5478829d949165445c3bd36c62e07092b4ba8dbe5426574d1"}, + {file = "orjson-3.8.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4bcce53e9e088f82633f784f79551fcd7637943ab56c51654aaf9d4c1d5cfa54"}, + {file = "orjson-3.8.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:33edb5379c6e6337f9383c85fe4080ce3aa1057cc2ce29345b7239461f50cbd6"}, + {file = "orjson-3.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:da35d347115758bbc8bfaf39bb213c42000f2a54e3f504c84374041d20835cd6"}, + {file = "orjson-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d755d94a90a941b91b4d39a6b02e289d8ba358af2d1a911edf266be7942609dc"}, + {file = "orjson-3.8.2-cp310-none-win_amd64.whl", hash = "sha256:7ea96923e26390b2142602ebb030e2a4db9351134696e0b219e5106bddf9b48e"}, + {file = "orjson-3.8.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:a0d89de876e6f1cef917a2338378a60a98584e1c2e1c67781e20b6ed1c512478"}, + {file = "orjson-3.8.2-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8d47e7592fe938aec898eb22ea4946298c018133df084bc78442ff18e2c6347c"}, + {file = "orjson-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3d9f1043f618d0c64228aab9711e5bd822253c50b6c56223951e32b51f81d62"}, + {file = "orjson-3.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed10600e8b08f1e87b656ad38ab316191ce94f2c9adec57035680c0dc9e93c81"}, + {file = "orjson-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99c49e49a04bf61fee7aaea6d92ac2b1fcf6507aea894bbdf3fbb25fe792168c"}, + {file = "orjson-3.8.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1463674f8efe6984902473d7b5ce3edf444c1fcd09dc8aa4779638a28fb9ca01"}, + {file = "orjson-3.8.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c1ef75f1d021d817e5c60a42da0b4b7e3123b1b37415260b8415666ddacc7cd7"}, + {file = "orjson-3.8.2-cp311-none-win_amd64.whl", hash = "sha256:b6007e1ac8564b13b2521720929e8bb3ccd3293d9fdf38f28728dcc06db6248f"}, + {file = "orjson-3.8.2-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:a02c13ae523221576b001071354380e277346722cc6b7fdaacb0fd6db5154b3e"}, + {file = "orjson-3.8.2-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:fa2e565cf8ffdb37ce1887bd1592709ada7f701e61aa4b1e710be94b0aecbab4"}, + {file = "orjson-3.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1d8864288f7c5fccc07b43394f83b721ddc999f25dccfb5d0651671a76023f5"}, + {file = "orjson-3.8.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1874c05d0bb994601fa2d51605cb910d09343c6ebd36e84a573293523fab772a"}, + {file = "orjson-3.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:349387ed6989e5db22e08c9af8d7ca14240803edc50de451d48d41a0e7be30f6"}, + {file = "orjson-3.8.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:4e42b19619d6e97e201053b865ca4e62a48da71165f4081508ada8e1b91c6a30"}, + {file = "orjson-3.8.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:bc112c17e607c59d1501e72afb44226fa53d947d364aed053f0c82d153e29616"}, + {file = "orjson-3.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6fda669211f2ed1fc2c8130187ec90c96b4f77b6a250004e666d2ef8ed524e5f"}, + {file = "orjson-3.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:aebd4e80fea0f20578fd0452908b9206a6a0d5ae9f5c99b6e665bbcd989e56cd"}, + {file = "orjson-3.8.2-cp37-none-win_amd64.whl", hash = "sha256:9f3cd0394eb6d265beb2a1572b5663bc910883ddbb5cdfbcb660f5a0444e7fd8"}, + {file = "orjson-3.8.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:74e7d54d11b3da42558d69a23bf92c2c48fabf69b38432d5eee2c5b09cd4c433"}, + {file = "orjson-3.8.2-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8cbadc9be748a823f9c743c7631b1ee95d3925a9c0b21de4e862a1d57daa10ec"}, + {file = "orjson-3.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07d5a8c69a2947d9554a00302734fe3d8516415c8b280963c92bc1033477890"}, + {file = "orjson-3.8.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b364ea01d1b71b9f97bf97af9eb79ebee892df302e127a9e2e4f8eaa74d6b98"}, + {file = "orjson-3.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b98a8c825a59db94fbe8e0cce48618624c5a6fb1436467322d90667c08a0bf80"}, + {file = "orjson-3.8.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ab63103f60b516c0fce9b62cb4773f689a82ab56e19ef2387b5a3182f80c0d78"}, + {file = "orjson-3.8.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:73ab3f4288389381ae33ab99f914423b69570c88d626d686764634d5e0eeb909"}, + {file = "orjson-3.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2ab3fd8728e12c36e20c6d9d70c9e15033374682ce5acb6ed6a08a80dacd254d"}, + {file = "orjson-3.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cde11822cf71a7f0daaa84223249b2696a2b6cda7fa587e9fd762dff1a8848e4"}, + {file = "orjson-3.8.2-cp38-none-win_amd64.whl", hash = "sha256:b14765ea5aabfeab1a194abfaa0be62c9fee6480a75ac8c6974b4eeede3340b4"}, + {file = "orjson-3.8.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:6068a27d59d989d4f2864c2fc3440eb7126a0cfdfaf8a4ad136b0ffd932026ae"}, + {file = "orjson-3.8.2-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6bf36fa759a1b941fc552ad76b2d7fb10c1d2a20c056be291ea45eb6ae1da09b"}, + {file = "orjson-3.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f436132e62e647880ca6988974c8e3165a091cb75cbed6c6fd93e931630c22fa"}, + {file = "orjson-3.8.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ecd8936259a5920b52a99faf62d4efeb9f5e25a0aacf0cce1e9fa7c37af154f"}, + {file = "orjson-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c13114b345cda33644f64e92fe5d8737828766cf02fbbc7d28271a95ea546832"}, + {file = "orjson-3.8.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6e43cdc3ddf96bdb751b748b1984b701125abacca8fc2226b808d203916e8cba"}, + {file = "orjson-3.8.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ee39071da2026b11e4352d6fc3608a7b27ee14bc699fd240f4e604770bc7a255"}, + {file = "orjson-3.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1c3833976ebbeb3b5b6298cb22e23bf18453f6b80802103b7d08f7dd8a61611d"}, + {file = "orjson-3.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b9a34519d3d70935e1cd3797fbed8fbb6f61025182bea0140ca84d95b6f8fbe5"}, + {file = "orjson-3.8.2-cp39-none-win_amd64.whl", hash = "sha256:2734086d9a3dd9591c4be7d05aff9beccc086796d3f243685e56b7973ebac5bc"}, + {file = "orjson-3.8.2.tar.gz", hash = "sha256:a2fb95a45031ccf278e44341027b3035ab99caa32aa173279b1f0a06324f434b"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -1704,43 +1682,37 @@ pydantic = [ {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] pygit2 = [ - {file = "pygit2-1.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3f60e47c6a7a87f18a112753eb98848f4c5333986bec1940558ce09cdaf53bf"}, - {file = "pygit2-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f0f69ea42231bebf08006c85cd5aa233c9c047c5a88b7fcfb4b639476b70e31b"}, - {file = "pygit2-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0097b6631ef05c837c4800fad559d0865a90c55475a18f38c6f2f5a12750e914"}, - {file = "pygit2-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b5bdcdfae205d9cc0c80bc53fad222a5ba67e66fd336ef223f86b0ac5835"}, - {file = "pygit2-1.10.1-cp310-cp310-win32.whl", hash = "sha256:3efd2a2ab2bb443e1b758525546d74a5a12fe27006194d3c02b3e6ecc1e101e6"}, - {file = "pygit2-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:11225811194ae6b9dbb34c2e8900e0eba6eacc180d82766e3dbddcbd2c6e6454"}, - {file = "pygit2-1.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:73e251d0b73f1010ad28c20bcdcf33e312fb363f10b7268ad2bcfa09770f9ac2"}, - {file = "pygit2-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cb73f7967207a9ac485722ef0e517e5ca482f3c1308a0ac934707cb267b0ac7a"}, - {file = "pygit2-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b115bef251af4daf18f2f967287b56da2eae2941d5389dc1666bd0160892d769"}, - {file = "pygit2-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd55a6cf7ad6276fb5772e5c60c51fca2d9a5e68ea3e7237847421c10080a68"}, - {file = "pygit2-1.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:33138c256ad0ff084f5d8a82ab7d280f9ed6706ebb000ac82e3d133e2d82b366"}, - {file = "pygit2-1.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f4f507e5cd775f6d5d95ec65761af4cdb33b2f859af15bf10a06d11efd0d3b2"}, - {file = "pygit2-1.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:752f844d5379081fae5ef78e3bf6f0f35ae9b11aafc37e5e03e1c3607b196806"}, - {file = "pygit2-1.10.1-cp37-cp37m-win32.whl", hash = "sha256:b31ffdbc87629613ae03a533e01eee79112a12f66faf375fa08934074044a664"}, - {file = "pygit2-1.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e09386b71ad474f2c2c02b6b251fa904b1145dabfe9095955ab30a789aaf84c0"}, - {file = "pygit2-1.10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:564e832e750f889aea3bb3e82674e1c860c9b89a141404530271e1341723a258"}, - {file = "pygit2-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43bb910272866eb822e930dbd0feecc340e0c24934143aab651fa180cc5ebfb0"}, - {file = "pygit2-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e4905cbb87db598b1cb38800ff995c0ba1f58745e2f52af4d54dbc93b9bda8"}, - {file = "pygit2-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f4689ce38cd62a7000d38602ba4d704df5cec708e5d98dadaffcf510f3317"}, - {file = "pygit2-1.10.1-cp38-cp38-win32.whl", hash = "sha256:b67ef30f3c022be1d6da9ef0188f60fc2d20639bff44693ef5653818e887001b"}, - {file = "pygit2-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:dcd849c44bd743d829dbd9dc9d7e13c14cf31a47c22e2e3f9e98fa845a8b8b28"}, - {file = "pygit2-1.10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e8bb9002924975271d64e8869b44ea97f068e85b5edd03e802e4917b770aaf2d"}, - {file = "pygit2-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:889ca83528c0649afd970da700cc6ed47dc340481f146a39ba5bfbeca1ddd6f8"}, - {file = "pygit2-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5465db21c6fd481ec29aa7afcca9a85b1fdb19b2f2d09a31b4bdba2f1bd0e75"}, - {file = "pygit2-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ceecd5d30583f9db56aadcd7238bb3c76a2934d8a932de47aed77fe3c188e7"}, - {file = "pygit2-1.10.1-cp39-cp39-win32.whl", hash = "sha256:9d6e1270b91e7bf70185bb4c3686e04cca87a385c8a2d5c74eec8770091531be"}, - {file = "pygit2-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:d4251830276018c2346ddccad4b4ce06ed1d983b002a633c4d894b13669052d0"}, - {file = "pygit2-1.10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7eb2cee54a1cb468b5502493ee4f3ec2f1f82db9c46fab7dacaa37afc4fcde8e"}, - {file = "pygit2-1.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411dc8af5f25c30a0c3d79ee1e22fb892d6fd6ccb54d4c1fb7746e6274e36426"}, - {file = "pygit2-1.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe41da630f4e7cb290dc7e97edf30a59d634426af52a89d4ab5c0fb1ea9ccfe4"}, - {file = "pygit2-1.10.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9da53c6f5c08308450059d7dfb3067d59c45f14bee99743e536c5f9d9823f154"}, - {file = "pygit2-1.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb49f9469a893f75f105cdf2c79254859aaf2fdce1078c38514ca12fe185a759"}, - {file = "pygit2-1.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff838665d6410b5a605f53c1ccd2d2f87ca30de59e89773e7cb5e10211426f90"}, - {file = "pygit2-1.10.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9d23bb613f5692da78c09a79ae40d6ced57b772ae9153aed23a9aa1889a16c85"}, - {file = "pygit2-1.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a3cc867fa6907bfc78d7d1322f3dabd4107b16238205df7e2dec9ee265f0c0"}, - {file = "pygit2-1.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb3eb2f1d437db6e115d5f56d122f2f3737fa2e6063aa42e4d856ca76d785ce6"}, - {file = "pygit2-1.10.1.tar.gz", hash = "sha256:354651bf062c02d1f08041d6fbf1a9b4bf7a93afce65979bdc08bdc65653aa2e"}, + {file = "pygit2-1.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:263e05ac655a4ce0a1083aaaedfd0a900b8dee2c3bb3ecf4f4e504a404467d1f"}, + {file = "pygit2-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee6b4a0e181c576cdb64b1568bfbff3d1c2cd7e99808f578c8b08875c0f43739"}, + {file = "pygit2-1.11.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:d1b5fcaac1f29337f2d1465fa095e2e375b76a06385bda9391cb418c7937fb54"}, + {file = "pygit2-1.11.1-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:96ff745d3199909d06cab5e419a6b953be99992414a08ec4dddb682f395de8f1"}, + {file = "pygit2-1.11.1-cp310-cp310-win32.whl", hash = "sha256:b3c8726f0c9a2b0e04aac37b18027c58c2697b9c021d3458b28bc250b9b6aecf"}, + {file = "pygit2-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:f42409d25bbfc090fd1af1f5f47584d7e0c4212b037a7f86639a02c30420c6ee"}, + {file = "pygit2-1.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29f89d96bbb404ca1566418463521039903094fad2f81a76d7083810d2ea3aad"}, + {file = "pygit2-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5c158b9430c5e76ca728b1a214bf21d355af6ac6e2da86ed17775b870b6c6eb"}, + {file = "pygit2-1.11.1-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:6c3434b143e7570ec45cd1a0e344fe7a12e64b99e7155fa38b74f724c8fc243c"}, + {file = "pygit2-1.11.1-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:550aa503c86ef0061ce64d61c3672b15b500c2b1e4224c405acecfac2211b5d9"}, + {file = "pygit2-1.11.1-cp311-cp311-win32.whl", hash = "sha256:f270f86a0185ca2064e1aa6b8db3bb677b1bf76ee35f48ca5ce28a921fad5632"}, + {file = "pygit2-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:56b9deeab214653805214f05337f5e9552b47bf268c285551f20ea51a6056c3e"}, + {file = "pygit2-1.11.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3c5838e6516abc4384498f4b4c7f88578221596dc2ba8db2320ff2cfebe9787e"}, + {file = "pygit2-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a886aab5aae8d8db572e20b9f56c13cd506775265222ea7f35b2c781e4fa3a5e"}, + {file = "pygit2-1.11.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:3be4534180edd53e3e1da93c5b091975566bfdffdc73f21930d79fef096a25d2"}, + {file = "pygit2-1.11.1-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:4d6209c703764ae0ba57b17038482f3e54f432f80f88ccd490d7f8b70b167db6"}, + {file = "pygit2-1.11.1-cp38-cp38-win32.whl", hash = "sha256:ddb032fa71d4b4a64bf101e37eaa21f5369f20a862b5e34bbc33854a3a35f641"}, + {file = "pygit2-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8de0091e5eeaea2004f63f7dcb4540780f2124f68c0bcb670ae0fa9ada8bf66"}, + {file = "pygit2-1.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b44674e53efa9eca36e44f2f3d1a29e53e78649ba13105ae0b037d557f2c076"}, + {file = "pygit2-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0170f31c2efb15f6779689df328c05a8005ecb2b92784a37ff967d713cdafe82"}, + {file = "pygit2-1.11.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:960a55ff78f48887a7aa8ece952aad0f52f0a2ba1ad7bddd7064fbbefd85dfbb"}, + {file = "pygit2-1.11.1-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:df722c90fb54a42fa019dcf8d8f82961c3099c3024f1fda46c53e0886ff8f0f3"}, + {file = "pygit2-1.11.1-cp39-cp39-win32.whl", hash = "sha256:3b091e7fd00dd2a2cd3a6b5e235b6cbfbc1c07f15ee83a5cb3f188e1d6d1bca1"}, + {file = "pygit2-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:da040dc28800831bcbefef0850466739f103bfc769d952bd10c449646d52ce8f"}, + {file = "pygit2-1.11.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:585daa3956f1dc10d08e3459c20b57be42c7f9c0fbde21e797b3a00b5948f061"}, + {file = "pygit2-1.11.1-pp38-pypy38_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:273878adeced2aec7885745b73fffb91a8e67868c105bf881b61008d42497ad6"}, + {file = "pygit2-1.11.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:48cfd72283a08a9226aca115870799ee92898d692699f541a3b3f519805108ec"}, + {file = "pygit2-1.11.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a9ca4cb2481d2df14d23c765facef325f717d9a3966a986b86e88d92eef11929"}, + {file = "pygit2-1.11.1-pp39-pypy39_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:d5f64a424d9123b047458b0107c5dd33559184b56a1f58b10056ea5cbac74360"}, + {file = "pygit2-1.11.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f13e190cc080bde093138e12bcb609500276227e3e8e8bd8765a2fd49ae2efb8"}, + {file = "pygit2-1.11.1.tar.gz", hash = "sha256:793f583fd33620f0ac38376db0f57768ef2922b89b459e75b1ac440377eb64ec"}, ] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, @@ -1751,8 +1723,8 @@ pytest = [ {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.20.1.tar.gz", hash = "sha256:626699de2a747611f3eeb64168b3575f70439b06c3d0206e6ceaeeb956e65519"}, - {file = "pytest_asyncio-0.20.1-py3-none-any.whl", hash = "sha256:2c85a835df33fda40fe3973b451e0c194ca11bc2c007eabff90bb3d156fc172b"}, + {file = "pytest-asyncio-0.20.2.tar.gz", hash = "sha256:32a87a9836298a881c0ec637ebcc952cfe23a56436bdc0d09d1511941dd8a812"}, + {file = "pytest_asyncio-0.20.2-py3-none-any.whl", hash = "sha256:07e0abf9e6e6b95894a39f688a4a875d63c2128f76c02d03d16ccbc35bcc0f8a"}, ] pytest-cov = [ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, @@ -1774,8 +1746,8 @@ python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] redis = [ - {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"}, - {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"}, + {file = "redis-4.3.5-py3-none-any.whl", hash = "sha256:46652271dc7525cd5a9667e5b0ca983c848c75b2b8f7425403395bb8379dcf25"}, + {file = "redis-4.3.5.tar.gz", hash = "sha256:30c07511627a4c5c4d970e060000772f323174f75e745a26938319817ead7a12"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, @@ -1786,8 +1758,8 @@ rfc3986 = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] setuptools = [ - {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, - {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, + {file = "setuptools-65.6.0-py3-none-any.whl", hash = "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840"}, + {file = "setuptools-65.6.0.tar.gz", hash = "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1802,47 +1774,47 @@ sortedcontainers = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.4.42-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124"}, - {file = "SQLAlchemy-1.4.42-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88"}, - {file = "SQLAlchemy-1.4.42-cp27-cp27m-win32.whl", hash = "sha256:1d0c23ecf7b3bc81e29459c34a3f4c68ca538de01254e24718a7926810dc39a6"}, - {file = "SQLAlchemy-1.4.42-cp27-cp27m-win_amd64.whl", hash = "sha256:6c9d004eb78c71dd4d3ce625b80c96a827d2e67af9c0d32b1c1e75992a7916cc"}, - {file = "SQLAlchemy-1.4.42-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9e3a65ce9ed250b2f096f7b559fe3ee92e6605fab3099b661f0397a9ac7c8d95"}, - {file = "SQLAlchemy-1.4.42-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:2e56dfed0cc3e57b2f5c35719d64f4682ef26836b81067ee6cfad062290fd9e2"}, - {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42c59ffd2d625b28cdb2ae4cde8488543d428cba17ff672a543062f7caee525"}, - {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22459fc1718785d8a86171bbe7f01b5c9d7297301ac150f508d06e62a2b4e8d2"}, - {file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df76e9c60879fdc785a34a82bf1e8691716ffac32e7790d31a98d7dec6e81545"}, - {file = "SQLAlchemy-1.4.42-cp310-cp310-win32.whl", hash = "sha256:e7e740453f0149437c101ea4fdc7eea2689938c5760d7dcc436c863a12f1f565"}, - {file = "SQLAlchemy-1.4.42-cp310-cp310-win_amd64.whl", hash = "sha256:effc89e606165ca55f04f3f24b86d3e1c605e534bf1a96e4e077ce1b027d0b71"}, - {file = "SQLAlchemy-1.4.42-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:97ff50cd85bb907c2a14afb50157d0d5486a4b4639976b4a3346f34b6d1b5272"}, - {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12c6949bae10f1012ab5c0ea52ab8db99adcb8c7b717938252137cdf694c775"}, - {file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11b2ec26c5d2eefbc3e6dca4ec3d3d95028be62320b96d687b6e740424f83b7d"}, - {file = "SQLAlchemy-1.4.42-cp311-cp311-win32.whl", hash = "sha256:6045b3089195bc008aee5c273ec3ba9a93f6a55bc1b288841bd4cfac729b6516"}, - {file = "SQLAlchemy-1.4.42-cp311-cp311-win_amd64.whl", hash = "sha256:0501f74dd2745ec38f44c3a3900fb38b9db1ce21586b691482a19134062bf049"}, - {file = "SQLAlchemy-1.4.42-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6e39e97102f8e26c6c8550cb368c724028c575ec8bc71afbbf8faaffe2b2092a"}, - {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d878929c30e41fb3d757a5853b680a561974a0168cd33a750be4ab93181628"}, - {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa5b7eb2051e857bf83bade0641628efe5a88de189390725d3e6033a1fff4257"}, - {file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1c5f8182b4f89628d782a183d44db51b5af84abd6ce17ebb9804355c88a7b5"}, - {file = "SQLAlchemy-1.4.42-cp36-cp36m-win32.whl", hash = "sha256:a7dd5b7b34a8ba8d181402d824b87c5cee8963cb2e23aa03dbfe8b1f1e417cde"}, - {file = "SQLAlchemy-1.4.42-cp36-cp36m-win_amd64.whl", hash = "sha256:5ede1495174e69e273fad68ad45b6d25c135c1ce67723e40f6cf536cb515e20b"}, - {file = "SQLAlchemy-1.4.42-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:9256563506e040daddccaa948d055e006e971771768df3bb01feeb4386c242b0"}, - {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4948b6c5f4e56693bbeff52f574279e4ff972ea3353f45967a14c30fb7ae2beb"}, - {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1811a0b19a08af7750c0b69e38dec3d46e47c4ec1d74b6184d69f12e1c99a5e0"}, - {file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b01d9cd2f9096f688c71a3d0f33f3cd0af8549014e66a7a7dee6fc214a7277d"}, - {file = "SQLAlchemy-1.4.42-cp37-cp37m-win32.whl", hash = "sha256:bd448b262544b47a2766c34c0364de830f7fb0772d9959c1c42ad61d91ab6565"}, - {file = "SQLAlchemy-1.4.42-cp37-cp37m-win_amd64.whl", hash = "sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9"}, - {file = "SQLAlchemy-1.4.42-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ab7c158f98de6cb4f1faab2d12973b330c2878d0c6b689a8ca424c02d66e1b3"}, - {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee377eb5c878f7cefd633ab23c09e99d97c449dd999df639600f49b74725b80"}, - {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:934472bb7d8666727746a75670a1f8d91a9cae8c464bba79da30a0f6faccd9e1"}, - {file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb94a3d1ba77ff2ef11912192c066f01e68416f554c194d769391638c8ad09a"}, - {file = "SQLAlchemy-1.4.42-cp38-cp38-win32.whl", hash = "sha256:f0f574465b78f29f533976c06b913e54ab4980b9931b69aa9d306afff13a9471"}, - {file = "SQLAlchemy-1.4.42-cp38-cp38-win_amd64.whl", hash = "sha256:a85723c00a636eed863adb11f1e8aaa36ad1c10089537823b4540948a8429798"}, - {file = "SQLAlchemy-1.4.42-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5ce6929417d5dce5ad1d3f147db81735a4a0573b8fb36e3f95500a06eaddd93e"}, - {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723e3b9374c1ce1b53564c863d1a6b2f1dc4e97b1c178d9b643b191d8b1be738"}, - {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:876eb185911c8b95342b50a8c4435e1c625944b698a5b4a978ad2ffe74502908"}, - {file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd49af453e590884d9cdad3586415922a8e9bb669d874ee1dc55d2bc425aacd"}, - {file = "SQLAlchemy-1.4.42-cp39-cp39-win32.whl", hash = "sha256:e4ef8cb3c5b326f839bfeb6af5f406ba02ad69a78c7aac0fbeeba994ad9bb48a"}, - {file = "SQLAlchemy-1.4.42-cp39-cp39-win_amd64.whl", hash = "sha256:5f966b64c852592469a7eb759615bbd351571340b8b344f1d3fa2478b5a4c934"}, - {file = "SQLAlchemy-1.4.42.tar.gz", hash = "sha256:177e41914c476ed1e1b77fd05966ea88c094053e17a85303c4ce007f88eff363"}, + {file = "SQLAlchemy-1.4.44-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:da60b98b0f6f0df9fbf8b72d67d13b73aa8091923a48af79a951d4088530a239"}, + {file = "SQLAlchemy-1.4.44-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:95f4f8d62589755b507218f2e3189475a4c1f5cc9db2aec772071a7dc6cd5726"}, + {file = "SQLAlchemy-1.4.44-cp27-cp27m-win32.whl", hash = "sha256:afd1ac99179d1864a68c06b31263a08ea25a49df94e272712eb2824ef151e294"}, + {file = "SQLAlchemy-1.4.44-cp27-cp27m-win_amd64.whl", hash = "sha256:f8e5443295b218b08bef8eb85d31b214d184b3690d99a33b7bd8e5591e2b0aa1"}, + {file = "SQLAlchemy-1.4.44-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:53f90a2374f60e703c94118d21533765412da8225ba98659de7dd7998641ab17"}, + {file = "SQLAlchemy-1.4.44-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:65a0ad931944fcb0be12a8e0ac322dbd3ecf17c53f088bc10b6da8f0caac287b"}, + {file = "SQLAlchemy-1.4.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b185041a4dc5c685283ea98c2f67bbfa47bb28e4a4f5b27ebf40684e7a9f8"}, + {file = "SQLAlchemy-1.4.44-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:80ead36fb1d676cc019586ffdc21c7e906ce4bf243fe4021e4973dae332b6038"}, + {file = "SQLAlchemy-1.4.44-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68e0cd5d32a32c4395168d42f2fefbb03b817ead3a8f3704b8bd5697c0b26c24"}, + {file = "SQLAlchemy-1.4.44-cp310-cp310-win32.whl", hash = "sha256:ae1ed1ebc407d2f66c6f0ec44ef7d56e3f455859df5494680e2cf89dad8e3ae0"}, + {file = "SQLAlchemy-1.4.44-cp310-cp310-win_amd64.whl", hash = "sha256:6f0ea4d7348feb5e5d0bf317aace92e28398fa9a6e38b7be9ec1f31aad4a8039"}, + {file = "SQLAlchemy-1.4.44-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5e8ed9cde48b76318ab989deeddc48f833d2a6a7b7c393c49b704f67dedf01d"}, + {file = "SQLAlchemy-1.4.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857676d810ca196be73c98eb839125d6fa849bfa3589be06201a6517f9961c"}, + {file = "SQLAlchemy-1.4.44-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c56e6899fa6e767e4be5d106941804a4201c5cb9620a409c0b80448ec70b656"}, + {file = "SQLAlchemy-1.4.44-cp311-cp311-win32.whl", hash = "sha256:c46322354c58d4dc039a2c982d28284330f8919f31206894281f4b595b9d8dbe"}, + {file = "SQLAlchemy-1.4.44-cp311-cp311-win_amd64.whl", hash = "sha256:7313e4acebb9ae88dbde14a8a177467a7625b7449306c03a3f9f309b30e163d0"}, + {file = "SQLAlchemy-1.4.44-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:17aee7bfcef7bf0dea92f10e5dfdd67418dcf6fe0759f520e168b605855c003e"}, + {file = "SQLAlchemy-1.4.44-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9470633395e5f24d6741b4c8a6e905bce405a28cf417bba4ccbaadf3dab0111d"}, + {file = "SQLAlchemy-1.4.44-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:393f51a09778e8984d735b59a810731394308b4038acdb1635397c2865dae2b6"}, + {file = "SQLAlchemy-1.4.44-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7e3b9e01fdbe1ce3a165cc7e1ff52b24813ee79c6df6dee0d1e13888a97817e"}, + {file = "SQLAlchemy-1.4.44-cp36-cp36m-win32.whl", hash = "sha256:6a06c2506c41926d2769f7968759995f2505e31c5b5a0821e43ca5a3ddb0e8ae"}, + {file = "SQLAlchemy-1.4.44-cp36-cp36m-win_amd64.whl", hash = "sha256:3ca21b35b714ce36f4b8d1ee8d15f149db8eb43a472cf71600bf18dae32286e7"}, + {file = "SQLAlchemy-1.4.44-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:3cbdbed8cdcae0f83640a9c44fa02b45a6c61e149c58d45a63c9581aba62850f"}, + {file = "SQLAlchemy-1.4.44-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a22208c1982f1fe2ae82e5e4c3d4a6f2445a7a0d65fb7983a3d7cbbe3983f5a4"}, + {file = "SQLAlchemy-1.4.44-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d3b9ac11f36ab9a726097fba7c7f6384f0129aedb017f1d4d1d4fce9052a1320"}, + {file = "SQLAlchemy-1.4.44-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d654870a66027af3a26df1372cf7f002e161c6768ebe4c9c6fdc0da331cb5173"}, + {file = "SQLAlchemy-1.4.44-cp37-cp37m-win32.whl", hash = "sha256:0be9b479c5806cece01f1581726573a8d6515f8404e082c375b922c45cfc2a7b"}, + {file = "SQLAlchemy-1.4.44-cp37-cp37m-win_amd64.whl", hash = "sha256:3eba07f740488c3a125f17c092a81eeae24a6c7ec32ac9dbc52bf7afaf0c4f16"}, + {file = "SQLAlchemy-1.4.44-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ad5f966623905ee33694680dda1b735544c99c7638f216045d21546d3d8c6f5b"}, + {file = "SQLAlchemy-1.4.44-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f68eab46649504eb95be36ca529aea16cd199f080726c28cbdbcbf23d20b2a2"}, + {file = "SQLAlchemy-1.4.44-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:21f3df74a0ab39e1255e94613556e33c1dc3b454059fe0b365ec3bbb9ed82e4a"}, + {file = "SQLAlchemy-1.4.44-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8080bc51a775627865e0f1dbfc0040ff4ace685f187f6036837e1727ba2ed10"}, + {file = "SQLAlchemy-1.4.44-cp38-cp38-win32.whl", hash = "sha256:b6a337a2643a41476fb6262059b8740f4b9a2ec29bf00ffb18c18c080f6e0aed"}, + {file = "SQLAlchemy-1.4.44-cp38-cp38-win_amd64.whl", hash = "sha256:b737fbeb2f78926d1f59964feb287bbbd050e7904766f87c8ce5cfb86e6d840c"}, + {file = "SQLAlchemy-1.4.44-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c9aa372b295a36771cffc226b6517df3011a7d146ac22d19fa6a75f1cdf9d7e6"}, + {file = "SQLAlchemy-1.4.44-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:237067ba0ef45a518b64606e1807f7229969ad568288b110ed5f0ca714a3ed3a"}, + {file = "SQLAlchemy-1.4.44-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6d7e1b28342b45f19e3dea7873a9479e4a57e15095a575afca902e517fb89652"}, + {file = "SQLAlchemy-1.4.44-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c0093678001f5d79f2dcbf3104c54d6c89e41ab50d619494c503a4d3f1aef2"}, + {file = "SQLAlchemy-1.4.44-cp39-cp39-win32.whl", hash = "sha256:7cf7c7adbf4417e3f46fc5a2dbf8395a5a69698217337086888f79700a12e93a"}, + {file = "SQLAlchemy-1.4.44-cp39-cp39-win_amd64.whl", hash = "sha256:d3b6d4588994da73567bb00af9d7224a16c8027865a8aab53ae9be83f9b7cbd1"}, + {file = "SQLAlchemy-1.4.44.tar.gz", hash = "sha256:2dda5f96719ae89b3ec0f1b79698d86eb9aecb1d54e990abb3fdd92c04b46a90"}, ] srcinfo = [ {file = "srcinfo-0.0.8-py3-none-any.whl", hash = "sha256:0922ee4302b927d7ddea74c47e539b226a0a7738dc89f95b66404a28d07f3f6b"}, @@ -1873,8 +1845,8 @@ urllib3 = [ {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] uvicorn = [ - {file = "uvicorn-0.19.0-py3-none-any.whl", hash = "sha256:cc277f7e73435748e69e075a721841f7c4a95dba06d12a72fe9874acced16f6f"}, - {file = "uvicorn-0.19.0.tar.gz", hash = "sha256:cf538f3018536edb1f4a826311137ab4944ed741d52aeb98846f52215de57f25"}, + {file = "uvicorn-0.20.0-py3-none-any.whl", hash = "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd"}, + {file = "uvicorn-0.20.0.tar.gz", hash = "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, @@ -1884,72 +1856,6 @@ werkzeug = [ {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, ] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] wsproto = [ {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, diff --git a/pyproject.toml b/pyproject.toml index 7fc0db47..e977ad4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,16 +62,16 @@ asgiref = "^3.4.1" bcrypt = "^4.0.0" bleach = "^5.0.0" email-validator = "^1.3.0" -fakeredis = "^1.10.0" +fakeredis = "^2.0.0" feedgen = "^0.9.0" -httpx = "^0.23.0" +httpx = "^0.23.1" itsdangerous = "^2.0.1" lxml = "^4.6.3" -orjson = "^3.8.1" +orjson = "^3.8.2" protobuf = "^4.21.9" -pygit2 = "^1.7.0" +pygit2 = "^1.11.1" python-multipart = "^0.0.5" -redis = "^4.0.0" +redis = "^4.3.5" requests = "^2.28.1" paginate = "^0.5.6" @@ -85,7 +85,7 @@ Werkzeug = "^2.0.2" SQLAlchemy = "^1.4.26" # ASGI -uvicorn = "^0.19.0" +uvicorn = "^0.20.0" gunicorn = "^20.1.0" Hypercorn = "^0.14.0" prometheus-fastapi-instrumentator = "^5.7.1" @@ -99,7 +99,7 @@ srcinfo = "^0.0.8" [tool.poetry.dev-dependencies] coverage = "^6.0.2" pytest = "^7.2.0" -pytest-asyncio = "^0.20.1" +pytest-asyncio = "^0.20.2" pytest-cov = "^4.0.0" pytest-tap = "^3.2" From 512ba0238914c28a4e445d04ae08b714bd14558c Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 23 Nov 2022 00:25:31 +0000 Subject: [PATCH 1197/1451] fix(deps): update dependency fastapi to ^0.87.0 --- poetry.lock | 80 +++++++++++++++++++------------------------------- pyproject.toml | 2 +- 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/poetry.lock b/poetry.lock index 22cbd3fd..12770f64 100644 --- a/poetry.lock +++ b/poetry.lock @@ -69,7 +69,7 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "authlib" @@ -138,7 +138,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode-backport = ["unicodedata2"] +unicode_backport = ["unicodedata2"] [[package]] name = "click" @@ -260,7 +260,7 @@ lua = ["lupa (>=1.13,<2.0)"] [[package]] name = "fastapi" -version = "0.85.2" +version = "0.87.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false @@ -268,13 +268,13 @@ python-versions = ">=3.7" [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.20.4" +starlette = "0.21.0" [package.extras] -all = ["email-validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] -dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] +all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.114)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.7.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "pytest-cov (>=2.12.0,<5.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<=1.4.41)", "types-orjson (==3.6.2)", "types-ujson (==5.5.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "coverage[toml] (>=6.5.0,<7.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.114)", "sqlalchemy (>=1.3.18,<=1.4.41)", "types-orjson (==3.6.2)", "types-ujson (==5.5.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "feedgen" @@ -309,7 +309,7 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["docutils (<0.18)", "sphinx"] test = ["faulthandler", "objgraph", "psutil"] [[package]] @@ -320,9 +320,6 @@ category = "main" optional = false python-versions = ">=3.5" -[package.dependencies] -setuptools = ">=3.0" - [package.extras] eventlet = ["eventlet (>=0.24.1)"] gevent = ["gevent (>=1.4.0)"] @@ -411,7 +408,7 @@ toml = "*" wsproto = ">=0.14.0" [package.extras] -docs = ["pydata_sphinx_theme"] +docs = ["pydata-sphinx-theme"] h3 = ["aioquic (>=0.9.0,<1.0)"] trio = ["trio (>=0.11.0)"] uvloop = ["uvloop"] @@ -489,7 +486,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] +htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] [[package]] @@ -504,7 +501,7 @@ python-versions = ">=3.7" MarkupSafe = ">=0.9.2" [package.extras] -babel = ["Babel"] +babel = ["babel"] lingua = ["lingua"] testing = ["pytest"] @@ -817,7 +814,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rfc3986" @@ -833,19 +830,6 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] -[[package]] -name = "setuptools" -version = "65.6.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -886,21 +870,21 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] -mysql-connector = ["mysql-connector-python"] +mysql_connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql", "pymysql (<1)"] -sqlcipher = ["sqlcipher3_binary"] +sqlcipher = ["sqlcipher3-binary"] [[package]] name = "srcinfo" @@ -915,7 +899,7 @@ parse = "*" [[package]] name = "starlette" -version = "0.20.4" +version = "0.21.0" description = "The little ASGI library that shines." category = "main" optional = false @@ -926,10 +910,10 @@ anyio = ">=3.4.0,<5" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] [[package]] -name = "tap-py" +name = "tap.py" version = "3.1" description = "Test Anything Protocol (TAP) tools" category = "dev" @@ -1039,7 +1023,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "b178f1fcbba93d9cbc8dd23193b25afd5e1ba971196757abf098a1dfa2666cba" +content-hash = "02d70de5b58cf84a7b9015fc1d1a598bdb139b32f7239846183eb924e336ce86" [metadata.files] aiofiles = [ @@ -1280,8 +1264,8 @@ fakeredis = [ {file = "fakeredis-2.0.0.tar.gz", hash = "sha256:6d1dc2417921b7ce56a80877afa390d6335a3154146f201a86e3a14417bdc79e"}, ] fastapi = [ - {file = "fastapi-0.85.2-py3-none-any.whl", hash = "sha256:6292db0edd4a11f0d938d6033ccec5f706e9d476958bf33b119e8ddb4e524bde"}, - {file = "fastapi-0.85.2.tar.gz", hash = "sha256:3e10ea0992c700e0b17b6de8c2092d7b9cd763ce92c49ee8d4be10fee3b2f367"}, + {file = "fastapi-0.87.0-py3-none-any.whl", hash = "sha256:254453a2e22f64e2a1b4e1d8baf67d239e55b6c8165c079d25746a5220c81bb4"}, + {file = "fastapi-0.87.0.tar.gz", hash = "sha256:07032e53df9a57165047b4f38731c38bdcc3be5493220471015e2b4b51b486a4"}, ] feedgen = [ {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, @@ -1757,10 +1741,6 @@ rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] -setuptools = [ - {file = "setuptools-65.6.0-py3-none-any.whl", hash = "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840"}, - {file = "setuptools-65.6.0.tar.gz", hash = "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"}, -] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1821,10 +1801,10 @@ srcinfo = [ {file = "srcinfo-0.0.8.tar.gz", hash = "sha256:5ac610cf8b15d4b0a0374bd1f7ad301675c2938f0414addf3ef7d7e3fcaf5c65"}, ] starlette = [ - {file = "starlette-0.20.4-py3-none-any.whl", hash = "sha256:c0414d5a56297d37f3db96a84034d61ce29889b9eaccf65eb98a0b39441fcaa3"}, - {file = "starlette-0.20.4.tar.gz", hash = "sha256:42fcf3122f998fefce3e2c5ad7e5edbf0f02cf685d646a83a08d404726af5084"}, + {file = "starlette-0.21.0-py3-none-any.whl", hash = "sha256:0efc058261bbcddeca93cad577efd36d0c8a317e44376bcfc0e097a2b3dc24a7"}, + {file = "starlette-0.21.0.tar.gz", hash = "sha256:b1b52305ee8f7cfc48cde383496f7c11ab897cd7112b33d998b1317dc8ef9027"}, ] -tap-py = [ +"tap.py" = [ {file = "tap.py-3.1-py3-none-any.whl", hash = "sha256:928c852f3361707b796c93730cc5402c6378660b161114461066acf53d65bf5d"}, {file = "tap.py-3.1.tar.gz", hash = "sha256:3c0cd45212ad5a25b35445964e2517efa000a118a1bfc3437dae828892eaf1e1"}, ] diff --git a/pyproject.toml b/pyproject.toml index e977ad4e..762a52c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ pytest-xdist = "^3.0.2" filelock = "^3.3.2" posix-ipc = "^1.0.5" pyalpm = "^0.10.6" -fastapi = "^0.85.1" +fastapi = "^0.87.0" srcinfo = "^0.0.8" [tool.poetry.dev-dependencies] From 1216399d53b3f3163eccc2ea0aacaeaf23562373 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 24 Nov 2022 22:23:37 +0100 Subject: [PATCH 1198/1451] fix(test): FastAPI 0.87.0 - error fixes FastAPI 0.87.0 switched to the httpx library for their TestClient * allow_redirects is deprecated and replaced by follow_redirects Signed-off-by: moson-mo --- test/test_accounts_routes.py | 78 ++++++--------- test/test_auth_routes.py | 28 +++--- test/test_homepage.py | 9 +- test/test_packages_routes.py | 45 +++++---- test/test_pkgbase_routes.py | 163 +++++++++++++++++-------------- test/test_requests.py | 31 +++--- test/test_routes.py | 8 +- test/test_trusted_user_routes.py | 82 +++++++--------- 8 files changed, 218 insertions(+), 226 deletions(-) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index f44fd44e..44226627 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -70,6 +70,9 @@ def client() -> TestClient: # Necessary for forged login CSRF protection on the login route. Set here # instead of only on the necessary requests for convenience. client.headers.update(TEST_REFERER) + + # disable redirects for our tests + client.follow_redirects = False yield client @@ -104,9 +107,7 @@ def test_get_passreset_authed_redirects(client: TestClient, user: User): assert sid is not None with client as request: - response = request.get( - "/passreset", cookies={"AURSID": sid}, allow_redirects=False - ) + response = request.get("/passreset", cookies={"AURSID": sid}) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -140,7 +141,7 @@ def test_get_passreset_translation(client: TestClient): def test_get_passreset_with_resetkey(client: TestClient): with client as request: - response = request.get("/passreset", data={"resetkey": "abcd"}) + response = request.get("/passreset", params={"resetkey": "abcd"}) assert response.status_code == int(HTTPStatus.OK) @@ -153,7 +154,6 @@ def test_post_passreset_authed_redirects(client: TestClient, user: User): "/passreset", cookies={"AURSID": sid}, data={"user": "blah"}, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -323,7 +323,7 @@ def post_register(request, **kwargs): for k, v in args.items(): data[k] = v - return request.post("/register", data=data, allow_redirects=False) + return request.post("/register", data=data) def test_post_register(client: TestClient): @@ -737,7 +737,7 @@ def test_get_account_edit_unauthorized(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: # Try to edit `test2` while authenticated as `test`. - response = request.get(endpoint, cookies={"AURSID": sid}, allow_redirects=False) + response = request.get(endpoint, cookies={"AURSID": sid}) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" @@ -755,7 +755,6 @@ def test_post_account_edit(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -841,9 +840,7 @@ def test_post_account_edit_dev(client: TestClient, tu_user: User): endpoint = f"/account/{tu_user.Username}/edit" with client as request: - response = request.post( - endpoint, cookies={"AURSID": sid}, data=post_data, allow_redirects=False - ) + response = request.post(endpoint, cookies={"AURSID": sid}, data=post_data) assert response.status_code == int(HTTPStatus.OK) expected = "The account, test, " @@ -867,7 +864,6 @@ def test_post_account_edit_language(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -897,7 +893,6 @@ def test_post_account_edit_timezone(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -914,7 +909,6 @@ def test_post_account_edit_error_missing_password(client: TestClient, user: User "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -934,7 +928,6 @@ def test_post_account_edit_error_invalid_password(client: TestClient, user: User "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1039,9 +1032,7 @@ def test_post_account_edit_error_unauthorized(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: # Attempt to edit 'test2' while logged in as 'test'. - response = request.post( - endpoint, cookies={"AURSID": sid}, data=post_data, allow_redirects=False - ) + response = request.post(endpoint, cookies={"AURSID": sid}, data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" @@ -1064,7 +1055,6 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -1077,7 +1067,6 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -1099,7 +1088,6 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -1116,7 +1104,6 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -1133,9 +1120,7 @@ def test_post_account_edit_invalid_ssh_pubkey(client: TestClient, user: User): } cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.post( - "/account/test/edit", data=data, cookies=cookies, allow_redirects=False - ) + response = request.post("/account/test/edit", data=data, cookies=cookies) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1157,7 +1142,6 @@ def test_post_account_edit_password(client: TestClient, user: User): "/account/test/edit", cookies={"AURSID": sid}, data=post_data, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -1197,7 +1181,7 @@ def test_post_account_edit_other_user_as_user(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/account/{user2.Username}" @@ -1208,7 +1192,7 @@ def test_post_account_edit_self_type_as_tu(client: TestClient, tu_user: User): # We cannot see the Account Type field on our own edit page. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" in resp.text @@ -1239,7 +1223,7 @@ def test_post_account_edit_other_user_type_as_tu( # As a TU, we can see the Account Type field for other users. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" in resp.text @@ -1277,19 +1261,20 @@ def test_post_account_edit_other_user_suspend_as_tu(client: TestClient, tu_user: # apart from `tu_user`s during our testing. user_client = TestClient(app=app) user_client.headers.update(TEST_REFERER) + user_client.follow_redirects = False # Test that `user` can view their account edit page while logged in. user_cookies = {"AURSID": sid} with client as request: endpoint = f"/account/{user.Username}/edit" - resp = request.get(endpoint, cookies=user_cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=user_cookies) assert resp.status_code == HTTPStatus.OK cookies = {"AURSID": tu_user.login(Request(), "testPassword")} assert cookies is not None # This is useless, we create the dict here ^ # As a TU, we can see the Account for other users. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) # As a TU, we can modify other user's account types. data = { @@ -1299,12 +1284,13 @@ def test_post_account_edit_other_user_suspend_as_tu(client: TestClient, tu_user: "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) # Test that `user` no longer has a session. with user_client as request: - resp = request.get(endpoint, cookies=user_cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=user_cookies) assert resp.status_code == HTTPStatus.SEE_OTHER # Since user is now suspended, they should not be able to login. @@ -1341,9 +1327,7 @@ def test_get_account(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get( - "/account/test", cookies={"AURSID": sid}, allow_redirects=False - ) + response = request.get("/account/test", cookies={"AURSID": sid}) assert response.status_code == int(HTTPStatus.OK) @@ -1353,16 +1337,14 @@ def test_get_account_not_found(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get( - "/account/not_found", cookies={"AURSID": sid}, allow_redirects=False - ) + response = request.get("/account/not_found", cookies={"AURSID": sid}) assert response.status_code == int(HTTPStatus.NOT_FOUND) def test_get_account_unauthenticated(client: TestClient, user: User): with client as request: - response = request.get("/account/test", allow_redirects=False) + response = request.get("/account/test") assert response.status_code == int(HTTPStatus.UNAUTHORIZED) content = response.content.decode() @@ -1832,7 +1814,7 @@ def test_get_terms_of_service(client: TestClient, user: User): ) with client as request: - response = request.get("/tos", allow_redirects=False) + response = request.get("/tos") assert response.status_code == int(HTTPStatus.SEE_OTHER) request = Request() @@ -1842,12 +1824,12 @@ def test_get_terms_of_service(client: TestClient, user: User): # First of all, let's test that we get redirected to /tos # when attempting to browse authenticated without accepting terms. with client as request: - response = request.get("/", cookies=cookies, allow_redirects=False) + response = request.get("/", cookies=cookies) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tos" with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + response = request.get("/tos", cookies=cookies) assert response.status_code == int(HTTPStatus.OK) with db.begin(): @@ -1856,7 +1838,7 @@ def test_get_terms_of_service(client: TestClient, user: User): ) with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + response = request.get("/tos", cookies=cookies) # We accepted the term, there's nothing left to accept. assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -1865,7 +1847,7 @@ def test_get_terms_of_service(client: TestClient, user: User): term.Revision = 2 with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + response = request.get("/tos", cookies=cookies) # This time, we have a modified term Revision that hasn't # yet been agreed to via AcceptedTerm update. assert response.status_code == int(HTTPStatus.OK) @@ -1874,7 +1856,7 @@ def test_get_terms_of_service(client: TestClient, user: User): accepted_term.Revision = term.Revision with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + response = request.get("/tos", cookies=cookies) # We updated the term revision, there's nothing left to accept. assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -1931,7 +1913,7 @@ def test_post_terms_of_service(client: TestClient, user: User): # Now, see that GET redirects us to / with no terms left to accept. with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + response = request.get("/tos", cookies=cookies) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -1946,7 +1928,7 @@ def test_account_comments_not_found(client: TestClient, user: User): def test_accounts_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/accounts", cookies=cookies, allow_redirects=False) + resp = request.get("/accounts", cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 87ad86f6..150625cd 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -33,6 +33,9 @@ def client() -> TestClient: # Necessary for forged login CSRF protection on the login route. Set here # instead of only on the necessary requests for convenience. client.headers.update(TEST_REFERER) + + # disable redirects for our tests + client.follow_redirects = False yield client @@ -58,21 +61,20 @@ def test_login_logout(client: TestClient, user: User): response = request.get("/login") assert response.status_code == int(HTTPStatus.OK) - response = request.post("/login", data=post_data, allow_redirects=False) + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) # Simulate following the redirect location from above's response. response = request.get(response.headers.get("location")) assert response.status_code == int(HTTPStatus.OK) - response = request.post("/logout", data=post_data, allow_redirects=False) + response = request.post("/logout", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) response = request.post( "/logout", data=post_data, cookies={"AURSID": response.cookies.get("AURSID")}, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -94,7 +96,7 @@ def test_login_email(client: TestClient, user: user): post_data = {"user": user.Email, "passwd": "testPassword", "next": "/"} with client as request: - resp = request.post("/login", data=post_data, allow_redirects=False) + resp = request.post("/login", data=post_data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" in resp.cookies @@ -119,14 +121,14 @@ def test_insecure_login(getboolean: mock.Mock, client: TestClient, user: User): # Perform a login request with the data matching our user. with client as request: - response = request.post("/login", data=post_data, allow_redirects=False) + response = request.post("/login", data=post_data) # Make sure we got the expected status out of it. assert response.status_code == int(HTTPStatus.SEE_OTHER) # Let's check what we got in terms of cookies for AURSID. # Make sure that a secure cookie got passed to us. - cookie = next(c for c in response.cookies if c.name == "AURSID") + cookie = next(c for c in response.cookies.jar if c.name == "AURSID") assert cookie.secure is False assert cookie.has_nonstandard_attr("HttpOnly") is False assert cookie.has_nonstandard_attr("SameSite") is True @@ -160,14 +162,14 @@ def test_secure_login(getboolean: mock.Mock, client: TestClient, user: User): # Perform a login request with the data matching our user. with client as request: - response = request.post("/login", data=post_data, allow_redirects=False) + response = request.post("/login", data=post_data) # Make sure we got the expected status out of it. assert response.status_code == int(HTTPStatus.SEE_OTHER) # Let's check what we got in terms of cookies for AURSID. # Make sure that a secure cookie got passed to us. - cookie = next(c for c in response.cookies if c.name == "AURSID") + cookie = next(c for c in response.cookies.jar if c.name == "AURSID") assert cookie.secure is True assert cookie.has_nonstandard_attr("HttpOnly") is True assert cookie.has_nonstandard_attr("SameSite") is True @@ -186,7 +188,7 @@ def test_authenticated_login(client: TestClient, user: User): with client as request: # Try to login. - response = request.post("/login", data=post_data, allow_redirects=False) + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -194,9 +196,7 @@ def test_authenticated_login(client: TestClient, user: User): # when requesting GET /login as an authenticated user. # Now, let's verify that we receive 403 Forbidden when we # try to get /login as an authenticated user. - response = request.get( - "/login", cookies=response.cookies, allow_redirects=False - ) + response = request.get("/login", cookies=response.cookies) assert response.status_code == int(HTTPStatus.OK) assert "Logged-in as: test" in response.text @@ -205,7 +205,7 @@ def test_unauthenticated_logout_unauthorized(client: TestClient): with client as request: # Alright, let's verify that attempting to /logout when not # authenticated returns 401 Unauthorized. - response = request.post("/logout", allow_redirects=False) + response = request.post("/logout") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location").startswith("/login") @@ -232,7 +232,7 @@ def test_login_remember_me(client: TestClient, user: User): } with client as request: - response = request.post("/login", data=post_data, allow_redirects=False) + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" in response.cookies diff --git a/test/test_homepage.py b/test/test_homepage.py index 521f71c4..1aad30f7 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -253,7 +253,8 @@ def test_homepage_dashboard_requests(redis, packages, user): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/", cookies=cookies) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -270,7 +271,8 @@ def test_homepage_dashboard_flagged_packages(redis, packages, user): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/", cookies=cookies) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.OK) # Check to see that the package showed up in the Flagged Packages table. @@ -293,7 +295,8 @@ def test_homepage_dashboard_flagged(user: User, user2: User, package: Package): # flagged co-maintained packages. comaint_cookies = {"AURSID": user2.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=comaint_cookies) + request.cookies = comaint_cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 3b717783..29872cb8 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -65,7 +65,11 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: """Yield a FastAPI TestClient.""" - yield TestClient(app=asgi.app) + client = TestClient(app=asgi.app) + + # disable redirects for our tests + client.follow_redirects = False + yield client def create_user(username: str) -> User: @@ -1142,7 +1146,6 @@ def test_packages_post_unknown_action(client: TestClient, user: User, package: P "/packages", data={"action": "unknown"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1159,7 +1162,6 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): "/packages", data={"action": "stub"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1180,7 +1182,6 @@ def test_packages_post(client: TestClient, user: User, package: Package): "/packages", data={"action": "stub"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.OK) @@ -1203,7 +1204,8 @@ def test_packages_post_unflag( # Don't supply any packages. post_data = {"action": "unflag", "IDs": []} with client as request: - resp = request.post("/packages", data=post_data, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data=post_data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to unflag." @@ -1212,7 +1214,8 @@ def test_packages_post_unflag( # Unflag the package as `user`. post_data = {"action": "unflag", "IDs": [package.ID]} with client as request: - resp = request.post("/packages", data=post_data, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data=post_data) assert resp.status_code == int(HTTPStatus.OK) assert package.PackageBase.Flagger is None successes = get_successes(resp.text) @@ -1229,7 +1232,8 @@ def test_packages_post_unflag( maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} post_data = {"action": "unflag", "IDs": [package.ID]} with client as request: - resp = request.post("/packages", data=post_data, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post("/packages", data=post_data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to unflag." @@ -1387,7 +1391,8 @@ def test_packages_post_disown_as_maintainer( # Try to run the disown action with no IDs; get an error. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "disown"}, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "disown"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to disown." @@ -1396,9 +1401,8 @@ def test_packages_post_disown_as_maintainer( # Try to disown `package` without giving the confirm argument. with client as request: - resp = request.post( - "/packages", data={"action": "disown", "IDs": [package.ID]}, cookies=cookies - ) + request.cookies = cookies + resp = request.post("/packages", data={"action": "disown", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert package.PackageBase.Maintainer is not None errors = get_errors(resp.text) @@ -1411,10 +1415,10 @@ def test_packages_post_disown_as_maintainer( # Now, try to disown `package` without credentials (as `user`). user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = user_cookies resp = request.post( "/packages", data={"action": "disown", "IDs": [package.ID], "confirm": True}, - cookies=user_cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert package.PackageBase.Maintainer is not None @@ -1424,10 +1428,10 @@ def test_packages_post_disown_as_maintainer( # Now, let's really disown `package` as `maintainer`. with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "disown", "IDs": [package.ID], "confirm": True}, - cookies=cookies, ) assert package.PackageBase.Maintainer is None @@ -1463,9 +1467,8 @@ def test_packages_post_delete( # First, let's try to use the delete action with no packages IDs. user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post( - "/packages", data={"action": "delete"}, cookies=user_cookies - ) + request.cookies = user_cookies + resp = request.post("/packages", data={"action": "delete"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to delete." @@ -1473,10 +1476,10 @@ def test_packages_post_delete( # Now, let's try to delete real packages without supplying "confirm". with client as request: + request.cookies = user_cookies resp = request.post( "/packages", data={"action": "delete", "IDs": [package.ID]}, - cookies=user_cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1488,10 +1491,10 @@ def test_packages_post_delete( # And again, with everything, but `user` doesn't have permissions. with client as request: + request.cookies = user_cookies resp = request.post( "/packages", data={"action": "delete", "IDs": [package.ID], "confirm": True}, - cookies=user_cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1503,10 +1506,10 @@ def test_packages_post_delete( # an invalid package ID. tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: + request.cookies = tu_cookies resp = request.post( "/packages", data={"action": "delete", "IDs": [0], "confirm": True}, - cookies=tu_cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1516,10 +1519,10 @@ def test_packages_post_delete( # Whoo. Now, let's finally make a valid request as `tu_user` # to delete `package`. with client as request: + request.cookies = tu_cookies resp = request.post( "/packages", data={"action": "delete", "IDs": [package.ID], "confirm": True}, - cookies=tu_cookies, ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) @@ -1541,7 +1544,7 @@ def test_account_comments_unauthorized(client: TestClient, user: User): leverage existing fixtures.""" endpoint = f"/account/{user.Username}/comments" with client as request: - resp = request.get(endpoint, allow_redirects=False) + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location").startswith("/login") diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index 18c11626..dd92d72d 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -59,7 +59,11 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: """Yield a FastAPI TestClient.""" - yield TestClient(app=asgi.app) + client = TestClient(app=asgi.app) + + # disable redirects for our tests + client.follow_redirects = False + yield client def create_user(username: str) -> User: @@ -245,7 +249,7 @@ def test_pkgbase_not_found(client: TestClient): def test_pkgbase_redirect(client: TestClient, package: Package): with client as request: - resp = request.get(f"/pkgbase/{package.Name}", allow_redirects=False) + resp = request.get(f"/pkgbase/{package.Name}") assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/packages/{package.Name}" @@ -256,7 +260,7 @@ def test_pkgbase(client: TestClient, package: Package): expected = [package.Name, second.Name] with client as request: - resp = request.get(f"/pkgbase/{package.Name}", allow_redirects=False) + resp = request.get(f"/pkgbase/{package.Name}") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -287,7 +291,7 @@ def test_pkgbase_maintainer( ) with client as request: - resp = request.get(f"/pkgbase/{package.Name}") + resp = request.get(f"/pkgbase/{package.Name}", follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -308,7 +312,7 @@ def test_pkgbase_voters(client: TestClient, tu_user: User, package: Package): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) # We should've gotten one link to the voter, tu_user. @@ -327,7 +331,7 @@ def test_pkgbase_voters_unauthorized(client: TestClient, user: User, package: Pa db.create(PackageVote, User=user, PackageBase=pkgbase, VoteTS=now) with client as request: - resp = request.get(endpoint, allow_redirects=False) + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -420,7 +424,7 @@ def test_pkgbase_comments( assert resp.headers.get("location")[:prefix_len] == expected_prefix with client as request: - resp = request.get(resp.headers.get("location")) + resp = request.get(resp.headers.get("location"), follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -461,7 +465,7 @@ def test_pkgbase_comments( assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: - resp = request.get(resp.headers.get("location")) + resp = request.get(resp.headers.get("location"), follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -527,7 +531,8 @@ def test_pkgbase_comment_delete( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/delete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/pkgbase/{pkgbasename}" @@ -537,12 +542,14 @@ def test_pkgbase_comment_delete( maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/undelete" with client as request: - resp = request.post(endpoint, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) # And move on to undeleting it. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -670,7 +677,7 @@ def test_pkgbase_comaintainers_not_found(client: TestClient, maintainer: User): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = "/pkgbase/fake/comaintainers" with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -678,7 +685,7 @@ def test_pkgbase_comaintainers_post_not_found(client: TestClient, maintainer: Us cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = "/pkgbase/fake/comaintainers" with client as request: - resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -689,7 +696,7 @@ def test_pkgbase_comaintainers_unauthorized( endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -701,7 +708,7 @@ def test_pkgbase_comaintainers_post_unauthorized( endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -713,9 +720,7 @@ def test_pkgbase_comaintainers_post_invalid_user( endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post( - endpoint, data={"users": "\nfake\n"}, cookies=cookies, allow_redirects=False - ) + resp = request.post(endpoint, data={"users": "\nfake\n"}, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -737,7 +742,6 @@ def test_pkgbase_comaintainers( endpoint, data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -748,7 +752,6 @@ def test_pkgbase_comaintainers( endpoint, data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -757,7 +760,7 @@ def test_pkgbase_comaintainers( # let's perform a GET request to make sure that the backend produces # the user we added in the users textarea. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -766,14 +769,12 @@ def test_pkgbase_comaintainers( # Finish off by removing all the comaintainers. with client as request: - resp = request.post( - endpoint, data={"users": str()}, cookies=cookies, allow_redirects=False - ) + resp = request.post(endpoint, data={"users": str()}, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -856,7 +857,6 @@ def test_pkgbase_request_post_merge_not_found_error( "comments": "We want to merge this.", }, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.OK) @@ -880,7 +880,6 @@ def test_pkgbase_request_post_merge_no_merge_into_error( "comments": "We want to merge this.", }, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.OK) @@ -904,7 +903,6 @@ def test_pkgbase_request_post_merge_self_error( "comments": "We want to merge this.", }, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.OK) @@ -927,26 +925,28 @@ def test_pkgbase_flag( # Get the flag page. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # Now, let's check the /pkgbase/{name}/flag-comment route. flag_comment_endpoint = f"/pkgbase/{pkgbase.Name}/flag-comment" with client as request: - resp = request.get( - flag_comment_endpoint, cookies=cookies, allow_redirects=False - ) + request.cookies = cookies + resp = request.get(flag_comment_endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" # Try to flag it without a comment. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) # Flag it with a valid comment. with client as request: - resp = request.post(endpoint, data={"comments": "Test"}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"comments": "Test"}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger == user assert pkgbase.FlaggerComment == "Test" @@ -957,15 +957,15 @@ def test_pkgbase_flag( # Now, let's check the /pkgbase/{name}/flag-comment route. flag_comment_endpoint = f"/pkgbase/{pkgbase.Name}/flag-comment" with client as request: - resp = request.get( - flag_comment_endpoint, cookies=cookies, allow_redirects=False - ) + request.cookies = cookies + resp = request.get(flag_comment_endpoint) assert resp.status_code == int(HTTPStatus.OK) # Now try to perform a get; we should be redirected because # it's already flagged. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with db.begin(): @@ -982,27 +982,29 @@ def test_pkgbase_flag( user2_cookies = {"AURSID": user2.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/unflag" with client as request: - resp = request.post(endpoint, cookies=user2_cookies) + request.cookies = user2_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger == user # Now, test that the 'maintainer' user can. maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger is None # Flag it again. with client as request: - resp = request.post( - f"/pkgbase/{pkgbase.Name}/flag", data={"comments": "Test"}, cookies=cookies - ) + request.cookies = cookies + resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data={"comments": "Test"}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Now, unflag it for real. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger is None @@ -1113,7 +1115,7 @@ def test_pkgbase_disown_as_maint_with_comaint( maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: resp = request.post( - endp, data=post_data, cookies=maint_cookies, allow_redirects=True + endp, data=post_data, cookies=maint_cookies, follow_redirects=True ) assert resp.status_code == int(HTTPStatus.OK) @@ -1145,52 +1147,62 @@ def test_pkgbase_disown( # GET as a normal user, which is rejected for lack of credentials. with client as request: - resp = request.get(endpoint, cookies=user_cookies, allow_redirects=False) + request.cookies = user_cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # GET as a comaintainer. with client as request: - resp = request.get(endpoint, cookies=comaint_cookies, allow_redirects=False) + request.cookies = comaint_cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # Ensure that the comaintainer can see "Disown Package" link with client as request: - resp = request.get(pkgbase_endp, cookies=comaint_cookies) + request.cookies = comaint_cookies + resp = request.get(pkgbase_endp, follow_redirects=True) assert "Disown Package" in resp.text # GET as the maintainer. with client as request: - resp = request.get(endpoint, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # Ensure that the maintainer can see "Disown Package" link with client as request: - resp = request.get(pkgbase_endp, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.get(pkgbase_endp, follow_redirects=True) assert "Disown Package" in resp.text # POST as a normal user, which is rejected for lack of credentials. with client as request: - resp = request.post(endpoint, cookies=user_cookies) + request.cookies = user_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # POST as the comaintainer without "confirm". with client as request: - resp = request.post(endpoint, cookies=comaint_cookies) + request.cookies = comaint_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) # POST as the maintainer without "confirm". with client as request: - resp = request.post(endpoint, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) # POST as the comaintainer with "confirm". with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=comaint_cookies) + request.cookies = comaint_cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # POST as the maintainer with "confirm". with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -1207,21 +1219,21 @@ def test_pkgbase_adopt( # Adopt the package base. with client as request: - resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + resp = request.post(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == maintainer # Try to adopt it when it already has a maintainer; nothing changes. user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=user_cookies, allow_redirects=False) + resp = request.post(endpoint, cookies=user_cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == maintainer # Steal the package as a TU. tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=tu_cookies, allow_redirects=False) + resp = request.post(endpoint, cookies=tu_cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == tu_user @@ -1233,7 +1245,7 @@ def test_pkgbase_delete_unauthorized(client: TestClient, user: User, package: Pa # Test GET. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + resp = request.get(endpoint, cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -1308,7 +1320,6 @@ def test_packages_post_unknown_action(client: TestClient, user: User, package: P "/packages", data={"action": "unknown"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1325,7 +1336,6 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): "/packages", data={"action": "stub"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1346,7 +1356,6 @@ def test_packages_post(client: TestClient, user: User, package: Package): "/packages", data={"action": "stub"}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.OK) @@ -1521,7 +1530,7 @@ def test_pkgbase_merge_post( def test_pkgbase_keywords(client: TestClient, user: User, package: Package): endpoint = f"/pkgbase/{package.PackageBase.Name}" with client as request: - resp = request.get(endpoint) + resp = request.get(endpoint, follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -1532,13 +1541,16 @@ def test_pkgbase_keywords(client: TestClient, user: User, package: Package): cookies = {"AURSID": maint.login(Request(), "testPassword")} post_endpoint = f"{endpoint}/keywords" with client as request: + request.cookies = cookies resp = request.post( - post_endpoint, data={"keywords": "abc test"}, cookies=cookies + post_endpoint, + data={"keywords": "abc test"}, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: - resp = request.get(resp.headers.get("location")) + request.cookies = {} + resp = request.get(resp.headers.get("location"), follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -1552,7 +1564,8 @@ def test_pkgbase_keywords(client: TestClient, user: User, package: Package): def test_pkgbase_empty_keywords(client: TestClient, user: User, package: Package): endpoint = f"/pkgbase/{package.PackageBase.Name}" with client as request: - resp = request.get(endpoint) + request.cookies = {} + resp = request.get(endpoint, follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -1563,15 +1576,16 @@ def test_pkgbase_empty_keywords(client: TestClient, user: User, package: Package cookies = {"AURSID": maint.login(Request(), "testPassword")} post_endpoint = f"{endpoint}/keywords" with client as request: + request.cookies = cookies resp = request.post( post_endpoint, data={"keywords": "abc test foo bar "}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: - resp = request.get(resp.headers.get("location")) + request.cookies = {} + resp = request.get(resp.headers.get("location"), follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -1608,12 +1622,12 @@ def test_independent_user_unflag(client: TestClient, user: User, package: Packag pkgbase = package.PackageBase cookies = {"AURSID": flagger.login(Request(), "testPassword")} with client as request: + request.cookies = cookies endp = f"/pkgbase/{pkgbase.Name}/flag" response = request.post( endp, data={"comments": "This thing needs a flag!"}, - cookies=cookies, - allow_redirects=True, + follow_redirects=True, ) assert response.status_code == HTTPStatus.OK @@ -1622,7 +1636,8 @@ def test_independent_user_unflag(client: TestClient, user: User, package: Packag # page when browsing as that `flagger` user. with client as request: endp = f"/pkgbase/{pkgbase.Name}" - response = request.get(endp, cookies=cookies, allow_redirects=True) + request.cookies = cookies + response = request.get(endp, follow_redirects=True) assert response.status_code == HTTPStatus.OK # Assert that the "Unflag package" link appears in the DOM. @@ -1633,7 +1648,8 @@ def test_independent_user_unflag(client: TestClient, user: User, package: Packag # Now, unflag the package by "clicking" the "Unflag package" link. with client as request: endp = f"/pkgbase/{pkgbase.Name}/unflag" - response = request.post(endp, cookies=cookies, allow_redirects=True) + request.cookies = cookies + response = request.post(endp, follow_redirects=True) assert response.status_code == HTTPStatus.OK # For the last time, let's check the GET response. The package should @@ -1641,7 +1657,8 @@ def test_independent_user_unflag(client: TestClient, user: User, package: Packag # should be missing. with client as request: endp = f"/pkgbase/{pkgbase.Name}" - response = request.get(endp, cookies=cookies, allow_redirects=True) + request.cookies = cookies + response = request.get(endp, follow_redirects=True) assert response.status_code == HTTPStatus.OK # Assert that the "Unflag package" link does not appear in the DOM. diff --git a/test/test_requests.py b/test/test_requests.py index 6475fae6..1d681d58 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -29,7 +29,11 @@ def setup(db_test) -> None: @pytest.fixture def client() -> TestClient: """Yield a TestClient.""" - yield TestClient(app=asgi.app) + client = TestClient(app=asgi.app) + + # disable redirects for our tests + client.follow_redirects = False + yield client def create_user(username: str, email: str) -> User: @@ -321,7 +325,8 @@ def test_request_post_deletion_autoaccept( endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = ( @@ -642,7 +647,8 @@ def test_request_post_orphan_autoaccept( "comments": "Test request.", } with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -715,7 +721,7 @@ def test_pkgreq_by_id_not_found(): def test_requests_unauthorized(client: TestClient): with client as request: - resp = request.get("/requests", allow_redirects=False) + resp = request.get("/requests") assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -879,9 +885,7 @@ def test_requests_selfmade( def test_requests_close(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get( - f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False - ) + resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies) assert resp.status_code == int(HTTPStatus.OK) @@ -890,9 +894,7 @@ def test_requests_close_unauthorized( ): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.get( - f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False - ) + resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" @@ -906,7 +908,6 @@ def test_requests_close_post_unauthorized( f"/requests/{pkgreq.ID}/close", data={"reason": ACCEPTED_ID}, cookies=cookies, - allow_redirects=False, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" @@ -915,9 +916,7 @@ def test_requests_close_post_unauthorized( def test_requests_close_post(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post( - f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False - ) + resp = request.post(f"/requests/{pkgreq.ID}/close", cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID @@ -930,9 +929,7 @@ def test_requests_close_post_rejected( ): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post( - f"/requests/{pkgreq.ID}/close", cookies=cookies, allow_redirects=False - ) + resp = request.post(f"/requests/{pkgreq.ID}/close", cookies=cookies) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID diff --git a/test/test_routes.py b/test/test_routes.py index 78b0a65b..b4bc30ee 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -20,7 +20,11 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: - yield TestClient(app=app) + client = TestClient(app=app) + + # disable redirects for our tests + client.follow_redirects = False + yield client @pytest.fixture @@ -66,7 +70,7 @@ def test_favicon(client: TestClient): """Test the favicon route at '/favicon.ico'.""" with client as request: response1 = request.get("/static/images/favicon.ico") - response2 = request.get("/favicon.ico") + response2 = request.get("/favicon.ico", follow_redirects=True) assert response1.status_code == int(HTTPStatus.OK) assert response1.content == response2.content diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index 203008e3..dc468808 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -81,7 +81,11 @@ def setup(db_test): def client(): from aurweb.asgi import app - yield TestClient(app=app) + client = TestClient(app=app) + + # disable redirects for our tests + client.follow_redirects = False + yield client @pytest.fixture @@ -151,7 +155,7 @@ def proposal(user, tu_user): def test_tu_index_guest(client): headers = {"referer": config.get("options", "aur_location") + "/tu"} with client as request: - response = request.get("/tu", allow_redirects=False, headers=headers) + response = request.get("/tu", headers=headers) assert response.status_code == int(HTTPStatus.SEE_OTHER) params = filters.urlencode({"next": "/tu"}) @@ -162,7 +166,7 @@ def test_tu_index_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: # Login as a normal user, not a TU. - response = request.get("/tu", cookies=cookies, allow_redirects=False) + response = request.get("/tu", cookies=cookies) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -173,7 +177,7 @@ def test_tu_empty_index(client, tu_user): # Make a default get request to /tu. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + response = request.get("/tu", cookies=cookies) assert response.status_code == int(HTTPStatus.OK) # Parse lxml root. @@ -226,7 +230,6 @@ def test_tu_index(client, tu_user): "/tu", cookies=cookies, params={"cby": "BAD!", "pby": "blah"}, - allow_redirects=False, ) assert response.status_code == int(HTTPStatus.OK) @@ -292,7 +295,7 @@ def test_tu_index(client, tu_user): def test_tu_stats(client: TestClient, tu_user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + response = request.get("/tu", cookies=cookies) assert response.status_code == HTTPStatus.OK root = parse_root(response.text) @@ -313,7 +316,7 @@ def test_tu_stats(client: TestClient, tu_user: User): tu_user.InactivityTS = time.utcnow() with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + response = request.get("/tu", cookies=cookies) assert response.status_code == HTTPStatus.OK root = parse_root(response.text) @@ -361,7 +364,7 @@ def test_tu_index_table_paging(client, tu_user): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + response = request.get("/tu", cookies=cookies) assert response.status_code == int(HTTPStatus.OK) # Parse lxml.etree root. @@ -391,9 +394,7 @@ def test_tu_index_table_paging(client, tu_user): # Now, get the next page of current votes. offset = 10 # Specify coff=10 with client as request: - response = request.get( - "/tu", cookies=cookies, params={"coff": offset}, allow_redirects=False - ) + response = request.get("/tu", cookies=cookies, params={"coff": offset}) assert response.status_code == int(HTTPStatus.OK) old_rows = rows @@ -420,9 +421,7 @@ def test_tu_index_table_paging(client, tu_user): offset = 20 # Specify coff=10 with client as request: - response = request.get( - "/tu", cookies=cookies, params={"coff": offset}, allow_redirects=False - ) + response = request.get("/tu", cookies=cookies, params={"coff": offset}) assert response.status_code == int(HTTPStatus.OK) # Do it again, we only have five left. @@ -471,7 +470,7 @@ def test_tu_index_sorting(client, tu_user): # Make a default request to /tu. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + response = request.get("/tu", cookies=cookies) assert response.status_code == int(HTTPStatus.OK) # Get lxml handles of the document. @@ -498,9 +497,7 @@ def test_tu_index_sorting(client, tu_user): # Make another request; one that sorts the current votes # in ascending order instead of the default descending order. with client as request: - response = request.get( - "/tu", cookies=cookies, params={"cby": "asc"}, allow_redirects=False - ) + response = request.get("/tu", cookies=cookies, params={"cby": "asc"}) assert response.status_code == int(HTTPStatus.OK) # Get lxml handles of the document. @@ -573,7 +570,8 @@ def test_tu_index_last_votes( def test_tu_proposal_not_found(client, tu_user): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", params={"id": 1}, cookies=cookies) + request.cookies = cookies + response = request.get("/tu", params={"id": 1}, follow_redirects=True) assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -583,14 +581,12 @@ def test_tu_proposal_unauthorized( cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/tu/{proposal[2].ID}" with client as request: - response = request.get(endpoint, cookies=cookies, allow_redirects=False) + response = request.get(endpoint, cookies=cookies) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" with client as request: - response = request.post( - endpoint, cookies=cookies, data={"decision": False}, allow_redirects=False - ) + response = request.post(endpoint, cookies=cookies, data={"decision": False}) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" @@ -606,7 +602,9 @@ def test_tu_running_proposal( proposal_id = voteinfo.ID cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get(f"/tu/{proposal_id}", cookies=cookies) + response = request.get( + f"/tu/{proposal_id}", cookies=cookies, follow_redirects=True + ) assert response.status_code == int(HTTPStatus.OK) # Alright, now let's continue on to verifying some markup. @@ -676,7 +674,9 @@ def test_tu_running_proposal( # Make another request now that we've voted. with client as request: - response = request.get("/tu", params={"id": voteinfo.ID}, cookies=cookies) + response = request.get( + "/tu", params={"id": voteinfo.ID}, cookies=cookies, follow_redirects=True + ) assert response.status_code == int(HTTPStatus.OK) # Parse our new root. @@ -734,9 +734,7 @@ def test_tu_proposal_vote_not_found(client, tu_user): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post( - "/tu/1", cookies=cookies, data=data, allow_redirects=False - ) + response = request.post("/tu/1", cookies=cookies, data=data) assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -777,9 +775,7 @@ def test_tu_proposal_vote_unauthorized( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post( - f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False - ) + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) assert response.status_code == int(HTTPStatus.UNAUTHORIZED) root = parse_root(response.text) @@ -788,9 +784,7 @@ def test_tu_proposal_vote_unauthorized( with client as request: data = {"decision": "Yes"} - response = request.get( - f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False - ) + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -808,9 +802,7 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post( - f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False - ) + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -819,9 +811,7 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get( - f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False - ) + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -840,9 +830,7 @@ def test_tu_proposal_vote_already_voted(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post( - f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False - ) + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -851,9 +839,7 @@ def test_tu_proposal_vote_already_voted(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get( - f"/tu/{voteinfo.ID}", cookies=cookies, data=data, allow_redirects=False - ) + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -884,12 +870,12 @@ def test_tu_addvote_unauthorized( ): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", cookies=cookies, allow_redirects=False) + response = request.get("/addvote", cookies=cookies) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" with client as request: - response = request.post("/addvote", cookies=cookies, allow_redirects=False) + response = request.post("/addvote", cookies=cookies) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" From a832b3cddb999f8b31a54b111ae6340c64f07cd0 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 24 Nov 2022 22:43:31 +0100 Subject: [PATCH 1199/1451] fix(test): FastAPI 0.87.0 - warning fixes FastAPI 0.87.0 switched to the httpx library for their TestClient * cookies need to be defined on the request instance instead of method calls Signed-off-by: moson-mo --- test/test_accounts_routes.py | 263 +++++++++++++++++++------------ test/test_auth_routes.py | 9 +- test/test_git_archives.py | 3 +- test/test_homepage.py | 6 +- test/test_html.py | 15 +- test/test_packages_routes.py | 59 +++---- test/test_pkgbase_routes.py | 200 ++++++++++++++--------- test/test_requests.py | 74 ++++++--- test/test_routes.py | 4 +- test/test_trusted_user_routes.py | 111 ++++++++----- 10 files changed, 463 insertions(+), 281 deletions(-) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 44226627..d3ddb174 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -107,7 +107,8 @@ def test_get_passreset_authed_redirects(client: TestClient, user: User): assert sid is not None with client as request: - response = request.get("/passreset", cookies={"AURSID": sid}) + request.cookies = {"AURSID": sid} + response = request.get("/passreset") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -122,7 +123,8 @@ def test_get_passreset(client: TestClient): def test_get_passreset_translation(client: TestClient): # Test that translation works; set it to de. with client as request: - response = request.get("/passreset", cookies={"AURLANG": "de"}) + request.cookies = {"AURLANG": "de"} + response = request.get("/passreset") # The header title should be translated. assert "Passwort zurücksetzen" in response.text @@ -136,7 +138,8 @@ def test_get_passreset_translation(client: TestClient): # Restore english. with client as request: - response = request.get("/passreset", cookies={"AURLANG": "en"}) + request.cookies = {"AURLANG": "en"} + response = request.get("/passreset") def test_get_passreset_with_resetkey(client: TestClient): @@ -150,9 +153,9 @@ def test_post_passreset_authed_redirects(client: TestClient, user: User): assert sid is not None with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/passreset", - cookies={"AURSID": sid}, data={"user": "blah"}, ) @@ -652,7 +655,8 @@ def test_get_account_edit_tu_as_tu(client: TestClient, tu_user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) # Verify that we have an account type selection and that the @@ -677,7 +681,8 @@ def test_get_account_edit_as_tu(client: TestClient, tu_user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) # Verify that we have an account type selection and that the @@ -700,7 +705,8 @@ def test_get_account_edit_type(client: TestClient, user: User): endpoint = f"/account/{user.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) assert "id_type" not in response.text @@ -713,7 +719,8 @@ def test_get_account_edit_type_as_tu(client: TestClient, tu_user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -737,7 +744,8 @@ def test_get_account_edit_unauthorized(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: # Try to edit `test2` while authenticated as `test`. - response = request.get(endpoint, cookies={"AURSID": sid}) + request.cookies = {"AURSID": sid} + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" @@ -751,9 +759,9 @@ def test_post_account_edit(client: TestClient, user: User): post_data = {"U": "test", "E": "test666@example.org", "passwd": "testPassword"} with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -777,7 +785,8 @@ def test_post_account_edit_type_as_tu(client: TestClient, tu_user: User): "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) @@ -795,7 +804,8 @@ def test_post_account_edit_type_as_dev(client: TestClient, tu_user: User): "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) assert user2.AccountTypeID == at.DEVELOPER_ID @@ -814,7 +824,8 @@ def test_post_account_edit_invalid_type_as_tu(client: TestClient, tu_user: User) "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert user2.AccountTypeID == at.USER_ID @@ -840,7 +851,8 @@ def test_post_account_edit_dev(client: TestClient, tu_user: User): endpoint = f"/account/{tu_user.Username}/edit" with client as request: - response = request.post(endpoint, cookies={"AURSID": sid}, data=post_data) + request.cookies = {"AURSID": sid} + response = request.post(endpoint, data=post_data) assert response.status_code == int(HTTPStatus.OK) expected = "The account, test, " @@ -860,9 +872,9 @@ def test_post_account_edit_language(client: TestClient, user: User): } with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -889,9 +901,9 @@ def test_post_account_edit_timezone(client: TestClient, user: User): } with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -905,9 +917,9 @@ def test_post_account_edit_error_missing_password(client: TestClient, user: User post_data = {"U": "test", "E": "test@example.org", "TZ": "CET", "passwd": ""} with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -924,9 +936,9 @@ def test_post_account_edit_error_invalid_password(client: TestClient, user: User post_data = {"U": "test", "E": "test@example.org", "TZ": "CET", "passwd": "invalid"} with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -945,9 +957,8 @@ def test_post_account_edit_suspend_unauthorized(client: TestClient, user: User): "passwd": "testPassword", } with client as request: - resp = request.post( - f"/account/{user.Username}/edit", data=post_data, cookies=cookies - ) + request.cookies = cookies + resp = request.post(f"/account/{user.Username}/edit", data=post_data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -968,9 +979,8 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): "passwd": "testPassword", } with client as request: - resp = request.post( - f"/account/{user.Username}/edit", data=post_data, cookies=cookies - ) + request.cookies = cookies + resp = request.post(f"/account/{user.Username}/edit", data=post_data) assert resp.status_code == int(HTTPStatus.OK) # Make sure the user record got updated correctly. @@ -978,9 +988,8 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): post_data.update({"J": False}) with client as request: - resp = request.post( - f"/account/{user.Username}/edit", data=post_data, cookies=cookies - ) + request.cookies = cookies + resp = request.post(f"/account/{user.Username}/edit", data=post_data) assert resp.status_code == int(HTTPStatus.OK) assert user.InactivityTS == 0 @@ -1000,7 +1009,8 @@ def test_post_account_edit_suspended(client: TestClient, user: User): } endpoint = f"/account/{user.Username}/edit" with client as request: - resp = request.post(endpoint, data=post_data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=post_data) assert resp.status_code == int(HTTPStatus.OK) # Make sure the user record got updated correctly. @@ -1032,7 +1042,8 @@ def test_post_account_edit_error_unauthorized(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: # Attempt to edit 'test2' while logged in as 'test'. - response = request.post(endpoint, cookies={"AURSID": sid}, data=post_data) + request.cookies = {"AURSID": sid} + response = request.post(endpoint, data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" @@ -1051,9 +1062,9 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): } with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -1063,9 +1074,9 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): post_data["PK"] = make_ssh_pubkey() with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -1084,9 +1095,9 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): } with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -1100,9 +1111,9 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): } with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -1120,7 +1131,8 @@ def test_post_account_edit_invalid_ssh_pubkey(client: TestClient, user: User): } cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.post("/account/test/edit", data=data, cookies=cookies) + request.cookies = cookies + response = request.post("/account/test/edit", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1138,9 +1150,9 @@ def test_post_account_edit_password(client: TestClient, user: User): } with client as request: + request.cookies = {"AURSID": sid} response = request.post( "/account/test/edit", - cookies={"AURSID": sid}, data=post_data, ) @@ -1154,7 +1166,8 @@ def test_post_account_edit_self_type_as_user(client: TestClient, user: User): endpoint = f"/account/{user.Username}/edit" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" not in resp.text @@ -1165,7 +1178,8 @@ def test_post_account_edit_self_type_as_user(client: TestClient, user: User): "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1181,7 +1195,8 @@ def test_post_account_edit_other_user_as_user(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/account/{user2.Username}" @@ -1192,7 +1207,8 @@ def test_post_account_edit_self_type_as_tu(client: TestClient, tu_user: User): # We cannot see the Account Type field on our own edit page. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" in resp.text @@ -1204,7 +1220,8 @@ def test_post_account_edit_self_type_as_tu(client: TestClient, tu_user: User): "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) assert tu_user.AccountTypeID == USER_ID @@ -1223,7 +1240,8 @@ def test_post_account_edit_other_user_type_as_tu( # As a TU, we can see the Account Type field for other users. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" in resp.text @@ -1234,8 +1252,10 @@ def test_post_account_edit_other_user_type_as_tu( "T": TRUSTED_USER_ID, "passwd": "testPassword", } + with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) # Let's make sure the DB got updated properly. @@ -1267,14 +1287,16 @@ def test_post_account_edit_other_user_suspend_as_tu(client: TestClient, tu_user: user_cookies = {"AURSID": sid} with client as request: endpoint = f"/account/{user.Username}/edit" - resp = request.get(endpoint, cookies=user_cookies) + request.cookies = user_cookies + resp = request.get(endpoint) assert resp.status_code == HTTPStatus.OK cookies = {"AURSID": tu_user.login(Request(), "testPassword")} assert cookies is not None # This is useless, we create the dict here ^ # As a TU, we can see the Account for other users. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # As a TU, we can modify other user's account types. data = { @@ -1290,7 +1312,8 @@ def test_post_account_edit_other_user_suspend_as_tu(client: TestClient, tu_user: # Test that `user` no longer has a session. with user_client as request: - resp = request.get(endpoint, cookies=user_cookies) + request.cookies = user_cookies + resp = request.get(endpoint) assert resp.status_code == HTTPStatus.SEE_OTHER # Since user is now suspended, they should not be able to login. @@ -1314,7 +1337,8 @@ def test_post_account_edit_other_user_type_as_tu_invalid_type( # As a TU, we can modify other user's account types. data = {"U": user2.Username, "E": user2.Email, "T": 0, "passwd": "testPassword"} with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1327,7 +1351,8 @@ def test_get_account(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get("/account/test", cookies={"AURSID": sid}) + request.cookies = {"AURSID": sid} + response = request.get("/account/test") assert response.status_code == int(HTTPStatus.OK) @@ -1337,7 +1362,8 @@ def test_get_account_not_found(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get("/account/not_found", cookies={"AURSID": sid}) + request.cookies = {"AURSID": sid} + response = request.get("/account/not_found") assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -1358,7 +1384,8 @@ def test_get_accounts(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.get("/accounts", cookies=cookies) + request.cookies = cookies + response = request.get("/accounts") assert response.status_code == int(HTTPStatus.OK) parser = lxml.etree.HTMLParser() @@ -1426,7 +1453,8 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1468,7 +1496,8 @@ def test_post_accounts_username(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, data={"U": user.Username}) + request.cookies = cookies + response = request.post("/accounts", data={"U": user.Username}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1501,13 +1530,15 @@ def test_post_accounts_account_type(client: TestClient, user: User, tu_user: Use # Expect no entries; we marked our only user as a User type. with client as request: - response = request.post("/accounts", cookies=cookies, data={"T": "t"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "t"}) assert response.status_code == int(HTTPStatus.OK) assert len(get_rows(response.text)) == 0 # So, let's also ensure that specifying "u" returns our user. with client as request: - response = request.post("/accounts", cookies=cookies, data={"T": "u"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "u"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1525,7 +1556,8 @@ def test_post_accounts_account_type(client: TestClient, user: User, tu_user: Use ) with client as request: - response = request.post("/accounts", cookies=cookies, data={"T": "t"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "t"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1542,7 +1574,8 @@ def test_post_accounts_account_type(client: TestClient, user: User, tu_user: Use ) with client as request: - response = request.post("/accounts", cookies=cookies, data={"T": "d"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "d"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1559,7 +1592,8 @@ def test_post_accounts_account_type(client: TestClient, user: User, tu_user: Use ) with client as request: - response = request.post("/accounts", cookies=cookies, data={"T": "td"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "td"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1577,7 +1611,8 @@ def test_post_accounts_status(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1591,7 +1626,8 @@ def test_post_accounts_status(client: TestClient, user: User, tu_user: User): user.Suspended = True with client as request: - response = request.post("/accounts", cookies=cookies, data={"S": True}) + request.cookies = cookies + response = request.post("/accounts", data={"S": True}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1608,7 +1644,8 @@ def test_post_accounts_email(client: TestClient, user: User, tu_user: User): # Search via email. with client as request: - response = request.post("/accounts", cookies=cookies, data={"E": user.Email}) + request.cookies = cookies + response = request.post("/accounts", data={"E": user.Email}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1621,7 +1658,8 @@ def test_post_accounts_realname(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, data={"R": user.RealName}) + request.cookies = cookies + response = request.post("/accounts", data={"R": user.RealName}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1634,7 +1672,8 @@ def test_post_accounts_irc(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, data={"I": user.IRCNick}) + request.cookies = cookies + response = request.post("/accounts", data={"I": user.IRCNick}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1652,14 +1691,16 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Show that "u" is the default search order, by username. with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 first_rows = rows with client as request: - response = request.post("/accounts", cookies=cookies, data={"SB": "u"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "u"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1671,7 +1712,8 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): assert compare_text_values(0, first_rows, rows) is True with client as request: - response = request.post("/accounts", cookies=cookies, data={"SB": "i"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "i"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1681,7 +1723,8 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Sort by "i" -> RealName. with client as request: - response = request.post("/accounts", cookies=cookies, data={"SB": "r"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "r"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1696,7 +1739,8 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Fetch first_rows again with our new AccountType ordering. with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1704,7 +1748,8 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Sort by "t" -> AccountType. with client as request: - response = request.post("/accounts", cookies=cookies, data={"SB": "t"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "t"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1722,7 +1767,8 @@ def test_post_accounts_pgp_key(client: TestClient, user: User, tu_user: User): # Search via PGPKey. with client as request: - response = request.post("/accounts", cookies=cookies, data={"K": user.PGPKey}) + request.cookies = cookies + response = request.post("/accounts", data={"K": user.PGPKey}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1749,7 +1795,8 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1775,9 +1822,8 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): assert "disabled" not in page_next.attrib with client as request: - response = request.post( - "/accounts", cookies=cookies, data={"O": 50} - ) # +50 offset. + request.cookies = cookies + response = request.post("/accounts", data={"O": 50}) # +50 offset. assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1791,9 +1837,8 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): assert username.text.strip() == _user.Username with client as request: - response = request.post( - "/accounts", cookies=cookies, data={"O": 101} - ) # Last page. + request.cookies = cookies + response = request.post("/accounts", data={"O": 101}) # Last page. assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1824,12 +1869,14 @@ def test_get_terms_of_service(client: TestClient, user: User): # First of all, let's test that we get redirected to /tos # when attempting to browse authenticated without accepting terms. with client as request: - response = request.get("/", cookies=cookies) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tos" with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.OK) with db.begin(): @@ -1838,7 +1885,8 @@ def test_get_terms_of_service(client: TestClient, user: User): ) with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") # We accepted the term, there's nothing left to accept. assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -1847,7 +1895,8 @@ def test_get_terms_of_service(client: TestClient, user: User): term.Revision = 2 with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") # This time, we have a modified term Revision that hasn't # yet been agreed to via AcceptedTerm update. assert response.status_code == int(HTTPStatus.OK) @@ -1856,7 +1905,8 @@ def test_get_terms_of_service(client: TestClient, user: User): accepted_term.Revision = term.Revision with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") # We updated the term revision, there's nothing left to accept. assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -1876,17 +1926,20 @@ def test_post_terms_of_service(client: TestClient, user: User): # Test that the term we just created is listed. with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.OK) # Make a POST request to /tos with the agree checkbox disabled (False). with client as request: - response = request.post("/tos", data={"accept": False}, cookies=cookies) + request.cookies = cookies + response = request.post("/tos", data={"accept": False}) assert response.status_code == int(HTTPStatus.OK) # Make a POST request to /tos with the agree checkbox enabled (True). with client as request: - response = request.post("/tos", data=data, cookies=cookies) + request.cookies = cookies + response = request.post("/tos", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) # Query the db for the record created by the post request. @@ -1900,12 +1953,14 @@ def test_post_terms_of_service(client: TestClient, user: User): # A GET request gives us the new revision to accept. with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.OK) # Let's POST again and agree to the new term revision. with client as request: - response = request.post("/tos", data=data, cookies=cookies) + request.cookies = cookies + response = request.post("/tos", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) # Check that the records ended up matching. @@ -1913,7 +1968,8 @@ def test_post_terms_of_service(client: TestClient, user: User): # Now, see that GET redirects us to / with no terms left to accept. with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -1921,14 +1977,16 @@ def test_post_terms_of_service(client: TestClient, user: User): def test_account_comments_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/account/non-existent/comments", cookies=cookies) + request.cookies = cookies + resp = request.get("/account/non-existent/comments") assert resp.status_code == int(HTTPStatus.NOT_FOUND) def test_accounts_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/accounts", cookies=cookies) + request.cookies = cookies + resp = request.get("/accounts") assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" @@ -1941,16 +1999,18 @@ def test_account_delete_self_unauthorized(client: TestClient, tu_user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/delete" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == HTTPStatus.UNAUTHORIZED - resp = request.post(endpoint, cookies=cookies) + resp = request.post(endpoint) assert resp.status_code == HTTPStatus.UNAUTHORIZED # But a TU does have access cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with TestClient(app=app) as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == HTTPStatus.OK @@ -1958,10 +2018,11 @@ def test_account_delete_self_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = "/account/non-existent-user/delete" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == HTTPStatus.NOT_FOUND - resp = request.post(endpoint, cookies=cookies) + resp = request.post(endpoint) assert resp.status_code == HTTPStatus.NOT_FOUND @@ -1972,15 +2033,16 @@ def test_account_delete_self(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{username}/delete" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == HTTPStatus.OK # The checkbox must be checked with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"passwd": "fakePassword", "confirm": False}, - cookies=cookies, ) assert resp.status_code == HTTPStatus.BAD_REQUEST errors = get_errors(resp.text) @@ -1991,10 +2053,10 @@ def test_account_delete_self(client: TestClient, user: User): # The correct password must be supplied with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"passwd": "fakePassword", "confirm": True}, - cookies=cookies, ) assert resp.status_code == HTTPStatus.BAD_REQUEST errors = get_errors(resp.text) @@ -2002,10 +2064,10 @@ def test_account_delete_self(client: TestClient, user: User): # Supply everything correctly and delete ourselves with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"passwd": "testPassword", "confirm": True}, - cookies=cookies, ) assert resp.status_code == HTTPStatus.SEE_OTHER @@ -2026,15 +2088,16 @@ def test_account_delete_self_with_ssh_public_key(client: TestClient, user: User) cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{username}/delete" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == HTTPStatus.OK # Supply everything correctly and delete ourselves with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"passwd": "testPassword", "confirm": True}, - cookies=cookies, ) assert resp.status_code == HTTPStatus.SEE_OTHER @@ -2055,10 +2118,10 @@ def test_account_delete_as_tu(client: TestClient, tu_user: User): # Delete the user with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"passwd": "testPassword", "confirm": True}, - cookies=cookies, ) assert resp.status_code == HTTPStatus.SEE_OTHER diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 150625cd..066457c4 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -71,10 +71,10 @@ def test_login_logout(client: TestClient, user: User): response = request.post("/logout", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) + request.cookies = {"AURSID": response.cookies.get("AURSID")} response = request.post( "/logout", data=post_data, - cookies={"AURSID": response.cookies.get("AURSID")}, ) assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -196,7 +196,9 @@ def test_authenticated_login(client: TestClient, user: User): # when requesting GET /login as an authenticated user. # Now, let's verify that we receive 403 Forbidden when we # try to get /login as an authenticated user. - response = request.get("/login", cookies=response.cookies) + request.cookies = response.cookies + response = request.get("/login") + assert response.status_code == int(HTTPStatus.OK) assert "Logged-in as: test" in response.text @@ -356,7 +358,8 @@ def test_generate_unique_sid_exhausted( with mock.patch(generate_unique_sid_, mock_generate_sid): with client as request: # Set cookies = {} to remove any previous login kept by TestClient. - response = request.post("/login", data=post_data, cookies={}) + request.cookies = {} + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.INTERNAL_SERVER_ERROR) assert "500 - Internal Server Error" in response.text diff --git a/test/test_git_archives.py b/test/test_git_archives.py index 8ee4c2ba..c90706a4 100644 --- a/test/test_git_archives.py +++ b/test/test_git_archives.py @@ -197,7 +197,8 @@ def test_metadata_change( with client as request: endp = f"/pkgbase/{pkgbasename}/keywords" post_data = {"keywords": "abc def"} - resp = request.post(endp, data=post_data, cookies=cookies, allow_redirects=True) + request.cookies = cookies + resp = request.post(endp, data=post_data) assert resp.status_code == HTTPStatus.OK # Run main() again, which should now produce a new commit with the diff --git a/test/test_homepage.py b/test/test_homepage.py index 1aad30f7..a573bdd6 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -210,7 +210,8 @@ def test_homepage_dashboard(redis, packages, user): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/", cookies=cookies) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -307,7 +308,8 @@ def test_homepage_dashboard_flagged(user: User, user2: User, package: Package): # flagged maintained packages. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=cookies) + request.cookies = cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) diff --git a/test/test_html.py b/test/test_html.py index 88c75a7c..681bd245 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -71,7 +71,8 @@ def test_archdev_navbar_authenticated(client: TestClient, user: User): expected = ["Dashboard", "Packages", "Requests", "My Account", "Logout"] cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=cookies) + request.cookies = cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -92,7 +93,8 @@ def test_archdev_navbar_authenticated_tu(client: TestClient, trusted_user: User) ] cookies = {"AURSID": trusted_user.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=cookies) + request.cookies = cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -173,9 +175,12 @@ def test_rtl(client: TestClient): expected = [[], [], ["rtl"], ["rtl"]] with client as request: responses["default"] = request.get("/") - responses["de"] = request.get("/", cookies={"AURLANG": "de"}) - responses["he"] = request.get("/", cookies={"AURLANG": "he"}) - responses["ar"] = request.get("/", cookies={"AURLANG": "ar"}) + request.cookies = {"AURLANG": "de"} + responses["de"] = request.get("/") + request.cookies = {"AURLANG": "he"} + responses["he"] = request.get("/") + request.cookies = {"AURLANG": "ar"} + responses["ar"] = request.get("/") for i, (lang, resp) in enumerate(responses.items()): assert resp.status_code == int(HTTPStatus.OK) t = parse_root(resp.text) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 29872cb8..0da6cfab 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -410,7 +410,8 @@ def test_package_comments(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -465,7 +466,8 @@ def test_package_authenticated(client: TestClient, user: User, package: Package) This process also occurs when pkgbase.html is rendered.""" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) expected = [ @@ -493,7 +495,8 @@ def test_package_authenticated_maintainer( ): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) expected = [ @@ -515,7 +518,8 @@ def test_package_authenticated_maintainer( def test_package_authenticated_tu(client: TestClient, tu_user: User, package: Package): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) expected = [ @@ -941,10 +945,10 @@ def test_packages_sort_by_voted( # Test that, by default, the first result is what we just set above. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: + request.cookies = cookies response = request.get( "/packages", params={"SB": "w", "SO": "d"}, # Voted # Descending, Voted first. - cookies=cookies, ) assert response.status_code == int(HTTPStatus.OK) @@ -966,10 +970,10 @@ def test_packages_sort_by_notify( # Test that, by default, the first result is what we just set above. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: + request.cookies = cookies response = request.get( "/packages", params={"SB": "o", "SO": "d"}, # Voted # Descending, Voted first. - cookies=cookies, ) assert response.status_code == int(HTTPStatus.OK) @@ -1142,10 +1146,10 @@ def test_packages_post_unknown_action(client: TestClient, user: User, package: P cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "unknown"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1158,10 +1162,10 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "stub"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1178,10 +1182,10 @@ def test_packages_post(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "stub"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -1250,7 +1254,8 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # an error to be rendered. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "notify"}, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "notify"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to be notified about." @@ -1258,9 +1263,8 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # Now let's actually enable notifications on `package`. with client as request: - resp = request.post( - "/packages", data={"action": "notify", "IDs": [package.ID]}, cookies=cookies - ) + request.cookies = cookies + resp = request.post("/packages", data={"action": "notify", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.OK) expected = "The selected packages' notifications have been enabled." successes = get_successes(resp.text) @@ -1269,9 +1273,8 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # Try to enable notifications when they're already enabled, # causing an error to be rendered. with client as request: - resp = request.post( - "/packages", data={"action": "notify", "IDs": [package.ID]}, cookies=cookies - ) + request.cookies = cookies + resp = request.post("/packages", data={"action": "notify", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to be notified about." @@ -1289,7 +1292,8 @@ def test_packages_post_unnotify(client: TestClient, user: User, package: Package # Request removal of the notification without any IDs. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "unnotify"}, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "unnotify"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages for notification removal." @@ -1297,10 +1301,10 @@ def test_packages_post_unnotify(client: TestClient, user: User, package: Package # Request removal of the notification; really. with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "unnotify", "IDs": [package.ID]}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) @@ -1315,10 +1319,10 @@ def test_packages_post_unnotify(client: TestClient, user: User, package: Package # Try it again. The notif no longer exists. with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "unnotify", "IDs": [package.ID]}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1331,7 +1335,8 @@ def test_packages_post_adopt(client: TestClient, user: User, package: Package): # Try to adopt an empty list of packages. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "adopt"}, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "adopt"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to adopt." @@ -1339,10 +1344,10 @@ def test_packages_post_adopt(client: TestClient, user: User, package: Package): # Now, let's try to adopt a package that's already maintained. with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "adopt", "IDs": [package.ID], "confirm": True}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1356,9 +1361,8 @@ def test_packages_post_adopt(client: TestClient, user: User, package: Package): # Now, let's try to adopt without confirming. with client as request: - resp = request.post( - "/packages", data={"action": "adopt", "IDs": [package.ID]}, cookies=cookies - ) + request.cookies = cookies + resp = request.post("/packages", data={"action": "adopt", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = ( @@ -1369,10 +1373,10 @@ def test_packages_post_adopt(client: TestClient, user: User, package: Package): # Let's do it again now that there is no maintainer. with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "adopt", "IDs": [package.ID], "confirm": True}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) @@ -1446,10 +1450,10 @@ def test_packages_post_disown( """Disown packages as a Trusted User, which cannot bypass idle time.""" cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "disown", "IDs": [package.ID], "confirm": True}, - cookies=cookies, ) errors = get_errors(resp.text) @@ -1576,7 +1580,8 @@ def test_account_comments(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{user.Username}/comments" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index dd92d72d..124eb71f 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -312,7 +312,8 @@ def test_pkgbase_voters(client: TestClient, tu_user: User, package: Package): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # We should've gotten one link to the voter, tu_user. @@ -343,7 +344,8 @@ def test_pkgbase_comment_not_found( comment_id = 12345 # A non-existing comment. endpoint = f"/pkgbase/{package.PackageBase.Name}/comments/{comment_id}" with client as request: - resp = request.post(endpoint, data={"comment": "Failure"}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"comment": "Failure"}) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -365,7 +367,8 @@ def test_pkgbase_comment_form_unauthorized( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/form" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -377,7 +380,8 @@ def test_pkgbase_comment_form_not_found( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/form" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -387,7 +391,8 @@ def test_pkgbase_comments_missing_comment( cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/comments" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -409,10 +414,10 @@ def test_pkgbase_comments( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments" with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"comment": "Test comment.", "enable_notifications": True}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -440,7 +445,8 @@ def test_pkgbase_comments( # Test the non-javascript version of comment editing by # visiting the /pkgbase/{name}/comments/{id}/edit route. with client as request: - resp = request.get(f"{endpoint}/{comment_id}/edit", cookies=cookies) + request.cookies = cookies + resp = request.get(f"{endpoint}/{comment_id}/edit") assert resp.status_code == int(HTTPStatus.OK) # Clear up the PackageNotification. This doubles as testing @@ -457,10 +463,10 @@ def test_pkgbase_comments( comment_id = int(headers[0].attrib["id"].split("-")[-1]) endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}" with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"comment": "Edited comment.", "enable_notifications": True}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -485,14 +491,16 @@ def test_pkgbase_comments( # Don't supply a comment; should return BAD_REQUEST. with client as request: - fail_resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + fail_resp = request.post(endpoint) assert fail_resp.status_code == int(HTTPStatus.BAD_REQUEST) # Now, test the form route, which should return form markup # via JSON. endpoint = f"{endpoint}/form" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) data = resp.json() @@ -510,11 +518,11 @@ def test_pkgbase_comment_edit_unauthorized( cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: + request.cookies = cookies endp = f"/pkgbase/{pkgbase.Name}/comments/{comment.ID}" response = request.post( endp, data={"comment": "abcd im trying to change this comment."}, - cookies=cookies, ) assert response.status_code == HTTPStatus.UNAUTHORIZED @@ -561,7 +569,8 @@ def test_pkgbase_comment_delete_unauthorized( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/delete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -573,7 +582,8 @@ def test_pkgbase_comment_delete_not_found( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/delete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -585,7 +595,8 @@ def test_pkgbase_comment_undelete_not_found( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/undelete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -607,7 +618,8 @@ def test_pkgbase_comment_pin_as_co( endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/pin" cookies = {"AURSID": comaint.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Assert that PinnedTS got set. @@ -616,7 +628,8 @@ def test_pkgbase_comment_pin_as_co( # Unpin the comment we just pinned. endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/unpin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Let's assert that PinnedTS was unset. @@ -633,7 +646,8 @@ def test_pkgbase_comment_pin( # Pin the comment. endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Assert that PinnedTS got set. @@ -642,7 +656,8 @@ def test_pkgbase_comment_pin( # Unpin the comment we just pinned. endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/unpin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Let's assert that PinnedTS was unset. @@ -657,7 +672,8 @@ def test_pkgbase_comment_pin_unauthorized( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -669,7 +685,8 @@ def test_pkgbase_comment_unpin_unauthorized( pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/unpin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -677,7 +694,8 @@ def test_pkgbase_comaintainers_not_found(client: TestClient, maintainer: User): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = "/pkgbase/fake/comaintainers" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -685,7 +703,8 @@ def test_pkgbase_comaintainers_post_not_found(client: TestClient, maintainer: Us cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = "/pkgbase/fake/comaintainers" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -696,7 +715,8 @@ def test_pkgbase_comaintainers_unauthorized( endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -708,7 +728,8 @@ def test_pkgbase_comaintainers_post_unauthorized( endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -720,7 +741,8 @@ def test_pkgbase_comaintainers_post_invalid_user( endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={"users": "\nfake\n"}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"users": "\nfake\n"}) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -738,20 +760,20 @@ def test_pkgbase_comaintainers( # Start off by adding user as a comaintainer to package. # The maintainer username given should be ignored. with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" # Do it again to exercise the last_priority bump path. with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -760,7 +782,8 @@ def test_pkgbase_comaintainers( # let's perform a GET request to make sure that the backend produces # the user we added in the users textarea. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -769,12 +792,14 @@ def test_pkgbase_comaintainers( # Finish off by removing all the comaintainers. with client as request: - resp = request.post(endpoint, data={"users": str()}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"users": str()}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -788,7 +813,8 @@ def test_pkgbase_request_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -798,16 +824,16 @@ def test_pkgbase_request(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) def test_pkgbase_request_post_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post( - "/pkgbase/fake/request", data={"type": "fake"}, cookies=cookies - ) + request.cookies = cookies + resp = request.post("/pkgbase/fake/request", data={"type": "fake"}) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -817,7 +843,8 @@ def test_pkgbase_request_post_invalid_type( endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={"type": "fake"}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"type": "fake"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -827,13 +854,13 @@ def test_pkgbase_request_post_no_comment_error( endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( endpoint, data={ "type": "deletion", "comments": "", # An empty comment field causes an error. }, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -849,6 +876,7 @@ def test_pkgbase_request_post_merge_not_found_error( endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( endpoint, data={ @@ -856,7 +884,6 @@ def test_pkgbase_request_post_merge_not_found_error( "merge_into": "fake", # There is no PackageBase.Name "fake" "comments": "We want to merge this.", }, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -872,6 +899,7 @@ def test_pkgbase_request_post_merge_no_merge_into_error( endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( endpoint, data={ @@ -879,7 +907,6 @@ def test_pkgbase_request_post_merge_no_merge_into_error( "merge_into": "", # There is no PackageBase.Name "fake" "comments": "We want to merge this.", }, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -895,6 +922,7 @@ def test_pkgbase_request_post_merge_self_error( endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( endpoint, data={ @@ -902,7 +930,6 @@ def test_pkgbase_request_post_merge_self_error( "merge_into": package.PackageBase.Name, "comments": "We want to merge this.", }, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -1017,7 +1044,8 @@ def test_pkgbase_flag_vcs(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag", cookies=cookies) + request.cookies = cookies + resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag") assert resp.status_code == int(HTTPStatus.OK) expected = ( @@ -1042,7 +1070,8 @@ def test_pkgbase_notify(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/notify" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() @@ -1051,7 +1080,8 @@ def test_pkgbase_notify(client: TestClient, user: User, package: Package): # Disable notifications. endpoint = f"/pkgbase/{pkgbase.Name}/unnotify" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() @@ -1069,7 +1099,8 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/vote" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() @@ -1079,7 +1110,8 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): # Remove vote. endpoint = f"/pkgbase/{pkgbase.Name}/unvote" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() @@ -1096,7 +1128,8 @@ def test_pkgbase_disown_as_sole_maintainer( # But we do here. with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -1114,9 +1147,8 @@ def test_pkgbase_disown_as_maint_with_comaint( maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post( - endp, data=post_data, cookies=maint_cookies, follow_redirects=True - ) + request.cookies = maint_cookies + resp = request.post(endp, data=post_data, follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) package = db.refresh(package) @@ -1219,21 +1251,24 @@ def test_pkgbase_adopt( # Adopt the package base. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == maintainer # Try to adopt it when it already has a maintainer; nothing changes. user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=user_cookies) + request.cookies = user_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == maintainer # Steal the package as a TU. tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=tu_cookies) + request.cookies = tu_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == tu_user @@ -1245,13 +1280,15 @@ def test_pkgbase_delete_unauthorized(client: TestClient, user: User, package: Pa # Test GET. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" # Test POST. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -1263,17 +1300,20 @@ def test_pkgbase_delete(client: TestClient, tu_user: User, package: Package): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/delete" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # Test that POST works and denies us because we haven't confirmed. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) # Test that we can actually delete the pkgbase. with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Let's assert that the package base record got removed. @@ -1300,7 +1340,8 @@ def test_pkgbase_delete_with_request( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/delete" with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/packages" @@ -1316,10 +1357,10 @@ def test_pkgbase_delete_with_request( def test_packages_post_unknown_action(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "unknown"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1332,10 +1373,10 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "stub"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1352,10 +1393,10 @@ def test_packages_post(client: TestClient, user: User, package: Package): with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( "/packages", data={"action": "stub"}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -1368,7 +1409,8 @@ def test_pkgbase_merge_unauthorized(client: TestClient, user: User, package: Pac cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -1376,7 +1418,8 @@ def test_pkgbase_merge(client: TestClient, tu_user: User, package: Package): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert not get_errors(resp.text) @@ -1387,7 +1430,8 @@ def test_pkgbase_merge_post_unauthorized( cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -1397,7 +1441,8 @@ def test_pkgbase_merge_post_unconfirmed( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = ( @@ -1413,9 +1458,8 @@ def test_pkgbase_merge_post_invalid_into( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post( - endpoint, data={"into": "not_real", "confirm": True}, cookies=cookies - ) + request.cookies = cookies + resp = request.post(endpoint, data={"into": "not_real", "confirm": True}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "Cannot find package to merge votes and comments into." @@ -1428,10 +1472,10 @@ def test_pkgbase_merge_post_self_invalid( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: + request.cookies = cookies resp = request.post( endpoint, data={"into": package.PackageBase.Name, "confirm": True}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1461,20 +1505,24 @@ def test_pkgbase_merge_post( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/vote" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Enable notifications. endpoint = f"/pkgbase/{package.PackageBase.Name}/notify" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Comment on the package. endpoint = f"/pkgbase/{package.PackageBase.Name}/comments" with client as request: + request.cookies = cookies resp = request.post( - endpoint, data={"comment": "Test comment."}, cookies=cookies + endpoint, + data={"comment": "Test comment."}, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) @@ -1486,9 +1534,8 @@ def test_pkgbase_merge_post( # Merge the package into target. endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post( - endpoint, data={"into": target.Name, "confirm": True}, cookies=cookies - ) + request.cookies = cookies + resp = request.post(endpoint, data={"into": target.Name, "confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) loc = resp.headers.get("location") assert loc == f"/pkgbase/{target.Name}" @@ -1604,9 +1651,10 @@ def test_unauthorized_pkgbase_keywords(client: TestClient, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies pkgbase = package.PackageBase endp = f"/pkgbase/{pkgbase.Name}/keywords" - response = request.post(endp, cookies=cookies) + response = request.post(endp) assert response.status_code == HTTPStatus.UNAUTHORIZED diff --git a/test/test_requests.py b/test/test_requests.py index 1d681d58..18b860f2 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -254,7 +254,8 @@ def test_request(client: TestClient, auser: User, pkgbase: PackageBase): """Test the standard pkgbase request route GET method.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" with client as request: - resp = request.get(endpoint, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) @@ -263,7 +264,8 @@ def test_request_post_deletion(client: TestClient, auser2: User, pkgbase: Packag endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: - resp = request.post(endpoint, data=data, cookies=auser2.cookies) + request.cookies = auser2.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -285,7 +287,8 @@ def test_request_post_deletion_as_maintainer( endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Check the pkgreq record got created and accepted. @@ -368,7 +371,8 @@ def test_request_post_merge( "comments": "Test request.", } with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -392,7 +396,8 @@ def test_request_post_orphan(client: TestClient, auser: User, pkgbase: PackageBa "comments": "Test request.", } with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -428,7 +433,8 @@ def test_deletion_request( comments = "Test closure." data = {"comments": comments, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = tu_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/packages" @@ -460,7 +466,8 @@ def test_deletion_autorequest(client: TestClient, tu_user: User, pkgbase: Packag endpoint = f"/pkgbase/{pkgbase.Name}/delete" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = tu_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/packages" @@ -498,7 +505,8 @@ def test_merge_request( comments = "Test merge closure." data = {"into": target.Name, "comments": comments, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = tu_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{target.Name}" @@ -545,7 +553,8 @@ def test_merge_autorequest( endpoint = f"/pkgbase/{pkgbase.Name}/merge" data = {"into": target.Name, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = tu_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{target.Name}" @@ -582,7 +591,8 @@ def test_orphan_request( comments = "Test orphan closure." data = {"comments": comments, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = tu_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -615,7 +625,8 @@ def test_request_post_orphan_autogenerated_closure( endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = tu_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -677,7 +688,8 @@ def test_orphan_as_maintainer(client: TestClient, auser: User, pkgbase: PackageB endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -694,7 +706,8 @@ def test_orphan_without_requests( endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = tu_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -733,6 +746,7 @@ def test_requests( ): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.get( "/requests", params={ @@ -742,7 +756,6 @@ def test_requests( "SeB": "nd", "SB": "n", }, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -756,7 +769,8 @@ def test_requests( # Request page 2 of the requests page. with client as request: - resp = request.get("/requests", params={"O": 50}, cookies=cookies) # Page 2 + request.cookies = cookies + resp = request.get("/requests", params={"O": 50}) # Page 2 assert resp.status_code == int(HTTPStatus.OK) assert "‹ Previous" in resp.text @@ -775,6 +789,7 @@ def test_requests_with_filters( ): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.get( "/requests", params={ @@ -789,7 +804,6 @@ def test_requests_with_filters( "filter_rejected": True, "filter_maintainer_requests": False, }, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -803,6 +817,7 @@ def test_requests_with_filters( # Request page 2 of the requests page. with client as request: + request.cookies = cookies resp = request.get( "/requests", params={ @@ -813,7 +828,6 @@ def test_requests_with_filters( "filter_rejected": True, "filter_maintainer_requests": False, }, - cookies=cookies, ) # Page 2 assert resp.status_code == int(HTTPStatus.OK) @@ -833,10 +847,10 @@ def test_requests_for_maintainer_requests( ): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.get( "/requests", params={"filter_maintainer_requests": True}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.OK) @@ -854,7 +868,8 @@ def test_requests_by_deleted_users( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - resp = request.get("/requests", cookies=cookies) + request.cookies = cookies + resp = request.get("/requests") assert resp.status_code == HTTPStatus.OK root = parse_root(resp.text) @@ -867,7 +882,8 @@ def test_requests_selfmade( ): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/requests", cookies=cookies) + request.cookies = cookies + resp = request.get("/requests") assert resp.status_code == int(HTTPStatus.OK) # As the user who creates all of the requests, we should see all of them. @@ -885,7 +901,8 @@ def test_requests_selfmade( def test_requests_close(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies) + request.cookies = cookies + resp = request.get(f"/requests/{pkgreq.ID}/close") assert resp.status_code == int(HTTPStatus.OK) @@ -894,7 +911,10 @@ def test_requests_close_unauthorized( ): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies) + request.cookies = cookies + resp = request.get( + f"/requests/{pkgreq.ID}/close", + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" @@ -904,10 +924,10 @@ def test_requests_close_post_unauthorized( ): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.post( f"/requests/{pkgreq.ID}/close", data={"reason": ACCEPTED_ID}, - cookies=cookies, ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" @@ -916,7 +936,8 @@ def test_requests_close_post_unauthorized( def test_requests_close_post(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", cookies=cookies) + request.cookies = cookies + resp = request.post(f"/requests/{pkgreq.ID}/close") assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID @@ -929,7 +950,10 @@ def test_requests_close_post_rejected( ): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", cookies=cookies) + request.cookies = cookies + resp = request.post( + f"/requests/{pkgreq.ID}/close", + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID diff --git a/test/test_routes.py b/test/test_routes.py index b4bc30ee..c104211e 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -99,7 +99,8 @@ def test_user_language(client: TestClient, user: User): assert sid is not None with client as req: - response = req.post("/language", data=post_data, cookies={"AURSID": sid}) + req.cookies = {"AURSID": sid} + response = req.post("/language", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert user.LangPreference == "de" @@ -154,6 +155,5 @@ def test_id_redirect(client: TestClient): "key": "value", # Test that this param persists. "key2": "value2", # And this one. }, - allow_redirects=False, ) assert response.headers.get("location") == "/test?key=value&key2=value2" diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py index dc468808..0bb9523e 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_trusted_user_routes.py @@ -166,7 +166,8 @@ def test_tu_index_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: # Login as a normal user, not a TU. - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/tu") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -177,7 +178,8 @@ def test_tu_empty_index(client, tu_user): # Make a default get request to /tu. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/tu") assert response.status_code == int(HTTPStatus.OK) # Parse lxml root. @@ -226,9 +228,9 @@ def test_tu_index(client, tu_user): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: # Pass an invalid cby and pby; let them default to "desc". + request.cookies = cookies response = request.get( "/tu", - cookies=cookies, params={"cby": "BAD!", "pby": "blah"}, ) @@ -295,7 +297,8 @@ def test_tu_index(client, tu_user): def test_tu_stats(client: TestClient, tu_user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/tu") assert response.status_code == HTTPStatus.OK root = parse_root(response.text) @@ -316,7 +319,8 @@ def test_tu_stats(client: TestClient, tu_user: User): tu_user.InactivityTS = time.utcnow() with client as request: - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/tu") assert response.status_code == HTTPStatus.OK root = parse_root(response.text) @@ -364,7 +368,8 @@ def test_tu_index_table_paging(client, tu_user): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/tu") assert response.status_code == int(HTTPStatus.OK) # Parse lxml.etree root. @@ -394,7 +399,8 @@ def test_tu_index_table_paging(client, tu_user): # Now, get the next page of current votes. offset = 10 # Specify coff=10 with client as request: - response = request.get("/tu", cookies=cookies, params={"coff": offset}) + request.cookies = cookies + response = request.get("/tu", params={"coff": offset}) assert response.status_code == int(HTTPStatus.OK) old_rows = rows @@ -421,7 +427,8 @@ def test_tu_index_table_paging(client, tu_user): offset = 20 # Specify coff=10 with client as request: - response = request.get("/tu", cookies=cookies, params={"coff": offset}) + request.cookies = cookies + response = request.get("/tu", params={"coff": offset}) assert response.status_code == int(HTTPStatus.OK) # Do it again, we only have five left. @@ -470,7 +477,8 @@ def test_tu_index_sorting(client, tu_user): # Make a default request to /tu. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/tu") assert response.status_code == int(HTTPStatus.OK) # Get lxml handles of the document. @@ -497,7 +505,8 @@ def test_tu_index_sorting(client, tu_user): # Make another request; one that sorts the current votes # in ascending order instead of the default descending order. with client as request: - response = request.get("/tu", cookies=cookies, params={"cby": "asc"}) + request.cookies = cookies + response = request.get("/tu", params={"cby": "asc"}) assert response.status_code == int(HTTPStatus.OK) # Get lxml handles of the document. @@ -548,7 +557,8 @@ def test_tu_index_last_votes( # Now, check that tu_user got populated in the .last-votes table. cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/tu") assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -581,12 +591,14 @@ def test_tu_proposal_unauthorized( cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/tu/{proposal[2].ID}" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" with client as request: - response = request.post(endpoint, cookies=cookies, data={"decision": False}) + request.cookies = cookies + response = request.post(endpoint, data={"decision": False}) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" @@ -602,9 +614,8 @@ def test_tu_running_proposal( proposal_id = voteinfo.ID cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get( - f"/tu/{proposal_id}", cookies=cookies, follow_redirects=True - ) + request.cookies = cookies + response = request.get(f"/tu/{proposal_id}") assert response.status_code == int(HTTPStatus.OK) # Alright, now let's continue on to verifying some markup. @@ -674,9 +685,8 @@ def test_tu_running_proposal( # Make another request now that we've voted. with client as request: - response = request.get( - "/tu", params={"id": voteinfo.ID}, cookies=cookies, follow_redirects=True - ) + request.cookies = cookies + response = request.get("/tu", params={"id": voteinfo.ID}, follow_redirects=True) assert response.status_code == int(HTTPStatus.OK) # Parse our new root. @@ -702,7 +712,8 @@ def test_tu_ended_proposal(client, proposal): proposal_id = voteinfo.ID cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get(f"/tu/{proposal_id}", cookies=cookies) + request.cookies = cookies + response = request.get(f"/tu/{proposal_id}") assert response.status_code == int(HTTPStatus.OK) # Alright, now let's continue on to verifying some markup. @@ -734,7 +745,8 @@ def test_tu_proposal_vote_not_found(client, tu_user): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post("/tu/1", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/tu/1", data=data) assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -747,7 +759,8 @@ def test_tu_proposal_vote(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) + request.cookies = cookies + response = request.post(f"/tu/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.OK) # Check that the proposal record got updated. @@ -775,7 +788,8 @@ def test_tu_proposal_vote_unauthorized( cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) + request.cookies = cookies + response = request.post(f"/tu/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.UNAUTHORIZED) root = parse_root(response.text) @@ -784,7 +798,8 @@ def test_tu_proposal_vote_unauthorized( with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, params=data) + request.cookies = cookies + response = request.get(f"/tu/{voteinfo.ID}", params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -802,7 +817,8 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) + request.cookies = cookies + response = request.post(f"/tu/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -811,7 +827,8 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, params=data) + request.cookies = cookies + response = request.get(f"/tu/{voteinfo.ID}", params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -830,7 +847,8 @@ def test_tu_proposal_vote_already_voted(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) + request.cookies = cookies + response = request.post(f"/tu/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -839,7 +857,8 @@ def test_tu_proposal_vote_already_voted(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, params=data) + request.cookies = cookies + response = request.get(f"/tu/{voteinfo.ID}", params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -853,7 +872,8 @@ def test_tu_proposal_vote_invalid_decision(client, proposal): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: data = {"decision": "EVIL"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, data=data) + request.cookies = cookies + response = request.post(f"/tu/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) assert response.text == "Invalid 'decision' value." @@ -861,7 +881,8 @@ def test_tu_proposal_vote_invalid_decision(client, proposal): def test_tu_addvote(client: TestClient, tu_user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", cookies=cookies) + request.cookies = cookies + response = request.get("/addvote") assert response.status_code == int(HTTPStatus.OK) @@ -870,12 +891,14 @@ def test_tu_addvote_unauthorized( ): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", cookies=cookies) + request.cookies = cookies + response = request.get("/addvote") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" with client as request: - response = request.post("/addvote", cookies=cookies) + request.cookies = cookies + response = request.post("/addvote") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tu" @@ -883,7 +906,8 @@ def test_tu_addvote_unauthorized( def test_tu_addvote_invalid_type(client: TestClient, tu_user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", params={"type": "faketype"}, cookies=cookies) + request.cookies = cookies + response = request.get("/addvote", params={"type": "faketype"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -897,7 +921,8 @@ def test_tu_addvote_post(client: TestClient, tu_user: User, user: User): data = {"user": user.Username, "type": "add_tu", "agenda": "Blah"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) voteinfo = db.query(TUVoteInfo, TUVoteInfo.Agenda == "Blah").first() @@ -912,14 +937,16 @@ def test_tu_addvote_post_cant_duplicate_username( data = {"user": user.Username, "type": "add_tu", "agenda": "Blah"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) voteinfo = db.query(TUVoteInfo, TUVoteInfo.Agenda == "Blah").first() assert voteinfo is not None with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -927,7 +954,8 @@ def test_tu_addvote_post_invalid_username(client: TestClient, tu_user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} data = {"user": "fakeusername"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -935,7 +963,8 @@ def test_tu_addvote_post_invalid_type(client: TestClient, tu_user: User, user: U cookies = {"AURSID": tu_user.login(Request(), "testPassword")} data = {"user": user.Username} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -943,7 +972,8 @@ def test_tu_addvote_post_invalid_agenda(client: TestClient, tu_user: User, user: cookies = {"AURSID": tu_user.login(Request(), "testPassword")} data = {"user": user.Username, "type": "add_tu"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -952,5 +982,6 @@ def test_tu_addvote_post_bylaws(client: TestClient, tu_user: User): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} data = {"type": "bylaws", "agenda": "Blah blah!"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) From a08681ba2391b955cc39a8f62dbddcc153ea6cca Mon Sep 17 00:00:00 2001 From: moson-mo Date: Fri, 25 Nov 2022 12:24:04 +0100 Subject: [PATCH 1200/1451] fix: Add "Show more..." link for "Required by" Fix glitch on the package page: "Show more..." not displayed for the "Required by" list Fix test case: Function name does not start with "test" hence it was never executed during test runs Issue report: #363 Signed-off-by: moson-mo --- templates/partials/packages/package_metadata.html | 10 ++++++---- test/test_packages_routes.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html index 123b994d..50d38b48 100644 --- a/templates/partials/packages/package_metadata.html +++ b/templates/partials/packages/package_metadata.html @@ -62,10 +62,12 @@ {{ dep | dep_extra }} {% endfor %} - {% if not all_reqs and (required_by | length) > max_listing %} - - {{ "Show %d more" | tr | format(reqs_count - (required_by | length)) }}... - + {% if not all_reqs and reqs_count > max_listing %} +
  • + + {{ "Show %d more" | tr | format(reqs_count - (required_by | length)) }}... + +
  • {% endif %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 0da6cfab..c8986b9c 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -352,7 +352,7 @@ def test_package_split_description(client: TestClient, user: User): assert row.text == pkg_b.Description -def paged_depends_required(client: TestClient, package: Package): +def test_paged_depends_required(client: TestClient, package: Package): maint = package.PackageBase.Maintainer new_pkgs = [] @@ -360,7 +360,7 @@ def paged_depends_required(client: TestClient, package: Package): # Create 25 new packages that'll be used to depend on our package. for i in range(26): base = db.create(PackageBase, Name=f"new_pkg{i}", Maintainer=maint) - new_pkgs.append(db.create(Package, Name=base.Name)) + new_pkgs.append(db.create(Package, Name=base.Name, PackageBase=base)) # Create 25 deps. for i in range(25): From 7864ac6dfeafd3995063e3b58cfbd393fb1b6551 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 27 Nov 2022 10:33:58 +0100 Subject: [PATCH 1201/1451] fix: search-by parameter for keyword links Fixes: Keyword-links on the package page pass wrong query-parameter. Thus a name/description search is performed instead of keywords Issue report: #397 Signed-off-by: moson-mo --- templates/partials/packages/details.html | 2 +- test/test_packages_routes.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index 8ecf9bd8..697ef724 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -53,7 +53,7 @@ {% for keyword in pkgbase.keywords.all() %} {{ keyword.Keyword }} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index c8986b9c..bf179963 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -271,6 +271,13 @@ def test_package(client: TestClient, package: Package): db.create(PackageLicense, PackageID=package.ID, License=licenses[0]) db.create(PackageLicense, PackageID=package.ID, License=licenses[1]) + # Create some keywords + keywords = ["test1", "test2"] + for keyword in keywords: + db.create( + PackageKeyword, PackageBaseID=package.PackageBaseID, Keyword=keyword + ) + with client as request: resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) @@ -307,6 +314,11 @@ def test_package(client: TestClient, package: Package): expected = ["test_conflict1", "test_conflict2"] assert conflicts[0].text.strip() == ", ".join(expected) + keywords = root.xpath('//a[@class="keyword"]') + expected = ["test1", "test2"] + for i, keyword in enumerate(expected): + assert keywords[i].text.strip() == keyword + def test_package_split_description(client: TestClient, user: User): From c74772cb3610c7f5be270f0edb1416fc9d1476ed Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sun, 27 Nov 2022 10:34:07 +0000 Subject: [PATCH 1202/1451] chore: bump to v6.1.9 Signed-off-by: Leonidas Spyropoulos --- aurweb/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aurweb/config.py b/aurweb/config.py index 49806738..8130376d 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -5,7 +5,7 @@ from typing import Any # Publicly visible version of aurweb. This is used to display # aurweb versioning in the footer and must be maintained. # Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.1.8" +AURWEB_VERSION = "v6.1.9" _parser = None diff --git a/pyproject.toml b/pyproject.toml index 762a52c1..ce5b0b43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.1.8" +version = "v6.1.9" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 8027ff936c030ebcd43bf4d8ae3a244fb3d28a56 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 28 Nov 2022 16:57:27 +0100 Subject: [PATCH 1203/1451] fix: alignment of pagination element pagination for comments should appear on the right instead of center Issue report: #390 Signed-off-by: moson-mo --- templates/partials/packages/comments.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index 9d49bc86..f00d62f2 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -39,7 +39,7 @@ {% if pages > 1 %}

    {{ page | pager_nav(comments_total, prefix) | safe }} -

    +

    {% endif %} {% for comment in comments.all() %} From 2b8dedb3a2dcfa4442591bf589e1586105064866 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 28 Nov 2022 17:01:44 +0100 Subject: [PATCH 1204/1451] feat: add pagination element below comments other pages like the "package search" have this as well. Issue report: #390 Signed-off-by: moson-mo --- templates/partials/packages/comments.html | 7 +++++++ web/html/css/aurweb.css | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index f00d62f2..55421bfa 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -45,5 +45,12 @@ {% for comment in comments.all() %} {% include "partials/packages/comment.html" %} {% endfor %} + {% endif %} diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 59f7ed1e..64a65742 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -193,6 +193,11 @@ label.confirmation { align-self: flex-end; } +.comments-footer { + display: flex; + justify-content: flex-end; +} + .comment-header { clear: both; font-size: 1em; From d8e91d058cd494dfb7812994796d1a46eb532f6b Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 22 Dec 2022 12:41:29 +0100 Subject: [PATCH 1205/1451] fix(rpc): provides search should return name match We need to return packages matching on the name as well. (A package always provides itself) Signed-off-by: moson-mo --- aurweb/rpc.py | 12 +++++++++++- test/test_rpc.py | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 2aa27500..1440703a 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -376,8 +376,18 @@ class RPC: search.search_by(by, arg) max_results = config.getint("options", "max_rpc_results") - results = self.entities(search.results()).limit(max_results + 1).all() + query = self.entities(search.results()).limit(max_results + 1) + + # For "provides", we need to union our relation search + # with an exact search since a package always provides itself. + # Turns out that doing this with an OR statement is extremely slow + if by == "provides": + search = RPCSearch() + search._search_by_exact_name(arg) + query = query.union(self.entities(search.results())) + + results = query.all() if len(results) > max_results: raise RPCError("Too many package results.") diff --git a/test/test_rpc.py b/test/test_rpc.py index 04efd38f..92714ff1 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -920,6 +920,19 @@ def test_rpc_search_provides( assert result.get("Name") == packages[0].Name +def test_rpc_search_provides_self( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "provides", "arg": "big-chungus"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + # expected to return "big-chungus" + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + def test_rpc_search_conflicts( client: TestClient, packages: list[Package], relations: list[PackageRelation] ): From 7a9448a3e52e216f4f11b996be12ab87b99fe4bc Mon Sep 17 00:00:00 2001 From: moson-mo Date: Tue, 29 Nov 2022 14:45:24 +0100 Subject: [PATCH 1206/1451] perf: improve packages search-query Improves performance for queries with large result sets. The "group by" clause can be removed for all search types but the keywords. Signed-off-by: moson-mo --- aurweb/packages/search.py | 5 ++++- aurweb/routers/packages.py | 28 ++++++++++++---------------- test/test_packages_routes.py | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index c0740cda..d5e00110 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -136,7 +136,10 @@ class PackageSearch: self._join_user() self._join_keywords() keywords = set(k.lower() for k in keywords) - self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords)) + self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords)).group_by( + models.Package.Name + ) + return self def _search_by_maintainer(self, keywords: str) -> orm.Query: diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index a4aac496..6a943dbf 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -93,22 +93,18 @@ async def packages_get( search.sort_by(sort_by, sort_order) # Insert search results into the context. - results = ( - search.results() - .with_entities( - models.Package.ID, - models.Package.Name, - models.Package.PackageBaseID, - models.Package.Version, - models.Package.Description, - models.PackageBase.Popularity, - models.PackageBase.NumVotes, - models.PackageBase.OutOfDateTS, - models.User.Username.label("Maintainer"), - models.PackageVote.PackageBaseID.label("Voted"), - models.PackageNotification.PackageBaseID.label("Notify"), - ) - .group_by(models.Package.Name) + results = search.results().with_entities( + models.Package.ID, + models.Package.Name, + models.Package.PackageBaseID, + models.Package.Version, + models.Package.Description, + models.PackageBase.Popularity, + models.PackageBase.NumVotes, + models.PackageBase.OutOfDateTS, + models.User.Username.label("Maintainer"), + models.PackageVote.PackageBaseID.label("Voted"), + models.PackageNotification.PackageBaseID.label("Notify"), ) packages = results.limit(per_page).offset(offset) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index bf179963..f9cea694 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -740,6 +740,23 @@ def test_packages_search_by_keywords(client: TestClient, packages: list[Package] rows = root.xpath('//table[@class="results"]/tbody/tr') assert len(rows) == 1 + # Now let's add another keyword to the same package + with db.begin(): + db.create( + PackageKeyword, PackageBase=package.PackageBase, Keyword="testKeyword2" + ) + + # And request packages with both keywords, we should still get 1 result. + with client as request: + response = request.get( + "/packages", params={"SeB": "k", "K": "testKeyword testKeyword2"} + ) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + def test_packages_search_by_maintainer( client: TestClient, maintainer: User, package: Package From 413de914caa20f1dd848c9b59e6d8d065a3b8230 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:28:17 -0800 Subject: [PATCH 1207/1451] fix: remove trailing whitespace lint check for ./po Signed-off-by: Kevin Morris --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab4240c9..b2baec65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,7 @@ repos: - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace + exclude: ^po/ - id: debug-statements - repo: https://github.com/myint/autoflake From 65266d752b2671a8d175e85aafd8b27ae638aba0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:13 -0800 Subject: [PATCH 1208/1451] update-ar translations --- po/ar.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/ar.po b/po/ar.po index ea0e03cf..1fed4f4f 100644 --- a/po/ar.po +++ b/po/ar.po @@ -1,17 +1,17 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # safa1996alfulaij , 2015 # صفا الفليج , 2015-2016 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: صفا الفليج , 2015-2016\n" "Language-Team: Arabic (http://www.transifex.com/lfleischer/aurweb/language/ar/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 3a13eeb744e603d06bbe57025af5ebabaf3ba615 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:16 -0800 Subject: [PATCH 1209/1451] update-az translations --- po/az.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/az.po b/po/az.po index 1c7ca207..df14a5b0 100644 --- a/po/az.po +++ b/po/az.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Azerbaijani (http://www.transifex.com/lfleischer/aurweb/language/az/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From b89fe9eb1397529982c6ab099abef30214e7ce2e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:19 -0800 Subject: [PATCH 1210/1451] update-az_AZ translations --- po/az_AZ.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/az_AZ.po b/po/az_AZ.po index 2f5ceabd..293d7b0d 100644 --- a/po/az_AZ.po +++ b/po/az_AZ.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Azerbaijani (Azerbaijan) (http://www.transifex.com/lfleischer/aurweb/language/az_AZ/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 9229220e2107833846565f54f7cf814086f8b04d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:22 -0800 Subject: [PATCH 1211/1451] update-bg translations --- po/bg.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/bg.po b/po/bg.po index c7c70021..f373b761 100644 --- a/po/bg.po +++ b/po/bg.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Bulgarian (http://www.transifex.com/lfleischer/aurweb/language/bg/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From d6661403aae6ebc40d68a2b47170bbd626a79f8e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:26 -0800 Subject: [PATCH 1212/1451] update-ca translations --- po/ca.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/ca.po b/po/ca.po index d43c84dc..86d77e56 100644 --- a/po/ca.po +++ b/po/ca.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Adolfo Jayme-Barrientos, 2014 # Hector Mtz-Seara , 2011,2013 @@ -10,10 +10,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Ícar , 2021\n" "Language-Team: Catalan (http://www.transifex.com/lfleischer/aurweb/language/ca/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 618a382e6c32e3eef2efc20b3a15877754518cb4 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:29 -0800 Subject: [PATCH 1213/1451] update-ca_ES translations --- po/ca_ES.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/ca_ES.po b/po/ca_ES.po index aac7b03f..5c05ba0c 100644 --- a/po/ca_ES.po +++ b/po/ca_ES.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Catalan (Spain) (http://www.transifex.com/lfleischer/aurweb/language/ca_ES/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From da458ae70ab1c1c05c1d0965bb31990f09769676 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:33 -0800 Subject: [PATCH 1214/1451] update-cs translations --- po/cs.po | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/po/cs.po b/po/cs.po index 59a24007..9086bd75 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Daniel Milde , 2017 # Daniel Peukert , 2021 -# Daniel Peukert , 2021 +# Daniel Peukert , 2021-2022 # Jaroslav Lichtblau , 2015-2016 # Jaroslav Lichtblau , 2014 # Jiří Vírava , 2017-2018 @@ -15,10 +15,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Daniel Peukert , 2021-2022\n" "Language-Team: Czech (http://www.transifex.com/lfleischer/aurweb/language/cs/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -763,7 +763,7 @@ msgstr "Hlasující" msgid "" "Account registration has been disabled for your IP address, probably due to " "sustained spam attacks. Sorry for the inconvenience." -msgstr "Registrace účtu byla pro vaši IP adresu zakázána, pravděpodobně kvůli trvalým spamovým útokům. Omluvám se za nepříjemnost." +msgstr "Registrace účtu byla pro vaši IP adresu zakázána, pravděpodobně kvůli trvalým spamovým útokům. Za nepříjemnosti se omlouváme." #: lib/acctfuncs.inc.php msgid "Missing User ID" @@ -978,7 +978,7 @@ msgstr "Informace o balíčku nebyly nalezeny." #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "Chybná hlavička Referer" #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." @@ -2322,33 +2322,33 @@ msgstr "Pro změnu typu tohoto účtu na %s nemáte oprávnění." #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "Žádné žádosti o odebrání vlastnictví balíčku %s momentálně neexistují." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Interní chyba serveru" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Došlo k fatální chybě." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Detaily chyby byly zalogovány a budou co nejdříve zkontrolovány administrátorem. Za jakékoli způsobené nepříjemnosti se omlouváme." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "Chyba serveru AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Komentáře k uzavření žádostí vztahujících se k tomuto balíčku..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Tato akce uzavře všechny žádosti čekající na vyřízení vztahující se k tomuto balíčku. Pokud není vyplněno textové Pole %sKomentáře\"%s, komentář k uzavření žádostí bude vygenerován automaticky." From 5a7a9c2c9f8734842510cadc70b2e090a77c03dd Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:36 -0800 Subject: [PATCH 1215/1451] update-da translations --- po/da.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/da.po b/po/da.po index 822b5506..89f6a635 100644 --- a/po/da.po +++ b/po/da.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Linuxbruger , 2018 # Louis Tim Larsen , 2015 @@ -9,10 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Linuxbruger , 2018\n" "Language-Team: Danish (http://www.transifex.com/lfleischer/aurweb/language/da/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 791e715aee661d67152ca2bf20714d9697586590 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:39 -0800 Subject: [PATCH 1216/1451] update-de translations --- po/de.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/de.po b/po/de.po index a0f8fb0f..894494c6 100644 --- a/po/de.po +++ b/po/de.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # 9d91e189c22376bb4ee81489bc27fc28, 2013 # 9d91e189c22376bb4ee81489bc27fc28, 2013-2014 @@ -27,10 +27,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Stefan Auditor , 2021\n" "Language-Team: German (http://www.transifex.com/lfleischer/aurweb/language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 6bf408775c249f0938ce7dd59066bc91a2c872a7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:43 -0800 Subject: [PATCH 1217/1451] update-el translations --- po/el.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/po/el.po b/po/el.po index 37db785c..2b665c34 100644 --- a/po/el.po +++ b/po/el.po @@ -1,23 +1,23 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Achilleas Pipinellis, 2014 # Achilleas Pipinellis, 2013 # Achilleas Pipinellis, 2013 # Achilleas Pipinellis, 2011 # Achilleas Pipinellis, 2012 -# Leonidas Spyropoulos, 2021 +# Leonidas Spyropoulos, 2021-2022 # Lukas Fleischer , 2011 # flamelab , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Leonidas Spyropoulos, 2021-2022\n" "Language-Team: Greek (http://www.transifex.com/lfleischer/aurweb/language/el/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,7 +35,7 @@ msgstr "Μας συγχωρείτε, η σελίδα που ζητήσατε δ #: html/404.php template/pkgreq_close_form.php msgid "Note" -msgstr "" +msgstr "Σημείωση" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." From aeb38b599d68ac1c7cf50b3fdd22a3b222db688c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:46 -0800 Subject: [PATCH 1218/1451] update-es translations --- po/es.po | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/po/es.po b/po/es.po index 9cbe98a6..b6035d5b 100644 --- a/po/es.po +++ b/po/es.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Adolfo Jayme-Barrientos, 2015 # Angel Velasquez , 2011 @@ -9,25 +9,25 @@ # Lukas Fleischer , 2011 # neiko , 2011 # Nicolás de la Torre , 2012 -# prflr88 , 2012 -# prflr88 , 2016-2017 -# prflr88 , 2013-2016 -# prflr88 , 2016-2017 -# prflr88 , 2016 -# prflr88 , 2019 +# Pablo Lezaeta Reyes , 2012 +# Pablo Lezaeta Reyes , 2016-2017 +# Pablo Lezaeta Reyes , 2013-2016 +# Pablo Lezaeta Reyes , 2016-2017 +# Pablo Lezaeta Reyes , 2016 +# Pablo Lezaeta Reyes , 2019 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Pablo Lezaeta Reyes , 2019\n" "Language-Team: Spanish (http://www.transifex.com/lfleischer/aurweb/language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -1590,6 +1590,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "Hay %d solicitud pendiente" msgstr[1] "Hay %d solicitudes pendientes" +msgstr[2] "Hay %d solicitudes pendientes" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1864,6 +1865,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "Se encontró %d solicitud para el paquete." msgstr[1] "Se encontraron %d solicitudes para el paquete." +msgstr[2] "Se encontraron %d solicitudes para el paquete." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1888,6 +1890,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d día restante" msgstr[1] "~%d días restantes" +msgstr[2] "~%d días restantes" #: template/pkgreq_results.php #, php-format @@ -1895,6 +1898,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "Aprox. %d hora restante" msgstr[1] "Aprox. %d horas restantes" +msgstr[2] "Aprox. %d horas restantes" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2023,6 +2027,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d paquete fue encontrado." msgstr[1] "%d paquetes fueron encontrados." +msgstr[2] "%d paquetes fueron encontrados." #: template/pkg_search_results.php msgid "Version" From 076245e061786e762cc705fa5ad49f7292b456db Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:49 -0800 Subject: [PATCH 1219/1451] update-et translations --- po/et.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/et.po b/po/et.po index 44f2b3a0..4092823b 100644 --- a/po/et.po +++ b/po/et.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Estonian (http://www.transifex.com/lfleischer/aurweb/language/et/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From bce9bedaf460b8efd8c5e2eb9e9cde5da4384f7c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:53 -0800 Subject: [PATCH 1220/1451] update-fi translations --- po/fi.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/fi.po b/po/fi.po index 636681b7..98b3a03b 100644 --- a/po/fi.po +++ b/po/fi.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Elias Autio, 2016 # Jesse Jaara , 2011-2012,2015 @@ -10,10 +10,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Nikolay Korotkiy , 2018-2019\n" "Language-Team: Finnish (http://www.transifex.com/lfleischer/aurweb/language/fi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 3fa9047864d1a872f20027f26837ac1dfd9c971f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:56 -0800 Subject: [PATCH 1221/1451] update-fi_FI translations --- po/fi_FI.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/fi_FI.po b/po/fi_FI.po index 17a58b4a..cd516edc 100644 --- a/po/fi_FI.po +++ b/po/fi_FI.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Finnish (Finland) (http://www.transifex.com/lfleischer/aurweb/language/fi_FI/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From ff01947f3d260981bfdecf8488b54a9995256a6b Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:08:59 -0800 Subject: [PATCH 1222/1451] update-fr translations --- po/fr.po | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/po/fr.po b/po/fr.po index 03192d48..2b0c5bab 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Alexandre Macabies , 2018 # Antoine Lubineau , 2012 @@ -10,7 +10,7 @@ # demostanis , 2020 # Kristien , 2020 # lordheavy , 2011 -# lordheavy , 2013-2014,2018 +# lordheavy , 2013-2014,2018,2022 # lordheavy , 2011-2012 # Lukas Fleischer , 2011 # Thibault , 2020 @@ -18,16 +18,16 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: lordheavy , 2013-2014,2018,2022\n" "Language-Team: French (http://www.transifex.com/lfleischer/aurweb/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -1590,6 +1590,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d requête en attente" msgstr[1] "%d requêtes en attente" +msgstr[2] "%d requêtes en attente" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1864,6 +1865,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "%d paquet demandé trouvé." msgstr[1] "%d paquets demandés trouvés." +msgstr[2] "%d paquets demandés trouvés." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1888,6 +1890,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d jour restant" msgstr[1] "~%d jours restants" +msgstr[2] "~%d jours restants" #: template/pkgreq_results.php #, php-format @@ -1895,6 +1898,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "~%d heure restante" msgstr[1] "%d heures restantes" +msgstr[2] "%d heures restantes" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2023,6 +2027,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d paquet trouvé." msgstr[1] "%d paquets trouvés." +msgstr[2] "%d paquets trouvés." #: template/pkg_search_results.php msgid "Version" @@ -2319,7 +2324,7 @@ msgstr "" #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Erreur interne du serveur" #: templates/errors/500.html msgid "A fatal error has occurred." From 9385c14f77d18d28ade7e2fa681412133f3daea5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:03 -0800 Subject: [PATCH 1223/1451] update-he translations --- po/he.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/po/he.po b/po/he.po index 936e93a1..88f2fddd 100644 --- a/po/he.po +++ b/po/he.po @@ -1,18 +1,18 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: -# GenghisKhan , 2016 +# gk , 2016 # Lukas Fleischer , 2011 # Yaron Shahrabani , 2016-2022 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Yaron Shahrabani , 2016-2022\n" "Language-Team: Hebrew (http://www.transifex.com/lfleischer/aurweb/language/he/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2339,10 +2339,10 @@ msgstr "שגיאת שרת ה־AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "הערות הסגירה התואמות של בקשת החבילה…" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "פעולה זו תסגור בקשות חבילות ממתינות שקשורות אליה. אם %sתגובות%s מושמטות, תיווצר תגובת סגירה אוטומטית." From b209cd962c25f0f51ea31625b7ede3784407c16c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:06 -0800 Subject: [PATCH 1224/1451] update-hi_IN translations --- po/hi_IN.po | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/po/hi_IN.po b/po/hi_IN.po index 114c9461..1ba83dae 100644 --- a/po/hi_IN.po +++ b/po/hi_IN.po @@ -1,16 +1,16 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: -# Panwar108 , 2018,2020-2021 +# Panwar108 , 2018,2020-2022 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Panwar108 , 2018,2020-2022\n" "Language-Team: Hindi (India) (http://www.transifex.com/lfleischer/aurweb/language/hi_IN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -897,7 +897,7 @@ msgstr "अनुचित उपयोक्ता नाम या कूट #: lib/acctfuncs.inc.php msgid "An error occurred trying to generate a user session." -msgstr "उपयोक्ता सत्र बनाने हेतु त्रुटि।" +msgstr "उपयोक्ता सत्र बनाने समय त्रुटि हुई।" #: lib/acctfuncs.inc.php msgid "Invalid e-mail and reset key combination." @@ -2308,29 +2308,29 @@ msgstr "%s स्वीकारनें हेतु कोई निरर् #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "आंतरिक सर्वर त्रुटि" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "गंभीर त्रुटि हुई।" #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "संबंधित सूचना लॉग फाइल में दर्ज की जा चुकी है एवं अतिशीघ्र ही पोस्ट प्रबंधक द्वारा उसकी समीक्षा की जाएगी। इस कारण हुई किसी भी प्रकार की असुविधा हेतु खेद है।" #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR सर्वर त्रुटि" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "पैकेज अनुरोध समापन संबंधी टिप्पणियाँ..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "इस कार्य द्वारा संबंधित सभी लंबित पैकेज अनुरोध बंद हो जाएँगे। %sटिप्पणियाँ%s न होने की स्थिति में एक समापन टिप्पणी का स्वतः ही सृजन होगा।" From bf348fa5721dd79800d152477e3056d15ff3d0b0 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:09 -0800 Subject: [PATCH 1225/1451] update-hr translations --- po/hr.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/hr.po b/po/hr.po index fe1857c1..a0474e23 100644 --- a/po/hr.po +++ b/po/hr.po @@ -1,16 +1,16 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Lukas Fleischer , 2011\n" "Language-Team: Croatian (http://www.transifex.com/lfleischer/aurweb/language/hr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 5f71e58db16e0f22db0261cf07741d35fd3b79e7 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:13 -0800 Subject: [PATCH 1226/1451] update-hu translations --- po/hu.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/hu.po b/po/hu.po index e6ebd451..7459a716 100644 --- a/po/hu.po +++ b/po/hu.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Balló György , 2013 # Balló György , 2011,2013-2016 @@ -11,10 +11,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: PB, 2020\n" "Language-Team: Hungarian (http://www.transifex.com/lfleischer/aurweb/language/hu/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 28e8b312110e917e72505bebc122be61d38a37ee Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:16 -0800 Subject: [PATCH 1227/1451] update-id translations --- po/id.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/id.po b/po/id.po index 103c47e6..96059ac9 100644 --- a/po/id.po +++ b/po/id.po @@ -1,17 +1,17 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # se7entime , 2013 # se7entime , 2016 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: se7entime , 2016\n" "Language-Team: Indonesian (http://www.transifex.com/lfleischer/aurweb/language/id/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 3a460faa6e97296cc8b308416c30bda68b13c016 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:19 -0800 Subject: [PATCH 1228/1451] update-id_ID translations --- po/id_ID.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/id_ID.po b/po/id_ID.po index c3acb167..f0612399 100644 --- a/po/id_ID.po +++ b/po/id_ID.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Indonesian (Indonesia) (http://www.transifex.com/lfleischer/aurweb/language/id_ID/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 0d950a0c9fe355f1ccb667181d6313da769f671d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:23 -0800 Subject: [PATCH 1229/1451] update-is translations --- po/is.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/is.po b/po/is.po index aee80ce5..0f3a3fcb 100644 --- a/po/is.po +++ b/po/is.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Icelandic (http://www.transifex.com/lfleischer/aurweb/language/is/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From a12dbd191a9d857f3474c3b8557cb3cd787fb603 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:26 -0800 Subject: [PATCH 1230/1451] update-it translations --- po/it.po | 132 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 63 deletions(-) diff --git a/po/it.po b/po/it.po index f583cb2f..466d486a 100644 --- a/po/it.po +++ b/po/it.po @@ -1,26 +1,27 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Fanfurlio Farolfi , 2021-2022 -# Giovanni Scafora , 2011-2015 +# Giovanni Scafora , 2011-2015,2022 +# Giovanni Scafora , 2022 # Lorenzo Porta , 2014 # Lukas Fleischer , 2011 # mattia_b89 , 2019 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Giovanni Scafora , 2022\n" "Language-Team: Italian (http://www.transifex.com/lfleischer/aurweb/language/it/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: it\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -41,12 +42,12 @@ msgstr "Le URL per clonare un repository git non sono visualizzabili nel browser #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "Per clonare il reposiroty git di %s, esegui %s." +msgstr "Per clonare il repository git di %s, esegui %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "Clicca %squi%s per tornare alla pagina dei dettagli di %s." +msgstr "Clicca %squi%s per ritornare alla pagina dei dettagli di %s." #: html/503.php msgid "Service Unavailable" @@ -79,7 +80,7 @@ msgstr "Non hai i permessi necessari per modificare questo account." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "Password non valida." +msgstr "La password non è valida." #: html/account.php msgid "Use this form to search existing accounts." @@ -169,7 +170,7 @@ msgstr "Edita il commento" #: html/home.php template/header.php msgid "Dashboard" -msgstr "Cruscotto" +msgstr "Pannello" #: html/home.php template/header.php msgid "Home" @@ -573,7 +574,7 @@ msgstr "Solo i TU e gli sviluppatori possono abbandonare i pacchetti." #: html/pkgflagcomment.php msgid "Flag Comment" -msgstr "Segnala Commento" +msgstr "Segnala il commento" #: html/pkgflag.php msgid "Flag Package Out-Of-Date" @@ -585,7 +586,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "Questo appare essere un pacchetto da VCS. Per favore %snon%s marcarlo come non aggiornato se la versione in AUR non corrisponde con il commit più recente, Questo pacchetto dovrebbe essere marcato solo se i sorgenti sono stati spostati o se sono necessari dei cambiamenti al PKGBUILD a causa delle recenti modifiche al sorgente." +msgstr "Sembra un pacchetto VCS. Fai %snon%s segnalarlo come non aggiornato, se la versione in AUR non corrisponde con il commit più recente. Questo pacchetto dovrebbe essere segnalato solo se i sorgenti vengono spostati o se sono necessarie delle modifiche al PKGBUILD a causa delle recenti modifiche al sorgente." #: html/pkgflag.php #, php-format @@ -699,12 +700,12 @@ msgstr "Usa questo modulo per creare un account." #: html/tos.php msgid "Terms of Service" -msgstr "Termini del Servizio" +msgstr "Termini di servizio" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "I seguenti documenti sono stati aggiornati. Per favore riesaminali correttamente:" +msgstr "I seguenti documenti sono stati aggiornati. Riesaminali attentamente:" #: html/tos.php #, php-format @@ -784,7 +785,7 @@ msgstr "Può contenere solo un punto, un trattino basso o un trattino." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "Per favore conferma la tua nuova password." +msgstr "Conferma la tua nuova password." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -796,7 +797,7 @@ msgstr "L'indirizzo email di scorta non è valido." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." -msgstr "La homepage non è valida, per favore specificare l'URL HTTP(s) completo." +msgstr "La homepage non è valida, specifica l'URL HTTP(s) completo." #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." @@ -816,7 +817,7 @@ msgstr "Lingua attualmente non supportata." #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." -msgstr "Fuso orario non attualmente supportato." +msgstr "Il fuso orario non è attualmente supportato." #: lib/acctfuncs.inc.php #, php-format @@ -835,15 +836,15 @@ msgstr "La chiave pubblica SSH %s%s%s, è già in uso." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "Manca la risposta CAPTCHA." +msgstr "Manca il CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "Il CAPTCHA è scaduto, Per favore riprova." +msgstr "Il CAPTCHA è scaduto. Riprova." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "La risposta CAPTCHA inserita non è valida." +msgstr "Il CAPTCHA inserito non è valido." #: lib/acctfuncs.inc.php #, php-format @@ -885,7 +886,7 @@ msgstr "Account sospeso" #: aurweb/routers/accounts.py msgid "You do not have permission to suspend accounts." -msgstr "Non hai il permesso per sospendere account." +msgstr "Non hai il permesso per sospendere gli account." #: lib/acctfuncs.inc.php #, php-format @@ -946,7 +947,7 @@ msgstr "Manca l'ID del commento." #: lib/pkgbasefuncs.inc.php msgid "No more than 5 comments can be pinned." -msgstr "Non possono essere inseriti più di 5 commenti." +msgstr "Non possono essere evidenziati più di 5 commenti." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to pin this comment." @@ -958,11 +959,11 @@ msgstr "Non sei autorizzato a rimuovere questo commento." #: lib/pkgbasefuncs.inc.php msgid "Comment has been pinned." -msgstr "Il commento è stato rimosso." +msgstr "Il commento è ora in evidenza." #: lib/pkgbasefuncs.inc.php msgid "Comment has been unpinned." -msgstr "I commenti sono stati rimossi." +msgstr "I commenti non sono più in evidenza." #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Error retrieving package details." @@ -1296,7 +1297,7 @@ msgstr "Modifica l'account di quest'utente" #: template/account_details.php msgid "List this user's comments" -msgstr "Elenca i commenti di quest'utente" +msgstr "Elenca i commenti di questo utente" #: template/account_edit_form.php #, php-format @@ -1306,7 +1307,7 @@ msgstr "Clicca %squi%s se vuoi eliminare definitivamente questo account." #: template/account_edit_form.php #, php-format msgid "Click %shere%s for user details." -msgstr "Click %squì%s per il dettagli dell'utente." +msgstr "Click %squì%s per visualizzare i dettagli dell'utente." #: template/account_edit_form.php #, php-format @@ -1364,7 +1365,7 @@ msgstr "Indirizzo email di scorta" msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "Puoi fornire un secondo indirizzo email che potrà essere usato per ripristinare il tuo account, nel caso tu perda l'accesso al tuo indirizzo email primario." +msgstr "Puoi fornire un secondo indirizzo email che potrà essere usato per ripristinare il tuo account, nel caso tu perdessi l'accesso al tuo indirizzo email primario." #: template/account_edit_form.php msgid "" @@ -1391,7 +1392,7 @@ msgstr "Fuso orario" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "Se vuoi cambiare la tua password, inseriscine una nuova e confermala inserendola di nuovo." +msgstr "Se vuoi cambiare la tua password, inseriscine una nuova e confermala digitandola di nuovo." #: template/account_edit_form.php msgid "Re-type password" @@ -1409,7 +1410,7 @@ msgstr "Chiave pubblica SSH" #: template/account_edit_form.php msgid "Notification settings" -msgstr "Impostazioni notifiche" +msgstr "Impostazioni delle notifiche" #: template/account_edit_form.php msgid "Notify of new comments" @@ -1421,11 +1422,11 @@ msgstr "Notifica degli aggiornamenti dei pacchetti" #: template/account_edit_form.php msgid "Notify of ownership changes" -msgstr "Notifica cambiamenti di proprietà" +msgstr "Notifica dei cambiamenti di proprietà" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "Per confermare le modifiche al profilo, per favore inserisci la tua password:" +msgstr "Per confermare le modifiche al profilo, inserisci la tua password:" #: template/account_edit_form.php msgid "Your current password" @@ -1499,7 +1500,7 @@ msgstr "Salva" #: template/flag_comment.php #, php-format msgid "Flagged Out-of-Date Comment: %s" -msgstr "Commento per la marcatura come Non Aggiornato: %s" +msgstr "Commento per la segnalazione come Non Aggiornato: %s" #: template/flag_comment.php #, php-format @@ -1513,7 +1514,7 @@ msgstr "%s%s%s non è segnalato come non aggiornato." #: template/flag_comment.php msgid "Return to Details" -msgstr "Ritorna ai Dettagli" +msgstr "Ritorna ai dettagli" #: template/footer.php #, php-format @@ -1583,6 +1584,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d richiesta in attesa" msgstr[1] "%d richieste in attesa" +msgstr[2] "%d richieste in attesa" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1654,7 +1656,7 @@ msgstr "Aggiungi un commento" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "Gli identificatori dei commit Git nel repository dei pacchetti AUR e le URL vengono convertite automaticamente in link." +msgstr "Gli identificatori dei commit Git nel repository dei pacchetti AUR e le URL vengono convertiti automaticamente in link." #: template/pkg_comment_form.php #, php-format @@ -1663,7 +1665,7 @@ msgstr "La %ssintassi Markdown%s è parzialmente supportata." #: template/pkg_comments.php msgid "Pinned Comments" -msgstr "Elimina i commenti" +msgstr "Commenti in evidenza" #: template/pkg_comments.php msgid "Latest Comments" @@ -1676,7 +1678,7 @@ msgstr "Commenti per" #: template/pkg_comments.php #, php-format msgid "%s commented on %s" -msgstr "%s ha commentato su %s" +msgstr "%s ha commentato il %s" #: template/pkg_comments.php #, php-format @@ -1686,27 +1688,27 @@ msgstr "Commento anonimo su %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "Ha commentato sul pacchetto %s su %s" +msgstr "Ha commentato sul pacchetto %s il %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" -msgstr "Eliminato su %s da %s" +msgstr "eliminato il %s da %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s" -msgstr "cancellato su %s" +msgstr "eliminato il %s" #: template/pkg_comments.php #, php-format msgid "edited on %s by %s" -msgstr "modificato su %s da %s" +msgstr "modificato il %s da %s" #: template/pkg_comments.php #, php-format msgid "edited on %s" -msgstr "modificato su %s" +msgstr "modificato il %s" #: template/pkg_comments.php msgid "Undelete comment" @@ -1829,7 +1831,7 @@ msgid "" "By submitting a deletion request, you ask a Trusted User to delete the " "package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Inserendo una richiesta di cancellazione, stai chiedendo ad un Trusted User di cancellare il pacchetto base. Questo tipo di richiesta dovrebbe essere usato per duplicati, software abbandonati dall'autore, sotware illegalmente distribuiti o pacchetti irreparabili." +msgstr "Inserendo una richiesta di cancellazione, stai chiedendo ad un Trusted User di cancellare il pacchetto base. Questo tipo di richiesta dovrebbe essere usata per i duplicati, per software abbandonati dall'autore, per sotware illegalmente distribuiti oppure per pacchetti irreparabili." #: template/pkgreq_form.php msgid "" @@ -1845,7 +1847,7 @@ msgid "" "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Inserendo una richiesta di abbandono, stai chiedendo ad un Trusted User di rimuovere la proprietà del pacchetto base. Per favore procedi soltanto se il pacchetto necessita di manutenzione, il manutentore attuale non risponde, e hai già provato a contattarlo precedentemente." +msgstr "Inserendo una richiesta di abbandono, stai chiedendo ad un Trusted User di rimuovere la proprietà del pacchetto base. Procedi soltanto se il pacchetto necessita di manutenzione, se il manutentore attuale non risponde e se hai già provato a contattarlo precedentemente." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1857,6 +1859,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "È stato trovato %d pacchetto." msgstr[1] "Sono stati trovati %d pacchetti." +msgstr[2] "Sono stati trovati %d pacchetti." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1881,6 +1884,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d giorno rimanente" msgstr[1] "~%d giorni rimanenti" +msgstr[2] "~%d giorni rimanenti" #: template/pkgreq_results.php #, php-format @@ -1888,6 +1892,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "~%d ora rimanente" msgstr[1] "~%d ore rimanenti" +msgstr[2] "~%d ore rimanenti" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2016,6 +2021,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "È stato trovato %d pacchetto." msgstr[1] "Sono stati trovati %d pacchetti." +msgstr[2] "Sono stati trovati %d pacchetti." #: template/pkg_search_results.php msgid "Version" @@ -2026,7 +2032,7 @@ msgstr "Versione" msgid "" "Popularity is calculated as the sum of all votes with each vote being " "weighted with a factor of %.2f per day since its creation." -msgstr "La popolarità è calcolata come somma di tutti i voti pesati con un fattore di %.2f al giorno, dalla sua creazione." +msgstr "La popolarità è calcolata come somma di tutti i voti ponderati con un fattore di %.2f al giorno dalla sua creazione." #: template/pkg_search_results.php template/tu_details.php #: template/tu_list.php @@ -2172,7 +2178,7 @@ msgstr "Precedente" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "Ripristino Password di AUR" +msgstr "Ripristino della password di AUR" #: scripts/notify.py #, python-brace-format @@ -2180,18 +2186,18 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "È stata inviata una richiesta per ripristinare la password dell'account {user} associato al tuo indirizzo e-mail. Se desideri ripristinare la tua password, clicca sul link [1] sottostante, altrimenti ignora questo messaggio e non succederà nulla." +msgstr "È stata inviata una richiesta per ripristinare la password dell'account {user} associato al tuo indirizzo email. Se desideri ripristinare la tua password, clicca sul link [1] sottostante, altrimenti ignora questo messaggio e non succederà nulla." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "Benvenuto nel Arch User Repository" +msgstr "Benvenuto nell' Arch User Repository" #: scripts/notify.py msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "Benvenuto nel Arch User Repository! Per impostare una password iniziale per il tuo nuovo account, per favore segui il collegamento [1] sottostante. Se il collegamento non funziona, prova a copiarlo e incollarlo nel tuo browser." +msgstr "Benvenuto nell' Arch User Repository! Per impostare una password iniziale per il tuo nuovo account, clicca sul link [1] sottostante. Se il link non funzionasse, prova a copiarlo e ad incollarlo nella barra degli indirizzi del tuo browser." #: scripts/notify.py #, python-brace-format @@ -2208,12 +2214,12 @@ msgstr "{user} [1] ha commentato su {pkgbase} [2]:" msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "Se non vuoi più ricevere notifiche su questo pacchetto, per favore vai alla pagina del pacchetto [2] e seleziona \"{label}\"." +msgstr "Se non vuoi più ricevere notifiche su questo pacchetto, vai alla pagina del pacchetto [2] e seleziona \"{label}\"." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "Aggiornamento pachetto base: {pkgbase}" +msgstr "Aggiornamento del pacchetto base: {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2223,7 +2229,7 @@ msgstr "{user} [1] ha inviato un nuovo commit su {pkgbase} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "Notifica AUR per pacchetto {pkgbase} non aggiornato" +msgstr "Notifica AUR per il pacchetto {pkgbase} non aggiornato" #: scripts/notify.py #, python-brace-format @@ -2233,7 +2239,7 @@ msgstr "Il tuo pacchetto {pkgbase} [1] è stato marcato come non aggiornato dall #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "Notifica AUR di proprietà per pacchetto {pkgbase} " +msgstr "Notifica AUR di proprietà del pacchetto {pkgbase} " #: scripts/notify.py #, python-brace-format @@ -2248,7 +2254,7 @@ msgstr "Il pacchetto {pkgbase} [1] è stato abbandonato da {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "Notifica AUR di co-manutenzione per pacchetto {pkgbase} " +msgstr "Notifica AUR di co-manutenzione per il pacchetto {pkgbase} " #: scripts/notify.py #, python-brace-format @@ -2272,7 +2278,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] ha unito {old} [2] in {new} [3].\n\n-- \nSe non vuoi più ricevere notifiche sul nuovo pacchetto, per favore vai a [3] e clicca su \"{label}\"." +msgstr "{user} [1] ha unito {old} [2] in {new} [3].\n\n-- \nSe non desideri più ricevere notifiche sul nuovo pacchetto, vai a [3] e clicca su \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2280,7 +2286,7 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "{user} [1] ha eliminato {pkgbase} [2].\n\nNon riceverai più notifiche su questo pacchetto." +msgstr "{user} [1] ha eliminato {pkgbase} [2].\n\nNon riceverai più notifiche per questo pacchetto." #: scripts/notify.py #, python-brace-format @@ -2292,19 +2298,19 @@ msgstr "Promemoria per voto TU: Proposta {id}" msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." -msgstr "Per favore ricordati di votare sulla proposta {id} [1]. La finestra di voto si chiude fra meno di 48 ore." +msgstr "Ricordati di votare la proposta di {id} [1]. La finestra di voto si chiude fra meno di 48 ore." #: aurweb/routers/accounts.py msgid "Invalid account type provided." -msgstr "Tipo di account non valido." +msgstr "L' account fornito non è valido." #: aurweb/routers/accounts.py msgid "You do not have permission to change account types." -msgstr "Non hai il permesso per cambiare il tipo di account." +msgstr "Non hai il permesso per modificare il tipo di account." #: aurweb/routers/accounts.py msgid "You do not have permission to change this user's account type to %s." -msgstr "Non hai il permesso per cambiare il tipo di account di questo utente in %s." +msgstr "Non hai il permesso per modificare il tipo di account di questo utente in %s." #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." @@ -2322,19 +2328,19 @@ msgstr "Si è verificato un errore irreversibile." msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "I dettagli sono stati registrati e verranno visionati da postmaster velocemente. Ci scusiamo per l'inconvenienza che questo possa aver causato." +msgstr "I dettagli sono stati registrati e verranno visionati al più presto dal postmaster. Ci scusiamo per gli eventuali disagi causati." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "Errore server AUR" +msgstr "Errore del server di AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Commenti relativi alla richiesta di chiusura del pacchetto..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Questa azione chiuderà tutte le richieste in sospeso dei pacchetti ad essa correlate. Se %scommenti%s vengono omessi, verrà generato automaticamente un commento di chiusura." From 08af8cad8d2c085770633a11198e4acd7a2774f1 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:29 -0800 Subject: [PATCH 1231/1451] update-ja translations --- po/ja.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/po/ja.po b/po/ja.po index 280edb46..40349f28 100644 --- a/po/ja.po +++ b/po/ja.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # kusakata, 2013 # kusakata, 2013 @@ -10,10 +10,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: kusakata, 2013-2018,2020-2022\n" "Language-Team: Japanese (http://www.transifex.com/lfleischer/aurweb/language/ja/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2325,10 +2325,10 @@ msgstr "AUR サーバーエラー" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "関連するパッケージリクエストの取り消しコメント..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "このアクションは関連するパッケージリクエストをすべて取り消します。%sコメント%sを省略した場合、自動的にコメントが生成されます。" From e6d36101d9f26f7e71570bd02961b3ed3a21fa3c Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:33 -0800 Subject: [PATCH 1232/1451] update-ko translations --- po/ko.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/ko.po b/po/ko.po index 6da57759..a4c694c9 100644 --- a/po/ko.po +++ b/po/ko.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Korean (http://www.transifex.com/lfleischer/aurweb/language/ko/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From e5137e0c4297a82bfe420228a38465fb396a34eb Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:36 -0800 Subject: [PATCH 1233/1451] update-lt translations --- po/lt.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/lt.po b/po/lt.po index c9f55632..627fefd0 100644 --- a/po/lt.po +++ b/po/lt.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Lithuanian (http://www.transifex.com/lfleischer/aurweb/language/lt/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From d20dbbcf7419c8b76eb338384467172db4af9189 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:39 -0800 Subject: [PATCH 1234/1451] update-nb translations --- po/nb.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/nb.po b/po/nb.po index 307a80d6..b503de85 100644 --- a/po/nb.po +++ b/po/nb.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Alexander F. Rødseth , 2015,2017-2019 # Alexander F. Rødseth , 2011,2013-2014 @@ -12,10 +12,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Alexander F. Rødseth , 2015,2017-2019\n" "Language-Team: Norwegian Bokmål (http://www.transifex.com/lfleischer/aurweb/language/nb/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 57a2b4b516a43a33182399b8fdaa4473cfa91e6f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:43 -0800 Subject: [PATCH 1235/1451] update-nb_NO translations --- po/nb_NO.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/nb_NO.po b/po/nb_NO.po index 5d958172..49d2eccf 100644 --- a/po/nb_NO.po +++ b/po/nb_NO.po @@ -1,17 +1,17 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Kim Nordmo , 2017,2019 # Lukas Fleischer , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Kim Nordmo , 2017,2019\n" "Language-Team: Norwegian Bokmål (Norway) (http://www.transifex.com/lfleischer/aurweb/language/nb_NO/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 05c6266986ac6652a1755a89f229427195a7305d Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:46 -0800 Subject: [PATCH 1236/1451] update-nl translations --- po/nl.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/po/nl.po b/po/nl.po index 54519d21..d23fe04a 100644 --- a/po/nl.po +++ b/po/nl.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Heimen Stoffels , 2021-2022 # Heimen Stoffels , 2015,2021 @@ -13,10 +13,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Heimen Stoffels , 2021-2022\n" "Language-Team: Dutch (http://www.transifex.com/lfleischer/aurweb/language/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2333,10 +2333,10 @@ msgstr "AUR-serverfout" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Gerelateerde pakketverzoekreacties…" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Met deze actie sluit u elk gerelateerd openstaand verzoek. Als %s reacties%s genegeerd worden, dan wordt er een automatische afsluitreactie geplaatst." From e572b86fd3d2acf041c0882ba669ad6a5bcfac0f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:50 -0800 Subject: [PATCH 1237/1451] update-pl translations --- po/pl.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/po/pl.po b/po/pl.po index 94a6fb67..97c7d730 100644 --- a/po/pl.po +++ b/po/pl.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Bartłomiej Piotrowski , 2011 # Bartłomiej Piotrowski , 2014 @@ -13,16 +13,16 @@ # marcin mikołajczak , 2017 # Michal T , 2016 # Nuc1eoN , 2014 -# Piotr Strębski , 2017-2018 +# Piotr Strębski , 2017-2018,2022 # Piotr Strębski , 2013-2016 # Przemyslaw Ka. , 2021 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Piotr Strębski , 2017-2018,2022\n" "Language-Team: Polish (http://www.transifex.com/lfleischer/aurweb/language/pl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -712,7 +712,7 @@ msgstr "Zasady korzystania" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "" +msgstr "Zaktualizowano następujące dokumenty. Przejrzyj je dokładnie:" #: html/tos.php #, php-format @@ -792,7 +792,7 @@ msgstr "Może zawierać tylko jedną kropkę, podkreślnik lub myślnik." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Potwierdź nowe hasło." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -800,7 +800,7 @@ msgstr "Adres e-mail jest nieprawidłowy." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Zapasowy adres e-mail jest nieprawidłowy." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -824,7 +824,7 @@ msgstr "Język nie jest obecnie obsługiwany." #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." -msgstr "" +msgstr "Strefa czasowa nie jest obecnie obsługiwana." #: lib/acctfuncs.inc.php #, php-format From 6ee7598211d5358cf94bf4b8936f486b439add45 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:53 -0800 Subject: [PATCH 1238/1451] update-pt translations --- po/pt.po | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/po/pt.po b/po/pt.po index aed32031..05778859 100644 --- a/po/pt.po +++ b/po/pt.po @@ -1,22 +1,22 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Lukas Fleischer , 2011\n" "Language-Team: Portuguese (http://www.transifex.com/lfleischer/aurweb/language/pt/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -1579,6 +1579,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1853,6 +1854,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1877,6 +1879,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php #, php-format @@ -1884,6 +1887,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2012,6 +2016,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkg_search_results.php msgid "Version" From bb00a4ecfde887741f1bab5b8f71e902e5fee252 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:09:56 -0800 Subject: [PATCH 1239/1451] update-pt_BR translations --- po/pt_BR.po | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/po/pt_BR.po b/po/pt_BR.po index d29a9448..6bc6a596 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Albino Biasutti Neto Bino , 2011 # Fábio Nogueira , 2016 @@ -13,16 +13,16 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Rafael Fontenelle , 2011,2015-2018,2020-2022\n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/lfleischer/aurweb/language/pt_BR/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt_BR\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -1585,6 +1585,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d requisição pendente" msgstr[1] "%d requisições pendentes" +msgstr[2] "%d requisições pendentes" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1859,6 +1860,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "%d requisição de pacote encontrada." msgstr[1] "%d requisições de pacotes encontradas." +msgstr[2] "%d requisições de pacotes encontradas." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1883,6 +1885,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d dia restante" msgstr[1] "~%d dias restantes" +msgstr[2] "~%d dias restantes" #: template/pkgreq_results.php #, php-format @@ -1890,6 +1893,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "~%d hora restante" msgstr[1] "~%d horas restantes" +msgstr[2] "~%d horas restantes" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2018,6 +2022,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d pacote encontrado." msgstr[1] "%d pacotes encontrados." +msgstr[2] "%d pacotes encontrados." #: template/pkg_search_results.php msgid "Version" @@ -2333,10 +2338,10 @@ msgstr "Erro do Servidor AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Comentários relacionados ao fechamento de requisição de pacote..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Esta ação fechará todas as requisições de pacote pendentes relacionadas a ela. Se %sComentários%s for omitido, um comentário de encerramento será gerado automaticamente." From e7bcf2fc9786afcb761e98ec5d6cde0b6efa9396 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:00 -0800 Subject: [PATCH 1240/1451] update-pt_PT translations --- po/pt_PT.po | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/po/pt_PT.po b/po/pt_PT.po index 7f6ea67a..5d2ff7de 100644 --- a/po/pt_PT.po +++ b/po/pt_PT.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Christophe Silva , 2018 # Gaspar Santos , 2011 @@ -12,16 +12,16 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Christophe Silva , 2018\n" "Language-Team: Portuguese (Portugal) (http://www.transifex.com/lfleischer/aurweb/language/pt_PT/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt_PT\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -1584,6 +1584,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d pedido por atender" msgstr[1] "%d pedidos por atender" +msgstr[2] "%d pedidos por atender" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1858,6 +1859,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "%d pedido de pacote encontrado." msgstr[1] "%d pedidos de pacotes encontrados." +msgstr[2] "%d pedidos de pacotes encontrados." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1882,6 +1884,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php #, php-format @@ -1889,6 +1892,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2017,6 +2021,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d pacote encontrado." msgstr[1] "%d pacotes encontrados." +msgstr[2] "%d pacotes encontrados." #: template/pkg_search_results.php msgid "Version" From fa20a3b5d81cd7554da6b1cd1ca52ddb76681b43 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:03 -0800 Subject: [PATCH 1241/1451] update-ro translations --- po/ro.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/ro.po b/po/ro.po index 4409b698..ecee97fd 100644 --- a/po/ro.po +++ b/po/ro.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Arthur Țițeică , 2013-2015 # Lukas Fleischer , 2011 @@ -9,10 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Arthur Țițeică , 2013-2015\n" "Language-Team: Romanian (http://www.transifex.com/lfleischer/aurweb/language/ro/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From ebae0d43045de2f38a2b9e09d7e847b044fc05f9 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:06 -0800 Subject: [PATCH 1242/1451] update-ru translations --- po/ru.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/ru.po b/po/ru.po index 44f000dd..4a8a18f7 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Alex , 2021 # Evgeniy Alekseev , 2014-2015 @@ -18,10 +18,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Kevin Morris , 2021\n" "Language-Team: Russian (http://www.transifex.com/lfleischer/aurweb/language/ru/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 8ee843b7b1f00e18b42f43984bf57b8d35dad695 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:10 -0800 Subject: [PATCH 1243/1451] update-sk translations --- po/sk.po | 62 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/po/sk.po b/po/sk.po index 853fc198..ca124981 100644 --- a/po/sk.po +++ b/po/sk.po @@ -1,18 +1,18 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # archetyp , 2013-2016 -# Jose Riha , 2018 +# Jose Riha , 2018,2022 # Matej Ľach , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Jose Riha , 2018,2022\n" "Language-Team: Slovak (http://www.transifex.com/lfleischer/aurweb/language/sk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -77,7 +77,7 @@ msgstr "Nemáte potrebné oprávnenia, pre úpravu tohoto účtu. " #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Neplatné heslo." #: html/account.php msgid "Use this form to search existing accounts." @@ -167,7 +167,7 @@ msgstr "Editovať komentár" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Nástenka" #: html/home.php template/header.php msgid "Home" @@ -175,11 +175,11 @@ msgstr "Domov" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "Moje označené balíčky" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Moje požiadavky" #: html/home.php msgid "My Packages" @@ -187,15 +187,15 @@ msgstr "Moje balíčky" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Hľadať balíčky, ktoré spravujem" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Spoločne spravované balíčky" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Hľadať balíčky, v ktorých pôsobím ako spolupracovník" #: html/home.php #, php-format @@ -239,7 +239,7 @@ msgstr "Podpora" #: html/home.php msgid "Package Requests" -msgstr "Žiadosti ohľadom balíčkov" +msgstr "Žiadosti týkajúce sa balíčkov" #: html/home.php #, php-format @@ -325,7 +325,7 @@ msgid "" "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" " %sonly%s. To report packaging bugs contact the package maintainer or leave " "a comment on the appropriate package page." -msgstr "Ak nájdete chybu vo webovom rozhradní AUR, pošlite prosím správu o chybe na náš %sbug tracker%s. Posielajte sem %slen%s chyby webového rozhrania AUR. Pre nahlásenie chýb balíčkov kontaktujte správcu balíčka alebo zanechate komentár na príslušnej stránke balíčka." +msgstr "Ak nájdete chybu vo webovom rozhraní AUR, pošlite prosím správu o chybe na náš %sbug tracker%s. Posielajte sem %slen%s chyby webového rozhrania AUR. Pre nahlásenie chýb balíčkov kontaktujte správcu balíčka alebo zanechate komentár na príslušnej stránke balíčka." #: html/home.php msgid "Package Search" @@ -374,7 +374,7 @@ msgstr "Zadajte prihlasovacie údaje" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Meno používateľa alebo primárna e-mailová adresa" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -438,7 +438,7 @@ msgstr "Heslo bolo úspešne obnovené." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Potvrďte vaše meno používateľa alebo primárnu e-mailovú adresu:" #: html/passreset.php msgid "Enter your new password:" @@ -707,7 +707,7 @@ msgstr "" #: html/tos.php #, php-format msgid "revision %d" -msgstr "" +msgstr "revízia: %d" #: html/tos.php msgid "I accept the terms and conditions above." @@ -790,7 +790,7 @@ msgstr "E-mailová adresa nie je platná." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Záložná e-mailová adresa nie je platná." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -1256,7 +1256,7 @@ msgstr "PGP otlačok kľúča" #: template/account_details.php template/account_search_results.php #: template/pkgreq_results.php msgid "Status" -msgstr "Status" +msgstr "Stav" #: template/account_details.php msgid "Inactive since" @@ -1520,7 +1520,7 @@ msgstr "Copyright %s 2004-%d aurweb Development Team." #: template/header.php msgid " My Account" -msgstr "Môj účet" +msgstr " Môj účet" #: template/pkgbase_actions.php msgid "Package Actions" @@ -1814,7 +1814,7 @@ msgstr "Typ žiadosti" #: template/pkgreq_form.php msgid "Deletion" -msgstr "Vymazanie" +msgstr "Vymazať" #: template/pkgreq_form.php msgid "Orphan" @@ -1855,10 +1855,10 @@ msgstr "" #, php-format msgid "%d package request found." msgid_plural "%d package requests found." -msgstr[0] "Bola nájdená %d požiadavka ohľadom balíčkov." -msgstr[1] "Boli nájdené %d požiadavky ohľadom balíčkov." -msgstr[2] "Bolo nájdených %d požiadaviek ohľadom balíčkov." -msgstr[3] "Bolo nájdených %d požiadaviek ohľadom balíčkov." +msgstr[0] "Bola nájdená %d požiadavka týkajúc sa balíčka." +msgstr[1] "Boli nájdené %d požiadavky týkajúcich sa balíčkov." +msgstr[2] "Bolo nájdených %d požiadaviek týkajúcich sa balíčkov." +msgstr[3] "Bolo nájdených %d požiadaviek týkajúcich sa balíčkov." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1986,7 +1986,7 @@ msgstr "Vyhľadávať podľa" #: template/pkg_search_form.php template/stats/user_table.php msgid "Out of Date" -msgstr "Neaktuálny" +msgstr "Neaktuálne" #: template/pkg_search_form.php template/search_accounts_form.php msgid "Sort by" @@ -2216,7 +2216,7 @@ msgstr "" msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Ak si už viac neželáte dostávať upozornenia na tento balíček, prejdite prosím na stránku balíčku [2] a vyberte \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2280,7 +2280,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] zlúčil(a) {old} [2] do {new} [3].\n\n-- \nAk si už viac neželáte dostávať upozornenia na tento balíček, prejdite prosím na stránku balíčku [2] a vyberte \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2288,7 +2288,7 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] odstránil(a) {pkgbase} [2].\n\nUpozornenia na tento balíček už viac nebudete dostávať." #: scripts/notify.py #, python-brace-format @@ -2339,10 +2339,10 @@ msgstr "" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Súvisiace komentáre k žiadosti o uzatvorenie balíčka..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Táto operácia uzavrie všetky súvisiace nevybavené žiadosti balíčkov. Ak %sComments%s vynecháte, použije sa automaticky generovaný komentár." From 46c925bc82722c35c7a0d55c5135e4174c8ec94f Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:13 -0800 Subject: [PATCH 1244/1451] update-sr translations --- po/sr.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/sr.po b/po/sr.po index 426ce599..4054d7df 100644 --- a/po/sr.po +++ b/po/sr.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 # Slobodan Terzić , 2011-2012,2015-2017 @@ -9,10 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Slobodan Terzić , 2011-2012,2015-2017\n" "Language-Team: Serbian (http://www.transifex.com/lfleischer/aurweb/language/sr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 8592bada16bec50a167b5c81ede867d5c8bc7b43 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:17 -0800 Subject: [PATCH 1245/1451] update-sr_RS translations --- po/sr_RS.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/sr_RS.po b/po/sr_RS.po index b7560965..a924dc4c 100644 --- a/po/sr_RS.po +++ b/po/sr_RS.po @@ -1,16 +1,16 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Nikola Stojković , 2013 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Nikola Stojković , 2013\n" "Language-Team: Serbian (Serbia) (http://www.transifex.com/lfleischer/aurweb/language/sr_RS/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 5609ddf791192a1d4b2d9a37b4af6d68b78b2839 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:20 -0800 Subject: [PATCH 1246/1451] update-sv_SE translations --- po/sv_SE.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/sv_SE.po b/po/sv_SE.po index 4887fdde..6abb8452 100644 --- a/po/sv_SE.po +++ b/po/sv_SE.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Johannes Löthberg , 2015-2016 # Kevin Morris , 2022 From b36cbd526b7cd6203401f30e11a3f6715725b9b5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:23 -0800 Subject: [PATCH 1247/1451] update-tr translations --- po/tr.po | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/po/tr.po b/po/tr.po index 559a0008..b36c04f4 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # tarakbumba , 2011,2013-2015 # tarakbumba , 2012,2014 -# Demiray Muhterem , 2015,2020-2021 +# Demiray Muhterem , 2015,2020-2022 # Koray Biçer , 2020 # Lukas Fleischer , 2011 # Samed Beyribey , 2012 @@ -15,10 +15,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Demiray Muhterem , 2015,2020-2022\n" "Language-Team: Turkish (http://www.transifex.com/lfleischer/aurweb/language/tr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2316,29 +2316,29 @@ msgstr "%s için kabul edilecek sahipsize gereksinim yok." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "İç Sunucu Hatası" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Önemli bir hata oluştu." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Ayrıntılar günlüğe kaydedildi ve posta yöneticisi tarafından gözden geçirilecek. Bunun neden olabileceği rahatsızlıktan dolayı özür dileriz." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR Sunucu Hatası" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "İlgili paket isteği kapatma yorumları..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Bu eylem, kendisiyle ilgili bekleyen paket isteklerini kapatacaktır. %s Yorum %s atlanırsa, bir kapatma yorumu otomatik olarak oluşturulur." From 4cff1e500bd3491947a90b4559d7eac40e1f24fc Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:27 -0800 Subject: [PATCH 1248/1451] update-uk translations --- po/uk.po | 110 +++++++++++++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/po/uk.po b/po/uk.po index 3bffe4f6..13f3ab90 100644 --- a/po/uk.po +++ b/po/uk.po @@ -1,21 +1,21 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Lukas Fleischer , 2011 # Rax Garfield , 2012 # Rax Garfield , 2012 # Vladislav Glinsky , 2019 -# Yarema aka Knedlyk , 2011-2018 +# Yarema aka Knedlyk , 2011-2018,2022 # Данило Коростіль , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Yarema aka Knedlyk , 2011-2018,2022\n" "Language-Team: Ukrainian (http://www.transifex.com/lfleischer/aurweb/language/uk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -80,7 +80,7 @@ msgstr "У вас недостатньо прав для редагування #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Неправильний пароль" #: html/account.php msgid "Use this form to search existing accounts." @@ -377,7 +377,7 @@ msgstr "Увійдіть, ввівши облікові дані." #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Назва користувача або адреса електронної пошти" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -441,7 +441,7 @@ msgstr "Ваш пароль успішно скинуто." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Підтвердити назву користувача або адреса електронної пошти:" #: html/passreset.php msgid "Enter your new password:" @@ -460,11 +460,11 @@ msgstr "Продовжити" msgid "" "If you have forgotten the user name and the primary e-mail address you used " "to register, please send a message to the %saur-general%s mailing list." -msgstr "" +msgstr "Якщо Ви забули назву користувача і адресу електронної пошти, використану при реєстрації, зверніться до списку розсилання %saur-general%s." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Введіть назву користувача або адресу електронної пошти:" #: html/pkgbase.php msgid "Package Bases" @@ -480,7 +480,7 @@ msgstr "Вибрані пакунки все ще мають власника, msgid "" "The selected packages have not been adopted, check the confirmation " "checkbox." -msgstr "" +msgstr "Обрані пакунки не прийнято, перевірте, чи поставлено галочку в полі підтвердження." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -586,7 +586,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "" +msgstr "Здається, це пакет VCS. Будь ласка, %sне%s позначайте його як застарілий, якщо версія пакета в AUR не відповідає останньому коміту. Позначити цей пакунок слід лише в тому випадку, якщо джерела переміщено або потрібні зміни в PKGBUILD в зв'язку з останніми змінами." #: html/pkgflag.php #, php-format @@ -785,7 +785,7 @@ msgstr "Може містити тільки один період, підкре #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Підтвердіть новий пароль, будь ласка." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -793,7 +793,7 @@ msgstr "Адреса електронної пошти неправильна." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Неправильна адреса електронної пошти для відновлення." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -836,15 +836,15 @@ msgstr "Публічний ключ SSH, %s%s%s, вже використовує #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "Пропущено CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "Термін дії цієї CAPTCHA закінчився. Будь ласка, спробуйте ще раз." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "Введена відповідь CAPTCHA недійсна." #: lib/acctfuncs.inc.php #, php-format @@ -886,7 +886,7 @@ msgstr "Обліковий запис вилучено" #: aurweb/routers/accounts.py msgid "You do not have permission to suspend accounts." -msgstr "" +msgstr " \nВи не маєте дозволу на призупинення облікових записів." #: lib/acctfuncs.inc.php #, php-format @@ -975,27 +975,27 @@ msgstr "Інформації про пакунок не знайдено." #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "Поганий заголовок Referer." #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." -msgstr "" +msgstr "Ви не вибрали жодних пакунків, про які потрібно сповіщати." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been enabled." -msgstr "" +msgstr "Сповіщення для вибраних пакунків увімкнено." #: aurweb/routers/packages.py msgid "You did not select any packages for notification removal." -msgstr "" +msgstr "Ви не вибрали жодних пакунків для видалення сповіщень." #: aurweb/routers/packages.py msgid "A package you selected does not have notifications enabled." -msgstr "" +msgstr "У вибраному вами пакунку не ввімкнено сповіщення." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been removed." -msgstr "" +msgstr "Сповіщення для вибраних пакунків видалено." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -1035,7 +1035,7 @@ msgstr "Не вибрано жодного пакунку для вилучен #: aurweb/routers/packages.py msgid "One of the packages you selected does not exist." -msgstr "" +msgstr "Один із вибраних Вами пакунків не існує." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." @@ -1047,7 +1047,7 @@ msgstr "Для перейняття пакунків слід увійти." #: aurweb/routers/package.py msgid "You are not allowed to adopt one of the packages you selected." -msgstr "" +msgstr "У Вас немає дозволу прийняти один з вибраних Вами пакунків." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." @@ -1055,7 +1055,7 @@ msgstr "Для зречення пакунків слід увійти." #: aurweb/routers/packages.py msgid "You are not allowed to disown one of the packages you selected." -msgstr "" +msgstr "У Вас немає дозволу відмовитися від одного з вибраних Вами пакунків" #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." @@ -1297,7 +1297,7 @@ msgstr "Редагувати обліковий запис цього корис #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Показати коментарі цього користувача" #: template/account_edit_form.php #, php-format @@ -1312,7 +1312,7 @@ msgstr "Клацніть %sтут%s, щоб дізнатися більше пр #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Натисніть %sтут%s, щоб показати коментарі, зроблені цим обліковим записом." #: template/account_edit_form.php msgid "required" @@ -1355,30 +1355,30 @@ msgid "" "If you do not hide your email address, it is visible to all registered AUR " "users. If you hide your email address, it is visible to members of the Arch " "Linux staff only." -msgstr "" +msgstr "Якщо ви не приховаєте свою адресу електронної пошти, тоді її можуть бачити всі зареєстровані користувачі AUR. Якщо Ви приховаєте свою адресу електронної пошти, тоді її зможуть бачити лише співробітники Arch Linux." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Резервна адреса електронної пошти" #: template/account_edit_form.php msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "" +msgstr "За бажанням вкажіть додаткову адресу електронної пошти, яку можна використовувати для відновлення облікового запису на випадок втрати доступу до своєї основної електронної адреси." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Посилання для скидання пароля завжди надсилаються як на вашу основну, так і на резервну адресу електронної пошти." #: template/account_edit_form.php #, php-format msgid "" "Your backup email address is always only visible to members of the Arch " "Linux staff, independent of the %s setting." -msgstr "" +msgstr "Вашу резервну електронну адресу завжди бачать лише співробітники Arch Linux, незалежно від налаштувань. %s ." #: template/account_edit_form.php msgid "Language" @@ -1392,7 +1392,7 @@ msgstr "Часова зона" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Якщо Ви бажаєте змінити пароль, введіть новий пароль і підтвердьте новий пароль, ввівши його ще раз." #: template/account_edit_form.php msgid "Re-type password" @@ -1426,21 +1426,21 @@ msgstr "Сповіщення про зміну власника" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Щоб підтвердити зміни профілю, введіть поточний пароль:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Ваш поточний пароль" #: template/account_edit_form.php msgid "" "To protect the AUR against automated account creation, we kindly ask you to " "provide the output of the following command:" -msgstr "" +msgstr "Щоб захистити AUR від автоматичного створення облікового запису, ми просимо Вас надати результат такої команди:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Відповідь" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1605,7 +1605,7 @@ msgstr "тільки для читання" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "натисніть, щоб скопіювати" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1657,12 +1657,12 @@ msgstr "Додати коментар" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Відповідні відсилачі комітів до ідентифікаторів комітів Git в сховищі пакунків AUR та URL-адреси автоматично перетворюються на посилання." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "%sСинтакс Markdown%s підтримується частково." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1674,7 +1674,7 @@ msgstr "Останні коментарі" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Коментарі для" #: template/pkg_comments.php #, php-format @@ -1689,7 +1689,7 @@ msgstr "Анонімний коментар про %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Коментовано пакунок %s з датою %s" #: template/pkg_comments.php #, php-format @@ -2283,7 +2283,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] з'єднав {old} [2] до {new} [3].\n\n-- \nЯкщо Ви не бажаєте більше отримувати сповіщення про новий пакунок, перейдіть на сторінку [3] і натисніть \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2307,45 +2307,45 @@ msgstr "Будь ласка, не забудьте подати свій гол #: aurweb/routers/accounts.py msgid "Invalid account type provided." -msgstr "" +msgstr "Вказано недійсний тип облікового запису." #: aurweb/routers/accounts.py msgid "You do not have permission to change account types." -msgstr "" +msgstr "У Вас немає дозволу змінювати типи облікових записів." #: aurweb/routers/accounts.py msgid "You do not have permission to change this user's account type to %s." -msgstr "" +msgstr "Ви не маєте дозволу змінити тип облікового запису цього користувача на %s." #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "Немає наявних запитів на прийняття для %s." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Внутрішня помилка сервера" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Сталася фатальна помилка." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Подробиці зареєстровані та будуть переглянуті поштмейстером posthaste. Просимо вибачення за можливі незручності." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "Помилка сервера AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Пов'язані коментарі щодо закриття запиту на пакунок..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Ця дія закриє всі запити на пакет, що очікують на розгляд. Якщо %sКоментарі%s пропущено, тоді буде автоматично згенеровано коментар закриття." From 2770952dfbaf2bf819d3670e885990f73da35078 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:30 -0800 Subject: [PATCH 1249/1451] update-vi translations --- po/vi.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/vi.po b/po/vi.po index 87f7faac..a71c9ed5 100644 --- a/po/vi.po +++ b/po/vi.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Vietnamese (http://www.transifex.com/lfleischer/aurweb/language/vi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From ef0e3b9f357a34577eeeb49bd32162ff12f8af62 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:33 -0800 Subject: [PATCH 1250/1451] update-zh translations --- po/zh.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/zh.po b/po/zh.po index c932df9c..77f31fe4 100644 --- a/po/zh.po +++ b/po/zh.po @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" "Language-Team: Chinese (http://www.transifex.com/lfleischer/aurweb/language/zh/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From 65d364fe9066e19f2a0c1dbad50642e9ed680096 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:36 -0800 Subject: [PATCH 1251/1451] update-zh_CN translations --- po/zh_CN.po | 93 +++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/po/zh_CN.po b/po/zh_CN.po index 675d15a3..a61781fb 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1,13 +1,14 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # Feng Chao , 2015-2016 # dongfengweixiao , 2015 # dongfengweixiao , 2015 # Felix Yan , 2014,2021 # Feng Chao , 2012,2021 +# lakejason0 , 2022 # Lukas Fleischer , 2011 # pingplug , 2017-2018 # Feng Chao , 2012 @@ -17,10 +18,10 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: lakejason0 , 2022\n" "Language-Team: Chinese (China) (http://www.transifex.com/lfleischer/aurweb/language/zh_CN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -42,7 +43,7 @@ msgstr "提示" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "Git clone URLs 并不意味着能被浏览器打开。" +msgstr "Git clone URL 并不应该使用浏览器打开。" #: html/404.php #, php-format @@ -65,11 +66,11 @@ msgstr "别慌!本站正在维护中,不久后将恢复。" #: html/account.php msgid "Account" -msgstr "帐户" +msgstr "账户" #: html/account.php template/header.php msgid "Accounts" -msgstr "帐户" +msgstr "账户" #: html/account.php html/addvote.php msgid "You are not allowed to access this area." @@ -81,7 +82,7 @@ msgstr "无法获取指定用户的信息。" #: html/account.php msgid "You do not have permission to edit this account." -msgstr "您没有权限编辑此帐户。" +msgstr "您没有权限编辑此账户。" #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." @@ -89,11 +90,11 @@ msgstr "密码无效。" #: html/account.php msgid "Use this form to search existing accounts." -msgstr "使用此表单查找存在的帐户。" +msgstr "使用此表单查找存在的账户。" #: html/account.php msgid "You must log in to view user information." -msgstr "您需要登录后才能察看用户信息。" +msgstr "您需要登录后才能查看用户信息。" #: html/addvote.php template/tu_list.php msgid "Add Proposal" @@ -485,7 +486,7 @@ msgstr "选中的软件包未被弃置,请检查确认复选框。" msgid "" "The selected packages have not been adopted, check the confirmation " "checkbox." -msgstr "" +msgstr "选中的软件包未被接管,请检查确认复选框。" #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -591,7 +592,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "" +msgstr "这似乎是 VCS 软件包。请%s不要%s因为 AUR 中的软件包版本与最新的 commit 不匹配就将其标记为过期。仅当来源移动或由于最新上游更改需要更改 PKGBUILD 时才标记此软件包。" #: html/pkgflag.php #, php-format @@ -701,7 +702,7 @@ msgstr "注册" #: html/register.php msgid "Use this form to create an account." -msgstr "使用此表单创建帐号。" +msgstr "使用此表单创建账户。" #: html/tos.php msgid "Terms of Service" @@ -854,12 +855,12 @@ msgstr "输入的验证码无效。" #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." -msgstr "尝试创建帐户 %s%s%s 失败。" +msgstr "尝试创建账户 %s%s%s 失败。" #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully created." -msgstr "帐户 %s%s%s 创建成功。" +msgstr "账户 %s%s%s 创建成功。" #: lib/acctfuncs.inc.php msgid "A password reset key has been sent to your e-mail address." @@ -867,7 +868,7 @@ msgstr "密码重置密钥已经发送到您的邮箱。" #: lib/acctfuncs.inc.php msgid "Click on the Login link above to use your account." -msgstr "点击上方的登录链接以使用您的帐号。" +msgstr "点击上方的登录链接以使用您的账户。" #: lib/acctfuncs.inc.php #, php-format @@ -877,7 +878,7 @@ msgstr "账户 %s%s%s 没有被修改。" #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully modified." -msgstr "帐号 %s%s%s 已被成功修改。" +msgstr "账户 %s%s%s 已被成功修改。" #: lib/acctfuncs.inc.php msgid "" @@ -887,11 +888,11 @@ msgstr "登录表单目前对您所使用的 IP 地址禁用,原因可能是 #: lib/acctfuncs.inc.php msgid "Account suspended" -msgstr "帐号被停用" +msgstr "账户被停用" #: aurweb/routers/accounts.py msgid "You do not have permission to suspend accounts." -msgstr "" +msgstr "您没有权限停用此账户。" #: lib/acctfuncs.inc.php #, php-format @@ -980,27 +981,27 @@ msgstr "无法找到软件包的详细信息。" #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "错误的 Referer 消息头。" #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." -msgstr "" +msgstr "您没有选择要接受通知的软件包。" #: aurweb/routers/packages.py msgid "The selected packages' notifications have been enabled." -msgstr "" +msgstr "选中的软件包的通知已被启用。" #: aurweb/routers/packages.py msgid "You did not select any packages for notification removal." -msgstr "" +msgstr "您没有选择要移除通知的软件包。" #: aurweb/routers/packages.py msgid "A package you selected does not have notifications enabled." -msgstr "" +msgstr "所选中的软件包并没有启用通知。" #: aurweb/routers/packages.py msgid "The selected packages' notifications have been removed." -msgstr "" +msgstr "选中的软件包的通知已被移除。" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -1040,7 +1041,7 @@ msgstr "您没有选择要删除的软件包。" #: aurweb/routers/packages.py msgid "One of the packages you selected does not exist." -msgstr "" +msgstr "选中的其中一个软件包不存在。" #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." @@ -1052,7 +1053,7 @@ msgstr "您需要登录后才能接管软件包。" #: aurweb/routers/package.py msgid "You are not allowed to adopt one of the packages you selected." -msgstr "" +msgstr "您不被允许接管选中的其中一个软件包。" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." @@ -1060,7 +1061,7 @@ msgstr "您需要登录后才能弃置软件包。" #: aurweb/routers/packages.py msgid "You are not allowed to disown one of the packages you selected." -msgstr "" +msgstr "您不被允许弃置选中的其中一个软件包。" #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." @@ -1197,7 +1198,7 @@ msgstr "请求关闭成功。" #: template/account_delete.php #, php-format msgid "You can use this form to permanently delete the AUR account %s." -msgstr "您可以使用这个表单永久删除 AUR 帐号 %s。" +msgstr "您可以使用这个表单永久删除 AUR 账号 %s。" #: template/account_delete.php #, php-format @@ -1216,7 +1217,7 @@ msgstr "用户名" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Account Type" -msgstr "帐户类别" +msgstr "账户类别" #: template/account_details.php template/tu_details.php #: template/tu_last_votes_list.php template/tu_list.php @@ -1298,7 +1299,7 @@ msgstr "查看这个用户提交的软件包" #: template/account_details.php msgid "Edit this user's account" -msgstr "编辑此用户的帐号" +msgstr "编辑此用户的账户" #: template/account_details.php msgid "List this user's comments" @@ -1307,7 +1308,7 @@ msgstr "显示此用户的评论" #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." -msgstr "如果你想永久删除这个帐号,请点击 %s这里%s。" +msgstr "如果你想永久删除这个账户,请点击 %s这里%s。" #: template/account_edit_form.php #, php-format @@ -1339,7 +1340,7 @@ msgstr "受信用户" #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" -msgstr "帐户被暂停" +msgstr "账户被停用" #: template/account_edit_form.php msgid "Inactive" @@ -1370,7 +1371,7 @@ msgstr "备用邮件地址" msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "选择性的提供的备用的邮件地址。该邮件地址将在你的主要邮件地址不可用时用于恢复你的帐号。" +msgstr "选择性的提供的备用的邮件地址。该邮件地址将在你的主要邮件地址不可用时用于恢复你的账户。" #: template/account_edit_form.php msgid "" @@ -1466,7 +1467,7 @@ msgstr "没有结果符合您的搜索条件。" #: template/account_search_results.php msgid "Edit Account" -msgstr "编辑帐户" +msgstr "编辑账户" #: template/account_search_results.php msgid "Suspended" @@ -1528,7 +1529,7 @@ msgstr "版权所有 %s 2004-%d aurweb 开发组。" #: template/header.php msgid " My Account" -msgstr " 我的帐户" +msgstr " 我的账户" #: template/pkgbase_actions.php msgid "Package Actions" @@ -2297,45 +2298,45 @@ msgstr "请记得为提案 {id} [1] 投票,投票时段将于48小时内结束 #: aurweb/routers/accounts.py msgid "Invalid account type provided." -msgstr "" +msgstr "提供的账户类别无效。" #: aurweb/routers/accounts.py msgid "You do not have permission to change account types." -msgstr "" +msgstr "您没有权限更改账户类别。" #: aurweb/routers/accounts.py msgid "You do not have permission to change this user's account type to %s." -msgstr "" +msgstr "您没有权限将此用户的账户类别更改为%s。" #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "没有为 %s 接受的现有孤立请求。" #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "内部服务器错误" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "发生了严重的错误。" #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "详细信息已被记录,并会交由 Postmaster 尽快调查。对您造成的不便,我们深感抱歉。" #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR 服务器错误" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "相关软件包请求关闭评论…" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "此操作将关闭任何有关的未处理的软件包请求。若省略%s评论%s,将会自动生成关闭评论。" From 154bb239bfb047ca6a6bc0ab244835570f6d14f5 Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Tue, 10 Jan 2023 14:10:40 -0800 Subject: [PATCH 1252/1451] update-zh_TW translations --- po/zh_TW.po | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/po/zh_TW.po b/po/zh_TW.po index 1526b4a9..56014aac 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -1,18 +1,19 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the AURWEB package. -# +# # Translators: # pan93412 , 2018 +# Cycatz , 2022 # 黃柏諺 , 2014-2017 # 黃柏諺 , 2020-2022 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Cycatz , 2022\n" "Language-Team: Chinese (Taiwan) (http://www.transifex.com/lfleischer/aurweb/language/zh_TW/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1990,7 +1991,7 @@ msgstr "每頁顯示" #: template/pkg_search_form.php template/pkg_search_results.php msgid "Go" -msgstr "到" +msgstr "搜尋" #: template/pkg_search_form.php msgid "Orphans" @@ -2324,10 +2325,10 @@ msgstr "AUR 伺服器錯誤" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "相關軟體包請求關閉留言……" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "此動作將會關閉任何關於此的擱置中軟體包請求。若省略%s留言%s,將會自動產生關閉留言。" From ff44eb02de7b45bf193b66a0695bca82dd8896b8 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Wed, 11 Jan 2023 20:12:28 +0100 Subject: [PATCH 1253/1451] feat: add link to mailing list article on requests page Provides a convenient way to check for responses on the mailing list prior to Accepting/Rejecting requests. We compute the Message-ID hash that can be used to link back to the article in the mailing list archive. Signed-off-by: moson-mo --- aurweb/models/package_request.py | 18 +++++++++++++- conf/config.defaults | 1 + templates/requests.html | 5 +++- test/test_package_request.py | 40 +++++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/aurweb/models/package_request.py b/aurweb/models/package_request.py index 31071df4..94ff064b 100644 --- a/aurweb/models/package_request.py +++ b/aurweb/models/package_request.py @@ -1,7 +1,10 @@ +import base64 +import hashlib + from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship -from aurweb import schema +from aurweb import config, schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase from aurweb.models.request_type import RequestType as _RequestType @@ -103,3 +106,16 @@ class PackageRequest(Base): def status_display(self) -> str: """Return a display string for the Status column.""" return self.STATUS_DISPLAY[self.Status] + + def ml_message_id_hash(self) -> str: + """Return the X-Message-ID-Hash that is used in the mailing list archive.""" + # X-Message-ID-Hash is a base32 encoded SHA1 hash + msgid = f"pkg-request-{str(self.ID)}@aur.archlinux.org" + sha1 = hashlib.sha1(msgid.encode()).digest() + + return base64.b32encode(sha1).decode() + + def ml_message_url(self) -> str: + """Return the mailing list URL for the request.""" + url = config.get("options", "ml_thread_url") % (self.ml_message_id_hash()) + return url diff --git a/conf/config.defaults b/conf/config.defaults index 6cdffe65..06e73afe 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -25,6 +25,7 @@ max_rpc_results = 5000 max_search_results = 2500 max_depends = 1000 aur_request_ml = aur-requests@lists.archlinux.org +ml_thread_url = https://lists.archlinux.org/archives/list/aur-requests@lists.archlinux.org/thread/%s request_idle_time = 1209600 request_archive_time = 15552000 auto_orphan_age = 15552000 diff --git a/templates/requests.html b/templates/requests.html index 669b46b0..697fbedb 100644 --- a/templates/requests.html +++ b/templates/requests.html @@ -115,8 +115,11 @@ {% if result.User %} {{ result.User.Username }} - +   {% endif %} + + (PRQ#{{ result.ID }}) + {% set idle_time = config_getint("options", "request_idle_time") %} {% set time_delta = (utcnow - result.RequestTS) | int %} diff --git a/test/test_package_request.py b/test/test_package_request.py index a69a0617..2bbf56c2 100644 --- a/test/test_package_request.py +++ b/test/test_package_request.py @@ -1,7 +1,7 @@ import pytest from sqlalchemy.exc import IntegrityError -from aurweb import db, time +from aurweb import config, db, time from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase from aurweb.models.package_request import ( @@ -190,3 +190,41 @@ def test_package_request_status_display(user: User, pkgbase: PackageBase): pkgreq.Status = 124 with pytest.raises(KeyError): pkgreq.status_display() + + +def test_package_request_ml_message_id_hash(user: User, pkgbase: PackageBase): + with db.begin(): + pkgreq = db.create( + PackageRequest, + ID=1, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + Status=PENDING_ID, + ) + + # A hash composed with ID=1 should result in BNNNRWOFDRSQP4LVPT77FF2GUFR45KW5 + assert pkgreq.ml_message_id_hash() == "BNNNRWOFDRSQP4LVPT77FF2GUFR45KW5" + + +def test_package_request_ml_message_url(user: User, pkgbase: PackageBase): + with db.begin(): + pkgreq = db.create( + PackageRequest, + ID=1, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + Status=PENDING_ID, + ) + + assert ( + config.get("options", "ml_thread_url") % (pkgreq.ml_message_id_hash()) + == pkgreq.ml_message_url() + ) From 2150f8bc191e92a0b4e99b438388add88963d827 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Fri, 13 Jan 2023 10:14:53 +0100 Subject: [PATCH 1254/1451] fix(docker): nginx health check nginx health check always results in "unhealthy": There is no such option "--no-verify" for curl. We can use "-k" or "--insecure" for disabling SSL checks. Signed-off-by: moson-mo --- docker/health/nginx.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/health/nginx.sh b/docker/health/nginx.sh index c530103d..df76bc2b 100755 --- a/docker/health/nginx.sh +++ b/docker/health/nginx.sh @@ -1,2 +1,2 @@ #!/bin/bash -exec curl --no-verify -q https://localhost:8444 +exec curl -k -q https://localhost:8444 From f6c4891415766b1030fa20f2d69af78a4482cc95 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sat, 14 Jan 2023 13:12:33 +0100 Subject: [PATCH 1255/1451] feat: add Support section to Dashboard Adds the "Support" section (displayed on "Home") to the "Dashboard" page as well. Signed-off-by: moson-mo --- templates/dashboard.html | 3 ++ templates/home.html | 66 +-------------------------------- templates/partials/support.html | 65 ++++++++++++++++++++++++++++++++ test/test_homepage.py | 40 +++++++++++++------- 4 files changed, 96 insertions(+), 78 deletions(-) create mode 100644 templates/partials/support.html diff --git a/templates/dashboard.html b/templates/dashboard.html index 48f42dc6..e88fde4a 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -62,6 +62,9 @@ {% endwith %} {% endif %} +
    + {% include 'partials/support.html' %} +
    diff --git a/templates/home.html b/templates/home.html index 3a7bc76d..e8296239 100644 --- a/templates/home.html +++ b/templates/home.html @@ -24,71 +24,7 @@

    {% trans %}Learn more...{% endtrans %}

    -

    {% trans %}Support{% endtrans %}

    -

    {% trans %}Package Requests{% endtrans %}

    -
    -

    - {{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:" - | tr - | format("", "") - | safe - }} -

    -
      -
    • {% trans %}Orphan Request{% endtrans %}: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}
    • -
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}
    • -
    • {% trans %}Merge Request{% endtrans %}: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}
    • -
    -

    - {{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests." - | tr - | format('', "") - | safe - }} -

    -
    -

    {% trans %}Submitting Packages{% endtrans %}

    -
    -

    - {{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details." - | tr - | format('', "") - | safe - }} -

    - {% if ssh_fingerprints %} -

    - {% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %} -

    -

      - {% for keytype in ssh_fingerprints %} -
    • {{ keytype }}: {{ ssh_fingerprints[keytype] }} - {% endfor %} -
    - {% endif %} -
    -

    {% trans %}Discussion{% endtrans %}

    -
    -

    - {{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." - | tr - | format('', "", - '', "") - | safe - }} -

    -

    -

    {% trans %}Bug Reporting{% endtrans %}

    -
    -

    - {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page." - | tr - | format('', "", - "", "") - | safe - }} -

    -
    + {% include 'partials/support.html' %}
    diff --git a/templates/partials/support.html b/templates/partials/support.html new file mode 100644 index 00000000..a2890cc5 --- /dev/null +++ b/templates/partials/support.html @@ -0,0 +1,65 @@ +

    {% trans %}Support{% endtrans %}

    +

    {% trans %}Package Requests{% endtrans %}

    +
    +

    + {{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:" + | tr + | format("", "") + | safe + }} +

    +
      +
    • {% trans %}Orphan Request{% endtrans %}: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}
    • +
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}
    • +
    • {% trans %}Merge Request{% endtrans %}: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}
    • +
    +

    +{{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests." + | tr + | format('', "") + | safe + }} +

    +
    +

    {% trans %}Submitting Packages{% endtrans %}

    +
    +

    + {{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details." + | tr + | format('', "") + | safe + }} +

    +{% if ssh_fingerprints %} +

    + {% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %} +

    +

      + {% for keytype in ssh_fingerprints %} +
    • {{ keytype }}: {{ ssh_fingerprints[keytype] }} + {% endfor %} +
    +{% endif %} +
    +

    {% trans %}Discussion{% endtrans %}

    +
    +

    + {{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." + | tr + | format('', "", + '', "") + | safe + }} +

    +

    +

    {% trans %}Bug Reporting{% endtrans %}

    +
    +

    + {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page." + | tr + | format('', "", + "", "") + | safe + }} +

    +
    diff --git a/test/test_homepage.py b/test/test_homepage.py index a573bdd6..08c52c09 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -125,33 +125,47 @@ def test_homepage(): @patch("aurweb.util.get_ssh_fingerprints") -def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock): +def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock, user): fingerprints = {"Ed25519": "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"} get_ssh_fingerprints_mock.return_value = fingerprints + # without authentication (Home) with client as request: response = request.get("/") - for key, value in fingerprints.items(): - assert key in response.content.decode() - assert value in response.content.decode() - assert ( - "The following SSH fingerprints are used for the AUR" - in response.content.decode() - ) + # with authentication (Dashboard) + with client as auth_request: + auth_request.cookies = {"AURSID": user.login(Request(), "testPassword")} + auth_response = auth_request.get("/") + + for resp in [response, auth_response]: + for key, value in fingerprints.items(): + assert key in resp.content.decode() + assert value in resp.content.decode() + assert ( + "The following SSH fingerprints are used for the AUR" + in resp.content.decode() + ) @patch("aurweb.util.get_ssh_fingerprints") -def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock): +def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock, user): get_ssh_fingerprints_mock.return_value = {} + # without authentication (Home) with client as request: response = request.get("/") - assert ( - "The following SSH fingerprints are used for the AUR" - not in response.content.decode() - ) + # with authentication (Dashboard) + with client as auth_request: + auth_request.cookies = {"AURSID": user.login(Request(), "testPassword")} + auth_response = auth_request.get("/") + + for resp in [response, auth_response]: + assert ( + "The following SSH fingerprints are used for the AUR" + not in resp.content.decode() + ) def test_homepage_stats(redis, packages): From 4d0a982c519cb087b4855922f65d73dbece45d33 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 14 Jan 2023 11:22:03 +0200 Subject: [PATCH 1256/1451] fix: assert offset and per_page are positive Signed-off-by: Leonidas Spyropoulos --- aurweb/routers/requests.py | 2 +- aurweb/util.py | 6 +++--- test/test_util.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index 6880abd9..713f88d2 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -48,7 +48,7 @@ async def requests( if not dict(request.query_params).keys() & FILTER_PARAMS: filter_pending = True - O, PP = util.sanitize_params(O, PP) + O, PP = util.sanitize_params(str(O), str(PP)) context["O"] = O context["PP"] = PP context["filter_pending"] = filter_pending diff --git a/aurweb/util.py b/aurweb/util.py index 7b997609..abf48938 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -96,14 +96,14 @@ def apply_all(iterable: Iterable, fn: Callable): return iterable -def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: +def sanitize_params(offset_str: str, per_page_str: str) -> Tuple[int, int]: try: - offset = int(offset) + offset = defaults.O if int(offset_str) < 0 else int(offset_str) except ValueError: offset = defaults.O try: - per_page = int(per_page) + per_page = defaults.PP if int(per_page_str) < 0 else int(per_page_str) except ValueError: per_page = defaults.PP diff --git a/test/test_util.py b/test/test_util.py index fd7d8655..fefa659a 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -121,6 +121,21 @@ fRSo6OFcejKc= assert_multiple_keys(pks) +@pytest.mark.parametrize( + "offset_str, per_page_str, expected", + [ + ("5", "100", (5, 100)), + ("", "100", (0, 100)), + ("5", "", (5, 50)), + ("", "", (0, 50)), + ("-1", "100", (0, 100)), + ("5", "-100", (5, 50)), + ], +) +def test_sanitize_params(offset_str: str, per_page_str: str, expected: tuple[int, int]): + assert util.sanitize_params(offset_str, per_page_str) == expected + + def assert_multiple_keys(pks): keys = util.parse_ssh_keys(pks) assert len(keys) == 2 From 0e44687ab11da81c611a2668b1249405d32cdb7f Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 12 Jan 2023 11:47:00 +0200 Subject: [PATCH 1257/1451] fix: only try to show dependencies if object exists Signed-off-by: Leonidas Spyropoulos --- templates/partials/packages/package_metadata.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html index 50d38b48..ebbfe3f9 100644 --- a/templates/partials/packages/package_metadata.html +++ b/templates/partials/packages/package_metadata.html @@ -48,6 +48,7 @@

    {{ "Required by" | tr }} ({{ reqs_count }})

    {% if form_type == "UpdateAccount" %} diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index e4818837..faac0753 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -6,6 +6,7 @@ {% endif %} {% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %} +{% if not (request.user.HideDeletedComments and comment.DelTS) %}

    {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} {% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %} @@ -41,3 +42,4 @@ {% include "partials/comment_content.html" %} {% endif %} +{% endif %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index d4b0babc..21ccdd7b 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -122,6 +122,22 @@ def tu_user(): yield tu_user +@pytest.fixture +def user_who_hates_grey_comments() -> User: + """Yield a specific User who doesn't like grey comments.""" + account_type = db.query(AccountType, AccountType.ID == USER_ID).first() + with db.begin(): + user_who_hates_grey_comments = db.create( + User, + Username="test_hater", + Email="test_hater@example.org", + Passwd="testPassword", + AccountType=account_type, + HideDeletedComments=True, + ) + yield user_who_hates_grey_comments + + @pytest.fixture def package(maintainer: User) -> Package: """Yield a Package created by user.""" @@ -193,6 +209,23 @@ def comment(user: User, package: Package) -> PackageComment: yield comment +@pytest.fixture +def deleted_comment(user: User, package: Package) -> PackageComment: + pkgbase = package.PackageBase + now = time.utcnow() + with db.begin(): + comment = db.create( + PackageComment, + User=user, + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment=str(), + CommentTS=now, + DelTS=now, + ) + yield comment + + @pytest.fixture def packages(maintainer: User) -> list[Package]: """Yield 55 packages named pkg_0 .. pkg_54.""" @@ -409,7 +442,9 @@ def test_paged_depends_required(client: TestClient, package: Package): assert "Show 6 more" not in resp.text -def test_package_comments(client: TestClient, user: User, package: Package): +def test_package_comments( + client: TestClient, user: User, user_who_hates_grey_comments: User, package: Package +): now = time.utcnow() with db.begin(): comment = db.create( @@ -419,6 +454,14 @@ def test_package_comments(client: TestClient, user: User, package: Package): Comments="Test comment", CommentTS=now, ) + deleted_comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Deleted Test comment", + CommentTS=now, + DelTS=now - 1, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: @@ -426,12 +469,29 @@ def test_package_comments(client: TestClient, user: User, package: Package): resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) + root = parse_root(resp.text) + expected = [comment.Comments, deleted_comment.Comments] + comments = root.xpath( + './/div[contains(@class, "package-comments")]' + '/div[@class="article-content"]/div/text()' + ) + assert len(comments) == 2 + for i, row in enumerate(expected): + assert comments[i].strip() == row + + cookies = {"AURSID": user_who_hates_grey_comments.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + root = parse_root(resp.text) expected = [comment.Comments] comments = root.xpath( './/div[contains(@class, "package-comments")]' '/div[@class="article-content"]/div/text()' ) + assert len(comments) == 1 # Deleted comment is hidden for i, row in enumerate(expected): assert comments[i].strip() == row From 1325c71712a12c529d7a3defa9cbabfad296922e Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Fri, 21 Apr 2023 23:55:02 +0100 Subject: [PATCH 1292/1451] chore: update poetry.lock Signed-off-by: Leonidas Spyropoulos --- poetry.lock | 616 ++++++++++++++++++++++++++-------------------------- 1 file changed, 302 insertions(+), 314 deletions(-) diff --git a/poetry.lock b/poetry.lock index fe81898e..1b98a5b8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "aiofiles" @@ -14,14 +14,14 @@ files = [ [[package]] name = "alembic" -version = "1.10.2" +version = "1.10.3" description = "A database migration tool for SQLAlchemy." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.10.2-py3-none-any.whl", hash = "sha256:8b48368f6533c064b39c024e1daba15ae7f947eac84185c28c06bbe1301a5497"}, - {file = "alembic-1.10.2.tar.gz", hash = "sha256:457eafbdc0769d855c2c92cbafe6b7f319f916c80cf4ed02b8f394f38b51b89d"}, + {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, + {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, ] [package.dependencies] @@ -80,25 +80,6 @@ files = [ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - [[package]] name = "authlib" version = "1.2.0" @@ -371,63 +352,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.1" +version = "7.2.3" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"}, - {file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"}, - {file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"}, - {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"}, - {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"}, - {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"}, - {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"}, - {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"}, - {file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"}, - {file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"}, - {file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"}, - {file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"}, - {file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"}, - {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"}, - {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"}, - {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"}, - {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"}, - {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"}, - {file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"}, - {file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"}, - {file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"}, - {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"}, - {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"}, - {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"}, - {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"}, - {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"}, - {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"}, - {file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"}, - {file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"}, - {file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"}, - {file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"}, - {file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"}, - {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"}, - {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"}, - {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"}, - {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"}, - {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"}, - {file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"}, - {file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"}, - {file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"}, - {file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"}, - {file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"}, - {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"}, - {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"}, - {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"}, - {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"}, - {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"}, - {file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"}, - {file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"}, - {file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"}, - {file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"}, + {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"}, + {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"}, + {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"}, + {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"}, + {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"}, + {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"}, + {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"}, + {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"}, + {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"}, + {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"}, + {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"}, + {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"}, + {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"}, + {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"}, + {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"}, ] [package.dependencies] @@ -438,35 +419,31 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "39.0.2" +version = "40.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06"}, - {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7"}, - {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612"}, - {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a"}, - {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97"}, - {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828"}, - {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011"}, - {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536"}, - {file = "cryptography-39.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5"}, - {file = "cryptography-39.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0"}, - {file = "cryptography-39.0.2-cp36-abi3-win32.whl", hash = "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480"}, - {file = "cryptography-39.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9"}, - {file = "cryptography-39.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac"}, - {file = "cryptography-39.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074"}, - {file = "cryptography-39.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1"}, - {file = "cryptography-39.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3"}, - {file = "cryptography-39.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354"}, - {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915"}, - {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84"}, - {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108"}, - {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3"}, - {file = "cryptography-39.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3"}, - {file = "cryptography-39.0.2.tar.gz", hash = "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f"}, + {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"}, + {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"}, + {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"}, + {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"}, + {file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"}, + {file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"}, + {file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"}, ] [package.dependencies] @@ -475,10 +452,10 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] +pep8test = ["black", "check-manifest", "mypy", "ruff"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] test-randomorder = ["pytest-randomly"] tox = ["tox"] @@ -551,18 +528,18 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "2.10.0" +version = "2.11.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false -python-versions = ">=3.8,<4.0" +python-versions = ">=3.7,<4.0" files = [ - {file = "fakeredis-2.10.0-py3-none-any.whl", hash = "sha256:7e66c96793688703a1da41256323ddaa1b3a2cab4ef793866839a937bb273915"}, - {file = "fakeredis-2.10.0.tar.gz", hash = "sha256:722644759bba4ad61fa38f0bb34939b7657f166ba35892f747e282407a196845"}, + {file = "fakeredis-2.11.0-py3-none-any.whl", hash = "sha256:156ef67713dd53000c28dd341be61a365c20230bc17c8fb8320b0c123e667aff"}, + {file = "fakeredis-2.11.0.tar.gz", hash = "sha256:d25883dc52c31546e586b6ec3c49c5999b3025bfc4812532d71dedcfed56fee1"}, ] [package.dependencies] -redis = ">=4,<5" +redis = ">=4" sortedcontainers = ">=2.4,<3.0" [package.extras] @@ -608,19 +585,19 @@ python-dateutil = "*" [[package]] name = "filelock" -version = "3.9.1" +version = "3.12.0" description = "A platform independent file lock." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "filelock-3.9.1-py3-none-any.whl", hash = "sha256:4427cdda14a1c68e264845142842d6de2d0fa2c15ba31571a3d9c9a1ec9d191c"}, - {file = "filelock-3.9.1.tar.gz", hash = "sha256:e393782f76abea324dee598d2ea145b857a20df0e0ee4f80fcf35e72a341d2c7"}, + {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"}, + {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"}, ] [package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.1)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] [[package]] name = "greenlet" @@ -854,14 +831,14 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.0.0" +version = "6.6.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, + {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"}, + {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"}, ] [package.dependencies] @@ -1029,14 +1006,14 @@ testing = ["pytest"] [[package]] name = "markdown" -version = "3.4.1" -description = "Python implementation of Markdown." +version = "3.4.3" +description = "Python implementation of John Gruber's Markdown." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"}, - {file = "Markdown-3.4.1.tar.gz", hash = "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff"}, + {file = "Markdown-3.4.3-py3-none-any.whl", hash = "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2"}, + {file = "Markdown-3.4.3.tar.gz", hash = "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520"}, ] [package.dependencies] @@ -1124,68 +1101,80 @@ files = [ [[package]] name = "orjson" -version = "3.8.7" +version = "3.8.10" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">= 3.7" files = [ - {file = "orjson-3.8.7-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:f98c82850b7b4b7e27785ca43706fa86c893cdb88d54576bbb9b0d9c1070e421"}, - {file = "orjson-3.8.7-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1dee503c6c1a0659c5b46f5f39d9ca9d3657b11ca8bb4af8506086df416887d9"}, - {file = "orjson-3.8.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4fa83831f42ce5c938f8cefc2e175fa1df6f661fdeaba3badf26d2b8cfcf73"}, - {file = "orjson-3.8.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e432c6c9c8b97ad825276d5795286f7cc9689f377a97e3b7ecf14918413303f"}, - {file = "orjson-3.8.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee519964a5a0efb9633f38b1129fd242807c5c57162844efeeaab1c8de080051"}, - {file = "orjson-3.8.7-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:109b539ce5bf60a121454d008fa67c3b67e5a3249e47d277012645922cf74bd0"}, - {file = "orjson-3.8.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ad4d441fbde4133af6fee37f67dbf23181b9c537ecc317346ec8c3b4c8ec7705"}, - {file = "orjson-3.8.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89dc786419e1ce2588345f58dd6a434e6728bce66b94989644234bcdbe39b603"}, - {file = "orjson-3.8.7-cp310-none-win_amd64.whl", hash = "sha256:697abde7350fb8076d44bcb6b4ab3ce415ae2b5a9bb91efc460e5ab0d96bb5d3"}, - {file = "orjson-3.8.7-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:1c19f47b35b9966a3abadf341b18ee4a860431bf2b00fd8d58906d51cf78aa70"}, - {file = "orjson-3.8.7-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:3ffaabb380cd0ee187b4fc362516df6bf739808130b1339445c7d8878fca36e7"}, - {file = "orjson-3.8.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d88837002c5a8af970745b8e0ca1b0fdb06aafbe7f1279e110d338ea19f3d23"}, - {file = "orjson-3.8.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff60187d1b7e0bfab376b6002b08c560b7de06c87cf3a8ac639ecf58f84c5f3b"}, - {file = "orjson-3.8.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0110970aed35dec293f30ed1e09f8604afd5d15c5ef83de7f6c427619b3ba47b"}, - {file = "orjson-3.8.7-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:51b275475d4e36118b65ad56f9764056a09d985c5d72e64579bf8816f1356a5e"}, - {file = "orjson-3.8.7-cp311-none-win_amd64.whl", hash = "sha256:63144d27735f3b60f079f247ac9a289d80dfe49a7f03880dfa0c0ba64d6491d5"}, - {file = "orjson-3.8.7-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:a16273d77db746bb1789a2bbfded81148a60743fd6f9d5185e02d92e3732fa18"}, - {file = "orjson-3.8.7-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:5bb32259ea22cc9dd47a6fdc4b8f9f1e2f798fcf56c7c1122a7df0f4c5d33bf3"}, - {file = "orjson-3.8.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad02e9102d4ba67db30a136e631e32aeebd1dce26c9f5942a457b02df131c5d0"}, - {file = "orjson-3.8.7-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbcfcec2b7ac52deb7be3685b551addc28ee8fa454ef41f8b714df6ba0e32a27"}, - {file = "orjson-3.8.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a0e5504a5fc86083cc210c6946e8d61e13fe9f1d7a7bf81b42f7050a49d4fb"}, - {file = "orjson-3.8.7-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:7bd4fd37adb03b1f2a1012d43c9f95973a02164e131dfe3ff804d7e180af5653"}, - {file = "orjson-3.8.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:188ed9f9a781333ad802af54c55d5a48991e292239aef41bd663b6e314377eb8"}, - {file = "orjson-3.8.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cc52f58c688cb10afd810280e450f56fbcb27f52c053463e625c8335c95db0dc"}, - {file = "orjson-3.8.7-cp37-none-win_amd64.whl", hash = "sha256:403c8c84ac8a02c40613b0493b74d5256379e65196d39399edbf2ed3169cbeb5"}, - {file = "orjson-3.8.7-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:7d6ac5f8a2a17095cd927c4d52abbb38af45918e0d3abd60fb50cfd49d71ae24"}, - {file = "orjson-3.8.7-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0295a7bfd713fa89231fd0822c995c31fc2343c59a1d13aa1b8b6651335654f5"}, - {file = "orjson-3.8.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feb32aaaa34cf2f891eb793ad320d4bb6731328496ae59b6c9eb1b620c42b529"}, - {file = "orjson-3.8.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a3ab1a473894e609b6f1d763838c6689ba2b97620c256a32c4d9f10595ac179"}, - {file = "orjson-3.8.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e8c430d82b532c5ab95634e034bbf6ca7432ffe175a3e63eadd493e00b3a555"}, - {file = "orjson-3.8.7-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:366cc75f7e09106f9dac95a675aef413367b284f25507d21e55bd7f45f445e80"}, - {file = "orjson-3.8.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:84d154d07e8b17d97e990d5d710b719a031738eb1687d8a05b9089f0564ff3e0"}, - {file = "orjson-3.8.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06180014afcfdc167ca984b312218aa62ce20093965c437c5f9166764cb65ef7"}, - {file = "orjson-3.8.7-cp38-none-win_amd64.whl", hash = "sha256:41244431ba13f2e6ef22b52c5cf0202d17954489f4a3c0505bd28d0e805c3546"}, - {file = "orjson-3.8.7-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:b20f29fa8371b8023f1791df035a2c3ccbd98baa429ac3114fc104768f7db6f8"}, - {file = "orjson-3.8.7-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:226bfc1da2f21ee74918cee2873ea9a0fec1a8830e533cb287d192d593e99d02"}, - {file = "orjson-3.8.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75c11023ac29e29fd3e75038d0e8dd93f9ea24d7b9a5e871967a8921a88df24"}, - {file = "orjson-3.8.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78604d3acfd7cd502f6381eea0c42281fe2b74755b334074ab3ebc0224100be1"}, - {file = "orjson-3.8.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7129a6847f0494aa1427167486ef6aea2e835ba05f6c627df522692ee228f65"}, - {file = "orjson-3.8.7-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1a1a8f4980059f48483782c608145b0f74538c266e01c183d9bcd9f8b71dbada"}, - {file = "orjson-3.8.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d60304172a33705ce4bd25a6261ab84bed2dab0b3d3b79672ea16c7648af4832"}, - {file = "orjson-3.8.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4f733062d84389c32c0492e5a4929056fac217034a94523debe0430bcc602cda"}, - {file = "orjson-3.8.7-cp39-none-win_amd64.whl", hash = "sha256:010e2970ec9e826c332819e0da4b14b29b19641da0f1a6af4cec91629ef9b988"}, - {file = "orjson-3.8.7.tar.gz", hash = "sha256:8460c8810652dba59c38c80d27c325b5092d189308d8d4f3e688dbd8d4f3b2dc"}, + {file = "orjson-3.8.10-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:4dfe0651e26492d5d929bbf4322de9afbd1c51ac2e3947a7f78492b20359711d"}, + {file = "orjson-3.8.10-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:bc30de5c7b3a402eb59cc0656b8ee53ca36322fc52ab67739c92635174f88336"}, + {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c08b426fae7b9577b528f99af0f7e0ff3ce46858dd9a7d1bf86d30f18df89a4c"}, + {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bce970f293825e008dbf739268dfa41dfe583aa2a1b5ef4efe53a0e92e9671ea"}, + {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b23fb0264bbdd7218aa685cb6fc71f0dcecf34182f0a8596a3a0dff010c06f9"}, + {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0826ad2dc1cea1547edff14ce580374f0061d853cbac088c71162dbfe2e52205"}, + {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7bce6e61cea6426309259b04c6ee2295b3f823ea51a033749459fe2dd0423b2"}, + {file = "orjson-3.8.10-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0b470d31244a6f647e5402aac7d2abaf7bb4f52379acf67722a09d35a45c9417"}, + {file = "orjson-3.8.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:48824649019a25d3e52f6454435cf19fe1eb3d05ee697e65d257f58ae3aa94d9"}, + {file = "orjson-3.8.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:faee89e885796a9cc493c930013fa5cfcec9bfaee431ddf00f0fbfb57166a8b3"}, + {file = "orjson-3.8.10-cp310-none-win_amd64.whl", hash = "sha256:3cfe32b1227fe029a5ad989fbec0b453a34e5e6d9a977723f7c3046d062d3537"}, + {file = "orjson-3.8.10-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:2073b62822738d6740bd2492f6035af5c2fd34aa198322b803dc0e70559a17b7"}, + {file = "orjson-3.8.10-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b2c4faf20b6bb5a2d7ac0c16f58eb1a3800abcef188c011296d1dc2bb2224d48"}, + {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c1825997232a324911d11c75d91e1e0338c7b723c149cf53a5fc24496c048a4"}, + {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7e85d4682f3ed7321d36846cad0503e944ea9579ef435d4c162e1b73ead8ac9"}, + {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8cdaacecb92997916603ab232bb096d0fa9e56b418ca956b9754187d65ca06"}, + {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ddabc5e44702d13137949adee3c60b7091e73a664f6e07c7b428eebb2dea7bbf"}, + {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27bb26e171e9cfdbec39c7ca4739b6bef8bd06c293d56d92d5e3a3fc017df17d"}, + {file = "orjson-3.8.10-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1810e5446fe68d61732e9743592da0ec807e63972eef076d09e02878c2f5958e"}, + {file = "orjson-3.8.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61e2e51cefe7ef90c4fbbc9fd38ecc091575a3ea7751d56fad95cbebeae2a054"}, + {file = "orjson-3.8.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f3e9ac9483c2b4cd794e760316966b7bd1e6afb52b0218f068a4e80c9b2db4f6"}, + {file = "orjson-3.8.10-cp311-none-win_amd64.whl", hash = "sha256:26aee557cf8c93b2a971b5a4a8e3cca19780573531493ce6573aa1002f5c4378"}, + {file = "orjson-3.8.10-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:11ae68f995a50724032af297c92f20bcde31005e0bf3653b12bff9356394615b"}, + {file = "orjson-3.8.10-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:35d879b46b8029e1e01e9f6067928b470a4efa1ca749b6d053232b873c2dcf66"}, + {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:345e41abd1d9e3ecfb554e1e75ff818cf42e268bd06ad25a96c34e00f73a327e"}, + {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:45a5afc9cda6b8aac066dd50d8194432fbc33e71f7164f95402999b725232d78"}, + {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad632dc330a7b39da42530c8d146f76f727d476c01b719dc6743c2b5701aaf6b"}, + {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf2556ba99292c4dc550560384dd22e88b5cdbe6d98fb4e202e902b5775cf9f"}, + {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b88afd662190f19c3bb5036a903589f88b1d2c2608fbb97281ce000db6b08897"}, + {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:abce8d319aae800fd2d774db1106f926dee0e8a5ca85998fd76391fcb58ef94f"}, + {file = "orjson-3.8.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e999abca892accada083f7079612307d94dd14cc105a699588a324f843216509"}, + {file = "orjson-3.8.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3fdee68c4bb3c5d6f89ed4560f1384b5d6260e48fbf868bae1a245a3c693d4d"}, + {file = "orjson-3.8.10-cp37-none-win_amd64.whl", hash = "sha256:e5d7f82506212e047b184c06e4bcd48c1483e101969013623cebcf51cf12cad9"}, + {file = "orjson-3.8.10-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:d953e6c2087dcd990e794f8405011369ee11cf13e9aaae3172ee762ee63947f2"}, + {file = "orjson-3.8.10-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:81aa3f321d201bff0bd0f4014ea44e51d58a9a02d8f2b0eeab2cee22611be8e1"}, + {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d27b6182f75896dd8c10ea0f78b9265a3454be72d00632b97f84d7031900dd4"}, + {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1486600bc1dd1db26c588dd482689edba3d72d301accbe4301db4b2b28bd7aa4"}, + {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344ea91c556a2ce6423dc13401b83ab0392aa697a97fa4142c2c63a6fd0bbfef"}, + {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:979f231e3bad1c835627eef1a30db12a8af58bfb475a6758868ea7e81897211f"}, + {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fa3a26dcf0f5f2912a8ce8e87273e68b2a9526854d19fd09ea671b154418e88"}, + {file = "orjson-3.8.10-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b6e79d8864794635974b18821b49a7f27859d17b93413d4603efadf2e92da7a5"}, + {file = "orjson-3.8.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce49999bcbbc14791c61844bc8a69af44f5205d219be540e074660038adae6bf"}, + {file = "orjson-3.8.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2ef690335b24f9272dbf6639353c1ffc3f196623a92b851063e28e9515cf7dd"}, + {file = "orjson-3.8.10-cp38-none-win_amd64.whl", hash = "sha256:5a0b1f4e4fa75e26f814161196e365fc0e1a16e3c07428154505b680a17df02f"}, + {file = "orjson-3.8.10-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:af7601a78b99f0515af2f8ab12c955c0072ffcc1e437fb2556f4465783a4d813"}, + {file = "orjson-3.8.10-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6bbd7b3a3e2030b03c68c4d4b19a2ef5b89081cbb43c05fe2010767ef5e408db"}, + {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4355c9aedfefe60904e8bd7901315ebbc8bb828f665e4c9bc94b1432e67cb6f7"}, + {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b0ba074375e25c1594e770e2215941e2017c3cd121889150737fa1123e8bfe"}, + {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34b6901c110c06ab9e8d7d0496db4bc9a0c162ca8d77f67539d22cb39e0a1ef4"}, + {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb62ec16a1c26ad9487727b529103cb6a94a1d4969d5b32dd0eab5c3f4f5a6f2"}, + {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595e1e7d04aaaa3d41113e4eb9f765ab642173c4001182684ae9ddc621bb11c8"}, + {file = "orjson-3.8.10-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:64ffd92328473a2f9af059410bd10c703206a4bbc7b70abb1bedcd8761e39eb8"}, + {file = "orjson-3.8.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b1f648ec89c6a426098868460c0ef8c86b457ce1378d7569ff4acb6c0c454048"}, + {file = "orjson-3.8.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6a286ad379972e4f46579e772f0477e6b505f1823aabcd64ef097dbb4549e1a4"}, + {file = "orjson-3.8.10-cp39-none-win_amd64.whl", hash = "sha256:d2874cee6856d7c386b596e50bc517d1973d73dc40b2bd6abec057b5e7c76b2f"}, + {file = "orjson-3.8.10.tar.gz", hash = "sha256:dcf6adb4471b69875034afab51a14b64f1026bc968175a2bb02c5f6b358bd413"}, ] [[package]] name = "packaging" -version = "23.0" +version = "23.1" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] [[package]] @@ -1271,14 +1260,14 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "5.11.1" +version = "5.11.2" description = "Instrument your FastAPI with Prometheus metrics." category = "main" optional = false python-versions = ">=3.7.0,<4.0.0" files = [ - {file = "prometheus_fastapi_instrumentator-5.11.1-py3-none-any.whl", hash = "sha256:8a58dc34b75620f634bd9c9d77978172bc5a6de05b921e301a1e4de896ca97ce"}, - {file = "prometheus_fastapi_instrumentator-5.11.1.tar.gz", hash = "sha256:34026f8735aff89a08ed71bd16a1f454d5eaaec20a9a52eb5c40aa2d585dfbba"}, + {file = "prometheus_fastapi_instrumentator-5.11.2-py3-none-any.whl", hash = "sha256:c84ae3dc98bebb44f29d0af0c17c9f0782c2fb964ef83353664d9858a632cf81"}, + {file = "prometheus_fastapi_instrumentator-5.11.2.tar.gz", hash = "sha256:9d8d0df8de7d6a73ae387121629dbf32fe022cdfc63e8bacd51f4b8ff21059ea"}, ] [package.dependencies] @@ -1287,25 +1276,25 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "4.22.1" +version = "4.22.3" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.22.1-cp310-abi3-win32.whl", hash = "sha256:85aa9acc5a777adc0c21b449dafbc40d9a0b6413ff3a4f77ef9df194be7f975b"}, - {file = "protobuf-4.22.1-cp310-abi3-win_amd64.whl", hash = "sha256:8bc971d76c03f1dd49f18115b002254f2ddb2d4b143c583bb860b796bb0d399e"}, - {file = "protobuf-4.22.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:5917412347e1da08ce2939eb5cd60650dfb1a9ab4606a415b9278a1041fb4d19"}, - {file = "protobuf-4.22.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e12e2810e7d297dbce3c129ae5e912ffd94240b050d33f9ecf023f35563b14f"}, - {file = "protobuf-4.22.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:953fc7904ef46900262a26374b28c2864610b60cdc8b272f864e22143f8373c4"}, - {file = "protobuf-4.22.1-cp37-cp37m-win32.whl", hash = "sha256:6e100f7bc787cd0a0ae58dbf0ab8bbf1ee7953f862b89148b6cf5436d5e9eaa1"}, - {file = "protobuf-4.22.1-cp37-cp37m-win_amd64.whl", hash = "sha256:87a6393fa634f294bf24d1cfe9fdd6bb605cbc247af81b9b10c4c0f12dfce4b3"}, - {file = "protobuf-4.22.1-cp38-cp38-win32.whl", hash = "sha256:e3fb58076bdb550e75db06ace2a8b3879d4c4f7ec9dd86e4254656118f4a78d7"}, - {file = "protobuf-4.22.1-cp38-cp38-win_amd64.whl", hash = "sha256:651113695bc2e5678b799ee5d906b5d3613f4ccfa61b12252cfceb6404558af0"}, - {file = "protobuf-4.22.1-cp39-cp39-win32.whl", hash = "sha256:67b7d19da0fda2733702c2299fd1ef6cb4b3d99f09263eacaf1aa151d9d05f02"}, - {file = "protobuf-4.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8700792f88e59ccecfa246fa48f689d6eee6900eddd486cdae908ff706c482b"}, - {file = "protobuf-4.22.1-py3-none-any.whl", hash = "sha256:3e19dcf4adbf608924d3486ece469dd4f4f2cf7d2649900f0efcd1a84e8fd3ba"}, - {file = "protobuf-4.22.1.tar.gz", hash = "sha256:dce7a55d501c31ecf688adb2f6c3f763cf11bc0be815d1946a84d74772ab07a7"}, + {file = "protobuf-4.22.3-cp310-abi3-win32.whl", hash = "sha256:8b54f56d13ae4a3ec140076c9d937221f887c8f64954673d46f63751209e839a"}, + {file = "protobuf-4.22.3-cp310-abi3-win_amd64.whl", hash = "sha256:7760730063329d42a9d4c4573b804289b738d4931e363ffbe684716b796bde51"}, + {file = "protobuf-4.22.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:d14fc1a41d1a1909998e8aff7e80d2a7ae14772c4a70e4bf7db8a36690b54425"}, + {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:70659847ee57a5262a65954538088a1d72dfc3e9882695cab9f0c54ffe71663b"}, + {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:13233ee2b9d3bd9a5f216c1fa2c321cd564b93d8f2e4f521a85b585447747997"}, + {file = "protobuf-4.22.3-cp37-cp37m-win32.whl", hash = "sha256:ecae944c6c2ce50dda6bf76ef5496196aeb1b85acb95df5843cd812615ec4b61"}, + {file = "protobuf-4.22.3-cp37-cp37m-win_amd64.whl", hash = "sha256:d4b66266965598ff4c291416be429cef7989d8fae88b55b62095a2331511b3fa"}, + {file = "protobuf-4.22.3-cp38-cp38-win32.whl", hash = "sha256:f08aa300b67f1c012100d8eb62d47129e53d1150f4469fd78a29fa3cb68c66f2"}, + {file = "protobuf-4.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:f2f4710543abec186aee332d6852ef5ae7ce2e9e807a3da570f36de5a732d88e"}, + {file = "protobuf-4.22.3-cp39-cp39-win32.whl", hash = "sha256:7cf56e31907c532e460bb62010a513408e6cdf5b03fb2611e4b67ed398ad046d"}, + {file = "protobuf-4.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:e0e630d8e6a79f48c557cd1835865b593d0547dce221c66ed1b827de59c66c97"}, + {file = "protobuf-4.22.3-py3-none-any.whl", hash = "sha256:52f0a78141078077cfe15fe333ac3e3a077420b9a3f5d1bf9b5fe9d286b4d881"}, + {file = "protobuf-4.22.3.tar.gz", hash = "sha256:23452f2fdea754a8251d0fc88c0317735ae47217e0d27bf330a30eec2848811a"}, ] [[package]] @@ -1333,48 +1322,48 @@ files = [ [[package]] name = "pydantic" -version = "1.10.6" +version = "1.10.7" description = "Data validation and settings management using python type hints" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9289065611c48147c1dd1fd344e9d57ab45f1d99b0fb26c51f1cf72cd9bcd31"}, - {file = "pydantic-1.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c32b6bba301490d9bb2bf5f631907803135e8085b6aa3e5fe5a770d46dd0160"}, - {file = "pydantic-1.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd9b9e98068fa1068edfc9eabde70a7132017bdd4f362f8b4fd0abed79c33083"}, - {file = "pydantic-1.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c84583b9df62522829cbc46e2b22e0ec11445625b5acd70c5681ce09c9b11c4"}, - {file = "pydantic-1.10.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b41822064585fea56d0116aa431fbd5137ce69dfe837b599e310034171996084"}, - {file = "pydantic-1.10.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61f1f08adfaa9cc02e0cbc94f478140385cbd52d5b3c5a657c2fceb15de8d1fb"}, - {file = "pydantic-1.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:32937835e525d92c98a1512218db4eed9ddc8f4ee2a78382d77f54341972c0e7"}, - {file = "pydantic-1.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd5c531b22928e63d0cb1868dee76123456e1de2f1cb45879e9e7a3f3f1779b"}, - {file = "pydantic-1.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e277bd18339177daa62a294256869bbe84df1fb592be2716ec62627bb8d7c81d"}, - {file = "pydantic-1.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f15277d720aa57e173954d237628a8d304896364b9de745dcb722f584812c7"}, - {file = "pydantic-1.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b243b564cea2576725e77aeeda54e3e0229a168bc587d536cd69941e6797543d"}, - {file = "pydantic-1.10.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3ce13a558b484c9ae48a6a7c184b1ba0e5588c5525482681db418268e5f86186"}, - {file = "pydantic-1.10.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ac1cd4deed871dfe0c5f63721e29debf03e2deefa41b3ed5eb5f5df287c7b70"}, - {file = "pydantic-1.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:b1eb6610330a1dfba9ce142ada792f26bbef1255b75f538196a39e9e90388bf4"}, - {file = "pydantic-1.10.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4ca83739c1263a044ec8b79df4eefc34bbac87191f0a513d00dd47d46e307a65"}, - {file = "pydantic-1.10.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4e2a7cb409951988e79a469f609bba998a576e6d7b9791ae5d1e0619e1c0f2"}, - {file = "pydantic-1.10.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53de12b4608290992a943801d7756f18a37b7aee284b9ffa794ee8ea8153f8e2"}, - {file = "pydantic-1.10.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:60184e80aac3b56933c71c48d6181e630b0fbc61ae455a63322a66a23c14731a"}, - {file = "pydantic-1.10.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:415a3f719ce518e95a92effc7ee30118a25c3d032455d13e121e3840985f2efd"}, - {file = "pydantic-1.10.6-cp37-cp37m-win_amd64.whl", hash = "sha256:72cb30894a34d3a7ab6d959b45a70abac8a2a93b6480fc5a7bfbd9c935bdc4fb"}, - {file = "pydantic-1.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3091d2eaeda25391405e36c2fc2ed102b48bac4b384d42b2267310abae350ca6"}, - {file = "pydantic-1.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:751f008cd2afe812a781fd6aa2fb66c620ca2e1a13b6a2152b1ad51553cb4b77"}, - {file = "pydantic-1.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12e837fd320dd30bd625be1b101e3b62edc096a49835392dcf418f1a5ac2b832"}, - {file = "pydantic-1.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d92831d0115874d766b1f5fddcdde0c5b6c60f8c6111a394078ec227fca6d"}, - {file = "pydantic-1.10.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:476f6674303ae7965730a382a8e8d7fae18b8004b7b69a56c3d8fa93968aa21c"}, - {file = "pydantic-1.10.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3a2be0a0f32c83265fd71a45027201e1278beaa82ea88ea5b345eea6afa9ac7f"}, - {file = "pydantic-1.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:0abd9c60eee6201b853b6c4be104edfba4f8f6c5f3623f8e1dba90634d63eb35"}, - {file = "pydantic-1.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6195ca908045054dd2d57eb9c39a5fe86409968b8040de8c2240186da0769da7"}, - {file = "pydantic-1.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43cdeca8d30de9a897440e3fb8866f827c4c31f6c73838e3a01a14b03b067b1d"}, - {file = "pydantic-1.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c19eb5163167489cb1e0161ae9220dadd4fc609a42649e7e84a8fa8fff7a80f"}, - {file = "pydantic-1.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:012c99a9c0d18cfde7469aa1ebff922e24b0c706d03ead96940f5465f2c9cf62"}, - {file = "pydantic-1.10.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:528dcf7ec49fb5a84bf6fe346c1cc3c55b0e7603c2123881996ca3ad79db5bfc"}, - {file = "pydantic-1.10.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:163e79386c3547c49366e959d01e37fc30252285a70619ffc1b10ede4758250a"}, - {file = "pydantic-1.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:189318051c3d57821f7233ecc94708767dd67687a614a4e8f92b4a020d4ffd06"}, - {file = "pydantic-1.10.6-py3-none-any.whl", hash = "sha256:acc6783751ac9c9bc4680379edd6d286468a1dc8d7d9906cd6f1186ed682b2b0"}, - {file = "pydantic-1.10.6.tar.gz", hash = "sha256:cf95adb0d1671fc38d8c43dd921ad5814a735e7d9b4d9e437c088002863854fd"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, + {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, + {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, + {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, + {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, + {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, + {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, + {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, + {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, ] [package.dependencies] @@ -1386,43 +1375,43 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygit2" -version = "1.11.1" +version = "1.12.0" description = "Python bindings for libgit2." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pygit2-1.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:263e05ac655a4ce0a1083aaaedfd0a900b8dee2c3bb3ecf4f4e504a404467d1f"}, - {file = "pygit2-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee6b4a0e181c576cdb64b1568bfbff3d1c2cd7e99808f578c8b08875c0f43739"}, - {file = "pygit2-1.11.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:d1b5fcaac1f29337f2d1465fa095e2e375b76a06385bda9391cb418c7937fb54"}, - {file = "pygit2-1.11.1-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:96ff745d3199909d06cab5e419a6b953be99992414a08ec4dddb682f395de8f1"}, - {file = "pygit2-1.11.1-cp310-cp310-win32.whl", hash = "sha256:b3c8726f0c9a2b0e04aac37b18027c58c2697b9c021d3458b28bc250b9b6aecf"}, - {file = "pygit2-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:f42409d25bbfc090fd1af1f5f47584d7e0c4212b037a7f86639a02c30420c6ee"}, - {file = "pygit2-1.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29f89d96bbb404ca1566418463521039903094fad2f81a76d7083810d2ea3aad"}, - {file = "pygit2-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5c158b9430c5e76ca728b1a214bf21d355af6ac6e2da86ed17775b870b6c6eb"}, - {file = "pygit2-1.11.1-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:6c3434b143e7570ec45cd1a0e344fe7a12e64b99e7155fa38b74f724c8fc243c"}, - {file = "pygit2-1.11.1-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:550aa503c86ef0061ce64d61c3672b15b500c2b1e4224c405acecfac2211b5d9"}, - {file = "pygit2-1.11.1-cp311-cp311-win32.whl", hash = "sha256:f270f86a0185ca2064e1aa6b8db3bb677b1bf76ee35f48ca5ce28a921fad5632"}, - {file = "pygit2-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:56b9deeab214653805214f05337f5e9552b47bf268c285551f20ea51a6056c3e"}, - {file = "pygit2-1.11.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3c5838e6516abc4384498f4b4c7f88578221596dc2ba8db2320ff2cfebe9787e"}, - {file = "pygit2-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a886aab5aae8d8db572e20b9f56c13cd506775265222ea7f35b2c781e4fa3a5e"}, - {file = "pygit2-1.11.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:3be4534180edd53e3e1da93c5b091975566bfdffdc73f21930d79fef096a25d2"}, - {file = "pygit2-1.11.1-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:4d6209c703764ae0ba57b17038482f3e54f432f80f88ccd490d7f8b70b167db6"}, - {file = "pygit2-1.11.1-cp38-cp38-win32.whl", hash = "sha256:ddb032fa71d4b4a64bf101e37eaa21f5369f20a862b5e34bbc33854a3a35f641"}, - {file = "pygit2-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8de0091e5eeaea2004f63f7dcb4540780f2124f68c0bcb670ae0fa9ada8bf66"}, - {file = "pygit2-1.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b44674e53efa9eca36e44f2f3d1a29e53e78649ba13105ae0b037d557f2c076"}, - {file = "pygit2-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0170f31c2efb15f6779689df328c05a8005ecb2b92784a37ff967d713cdafe82"}, - {file = "pygit2-1.11.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:960a55ff78f48887a7aa8ece952aad0f52f0a2ba1ad7bddd7064fbbefd85dfbb"}, - {file = "pygit2-1.11.1-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:df722c90fb54a42fa019dcf8d8f82961c3099c3024f1fda46c53e0886ff8f0f3"}, - {file = "pygit2-1.11.1-cp39-cp39-win32.whl", hash = "sha256:3b091e7fd00dd2a2cd3a6b5e235b6cbfbc1c07f15ee83a5cb3f188e1d6d1bca1"}, - {file = "pygit2-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:da040dc28800831bcbefef0850466739f103bfc769d952bd10c449646d52ce8f"}, - {file = "pygit2-1.11.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:585daa3956f1dc10d08e3459c20b57be42c7f9c0fbde21e797b3a00b5948f061"}, - {file = "pygit2-1.11.1-pp38-pypy38_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:273878adeced2aec7885745b73fffb91a8e67868c105bf881b61008d42497ad6"}, - {file = "pygit2-1.11.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:48cfd72283a08a9226aca115870799ee92898d692699f541a3b3f519805108ec"}, - {file = "pygit2-1.11.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a9ca4cb2481d2df14d23c765facef325f717d9a3966a986b86e88d92eef11929"}, - {file = "pygit2-1.11.1-pp39-pypy39_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:d5f64a424d9123b047458b0107c5dd33559184b56a1f58b10056ea5cbac74360"}, - {file = "pygit2-1.11.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f13e190cc080bde093138e12bcb609500276227e3e8e8bd8765a2fd49ae2efb8"}, - {file = "pygit2-1.11.1.tar.gz", hash = "sha256:793f583fd33620f0ac38376db0f57768ef2922b89b459e75b1ac440377eb64ec"}, + {file = "pygit2-1.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b44a3b38e62dbf8cb559a40d2b39506a638d13542502ddb927f1c05565048f27"}, + {file = "pygit2-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:834cf5b54d9b49c562669ec986be54c7915585638690c11f1dc4e6a55bc5d79d"}, + {file = "pygit2-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ecb096cdbbf142d8787cf879ab927fc9777d36580d2e5758d02c9474a3b015c"}, + {file = "pygit2-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15620696743ffac71cfdaf270944d9363b70442c1fbe96f5e4a69639c2fe7c23"}, + {file = "pygit2-1.12.0-cp310-cp310-win32.whl", hash = "sha256:de21194e18e4d93f793740b2b979dbe9dd6607f342a4fad3ecedeaf26ec743df"}, + {file = "pygit2-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0a9d49f71bec7c4f2ff8273e0c7caba4b2f21bfc56e2071e429028b20ab9d762"}, + {file = "pygit2-1.12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a428970b44827b703cc3267de8d71648f491546d5b9276505ef5f232a921a34e"}, + {file = "pygit2-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2bb7b674124a38b12a5aaacca3b8c1e29674f3b46cb907af0b3ba75d90e5952a"}, + {file = "pygit2-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de46940b46bee12f4c938aadf4f59617798f704c8ac5f08b5a84914459d604be"}, + {file = "pygit2-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbfb3ebe7f57fe7873d86e84b476869f407d6bb204a39a3e7d04e4a7f0e43c1"}, + {file = "pygit2-1.12.0-cp311-cp311-win32.whl", hash = "sha256:db98978d559d6e84187d463fb3aa83cf6120dadf62058e3d05a97457f9f27247"}, + {file = "pygit2-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:8734a44e0dab8a5e6668e4a926f7171b59b87d65981bd3732efba57c327cec6d"}, + {file = "pygit2-1.12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1bb73ffb345400f8c6fe391431e06b93e26bc4d2048b1ac3f7c54dae5f7b6dc2"}, + {file = "pygit2-1.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdeaf1631803616d303b808cd644ee17164fb675241ab1b0bb243d4a3d3de59f"}, + {file = "pygit2-1.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:652b3f0510ad21ec6275b246aa3e7a2e20f2f9c37a9844804887fabc2db49ca3"}, + {file = "pygit2-1.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2419cd1034bf593847466b188a65bd9d512e13b7da0e8c3a74b487d8014a6c1"}, + {file = "pygit2-1.12.0-cp38-cp38-win32.whl", hash = "sha256:6a445a537de152364b334e73047af9225fe8c6f54c7d815d8c751cb23b79cbef"}, + {file = "pygit2-1.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:ad1cca4533beee034277fe01f0d4029da40d2bd1a944a8cd17bffccc0331cc53"}, + {file = "pygit2-1.12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ad7b21e35e759d033dede5dc4971f6c9b3408f9096b26fabc7cedb49e319680"}, + {file = "pygit2-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e303aa9d7de6039cc4450a1fbd5911fab22867dc4e05f148b0cd7c56f7b84b2"}, + {file = "pygit2-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:869e68cfae7e0e00a799efa26bba3f829bdeafa1462225a7db1317dacb4e6a4e"}, + {file = "pygit2-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c779c15bf6ebce986cb753c8113ccfb329c12d4a73b303ee7ac2c8961288b8cd"}, + {file = "pygit2-1.12.0-cp39-cp39-win32.whl", hash = "sha256:c6ac2fd8ed30016235b06aacc28e5f10e1a17d0f02eab35f5f503138bbee763d"}, + {file = "pygit2-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:2483e4aa8bb4290ab157d575b00b830528c669869d710646a1d4af7209d59e81"}, + {file = "pygit2-1.12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8fca4ca59928436fca5df3d54a7d591e7aa12ebaeaeb1801a99e09970fb8f1d3"}, + {file = "pygit2-1.12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0746791741ba1879faafd12be0b7fb8edd06633508bbf8aabfd28415f1c0b13f"}, + {file = "pygit2-1.12.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b9d8b7e1d143415d462d82fc5d9dd5922c527474871c7b3c3a8aec009b74b1c"}, + {file = "pygit2-1.12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:69ee34f8b77fc60dcf93524fd843eacc416be906b7471746d2ee8214d5a591a0"}, + {file = "pygit2-1.12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c290dadcf42e9d857ea20c37781168de1d1ac31b196b450400f962279aa405f"}, + {file = "pygit2-1.12.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d9bdd2837f9f1cacb571889ac4226844a41476509c325732af06b622293782"}, + {file = "pygit2-1.12.0.tar.gz", hash = "sha256:e9440d08665e35278989939590a53f37a938eada4f9446844930aa2ee30d73be"}, ] [package.dependencies] @@ -1430,18 +1419,17 @@ cffi = ">=1.9.1" [[package]] name = "pytest" -version = "7.2.2" +version = "7.3.1" description = "pytest: simple powerful testing with Python" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, - {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, + {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, + {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, ] [package.dependencies] -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" @@ -1450,7 +1438,7 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" @@ -1559,18 +1547,18 @@ dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatc [[package]] name = "redis" -version = "4.5.1" +version = "4.5.4" description = "Python client for Redis database and key-value store" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "redis-4.5.1-py3-none-any.whl", hash = "sha256:5deb072d26e67d2be1712603bfb7947ec3431fb0eec9c578994052e33035af6d"}, - {file = "redis-4.5.1.tar.gz", hash = "sha256:1eec3741cda408d3a5f84b78d089c8b8d895f21b3b050988351e925faf202864"}, + {file = "redis-4.5.4-py3-none-any.whl", hash = "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2"}, + {file = "redis-4.5.4.tar.gz", hash = "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893"}, ] [package.dependencies] -async-timeout = ">=4.0.2" +async-timeout = {version = ">=4.0.2", markers = "python_version <= \"3.11.2\""} [package.extras] hiredis = ["hiredis (>=1.0.0)"] @@ -1618,14 +1606,14 @@ idna2008 = ["idna"] [[package]] name = "setuptools" -version = "67.6.0" +version = "67.7.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, - {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, + {file = "setuptools-67.7.1-py3-none-any.whl", hash = "sha256:6f0839fbdb7e3cfef1fc38d7954f5c1c26bf4eebb155a55c9bf8faf997b9fb67"}, + {file = "setuptools-67.7.1.tar.gz", hash = "sha256:bb16732e8eb928922eabaa022f881ae2b7cdcfaf9993ef1f5e841a96d32b8e0c"}, ] [package.extras] @@ -1671,53 +1659,53 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.4.46" +version = "1.4.47" description = "Database Abstraction Library" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.4.46-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7001f16a9a8e06488c3c7154827c48455d1c1507d7228d43e781afbc8ceccf6d"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c7a46639ba058d320c9f53a81db38119a74b8a7a1884df44d09fbe807d028aaf"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-win32.whl", hash = "sha256:c04144a24103135ea0315d459431ac196fe96f55d3213bfd6d39d0247775c854"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-win_amd64.whl", hash = "sha256:7b81b1030c42b003fc10ddd17825571603117f848814a344d305262d370e7c34"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:939f9a018d2ad04036746e15d119c0428b1e557470361aa798e6e7d7f5875be0"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b7f4b6aa6e87991ec7ce0e769689a977776db6704947e562102431474799a857"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf17ac9a61e7a3f1c7ca47237aac93cabd7f08ad92ac5b96d6f8dea4287fc1"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f8267682eb41a0584cf66d8a697fef64b53281d01c93a503e1344197f2e01fe"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cb0ad8a190bc22d2112001cfecdec45baffdf41871de777239da6a28ed74b6"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-win32.whl", hash = "sha256:5f752676fc126edc1c4af0ec2e4d2adca48ddfae5de46bb40adbd3f903eb2120"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-win_amd64.whl", hash = "sha256:31de1e2c45e67a5ec1ecca6ec26aefc299dd5151e355eb5199cd9516b57340be"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d68e1762997bfebf9e5cf2a9fd0bcf9ca2fdd8136ce7b24bbd3bbfa4328f3e4a"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d112b0f3c1bc5ff70554a97344625ef621c1bfe02a73c5d97cac91f8cd7a41e"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69fac0a7054d86b997af12dc23f581cf0b25fb1c7d1fed43257dee3af32d3d6d"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-win32.whl", hash = "sha256:887865924c3d6e9a473dc82b70977395301533b3030d0f020c38fd9eba5419f2"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-win_amd64.whl", hash = "sha256:984ee13543a346324319a1fb72b698e521506f6f22dc37d7752a329e9cd00a32"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:9167d4227b56591a4cc5524f1b79ccd7ea994f36e4c648ab42ca995d28ebbb96"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d61e9ecc849d8d44d7f80894ecff4abe347136e9d926560b818f6243409f3c86"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3ec187acf85984263299a3f15c34a6c0671f83565d86d10f43ace49881a82718"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9883f5fae4fd8e3f875adc2add69f8b945625811689a6c65866a35ee9c0aea23"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-win32.whl", hash = "sha256:535377e9b10aff5a045e3d9ada8a62d02058b422c0504ebdcf07930599890eb0"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-win_amd64.whl", hash = "sha256:18cafdb27834fa03569d29f571df7115812a0e59fd6a3a03ccb0d33678ec8420"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:a1ad90c97029cc3ab4ffd57443a20fac21d2ec3c89532b084b073b3feb5abff3"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4847f4b1d822754e35707db913396a29d874ee77b9c3c3ef3f04d5a9a6209618"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c5a99282848b6cae0056b85da17392a26b2d39178394fc25700bcf967e06e97a"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b1cc7835b39835c75cf7c20c926b42e97d074147c902a9ebb7cf2c840dc4e2"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-win32.whl", hash = "sha256:c522e496f9b9b70296a7675272ec21937ccfc15da664b74b9f58d98a641ce1b6"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-win_amd64.whl", hash = "sha256:ae067ab639fa499f67ded52f5bc8e084f045d10b5ac7bb928ae4ca2b6c0429a5"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:e3c1808008124850115a3f7e793a975cfa5c8a26ceeeb9ff9cbb4485cac556df"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d164df3d83d204c69f840da30b292ac7dc54285096c6171245b8d7807185aa"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b33ffbdbbf5446cf36cd4cc530c9d9905d3c2fe56ed09e25c22c850cdb9fac92"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d94682732d1a0def5672471ba42a29ff5e21bb0aae0afa00bb10796fc1e28dd"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-win32.whl", hash = "sha256:f8cb80fe8d14307e4124f6fad64dfd87ab749c9d275f82b8b4ec84c84ecebdbe"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-win_amd64.whl", hash = "sha256:07e48cbcdda6b8bc7a59d6728bd3f5f574ffe03f2c9fb384239f3789c2d95c2e"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1b1e5e96e2789d89f023d080bee432e2fef64d95857969e70d3cadec80bd26f0"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3714e5b33226131ac0da60d18995a102a17dddd42368b7bdd206737297823ad"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:955162ad1a931fe416eded6bb144ba891ccbf9b2e49dc7ded39274dd9c5affc5"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6e4cb5c63f705c9d546a054c60d326cbde7421421e2d2565ce3e2eee4e1a01f"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-win32.whl", hash = "sha256:51e1ba2884c6a2b8e19109dc08c71c49530006c1084156ecadfaadf5f9b8b053"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-win_amd64.whl", hash = "sha256:315676344e3558f1f80d02535f410e80ea4e8fddba31ec78fe390eff5fb8f466"}, - {file = "SQLAlchemy-1.4.46.tar.gz", hash = "sha256:6913b8247d8a292ef8315162a51931e2b40ce91681f1b6f18f697045200c4a30"}, + {file = "SQLAlchemy-1.4.47-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:dcfb480bfc9e1fab726003ae00a6bfc67a29bad275b63a4e36d17fe7f13a624e"}, + {file = "SQLAlchemy-1.4.47-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:28fda5a69d6182589892422c5a9b02a8fd1125787aab1d83f1392aa955bf8d0a"}, + {file = "SQLAlchemy-1.4.47-cp27-cp27m-win32.whl", hash = "sha256:45e799c1a41822eba6bee4e59b0e38764e1a1ee69873ab2889079865e9ea0e23"}, + {file = "SQLAlchemy-1.4.47-cp27-cp27m-win_amd64.whl", hash = "sha256:10edbb92a9ef611f01b086e271a9f6c1c3e5157c3b0c5ff62310fb2187acbd4a"}, + {file = "SQLAlchemy-1.4.47-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7a4df53472c9030a8ddb1cce517757ba38a7a25699bbcabd57dcc8a5d53f324e"}, + {file = "SQLAlchemy-1.4.47-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:511d4abc823152dec49461209607bbfb2df60033c8c88a3f7c93293b8ecbb13d"}, + {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbe57f39f531c5d68d5594ea4613daa60aba33bb51a8cc42f96f17bbd6305e8d"}, + {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca8ab6748e3ec66afccd8b23ec2f92787a58d5353ce9624dccd770427ee67c82"}, + {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299b5c5c060b9fbe51808d0d40d8475f7b3873317640b9b7617c7f988cf59fda"}, + {file = "SQLAlchemy-1.4.47-cp310-cp310-win32.whl", hash = "sha256:684e5c773222781775c7f77231f412633d8af22493bf35b7fa1029fdf8066d10"}, + {file = "SQLAlchemy-1.4.47-cp310-cp310-win_amd64.whl", hash = "sha256:2bba39b12b879c7b35cde18b6e14119c5f1a16bd064a48dd2ac62d21366a5e17"}, + {file = "SQLAlchemy-1.4.47-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:795b5b9db573d3ed61fae74285d57d396829e3157642794d3a8f72ec2a5c719b"}, + {file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:989c62b96596b7938cbc032e39431e6c2d81b635034571d6a43a13920852fb65"}, + {file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b67bda733da1dcdccaf354e71ef01b46db483a4f6236450d3f9a61efdba35a"}, + {file = "SQLAlchemy-1.4.47-cp311-cp311-win32.whl", hash = "sha256:9a198f690ac12a3a807e03a5a45df6a30cd215935f237a46f4248faed62e69c8"}, + {file = "SQLAlchemy-1.4.47-cp311-cp311-win_amd64.whl", hash = "sha256:03be6f3cb66e69fb3a09b5ea89d77e4bc942f3bf84b207dba84666a26799c166"}, + {file = "SQLAlchemy-1.4.47-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:16ee6fea316790980779268da47a9260d5dd665c96f225d28e7750b0bb2e2a04"}, + {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:557675e0befafa08d36d7a9284e8761c97490a248474d778373fb96b0d7fd8de"}, + {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb2797fee8a7914fb2c3dc7de404d3f96eb77f20fc60e9ee38dc6b0ca720f2c2"}, + {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28297aa29e035f29cba6b16aacd3680fbc6a9db682258d5f2e7b49ec215dbe40"}, + {file = "SQLAlchemy-1.4.47-cp36-cp36m-win32.whl", hash = "sha256:998e782c8d9fd57fa8704d149ccd52acf03db30d7dd76f467fd21c1c21b414fa"}, + {file = "SQLAlchemy-1.4.47-cp36-cp36m-win_amd64.whl", hash = "sha256:dde4d02213f1deb49eaaf8be8a6425948963a7af84983b3f22772c63826944de"}, + {file = "SQLAlchemy-1.4.47-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e98ef1babe34f37f443b7211cd3ee004d9577a19766e2dbacf62fce73c76245a"}, + {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14a3879853208a242b5913f3a17c6ac0eae9dc210ff99c8f10b19d4a1ed8ed9b"}, + {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7120a2f72599d4fed7c001fa1cbbc5b4d14929436135768050e284f53e9fbe5e"}, + {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:048509d7f3ac27b83ad82fd96a1ab90a34c8e906e4e09c8d677fc531d12c23c5"}, + {file = "SQLAlchemy-1.4.47-cp37-cp37m-win32.whl", hash = "sha256:6572d7c96c2e3e126d0bb27bfb1d7e2a195b68d951fcc64c146b94f088e5421a"}, + {file = "SQLAlchemy-1.4.47-cp37-cp37m-win_amd64.whl", hash = "sha256:a6c3929df5eeaf3867724003d5c19fed3f0c290f3edc7911616616684f200ecf"}, + {file = "SQLAlchemy-1.4.47-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:71d4bf7768169c4502f6c2b0709a02a33703544f611810fb0c75406a9c576ee1"}, + {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd45c60cc4f6d68c30d5179e2c2c8098f7112983532897566bb69c47d87127d3"}, + {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fdbb8e9d4e9003f332a93d6a37bca48ba8095086c97a89826a136d8eddfc455"}, + {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f216a51451a0a0466e082e163591f6dcb2f9ec182adb3f1f4b1fd3688c7582c"}, + {file = "SQLAlchemy-1.4.47-cp38-cp38-win32.whl", hash = "sha256:bd988b3362d7e586ef581eb14771bbb48793a4edb6fcf62da75d3f0f3447060b"}, + {file = "SQLAlchemy-1.4.47-cp38-cp38-win_amd64.whl", hash = "sha256:32ab09f2863e3de51529aa84ff0e4fe89a2cb1bfbc11e225b6dbc60814e44c94"}, + {file = "SQLAlchemy-1.4.47-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:07764b240645627bc3e82596435bd1a1884646bfc0721642d24c26b12f1df194"}, + {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e2a42017984099ef6f56438a6b898ce0538f6fadddaa902870c5aa3e1d82583"}, + {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6b6d807c76c20b4bc143a49ad47782228a2ac98bdcdcb069da54280e138847fc"}, + {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a94632ba26a666e7be0a7d7cc3f7acab622a04259a3aa0ee50ff6d44ba9df0d"}, + {file = "SQLAlchemy-1.4.47-cp39-cp39-win32.whl", hash = "sha256:f80915681ea9001f19b65aee715115f2ad310730c8043127cf3e19b3009892dd"}, + {file = "SQLAlchemy-1.4.47-cp39-cp39-win_amd64.whl", hash = "sha256:fc700b862e0a859a37faf85367e205e7acaecae5a098794aff52fdd8aea77b12"}, + {file = "SQLAlchemy-1.4.47.tar.gz", hash = "sha256:95fc02f7fc1f3199aaa47a8a757437134cf618e9d994c84effd53f530c38586f"}, ] [package.dependencies] @@ -1819,14 +1807,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.6" +version = "0.11.7" description = "Style preserving TOML library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, + {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, + {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, ] [[package]] @@ -1860,14 +1848,14 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.21.0" +version = "0.21.1" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "uvicorn-0.21.0-py3-none-any.whl", hash = "sha256:e69e955cb621ae7b75f5590a814a4fcbfb14cb8f44a36dfe3c5c75ab8aee3ad5"}, - {file = "uvicorn-0.21.0.tar.gz", hash = "sha256:8635a388062222082f4b06225b867b74a7e4ef942124453d4d1d1a5cb3750932"}, + {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, + {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, ] [package.dependencies] From 97d0eac303190f4af34d0814c77757e602f1384c Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sat, 22 Apr 2023 11:39:00 +0200 Subject: [PATCH 1293/1451] housekeep: copy static files we copy static files used by PHP and Python versions into /static preparation work for the removal of the PHP version Signed-off-by: moson-mo --- aurweb/asgi.py | 6 +- static/css/archnavbar/archlogo.png | Bin 0 -> 5359 bytes static/css/archnavbar/archnavbar.css | 26 + static/css/archnavbar/aurlogo.png | Bin 0 -> 5997 bytes static/css/archweb.css | 1255 ++++++++++++++++++++++++ static/css/aurweb.css | 292 ++++++ static/css/cgit.css | 866 ++++++++++++++++ static/images/ICON-LICENSE | 26 + static/images/action-undo.min.svg | 3 + static/images/action-undo.svg | 32 + static/images/ajax-loader.gif | Bin 0 -> 723 bytes static/images/favicon.ico | Bin 0 -> 575 bytes static/images/pencil.min.svg | 3 + static/images/pencil.svg | 55 ++ static/images/pin.min.svg | 1 + static/images/pin.svg | 3 + static/images/rss.svg | 3 + static/images/unpin.min.svg | 1 + static/images/unpin.svg | 4 + static/images/x.min.svg | 3 + static/images/x.svg | 31 + static/js/comment-edit.js | 61 ++ static/js/copy.js | 9 + static/js/typeahead-home.js | 6 + static/js/typeahead-pkgbase-merge.js | 6 + static/js/typeahead-pkgbase-request.js | 36 + static/js/typeahead.js | 151 +++ 27 files changed, 2874 insertions(+), 5 deletions(-) create mode 100644 static/css/archnavbar/archlogo.png create mode 100644 static/css/archnavbar/archnavbar.css create mode 100644 static/css/archnavbar/aurlogo.png create mode 100644 static/css/archweb.css create mode 100644 static/css/aurweb.css create mode 100644 static/css/cgit.css create mode 100644 static/images/ICON-LICENSE create mode 100644 static/images/action-undo.min.svg create mode 100644 static/images/action-undo.svg create mode 100644 static/images/ajax-loader.gif create mode 100644 static/images/favicon.ico create mode 100644 static/images/pencil.min.svg create mode 100644 static/images/pencil.svg create mode 100644 static/images/pin.min.svg create mode 100644 static/images/pin.svg create mode 100644 static/images/rss.svg create mode 100644 static/images/unpin.min.svg create mode 100644 static/images/unpin.svg create mode 100644 static/images/x.min.svg create mode 100644 static/images/x.svg create mode 100644 static/js/comment-edit.js create mode 100644 static/js/copy.js create mode 100644 static/js/typeahead-home.js create mode 100644 static/js/typeahead-pkgbase-merge.js create mode 100644 static/js/typeahead-pkgbase-request.js create mode 100644 static/js/typeahead.js diff --git a/aurweb/asgi.py b/aurweb/asgi.py index b6578f33..eb02413b 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -79,11 +79,7 @@ async def app_startup(): "endpoint is disabled." ) - app.mount("/static/css", StaticFiles(directory="web/html/css"), name="static_css") - app.mount("/static/js", StaticFiles(directory="web/html/js"), name="static_js") - app.mount( - "/static/images", StaticFiles(directory="web/html/images"), name="static_images" - ) + app.mount("/static", StaticFiles(directory="static"), name="static_files") # Add application routes. def add_router(module): diff --git a/static/css/archnavbar/archlogo.png b/static/css/archnavbar/archlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..3d2cd40fb499b2b37651ea67ec6f3b4400e3d157 GIT binary patch literal 5359 zcmZ{IS2!Hp_w|^fM(@4%HcCVgz4tPMgkaQ&HhM3EFiH?fM506sq7yX?BLq=`i12C| zL~o;a{(hI=#rK@O&w9?>10<3$laUEN!Q#6008Pn zbXOpBG|m5lHpl75-vVhpC(a!4x$h*`V2lakNUt?{-fw^e?ODm zN2l+hb*}S!PafO;yvB^)?EDQ@iLzn>_b!b9fT^^f*w@{?%2`uO4TKQ2Smxg5ikKW2 zQ*A?S8C;#@KdJ0k753J9XG*F&MOl%LT4?kK&;(>rVhi9U1?;<+SJ-zbGm)>gYnJLH z%w}?U0WeGe;1*6<3W^4axh(3Rm06q~vz;20p9m)iop2}sG)ey@z5ZEzGBay7^^(hv zOFvOQpz-HtC(no`dwszCljyn5{FA6S0{1JalqoLl^-)5KLk=qdBTC!pRYl~Zu4~eY z`jm=y&G7{n7Ek2g)hM=KE?#h6uBwFA4#F~l!gQh1QhEZ zVRX6TH*Dt7a2hD-{!i`M-_>j)z6g%ca~xCE?`N$AK|HXr$u$Q&P^B$#;&T%7mJM zuo8+h+l1P5KqsF_FkI5xbvE#m)@W=uI^t;J3`eFie;MX?$(u}bW!*u-&#nH?kt^%d zx|YaA-ExCV9U&V&;SIrCEb(@465tV50>|q>SR zoU@zOxr=q~2E8QP8G}nzeV$%*DuQQq|Tc`gx!LmYDeG= zxKY+=D~=fjPC4|=pTYudRG(20hFb0uLqD#-tI}K=Ka;(+@i-pI>=l)p6i$ z&9-~lq-@(Seb}1YT!Fy^x8xB-=ujz?L)W!@=ax`=LLfUDA2ZdD#!`(EzFdkzMFYWBytXn;~HGS8jwD~p^JH9 z(yY!Xaf-vSL#cK0=xru5#$e>dlTyIaB33aiIEI-;90sCX`viJ zA0i;5`@2n-HiiSnJS{XdI4WF@Dm#>@7=~DhfH|<5T9m?3X2M$XEY_)Boxvz8R1=d+ zB`^*2^JpfC?3HnEWQFUvg3{}9;A~0gk4%RM(^LTtAnciRWj)K8EL1z=nCL`vFJ0_8 z!lUO+=S6*pjK~uS5pwKHtY(>*-;1v^W&;MohRce{DXIOkviW#BfZjD`@$7ik~xbg$=6paDPdGZM^={xaJ@@q2g3F_z3%7tZk`C zur_-ZmzZiRoB!k5KDqsu&zw`4dhk;}>?d+hUXaSqbmfQjyn1xNf=oQU%Ay=+y!NUKi@<_oc32aa9U%msvJaVpldpOC0k^?27Mr#?k`zim@2j!tQM8L z>5NktPROk;Vy)csIG|uk19*2kOGxmT0O-PQd40OA%-8}209F~HZf=Td#(;;6XTF|h zGG5=G!a^xRAc~u#SS3Xwrx^tTPhb|}T=HW#cvOeJbg_ZtCNaD3TxyjDGOH`2F&N~i zK7%H?&d0K(b}N}2LKXZU=#8)o@P$KnIRtT-efg^}&%OhchdWNi^uZJJ5~ zsROEr60OfOob6dz%ll^ zw`BJj(gPB=SuLcx+ZjhamX2)_&8oO;QyKpP14>$%p2EHQWqkkVu`xg(V}oVi}Ri1z*S+I zg&vwS4$uun2<^C{X&aMF^DN3nO)lxHR>1oQ{Z?T`8iK%gj1M1j_B(}M{>u?&qPqCf z@uD>bS$InQJq`cN+P@oOa_&vEnx|U=u1hPu{XyV*CC>MBy&6G%>l26W*t=CBlXDDG z6=*#pihOP#lTE22?q`sZa3sigQ^t;7EsE0h^Jrtf7+{UX`CfGHD~LprH&0G2mS><_ zyZVj0jd5Wx@9sBtbgeo`9e}YJXj8`SWrhXbgC%E!X(`q$mp5gTKS>%Vl39a^@Fnh> zrQ}ro+g6k_&$ppZg>ZU`g8U#2p%uT|JE^pGL@X@KOl6nXY1 zV10N_gQU6nR({-gHmuQc?o@7=>Vm(tjfji_s`T&NmFf5I7ZHuF&zOH)4_MS*J1a5m zj^ydAYi5p=oW5!qyL@3mnsD;>k10{nJhlr|3H{Ut`O+P9Pj*qF5;xUngQOxl>`5OT z&)Tz_Gd^M50N7Kn(8`Ba!k$|~2>J){l{t}mhqLb8Xs!=Mz(vm42#U*y-ogL-AGoQQ8XB4ez$vAk@nHI82! z8d9IJr16mXG{~R@PZ>x}nY`_B1tCNdc4powbO1}zff z9u12=;9MC{dh4p~e6-0|uF6$`Pj{xAt0Jr+{39w6dq}am5a(PV>Um-HRfEPPS(G*8 z$>LlpE%mSB?%8W)=@7LK^+u-e%_XBp-gtU|1On9feMMGxLNVz3l0l#Z3wFJUrNZNq z=)&0bq%9%iA`=U@3afIyi?UKitiEN7$7@|MhLB9j`y0^mougfz51B1v%7Z-$^ZfI} zwQvKT$?R|7l%b=L+n|c)ZRmSslV|r|ir*yXkw!=j;o4)`)&9i&vQr`V;o?N7*4H~GRuFf>EYM8 z75NO70;8S&Z%M)#lxvZ-#&%;@_oFzzNuu?^L#dErg-H6l?T#=CR>*qr2Eb5oAS$-@ zoE>VjIJo&!`uPMVZlxmZYekd=eC_k5UPV}Oh21VDP6mFwYue2SrxK|*7%OfVUP{Lv z7Bo(pkXOYoC0`Q$Sho|f75Vm$&E>1!MS3I*0bA3=k`Ptq=` zl*jVdmYB(g`_Havu^{x)3A!sO&}>gg5Ju%aw8 z(+7dG`wX8zE`;xWtj@Ubij)aeaF&b*7q5Q`Z~xCB_jWZqY}f9 zM}Pd%k?O0iHVeBp^{Oh54+%Wb2-_%_+x|9(b-|WRit}c1S4wB}MUelnUs-3h^6Frg zO7;Vl?oT<|9WI z)L({xmKW)r8PZaTJv->_F7ufCkdUk_BqyGHn~#)mOu!*2f>+oWe;qhvDVW%|*AkrR zayWjz;XR8b51uDI6FGJC4NKRFx5m9Y>Jzus5nG5Cf@hoBqcE(*=Ir#pclmAOe)M@2 zY!S#sdBv{GRi1=+VAzIO^2(O7+;kBPd+Z|GuYN*qfJY~^SL6Ev(d0jSf^2IT>)CNT zZOqG06m_S5M@gi7+;62DdcmB@|KU<9sJEIwx(XDPyb?&a1H>~(pjJx$s`>iHwm@^- zh!zD$RqscY>gW+nnFhN?DKq<;laZ)~N3zETIFxNm7^d7(t?+14rhU(;bbM*VOZ%#q zn(GTJgf2XNsNRAaXk)G===ivdH|CQ1DJhk#!=yN9+%k33o3K-4GdEp*XQ|{0J)n)t zTUfA*VW55-_bEY^pleU?%!MJBBgK8ZE+d*_Yk`vF+dT5^v^R!j_S}SPDTDiJ4?&(a zdc)daUl3HUDW{@wn4x6v>y}V|;WQ1pVny=fX%=Tx=-I7HeC4#0q!Cd0sM3e7iim?E zr7WdCd-HrNeH@ZKjYy)>XB@q|?n>!{etU;K*_GiBSDEmy0qk1e`FXOHeiKL%&%M+) z84ChG>x!ZQ%&yHHk!p)30$b1nh^Uz~G}Q0kz?9BQmgBy=9b#6FI6KVC%gc#Lgdfj8 zlf8g_RGi3+ql39R3HP<(a1aLVeycB!uGTgx@KHD+5XB+kEmcJeuV>BU-?lNBnVz=>k(&9qn^S z7K2hxxY;uFiO@8m`**ob>EWLwtBYq-B*Bhvd_c|b{kywN<`Ue${v}YPZI;N(=AMsp zzt1^8&0lz`)WB6GE>KH_W~`CJ@i=Fd0~3=aTlTL?Hczl?@$ z@BcaT9p-r6)8HIU)FCvs6%bfX!F*8Cl4U^mgjr53K-SB@l%^OGPh^%ysA(2-InjRn z$L4ZSPF6O)%j7mrM<%UT_P5z%%R(~R3>Bb?TCPj$qsIG3j1jdM5^PKT%K)ZkYU%22 z1FY>E0pB-&9$NH}opdQ%zkIw&ZP?oUs>HGwsnzl+n&H;4zXZ4K8iH(%P-W&KX`Mvp4;W6k)w(@UKYq)zF%33UvR9^Vdk;p ze$x>&*Dolow}vb2^iK)dv>3j$e1f8)W_(v<&u&_R%F8QT-6et_4ugjT&e^(?)N74! zr2@lX=dG9+Sa7}CgE(~DW}Y>ntw@+ubsO7+u?M(0S$DLMecn?v9e*-+#A%EEo@0ha zx1FYSOT>&~{e@o~9eQl@oW}S|=?ihQX2$xo?Hbho4LBM{?XUEs)~MWMDzsd_Ps+)0 zm$z=F_RZ-tIx4Az(0(TKH&Tul@?vg`BH_PBN0ag(jz+7MvRw$+cbVT)b^MMkF%N^0 zn5`K-mQgtGmis_K@Q}hlM3J)?0H`$WM2|iBb-}+ajxO6#NnsY(sC%|`T~uY-SX@1z zc^lz?q7d#;BCtY4ota^k=$|s_$$Mft&vLi!yeM$eeL2pU1KwfN)Z!}^#Df}xSuXc8 zTfCh8LxwYVEYH_>0H3#aLxqFxW-=j4V+dh50`aTcMAjU=$Hd*a56>BsM}a|~4`SbF z%r@~aN+OVEgjMv158JDSyJK%X8vjgDdw4FyHQamUm-I~e{67C1Q|Jw27am{KRPd4G z&e!7)(6SD2Mg+JhIQhBU0YDlm^*|gda~Civ83m}cf~>44^q~S2+5z&A{(l5sKF;p0 zq5pruh}}fmoq+j&9V~oY1A-BLE`Z?RUR4J?A{`Jpl*=BG%MU(F5+Sz{!V)16=3lFeiZt5(_0ZH-#a$v|q>+9uvDnZ60#RRUswf)x%7pUU@TJ6OOWbM)&`B92~4tQuME0y@Gmst2;XK94s^n zOY~5t;;Ej-k{P77BRBGneohUNW?QAO@!S)DzanT>G*-OhvoI?qWZM!lNM3EPB9`nD zyJBe`KY7x%L9a*_I#up|5gPmq^ZIK(9<@pGQpA!ffzE%TS%+q{ei-rjQdEk0!ZJry zTXd3-a8k#059OQ@U)ANA+YCd-I$O~0cy00o9b_mb!drm{T2yvd!cFcj(d*vdh^4#I zu;)d>&D~98cZtN-UmX|;Z--uuZ^PUt;@HIcP-(d@hIQ3!NfmFS-C@YgXA^P-DA@QQ z?v7PKM|sozZ~EcQ59IXp1f>Q9RmMcX5tBbMHy4wdO8x1}mx7|AuDW>38n(|`qaz~< zO+D6CtQ64-$%TX0aIG>dYrM;sjFr0S}w*ye$Eo(&BRtdf#+HTHu_Q0Vh8B{sZ} z_G0bK#O!QmJ{CHOHfts^3zoV1WD|7Ss=4Kg`htw{mel`go zHR0&!DEjCq_{$d>2}Tk}M@Q(xeSpF1*NB3GPUd3aDDp6{B3(q#ODyV0($~+0zZj7+ z5F>kOvd(cyoPV^n$y?jlAWtNce>b?TlhM#bw)kJH)eOrCH$EH>JhR#8P2!wxZfU8q z=^+7w!D(+@ot?v9aAGNvrHMJyQ}dgIp-_;Tvxu-T+CYnX7bX%Wsj#>>B1o-Cje*0% z{asXieE0eO{Oamz2ZYww*LS8ZP~K(nXBYT&4fFEK29e#22ws_1`vK&jvD;J8VdZ2b zR%XQzHbRS`7r~{pE{JJz0Ya%Pv5GE29g&zNMHC@1?9jR0ceOi>0;qEatn6Nzd|vhU z2}+W&h;tI$wgdWJm!hMeYK)+gx|=ED6x-nEs9d?8L!xwWOlPeOy9$Tog2_$jWGcD! zY_*++R`mk4xVZS1kMfy?DTeUb)*P+N$~Hpvh+y`$kHNogH($By;T7&W@vsii%}- z939qJEp$~0hVH-(eIk?~Ti4E{eX+XRWRpF^Z;t;Kxv^o`U@l8iT|-P2 z8CmyR6vZqSp*RUzonU|mh|qC?Bx2 z2s%1Ce1R{7wIJd@4x6hzYq|SXS8inM;CxN(RZfBAH>*hPEYQq*gy-di5tyH$ppfz%#vb zjQ0L^OTOUycM_aX3^q14^uA%zw-l0$arB}D zMiu6wyGI|Dp~_fw&hruaB|3D9KLc*OYKFbMy&nq;Q#uUir~*V0p(ifXdrCn;k(io_ zBQGyc_2|)RLf~z|tfYp;Y>U5m>*X3DM1($N*Z<-`RZR^iObO9<*50lVArg7NJYX5Y za|(NgHT=l&5bD!=Posl;9TzaPW7vLoyF79kJ2;{lp}f0;9iKlsv7YjYser^vh>H!R zL2!{LxETOx?i`Ujc->%jT{a90PpIn~0EL7N4C=3(iMDeQ!(KyWJPtbZ+*u;jhRGW4 z(fkWBLPzLEgCN`VZBY`beDkoIE}}?KTT2U*Yx5D#-#_W0D=U^Y!yB8Mh}PB|wb*O= z5J_M33Wnh~V=Ia&4^L0kw{IEuXMxy$d)r9y@#9C%X;TvuD3AiZwnBVws=)wvL(N@% zDZGHp-~mt)VsP>71D4&!3yE3-1>HB8ARmcU`8@BW5dMmA)EJ_KW)h(x zMkRQ{V#@k5Dw2{q*q}DlG?3CU7cZ#Ly=7`*KX>!;|# zZTmU#_kN3;9)Y7kR_JTzH7Jb2S)b6uum3Nwr*n%&QO*rt;#dl@2+Jdg$MrDLIQ9nU zx7R%y#8uyqrUAZsL^%_ae4xOH9!uOpYe1&g;J`qv z*M9$J-pivN7A8YiBcm^mUXX&`hIy8a=XMhrzu2+FsnkY0aR1eV)8EEe>oN+Mm>U!O z-mc}K<~J)ke2vAw-ZPV-i5Z!JO0R7adOdP7^7CrW48Ga_nN|)aVUyKVT-=RQqA_a+ zz1#ddm`Nfk;j}MA`#xPkXq%N!?!Vb-OWYSO$mu=!>VK6iQ_6=s#4)=>=7XXQpK_T! z0!gEGJmPYQv0g#~ox%93e_$Fz@9WEbK70_`H}UnAtgfzx9xOHG%P zvbw$fuIt_GU&Qq|BS^Q@z$e2xw&VQx{GNGYZ)cbFB>bzKK_p6)@0a<4Fb#*NW5dFq z&dy75zkIoAcgC??F@Kos@FZJP`A zvfL;WJ64u?`7L}1HCm9%9ffNYA;Qp6WhoR|Aa%EPgr(LRZtd5K7lX2(eSlvs?B;2l zk7Kp6^K|X-AdyVPQ31Z~U4J$=vg8AqwTi~}PCkGB+~9xlU0K0Fz>S}Wr{6IQRjQw@ z4&q80SOX#={gGsbJbG-llHppUYEqJK1 zl*GSp5pDw@j7~TG`r41aH~kX+?4w}M;66gCtE=mWim;K9(Km~>CC6Eym6$Z&_a#t- z5&y7HH0b#TpXb*A!XR_J@S;MIE+_23sz4E90DX4LI2N+VI+2ZvOVhECJ$Nj`OzPs| z(oGuKDL6aMM@8U_1^3^B**s=?5|b9}e|H}%y55t}1GL*gxhAiDlZK`y=|UtE6B9QN zkC61%FL#7MD_{%%i+AzF(7DMV&{3DwfU@SCFRlWR#RPYgO`> zD_DHSl^{t;$**N)N-&sA@r(2jfS>d};YtYnAe`!9&w*6_sF;|1WKMB$vGwsrpM={Q z*k!qu__J1cW#w}YPELBt+2rKpcKezt_?=%jm7Th7wt2te*6AnDIiVGdykW=kgHbKv zSmIiByoNz<{`wSKMM=kqt1C>ushBy=3eXHA-ho+I-f`9Vs*y#+iXn!9mHtR-CDE9^ z={x)KNym>YpS7@oAbvIA!2@GTNJ}dtbd7O=2w;|{{wcr0NEV*|NJ#K&R}zw$hBMjw zG_?i32SeXmNuv`OB|~T|U`bw7>IMc>`T6;@1KHBvBtWq0GuJQlkQd8=ET;7?MRakw zxw(_CC*O@1uZp)_5zF18m78|z`Lp_dTQadsZZ`#WB~71)iQrR|C2AT5S48<2^$70N z%HmR;h8p=V@Q!HRcI-eROh@5@bTC_&0v*0dFuRVumvKYm2vonSz9J#bd$!;-NDGv(0aUB8_Q2oJ%KsgfT!q* z@9bl2Z0z3N%4hreWn~2Z=X+I|J2MjGmT4yx_Cfy<#~)k>!o7^7FB9|#q&kc9ZGQM( zmp;16d-9G+iXE^tvS6i(BAX%cYA!#>bA`8+427vVL24)wG8z~Ts#fuwqDv1&Vk$}(M_$J2ZEpkH~B=sEdsrq+A67-u)z<0VG`JxxCr=u?Gb;gaMbk;H%m zw#Tp^PGZi5Uq2liA9uU^xi391lp`CvIq;>Rw6xFV#r)b4KIgii6U4HRzOKqoKRcgy zIjCq)vmC-wPj*HqueLkk@uTM!6RDqya9i?+z_v;MX<&ei63bHm6=>spC!C{L!o&I+CglBl+hVD*AY+J32r|rH z1lsB!pN=_cj#cUysmN*#Q{yF3n2tIkGcc=e9B;ec;RX6@^VQ$XG)ebj!N{C^?)B8R zZ{PR~jEvSNO7$U*j!J+K;o;?dWoD+$fa$vU)9$#6^UE^}H4_v1Vx8<5-;=*=tgIk7 z9Nzt^$opm$NU_B9^uDk9CF2MnT;7LEU4TDw#%VP{s@d5+Sxh{2gl6z|?8}eo`BiRu zvWl83$1Fzgc=#OBg(0H6y?8V=@7*F_Yr>D`?%<#Y z4(|RUro3hE8`7UT{p;rPkWX$rwUP?R=+v_x)1A9BYW4|ss+TZ1SU|ul3kz2vibM|l zV1;(tU7FY~h?A4M#zrJ7o{z7uV#Rbpnkw2~h<4i=jt zGcqzT+~5S@eXwYZ<(Vy*6uO&}!v!vVC74BYlZ4O{|%qlSPK(q;obF zh^-4<9O#CpW$DI0v>Cr|$ekzLYT6RyP(Bt3>ppr)J^zB{Y;2tQ=~+(x_T9U8+1SVp@nyl70Pk=u;Pzy zV|_Y<4c{j<sl&c$9-HNKd(mT zg$D8m!2IyJ`1+KFkKWuv3=zZSy@tr zGW6qmW-%&mclblGnsLQf41bV1Ry+s#wt_=1N6?v%Wbhh#lw5Qe(UWvru5lPLAbr>z zT3s!)v9+a)G-Md(hrwXzDO1y0^SnbVJL0k{<4H#v7$N*Q>J2ce2Gg(efN7pxV-kO}7OPc2p82W-!_+6}IHYu}A zUTrojo`|TZ)%o6RgZoyzoAi!CI5Efs!6l{4l^Mox^{9)?OqRq;J*Cs)x#Fu8(NCe% zx+JT$l)5Tyc(2hM>u@tWWA?Z3KUk)-w||10yf+)XjlO7{`nXl(<$>Dn&ng7pjdA?$ z>av>p_67(_w-6GE1gI?%@~x?fVQ6Rw^yiQc#T#%qpYNJiKKH`;ut{@i=||kXvpk?Z z#>U3bJLMHh<&$ft+rh4?9Kkt-Y>9k9`Rx?~;nPRUih2z<(Lp)(zS&5!+y_ka9x)7q`OqmVRAgktG+dcw001Puhs9E#IcjKHk zJDs&o)4ImSQA0yIw`UcBUB{aPA8+CZ`uq1>Ist9t`q~0;4pS8tq`*c+O})0!m-3vJ zXr|GfD^MaV5?a!5!PV zm#>VBA^|H81dUBxd|GabJmZ|X%eg^y`0z4Xb)Otu>n-tipw*vKwoQdSSPAj`Z^|jD zVA-Uj$vuf?G8&BzQbJVvF-9Y;ChxBSsfs?Ewpecu#`=d)6?XFd7cA7V*wHFV1COdF zyqta&rf7=2p9z|+aj*eM>ve{Gvqiw?;@%~E_wXT|Bx-E;ERL$%^7)Lm>)vR|`1YSV z{fg;K?5{<2^Awxa*r#AcR&j9}>nd6c_*Ky&Nf2~H-iJ9kFktn_2bkQc1q8_Qr<`rE zpZpzE1}siYZ0tU1B@srT#ywLw<4y?MmG`FyT-<}Z*hdaU3&3C;q^YW-@?FU? a.headerlink { + visibility: visible; +} + +/* headings */ +h2 { + font-size: 1.5em; + margin-bottom: 0.5em; + border-bottom: 1px solid #888; +} + +h3 { + font-size: 1.25em; + margin-top: .5em; +} + +h4 { + font-size: 1.15em; + margin-top: 1em; +} + +h5 { + font-size: 1em; + margin-top: 1em; +} + +/* general layout */ +[dir="rtl"] #content { + text-align: right; +} + +#content { + width: 95%; + margin: 0 auto; + text-align: left; +} + +[dir="rtl"] #content-left-wrapper { + float: right; +} + +#content-left-wrapper { + float: left; + width: 100%; /* req to keep content above sidebar in source code */ +} + +[dir="rtl"] #content-left { + margin: 0 0 0 340px; +} + +#content-left { + margin: 0 340px 0 0; +} + +[dir="rtl"] #content-right { + float: right; + margin-right: -300px; +} + +#content-right { + float: left; + width: 300px; + margin-left: -300px; +} + +div.box { + margin-bottom: 1.5em; + padding: 0.65em; + background: #ecf2f5; + border: 1px solid #bcd; +} + +#footer { + clear: both; + margin: 2em 0 1em; +} + + #footer p { + margin: 0; + text-align: center; + font-size: 0.85em; + } + +/* alignment */ +div.center, +table.center, +img.center { + width: auto; + margin-left: auto; + margin-right: auto; +} + +p.center, +td.center, +th.center { + text-align: center; +} + +/* table generics */ +table { + width: 100%; + border-collapse: collapse; +} + + table .wrap { + white-space: normal; + } + +[dir="rtl"] th, +[dir="rtl"] td { + text-align: right; +} + +th, +td { + white-space: nowrap; + text-align: left; +} + + th { + vertical-align: middle; + font-weight: bold; + } + + td { + vertical-align: top; + } + +/* table pretty styles */ +table.pretty2 { + width: auto; + margin-top: 0.25em; + margin-bottom: 0.5em; + border-collapse: collapse; + border: 1px solid #bbb; +} + + .pretty2 th { + padding: 0.35em; + background: #eee; + border: 1px solid #bbb; + } + + .pretty2 td { + padding: 0.35em; + border: 1px dotted #bbb; + } + +table.compact { + width: auto; +} + + .compact td { + padding: 0.25em 0 0.25em 1.5em; + } + + +/* definition lists */ +dl { + clear: both; +} + + dl dt, + dl dd { + margin-bottom: 4px; + padding: 8px 0 4px; + font-weight: bold; + border-top: 1px dotted #bbb; + } + + [dir="rtl"] dl dt { + float: right; + padding-left: 15px; + } + dl dt { + color: #333; + float: left; + padding-right: 15px; + } + +/* forms and input styling */ +form p { + margin: 0.5em 0; +} + +fieldset { + border: 0; +} + +label { + width: 12em; + vertical-align: top; + display: inline-block; + font-weight: bold; +} + +input[type=text], +input[type=password], +input[type=email], +textarea { + padding: 0.10em; +} + +form.general-form label, +form.general-form .form-help { + width: 10em; + vertical-align: top; + display: inline-block; +} + +form.general-form input[type=text], +form.general-form textarea { + width: 45%; +} + +/* archdev navbar */ +#archdev-navbar { + margin: 1.5em 0; +} + + #archdev-navbar ul { + list-style: none; + margin: -0.5em 0; + padding: 0; + } + + #archdev-navbar li { + display: inline; + margin: 0; + padding: 0; + font-size: 0.9em; + } + + #archdev-navbar li a { + padding: 0 0.5em; + color: #07b; + } + +/* error/info messages (x pkg is already flagged out-of-date, etc) */ +#sys-message { + width: 35em; + text-align: center; + margin: 1em auto; + padding: 0.5em; + background: #fff; + border: 1px solid #f00; +} + + #sys-message p { + margin: 0; + } + +ul.errorlist { + color: red; +} + +form ul.errorlist { + margin: 0.5em 0; +} + +/* JS sorting via tablesorter */ +[dir="rtl"] table th.tablesorter-header { + padding-left: 20px; + background-position: center left ; +} +table th.tablesorter-header { + padding-right: 20px; + background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); + background-repeat: no-repeat; + background-position: center right; + cursor: pointer; +} + +table thead th.tablesorter-headerAsc { + background-color: #e4eeff; + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); +} + +table thead th.tablesorter-headerDesc { + background-color: #e4eeff; + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); +} + +table thead th.sorter-false { + background-image: none; + cursor: default; +} + +.tablesorter-header:focus { + outline: none; +} + +/** + * PAGE SPECIFIC STYLES + */ + +/* home: introduction */ +[dir="rtl"] #intro p.readmore { + text-align: left; +} +#intro p.readmore { + margin: -0.5em 0 0 0; + font-size: .9em; + text-align: right; +} + +/* home: news */ +#news { + margin-top: 1.5em; +} + + [dir="rtl"] #news h3 { + float: right; + } + #news h3 { + float: left; + padding-bottom: .5em + } + + #news div { + margin-bottom: 1em; + } + + #news div p { + margin-bottom: 0.5em; + } + + #news .more { + font-weight: normal; + } + [dir="rtl"] #news .rss-icon { + float: left; + } + #news .rss-icon { + float: right; + margin-top: 1em; + } + + #news h4 { + clear: both; + font-size: 1em; + margin-top: 1.5em; + border-bottom: 1px dotted #bbb; + } + [dir="rtl"] #news .timestamp { + float: left; + margin: -1.8em 0 0 0.5em; + } + #news .timestamp { + float: right; + font-size: 0.85em; + margin: -1.8em 0.5em 0 0; + } + +/* home: arrowed headings */ +#news h3 a { + display: block; + background: #1794D1; + font-size: 15px; + padding: 2px 10px; + color: white; +} + + #news a:active { + color: white; + } + +h3 span.arrow { + display: block; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid #1794D1; + margin: 0 auto; + font-size: 0; + line-height: 0px; +} + +/* home: pkgsearch box */ +#pkgsearch { + padding: 1em 0.75em; + background: #3ad; + color: #fff; + border: 1px solid #08b; +} + + #pkgsearch label { + width: auto; + padding: 0.1em 0; + } + + [dir="rtl"] #pkgsearch input { + float: left; + } + #pkgsearch input { + width: 10em; + float: right; + font-size: 1em; + color: #000; + background: #fff; + border: 1px solid #09c; + } + + [dir="rtl"] .pkgsearch-typeahead { + right: 0; + float: right; + text-align: right; + } + + .pkgsearch-typeahead { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + padding: 0.15em 0.1em; + margin: 0; + min-width: 10em; + font-size: 1em; + text-align: left; + list-style: none; + background-color: #f6f9fc; + border: 1px solid #09c; + } + + .pkgsearch-typeahead li a { + color: #000; + } + + .pkgsearch-typeahead li:hover a, + .pkgsearch-typeahead li.active a { + color: #07b; + } + +/* home: recent pkg updates */ +#pkg-updates h3 { + margin: 0 0 0.3em; +} + + #pkg-updates .more { + font-weight: normal; + } + [dir="rtl"] #pkg-updates .rss-icon { + float: left; + } + #pkg-updates .rss-icon { + float: right; + margin: -2em 0 0 0; + } + + [dir="rtl"] #pkg-updates .rss-icon.latest { + margin-left: 1em; + } + #pkg-updates .rss-icon.latest { + margin-right: 1em; + } + + #pkg-updates table { + margin: 0; + direction: ltr; + } + + #pkg-updates td.pkg-name { + white-space: normal; + text-align: left; + } + + [dir="rtl"] #pkg-updates td.pkg-arch { + text-align: left; + } + #pkg-updates td.pkg-arch { + text-align: right; + } + + #pkg-updates span.testing { + font-style: italic; + } + + #pkg-updates span.staging { + font-style: italic; + color: #ff8040; + } + +/* home: sidebar navigation */ +[dir="rtl"] #nav-sidebar ul { + margin: 0.5em 1em 0.5em 0; +} + +#nav-sidebar ul { + list-style: none; + margin: 0.5em 0 0.5em 1em; + padding: 0; +} + +/* home: sponsor banners */ +#arch-sponsors img { + padding: 0.3em 0; +} + +/* home: sidebar components (navlist, sponsors, pkgsearch, etc) */ +div.widget { + margin-bottom: 1.5em; +} + +/* feeds page */ +[dir="rtl"] #rss-feeds .rss { + padding-left: 20px; + background: url(rss.png) top left no-repeat; +} + +#rss-feeds .rss { + padding-right: 20px; + background: url(rss.png) top right no-repeat; +} + +/* artwork: logo images */ +#artwork img.inverted { + background: #333; + padding: 0; +} + +#artwork div.imagelist img { + display: inline; + margin: 0.75em; +} + +/* news: article list */ +[dir="rtl"] .news-nav { + float: left; +} +.news-nav { + float: right; + margin-top: -2.2em; +} + + .news-nav .prev, + .news-nav .next { + margin: 0 1em; + } + +/* news: article pages */ +div.news-article .article-info { + margin: 0; + color: #999; +} + +/* news: add/edit article */ +#newsform { + width: 60em; +} + + #newsform input[type=text], + #newsform textarea { + width: 75%; + } + +#news-preview { + display: none; +} + +/* todolists: list */ +[dir="rtl"] .todolist-nav { + float: left; +} +.todolist-nav { + float: right; + margin-top: -2.2em; +} + + .todolist-nav .prev, + .todolist-nav .next { + margin: 0 1em; + } + +/* donate: donor list */ +#donor-list ul { + width: 100%; +} + /* max 4 columns, but possibly fewer if screen size doesn't allow for more */ + [dir="rtl"] #donor-list li { + float: right; + } + #donor-list li { + float: left; + width: 25%; + min-width: 20em; + } + +/* download page */ +#arch-downloads h3 { + border-bottom: 1px dotted #bbb; +} + +/* pkglists/devlists */ +table.results { + font-size: 0.846em; + border-top: 1px dotted #999; + border-bottom: 1px dotted #999; + direction: ltr; +} + + [dir="rtl"] .results th {text-align: center; direction:rtl;} + .results th { + padding: 0.5em 1em 0.25em 0.25em; + border-bottom: 1px solid #999; + white-space: nowrap; + background-color:#fff; + } + + .results td { + padding: .3em 1em .3em 3px; + text-align: left; + } + + .results .flagged { + color: red; + } + + .results tr.empty td { + text-align: center; + } + +/* pkglist: layout */ +#pkglist-about { + margin-top: 1.5em; +} + +/* pkglist: results navigation */ +.pkglist-stats { + font-size: 0.85em; +} + +[dir="rtl"] #pkglist-results .pkglist-nav { + float: left; +} +#pkglist-results .pkglist-nav { + float: right; + margin-top: -2.2em; +} + +[dir="rtl"] .pkglist-nav .prev { + margin-left: 1em; +} + +.pkglist-nav .prev { + margin-right: 1em; +} + +[dir="rtl"] .pkglist-nav .next { + margin-left: 1em; +} +.pkglist-nav .next { + margin-right: 1em; +} + +/* search fields and other filter selections */ +.filter-criteria { + margin-bottom: 1em; +} + +.filter-criteria h3 { + font-size: 1em; + margin-top: 0; +} +[dir="rtl"] .filter-criteria div { + float: right; + margin-left: 1.65em; +} +.filter-criteria div { + float: left; + margin-right: 1.65em; + font-size: 0.85em; +} + +.filter-criteria legend { + display: none; +} + +.filter-criteria label { + width: auto; + display: block; + font-weight: normal; +} + +/* pkgdetails: details links that float on the right */ +[dir="rtl"] #pkgdetails #detailslinks { + float: left; +} +#pkgdetails #detailslinks { + float: right; +} + + #pkgdetails #detailslinks h4 { + margin-top: 0; + margin-bottom: 0.25em; + } + + #pkgdetails #detailslinks ul { + list-style: none; + padding: 0; + margin-bottom: 0; + font-size: 0.846em; + } + + #pkgdetails #detailslinks > div { + padding: 0.5em; + margin-bottom: 1em; + background: #eee; + border: 1px solid #bbb; + } + +#pkgdetails #actionlist .flagged { + color: red; + font-size: 0.9em; + font-style: italic; +} + +/* pkgdetails: pkg info */ +#pkgdetails #pkginfo { + width: auto; +} + +[dir="rtl"] #pkgdetails td { + padding: 0.25em 1.5em 0.25em 0; + } + + #pkgdetails #pkginfo td { + padding: 0.25em 0 0.25em 1.5em; + } + + #pkgdetails #pkginfo .userdata { + font-size: 0.85em; + padding: 0.5em; + } + +/* pkgdetails: flag package */ +#flag-pkg-form label { + width: 10em; +} + +#flag-pkg-form textarea, +#flag-pkg-form input[type=text] { + width: 45%; +} + +#flag-pkg-form #id_website { + display: none; +} + +/* pkgdetails: deps, required by and file lists */ +#pkgdetails #metadata { + clear: both; +} + +#pkgdetails #metadata h3 { + background: #555; + color: #fff; + font-size: 1em; + margin-bottom: 0.5em; + padding: 0.2em 0.35em; +} + +#pkgdetails #metadata ul { + list-style: none; + margin: 0; + padding: 0; +} + +[dir="rtl"] #pkgdetails #metadata li { + padding-right: 0.5em; +} + +#pkgdetails #metadata li { + padding-left: 0.5em; +} + +[dir="rtl"] #pkgdetails #metadata p { + padding-right: 0.5em; +} +#pkgdetails #metadata p { + padding-left: 0.5em; +} + +#pkgdetails #metadata .message { + font-style: italic; +} + +#pkgdetails #metadata br { + clear: both; +} + +[dir="rtl"] #pkgdetails #pkgdeps { + float: right; + width: 48%; + margin-left: 2%; + +} +#pkgdetails #pkgdeps { + float: left; + width: 48%; + margin-right: 2%; +} + +#pkgdetails #metadata .virtual-dep, +#pkgdetails #metadata .testing-dep, +#pkgdetails #metadata .staging-dep, +#pkgdetails #metadata .opt-dep, +#pkgdetails #metadata .make-dep, +#pkgdetails #metadata .check-dep, +#pkgdetails #metadata .dep-desc { + font-style: italic; +} + +[dir="rtl"] #pkgdetails #pkgreqs { + float: right; + width: 48%; +} + +#pkgdetails #pkgreqs { + float: left; + width: 50%; +} + +#pkgdetails #pkgfiles { + clear: both; + padding-top: 1em; +} + +#pkgfilelist li.d { + color: #666; +} + +#pkgfilelist li.f { +} + +/* mirror stuff */ +table td.country { + white-space: normal; +} + +#list-generator div ul { + list-style: none; + display: inline; + padding-left: 0; +} + + #list-generator div ul li { + display: inline; + } + +.visualize-mirror .axis path, +.visualize-mirror .axis line { + fill: none; + stroke: #000; + stroke-width: 3px; + shape-rendering: crispEdges; +} + +.visualize-mirror .url-dot { + stroke: #000; +} + +.visualize-mirror .url-line { + fill: none; + stroke-width: 1.5px; +} + +/* dev/TU biographies */ +#arch-bio-toc { + width: 75%; + margin: 0 auto; + text-align: center; +} + + #arch-bio-toc a { + white-space: nowrap; + } + +.arch-bio-entry { + width: 75%; + min-width: 640px; + margin: 0 auto; +} + .arch-bio-entry td.pic { + padding-left: 15px; + } + .arch-bio-entry td.pic { + vertical-align: top; + padding-right: 15px; + padding-top: 2.25em; + } + + .arch-bio-entry td.pic img { + padding: 4px; + border: 1px solid #ccc; + } + + .arch-bio-entry td h3 { + border-bottom: 1px dotted #ccc; + margin-bottom: 0.5em; + } + + .arch-bio-entry table.bio { + margin-bottom: 2em; + } + [dir="rtl"] .arch-bio-entry table.bio th { + text-align: left; + padding-left: 0.5em; + } + + .arch-bio-entry table.bio th { + color: #666; + font-weight: normal; + text-align: right; + padding-right: 0.5em; + vertical-align: top; + white-space: nowrap; + } + + .arch-bio-entry table.bio td { + width: 100%; + padding-bottom: 0.25em; + white-space: normal; + } + +/* dev: login/out */ +#dev-login { + width: auto; +} + +/* tables rows: highlight on mouse-vover */ +#article-list tr:hover, +#clocks-table tr:hover, +#dev-dashboard tr:hover, +#dev-todo-lists tr:hover, +#dev-todo-pkglist tr:hover, +#pkglist-results tr:hover, +#stats-area tr:hover { + background: #ffd; +} + +.results tr:nth-child(even), +#article-list tr:nth-child(even) { + background: #e4eeff; +} + +.results tr:nth-child(odd), +#article-list tr:nth-child(odd) { + background: #fff; +} + +/* dev dashboard: */ +table.dash-stats .key { + width: 50%; +} + +/* dev dashboard: admin actions (add news items, todo list, etc) */ +[dir="rtl"] ul.admin-actions { + float: left; +} +ul.admin-actions { + float: right; + list-style: none; + margin-top: -2.5em; +} + + ul.admin-actions li { + display: inline; + padding-left: 1.5em; + } + +/* colored yes/no type values */ +.todo-table .complete, +.signoff-yes, +#key-status .signed-yes, +#release-list .available-yes { + color: green; +} + +.todo-table .incomplete, +.signoff-no, +#key-status .signed-no, +#release-list .available-no { + color: red; +} + +.todo-table .inprogress, +.signoff-bad { + color: darkorange; +} + + +/* todo lists (public and private) */ +.todo-info { + color: #999; + border-bottom: 1px dotted #bbb; +} + +.todo-description { + margin-top: 1em; + padding-left: 2em; + max-width: 900px; +} + +.todo-pkgbases { + border-top: 1px dotted #bbb; +} + +.todo-list h4 { + margin-top: 0; + margin-bottom: 0.4em; +} + +/* dev: signoff page */ +#dev-signoffs tr:hover { + background: #ffd; +} + +ul.signoff-list { + list-style: none; + margin: 0; + padding: 0; +} + +.signoff-yes { + font-weight: bold; +} + +.signoff-disabled { + color: gray; +} + +/* highlight current website in the navbar */ +#archnavbar.anb-home ul li#anb-home a, +#archnavbar.anb-packages ul li#anb-packages a, +#archnavbar.anb-download ul li#anb-download a { + color: white !important; +} + +/* visualizations page */ +.visualize-buttons { + margin: 0.5em 0.33em; +} + +.visualize-chart { + position: relative; + height: 500px; + margin: 0.33em; +} + +#visualize-archrepo .treemap-cell { + border: solid 1px white; + overflow: hidden; + position: absolute; +} + + #visualize-archrepo .treemap-cell span { + padding: 3px; + font-size: 0.85em; + line-height: 1em; + } + +#visualize-keys svg { + width: 100%; + height: 100%; +} + +/* releases */ +#release-table th:first-of-type { + width: 30px; +} + +/* itemprops */ +.itemprop { + display: none; +} diff --git a/static/css/aurweb.css b/static/css/aurweb.css new file mode 100644 index 00000000..64a65742 --- /dev/null +++ b/static/css/aurweb.css @@ -0,0 +1,292 @@ +/* aurweb-specific customizations to archweb.css */ + +#archnavbar.anb-aur ul li#anb-aur a { + color: white !important; +} + +#archnavbarlogo { + background: url('archnavbar/aurlogo.png') !important; +} + +[dir="rtl"] #lang_sub { + float: left; + } +#lang_sub { + float: right; +} + +.pkglist-nav .page { + margin: 0 .25em; +} + +#pkg-stats td.stat-desc { + white-space: normal; +} + +#actionlist form { + margin: 0; + padding: 0; +} + +.arch-bio-entry ul { + list-style: none; + padding: 0; +} + +#pkg-updates table { + table-layout: fixed; + width:100%; +} + +#pkg-updates td.pkg-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +[dir="rtl"] #pkg-updates td.pkg-date { + text-align:left; +} +#pkg-updates td.pkg-date { + text-align:right; +} + +[dir="rtl"] .keyword:link, .keyword:visited { + float: right; +} + +.keyword:link, .keyword:visited { + float: left; + margin: 1px .5ex 1px 0; + padding: 0 1em; + color: white; + background-color: #36a; + border: 1px solid transparent; + border-radius: 2px; +} + +.keyword:hover { + cursor: pointer; +} + +.keyword:focus { + border: 1px dotted #000; +} + +.text-button { + background: transparent; + border: none !important; + margin: 0 !important; + padding: 0 !important; + font: normal 100% sans-serif; + text-decoration: none; + color: #07b; + cursor: pointer; +} + +.text-button:hover { + text-decoration: underline; + color: #666; +} + +.text-button::-moz-focus-inner { + padding: 0; + border: none; +} + +.comment-deleted { + color: #999; +} + +.edited { + font-size: 0.9em; + color: #999; +} + +[dir="rtl"] .delete-comment-form, .undelete-comment-form, .pin-comment-form, .edit-comment { + float: left; + margin-right: 8px; +} + +.delete-comment-form, .undelete-comment-form, .pin-comment-form, .edit-comment { + float: right; + margin-left: 8px; +} + +.edit-comment { + height: 11px; + position: relative; + top: 1px; +} + +.comment-enable-notifications { + display: inline-block; + margin-left: 1em; +} + +.rss-icon, .delete-comment, .undelete-comment, .edit-comment, .pin-comment { + filter: grayscale(100%); + opacity: 0.6; +} + +.rss-icon:hover, .delete-comment:hover, .undelete-comment:hover, .edit-comment:hover, .pin-comment:hover { + filter: none; + opacity: 1; +} + +[dir="rtl"] .ajax-loader { + float: left; +} + +.ajax-loader { + float: right; + position: relative; + top: 4px; +} + +.flagged a { + color: inherit; +} + +legend { + padding: 1em 0; +} + +p.important { + font-weight: bold; +} + +span.hover-help { + border-bottom: 1px dotted black; + cursor:help; +} + +label.confirmation { + width: auto; +} + +#pkgdepslist .broken { + color: red; + font-weight: bold; +} + +.package-comments { + margin-top: 1.5em; +} + +.comments-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +/* arrowed headings */ +.comments-header h3 span.text { + display: block; + background: #1794D1; + font-size: 15px; + padding: 2px 10px; + color: white; +} + +.comments-header .comments-header-nav { + align-self: flex-end; +} + +.comments-footer { + display: flex; + justify-content: flex-end; +} + +.comment-header { + clear: both; + font-size: 1em; + margin-top: 1.5em; + border-bottom: 1px dotted #bbb; +} + +.comments div { + margin-bottom: 1em; +} + +.comments div p { + margin-bottom: 0.5em; +} + +.comments .more { + font-weight: normal; +} + +.error { + color: red; +} + +.article-content > div { + overflow: hidden; + transition: height 1s; +} + +.proposal.details { + margin: .33em 0 1em; +} + +button[type="submit"], +button[type="reset"] { + padding: 0 0.6em; +} + +.results tr td[align="left"] fieldset { + text-align: left; +} + +.results tr td[align="right"] fieldset { + text-align: right; +} + +input#search-action-submit { + width: 80px; +} + +.success { + color: green; +} + +/* Styling used to clone styles for a form.link button. */ +form.link, form.link button { + display: inline; + font-family: sans-serif; +} +form.link button { + padding: 0 0.5em; + color: #07b; + background: none; + border: none; + font-family: inherit; + font-size: inherit; +} +form.link button:hover { + cursor: pointer; + text-decoration: underline; +} + +/* Customize form.link when used inside of a page. */ +div.box form.link p { + margin: .33em 0 1em; +} +div.box form.link button { + padding: 0; +} + +pre.traceback { + /* https://css-tricks.com/snippets/css/make-pre-text-wrap/ */ + white-space: pre-wrap; + word-wrap: break-all; +} + +/* By default, tables use 100% width, which we do not always want. */ +table.no-width { + width: auto; +} +table.no-width > tbody > tr > td { + padding-right: 2px; +} diff --git a/static/css/cgit.css b/static/css/cgit.css new file mode 100644 index 00000000..429b5f54 --- /dev/null +++ b/static/css/cgit.css @@ -0,0 +1,866 @@ +/* + * ARCH GLOBAL NAVBAR + * We're forcing all generic selectors with !important + * to help prevent other stylesheets from interfering. + */ + +/* container for the entire bar */ +#archnavbar { height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } +#archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; background: url('archnavbar/archlogo.png') no-repeat !important; } + +/* move the heading text offscreen */ +#archnavbarlogo h1 { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } + +/* make the link the same size as the logo */ +#archnavbarlogo a { display: block !important; height: 40px !important; width: 190px !important; } + +/* display the list inline, float it to the right and style it */ +#archnavbarlist { display: inline !important; float: right !important; list-style: none !important; margin: 0 !important; padding: 0 !important; } +#archnavbarlist li { float: left !important; font-size: 14px !important; font-family: sans-serif !important; line-height: 45px !important; padding-right: 15px !important; padding-left: 15px !important; } + +/* style the links */ +#archnavbarlist li a { color: #999; font-weight: bold !important; text-decoration: none !important; } +#archnavbarlist li a:hover { color: white !important; text-decoration: underline !important; } + +/* END ARCH GLOBAL NAVBAR */ + +#footer { + clear: both; + margin: 0; +} + +#footer p { + margin: 1em; +} + +#archnavbar.anb-aur ul li#anb-aur a { + color: white !important; +} + +#archnavbarlogo { + background: url('archnavbar/aurlogo.png') !important; +} + +body { + padding: 0; + margin: 0; + font-family: sans-serif; + font-size: 10pt; + color: #333; + background: white; +} + +div#cgit a { + color: blue; + text-decoration: none; +} + +div#cgit a:hover { + text-decoration: underline; +} + +div#cgit table { + border-collapse: collapse; +} + +div#cgit table#header { + width: 100%; + margin-bottom: 1em; +} + +div#cgit table#header td.logo { + width: 96px; + vertical-align: top; +} + +div#cgit table#header td.main { + font-size: 250%; + padding-left: 10px; + white-space: nowrap; +} + +div#cgit table#header td.main a { + color: #000; +} + +div#cgit table#header td.form { + text-align: right; + vertical-align: bottom; + padding-right: 1em; + padding-bottom: 2px; + white-space: nowrap; +} + +div#cgit table#header td.form form, +div#cgit table#header td.form input, +div#cgit table#header td.form select { + font-size: 90%; +} + +div#cgit table#header td.sub { + color: #777; + border-top: solid 1px #ccc; + padding-left: 10px; +} + +div#cgit table.tabs { + border-bottom: solid 3px #ccc; + border-collapse: collapse; + margin-top: 2em; + margin-bottom: 0px; + width: 100%; +} + +div#cgit table.tabs td { + padding: 0px 1em; + vertical-align: bottom; +} + +div#cgit table.tabs td a { + padding: 2px 0.75em; + color: #777; + font-size: 110%; +} + +div#cgit table.tabs td a.active { + color: #000; + background-color: #ccc; +} + +div#cgit table.tabs td.form { + text-align: right; +} + +div#cgit table.tabs td.form form { + padding-bottom: 2px; + font-size: 90%; + white-space: nowrap; +} + +div#cgit table.tabs td.form input, +div#cgit table.tabs td.form select { + font-size: 90%; +} + +div#cgit div.path { + margin: 0px; + padding: 5px 2em 2px 2em; + color: #000; + background-color: #eee; +} + +div#cgit div.content { + margin: 0px; + padding: 2em; + border-bottom: solid 3px #ccc; +} + + +div#cgit table.list { + width: 100%; + border: none; + border-collapse: collapse; +} + +div#cgit table.list tr { + background: white; +} + +div#cgit table.list tr.logheader { + background: #eee; +} + +div#cgit table.list tr:hover { + background: #eee; +} + +div#cgit table.list tr.nohover:hover { + background: white; +} + +div#cgit table.list th { + font-weight: bold; + /* color: #888; + border-top: dashed 1px #888; + border-bottom: dashed 1px #888; + */ + padding: 0.1em 0.5em 0.05em 0.5em; + vertical-align: baseline; +} + +div#cgit table.list td { + border: none; + padding: 0.1em 0.5em 0.1em 0.5em; +} + +div#cgit table.list td.commitgraph { + font-family: monospace; + white-space: pre; +} + +div#cgit table.list td.commitgraph .column1 { + color: #a00; +} + +div#cgit table.list td.commitgraph .column2 { + color: #0a0; +} + +div#cgit table.list td.commitgraph .column3 { + color: #aa0; +} + +div#cgit table.list td.commitgraph .column4 { + color: #00a; +} + +div#cgit table.list td.commitgraph .column5 { + color: #a0a; +} + +div#cgit table.list td.commitgraph .column6 { + color: #0aa; +} + +div#cgit table.list td.logsubject { + font-family: monospace; + font-weight: bold; +} + +div#cgit table.list td.logmsg { + font-family: monospace; + white-space: pre; + padding: 0 0.5em; +} + +div#cgit table.list td a { + color: black; +} + +div#cgit table.list td a.ls-dir { + font-weight: bold; + color: #00f; +} + +div#cgit table.list td a:hover { + color: #00f; +} + +div#cgit img { + border: none; +} + +div#cgit input#switch-btn { + margin: 2px 0px 0px 0px; +} + +div#cgit td#sidebar input.txt { + width: 100%; + margin: 2px 0px 0px 0px; +} + +div#cgit table#grid { + margin: 0px; +} + +div#cgit td#content { + vertical-align: top; + padding: 1em 2em 1em 1em; + border: none; +} + +div#cgit div#summary { + vertical-align: top; + margin-bottom: 1em; +} + +div#cgit table#downloads { + float: right; + border-collapse: collapse; + border: solid 1px #777; + margin-left: 0.5em; + margin-bottom: 0.5em; +} + +div#cgit table#downloads th { + background-color: #ccc; +} + +div#cgit div#blob { + border: solid 1px black; +} + +div#cgit div.error { + color: red; + font-weight: bold; + margin: 1em 2em; +} + +div#cgit a.ls-blob, div#cgit a.ls-dir, div#cgit a.ls-mod { + font-family: monospace; +} + +div#cgit td.ls-size { + text-align: right; + font-family: monospace; + width: 10em; +} + +div#cgit td.ls-mode { + font-family: monospace; + width: 10em; +} + +div#cgit table.blob { + margin-top: 0.5em; + border-top: solid 1px black; +} + +div#cgit table.blob td.lines { + margin: 0; padding: 0 0 0 0.5em; + vertical-align: top; + color: black; +} + +div#cgit table.blob td.linenumbers { + margin: 0; padding: 0 0.5em 0 0.5em; + vertical-align: top; + text-align: right; + border-right: 1px solid gray; +} + +div#cgit table.blob pre { + padding: 0; margin: 0; +} + +div#cgit table.blob a.no, div#cgit table.ssdiff a.no { + color: gray; + text-align: right; + text-decoration: none; +} + +div#cgit table.blob a.no a:hover { + color: black; +} + +div#cgit table.bin-blob { + margin-top: 0.5em; + border: solid 1px black; +} + +div#cgit table.bin-blob th { + font-family: monospace; + white-space: pre; + border: solid 1px #777; + padding: 0.5em 1em; +} + +div#cgit table.bin-blob td { + font-family: monospace; + white-space: pre; + border-left: solid 1px #777; + padding: 0em 1em; +} + +div#cgit table.nowrap td { + white-space: nowrap; +} + +div#cgit table.commit-info { + border-collapse: collapse; + margin-top: 1.5em; +} + +div#cgit div.cgit-panel { + float: right; + margin-top: 1.5em; +} + +div#cgit div.cgit-panel table { + border-collapse: collapse; + border: solid 1px #aaa; + background-color: #eee; +} + +div#cgit div.cgit-panel th { + text-align: center; +} + +div#cgit div.cgit-panel td { + padding: 0.25em 0.5em; +} + +div#cgit div.cgit-panel td.label { + padding-right: 0.5em; +} + +div#cgit div.cgit-panel td.ctrl { + padding-left: 0.5em; +} + +div#cgit table.commit-info th { + text-align: left; + font-weight: normal; + padding: 0.1em 1em 0.1em 0.1em; + vertical-align: top; +} + +div#cgit table.commit-info td { + font-weight: normal; + padding: 0.1em 1em 0.1em 0.1em; +} + +div#cgit div.commit-subject { + font-weight: bold; + font-size: 125%; + margin: 1.5em 0em 0.5em 0em; + padding: 0em; +} + +div#cgit div.commit-msg { + white-space: pre; + font-family: monospace; +} + +div#cgit div.notes-header { + font-weight: bold; + padding-top: 1.5em; +} + +div#cgit div.notes { + white-space: pre; + font-family: monospace; + border: solid 1px #ee9; + background-color: #ffd; + padding: 0.3em 2em 0.3em 1em; + float: left; +} + +div#cgit div.notes-footer { + clear: left; +} + +div#cgit div.diffstat-header { + font-weight: bold; + padding-top: 1.5em; +} + +div#cgit table.diffstat { + border-collapse: collapse; + border: solid 1px #aaa; + background-color: #eee; +} + +div#cgit table.diffstat th { + font-weight: normal; + text-align: left; + text-decoration: underline; + padding: 0.1em 1em 0.1em 0.1em; + font-size: 100%; +} + +div#cgit table.diffstat td { + padding: 0.2em 0.2em 0.1em 0.1em; + font-size: 100%; + border: none; +} + +div#cgit table.diffstat td.mode { + white-space: nowrap; +} + +div#cgit table.diffstat td span.modechange { + padding-left: 1em; + color: red; +} + +div#cgit table.diffstat td.add a { + color: green; +} + +div#cgit table.diffstat td.del a { + color: red; +} + +div#cgit table.diffstat td.upd a { + color: blue; +} + +div#cgit table.diffstat td.graph { + width: 500px; + vertical-align: middle; +} + +div#cgit table.diffstat td.graph table { + border: none; +} + +div#cgit table.diffstat td.graph td { + padding: 0px; + border: 0px; + height: 7pt; +} + +div#cgit table.diffstat td.graph td.add { + background-color: #5c5; +} + +div#cgit table.diffstat td.graph td.rem { + background-color: #c55; +} + +div#cgit div.diffstat-summary { + color: #888; + padding-top: 0.5em; +} + +div#cgit table.diff { + width: 100%; +} + +div#cgit table.diff td { + font-family: monospace; + white-space: pre; +} + +div#cgit table.diff td div.head { + font-weight: bold; + margin-top: 1em; + color: black; +} + +div#cgit table.diff td div.hunk { + color: #009; +} + +div#cgit table.diff td div.add { + color: green; +} + +div#cgit table.diff td div.del { + color: red; +} + +div#cgit .sha1 { + font-family: monospace; + font-size: 90%; +} + +div#cgit .left { + text-align: left; +} + +div#cgit .right { + text-align: right; + float: none !important; + width: auto !important; + padding: 0 !important; +} + +div#cgit table.list td.reposection { + font-style: italic; + color: #888; +} + +div#cgit a.button { + font-size: 80%; + padding: 0em 0.5em; +} + +div#cgit a.primary { + font-size: 100%; +} + +div#cgit a.secondary { + font-size: 90%; +} + +div#cgit td.toplevel-repo { + +} + +div#cgit table.list td.sublevel-repo { + padding-left: 1.5em; +} + +div#cgit ul.pager { + list-style-type: none; + text-align: center; + margin: 1em 0em 0em 0em; + padding: 0; +} + +div#cgit ul.pager li { + display: inline-block; + margin: 0.25em 0.5em; +} + +div#cgit ul.pager a { + color: #777; +} + +div#cgit ul.pager .current { + font-weight: bold; +} + +div#cgit span.age-mins { + font-weight: bold; + color: #080; +} + +div#cgit span.age-hours { + color: #080; +} + +div#cgit span.age-days { + color: #040; +} + +div#cgit span.age-weeks { + color: #444; +} + +div#cgit span.age-months { + color: #888; +} + +div#cgit span.age-years { + color: #bbb; +} +div#cgit div.footer { + margin-top: 0.5em; + text-align: center; + font-size: 80%; + color: #ccc; +} +div#cgit a.branch-deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #88ff88; + border: solid 1px #007700; +} +div#cgit a.tag-deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #ffff88; + border: solid 1px #777700; +} +div#cgit a.remote-deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #ccccff; + border: solid 1px #000077; +} +div#cgit a.deco { + color: #000; + margin: 0px 0.5em; + padding: 0px 0.25em; + background-color: #ff8888; + border: solid 1px #770000; +} + +div#cgit div.commit-subject a.branch-deco, +div#cgit div.commit-subject a.tag-deco, +div#cgit div.commit-subject a.remote-deco, +div#cgit div.commit-subject a.deco { + margin-left: 1em; + font-size: 75%; +} + +div#cgit table.stats { + border: solid 1px black; + border-collapse: collapse; +} + +div#cgit table.stats th { + text-align: left; + padding: 1px 0.5em; + background-color: #eee; + border: solid 1px black; +} + +div#cgit table.stats td { + text-align: right; + padding: 1px 0.5em; + border: solid 1px black; +} + +div#cgit table.stats td.total { + font-weight: bold; + text-align: left; +} + +div#cgit table.stats td.sum { + color: #c00; + font-weight: bold; +/* background-color: #eee; */ +} + +div#cgit table.stats td.left { + text-align: left; +} + +div#cgit table.vgraph { + border-collapse: separate; + border: solid 1px black; + height: 200px; +} + +div#cgit table.vgraph th { + background-color: #eee; + font-weight: bold; + border: solid 1px white; + padding: 1px 0.5em; +} + +div#cgit table.vgraph td { + vertical-align: bottom; + padding: 0px 10px; +} + +div#cgit table.vgraph div.bar { + background-color: #eee; +} + +div#cgit table.hgraph { + border: solid 1px black; + width: 800px; +} + +div#cgit table.hgraph th { + background-color: #eee; + font-weight: bold; + border: solid 1px black; + padding: 1px 0.5em; +} + +div#cgit table.hgraph td { + vertical-align: middle; + padding: 2px 2px; +} + +div#cgit table.hgraph div.bar { + background-color: #eee; + height: 1em; +} + +div#cgit table.ssdiff { + width: 100%; +} + +div#cgit table.ssdiff td { + font-size: 75%; + font-family: monospace; + white-space: pre; + padding: 1px 4px 1px 4px; + border-left: solid 1px #aaa; + border-right: solid 1px #aaa; +} + +div#cgit table.ssdiff td.add { + color: black; + background: #cfc; + min-width: 50%; +} + +div#cgit table.ssdiff td.add_dark { + color: black; + background: #aca; + min-width: 50%; +} + +div#cgit table.ssdiff span.add { + background: #cfc; + font-weight: bold; +} + +div#cgit table.ssdiff td.del { + color: black; + background: #fcc; + min-width: 50%; +} + +div#cgit table.ssdiff td.del_dark { + color: black; + background: #caa; + min-width: 50%; +} + +div#cgit table.ssdiff span.del { + background: #fcc; + font-weight: bold; +} + +div#cgit table.ssdiff td.changed { + color: black; + background: #ffc; + min-width: 50%; +} + +div#cgit table.ssdiff td.changed_dark { + color: black; + background: #cca; + min-width: 50%; +} + +div#cgit table.ssdiff td.lineno { + color: black; + background: #eee; + text-align: right; + width: 3em; + min-width: 3em; +} + +div#cgit table.ssdiff td.hunk { + color: black; + background: #ccf; + border-top: solid 1px #aaa; + border-bottom: solid 1px #aaa; +} + +div#cgit table.ssdiff td.head { + border-top: solid 1px #aaa; + border-bottom: solid 1px #aaa; +} + +div#cgit table.ssdiff td.head div.head { + font-weight: bold; + color: black; +} + +div#cgit table.ssdiff td.foot { + border-top: solid 1px #aaa; + border-left: none; + border-right: none; + border-bottom: none; +} + +div#cgit table.ssdiff td.space { + border: none; +} + +div#cgit table.ssdiff td.space div { + min-height: 3em; +} + +/* + * Style definitions generated by highlight 3.14, http://www.andre-simon.de/ + * Highlighting theme: Kwrite Editor + */ +div#cgit table.blob .num { color:#b07e00; } +div#cgit table.blob .esc { color:#ff00ff; } +div#cgit table.blob .str { color:#bf0303; } +div#cgit table.blob .pps { color:#818100; } +div#cgit table.blob .slc { color:#838183; font-style:italic; } +div#cgit table.blob .com { color:#838183; font-style:italic; } +div#cgit table.blob .ppc { color:#008200; } +div#cgit table.blob .opt { color:#000000; } +div#cgit table.blob .ipl { color:#0057ae; } +div#cgit table.blob .lin { color:#555555; } +div#cgit table.blob .kwa { color:#000000; font-weight:bold; } +div#cgit table.blob .kwb { color:#0057ae; } +div#cgit table.blob .kwc { color:#000000; font-weight:bold; } +div#cgit table.blob .kwd { color:#010181; } diff --git a/static/images/ICON-LICENSE b/static/images/ICON-LICENSE new file mode 100644 index 00000000..6b39f6fd --- /dev/null +++ b/static/images/ICON-LICENSE @@ -0,0 +1,26 @@ +The icons used in aurweb originate from the Open Iconic project and are +licensed under the following terms: + +---- +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +---- diff --git a/static/images/action-undo.min.svg b/static/images/action-undo.min.svg new file mode 100644 index 00000000..eb47bc47 --- /dev/null +++ b/static/images/action-undo.min.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/action-undo.svg b/static/images/action-undo.svg new file mode 100644 index 00000000..b93ebb78 --- /dev/null +++ b/static/images/action-undo.svg @@ -0,0 +1,32 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/static/images/ajax-loader.gif b/static/images/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..df07e7ec2076177c99b53d4d29a45f0db6b06a9a GIT binary patch literal 723 zcmZ?wbhEHb6kyWo5uik&V|NP^(AHU+;_dI&}>C3lYM=m|vboAbp zdv88|`N04KivPL&TtkAL9RpmA^bD98f#Qn)q@0UV6H8K46v{J8G87WC5-W1@6I1ju z^V0Ge6o0aCasyTAfJ^{6l7UrML7^`tbKa5#T#rsMt#c4)wm4&2aJl;4?H%*^*q;ct zZ+YZ!f=91--8C-PwbPuinV^!8D8ZUAZ$+j|`^0?*ZXH_r=F;-s=Wq7D-W{Q@F^9F$ zTCh`s37bYUpw-=pI*&V4IF+P$l9wbc(l{x7eoOCbBdG(^nGZDWjsAGTTd?u$#mhT{ z{bn8t<<=6J=66T{n^C4fqn2>E3WhNCJ~l~G@x1uTreFAcY2|b4S-i`cPqf%2ZE*i3 z+J9zZu_cRCJ%=P)K~y-6jgvo!6G0Tle=|GDx_9PaSMatt8xuJ=jueWZ*0zF( zjc8@z38oPY2!}RWYjKN}g`kaCi1-H-uCNkOgxf0BeA-5$w9LxMMtXHz@2@3 zyz+UJPuh|vi*mtJavK=cNwf1dpEbZ!a<``>o|1IZ?Bv;J>;8WS9J=%2sOyMVt`f_h zlJvCko4lXZH(|7*(w!f`Tnu;_ptK?Jqw7?)K+hZAu%R^ukDjFp75q4Pbjddz93wNAlTi;8fmk1LdSvP5vdgJg^M# zaG-vgp9c5>)Q7GRMsXQ9!>~RL)NlL5fD7Ck3IMJGg}hz^t^pqh0-C^eU>&Fc%V89s z01(qlD|><0z!Ts~QmejXjKU~B2rL4Jfq5~#v~m%6p46(=A2%lGz#nlao8kSq3cLUS N002ovPDHLkV1na%{Dc4i literal 0 HcmV?d00001 diff --git a/static/images/pencil.min.svg b/static/images/pencil.min.svg new file mode 100644 index 00000000..06125ae0 --- /dev/null +++ b/static/images/pencil.min.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/pencil.svg b/static/images/pencil.svg new file mode 100644 index 00000000..91f08991 --- /dev/null +++ b/static/images/pencil.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/images/pin.min.svg b/static/images/pin.min.svg new file mode 100644 index 00000000..ac08903d --- /dev/null +++ b/static/images/pin.min.svg @@ -0,0 +1 @@ + diff --git a/static/images/pin.svg b/static/images/pin.svg new file mode 100644 index 00000000..b4ee9eb7 --- /dev/null +++ b/static/images/pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/rss.svg b/static/images/rss.svg new file mode 100644 index 00000000..3c7f6ba1 --- /dev/null +++ b/static/images/rss.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/unpin.min.svg b/static/images/unpin.min.svg new file mode 100644 index 00000000..3cf2413c --- /dev/null +++ b/static/images/unpin.min.svg @@ -0,0 +1 @@ + diff --git a/static/images/unpin.svg b/static/images/unpin.svg new file mode 100644 index 00000000..de897152 --- /dev/null +++ b/static/images/unpin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/images/x.min.svg b/static/images/x.min.svg new file mode 100644 index 00000000..833d4f22 --- /dev/null +++ b/static/images/x.min.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/x.svg b/static/images/x.svg new file mode 100644 index 00000000..e323fe19 --- /dev/null +++ b/static/images/x.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/static/js/comment-edit.js b/static/js/comment-edit.js new file mode 100644 index 00000000..23ffdd34 --- /dev/null +++ b/static/js/comment-edit.js @@ -0,0 +1,61 @@ +function add_busy_indicator(sibling) { + const img = document.createElement('img'); + img.src = "/static/images/ajax-loader.gif"; + img.classList.add('ajax-loader'); + img.style.height = 11; + img.style.width = 16; + img.alt = "Busy…"; + + sibling.insertAdjacentElement('afterend', img); +} + +function remove_busy_indicator(sibling) { + const elem = sibling.nextElementSibling; + elem.parentNode.removeChild(elem); +} + +function getParentsUntil(elem, className) { + // Limit to 10 depth + for ( ; elem && elem !== document; elem = elem.parentNode) { + if (elem.matches(className)) { + break; + } + } + + return elem; +} + +function handleEditCommentClick(event, pkgbasename) { + event.preventDefault(); + const parent_element = getParentsUntil(event.target, '.comment-header'); + const parent_id = parent_element.id; + const comment_id = parent_id.substr(parent_id.indexOf('-') + 1); + // The div class="article-content" which contains the comment + const edit_form = parent_element.nextElementSibling; + + const url = "/pkgbase/" + pkgbasename + "/comments/" + comment_id + "/form?"; + + add_busy_indicator(event.target); + + fetch(url + new URLSearchParams({ next: window.location.pathname }), { + method: 'GET', + credentials: 'same-origin' + }) + .then(function(response) { + if (!response.ok) { + throw Error(response.statusText); + } + return response.json(); + }) + .then(function(data) { + remove_busy_indicator(event.target); + edit_form.innerHTML = data.form; + edit_form.querySelector('textarea').focus(); + }) + .catch(function(error) { + remove_busy_indicator(event.target); + console.error(error); + }); + + return false; +} diff --git a/static/js/copy.js b/static/js/copy.js new file mode 100644 index 00000000..3b659270 --- /dev/null +++ b/static/js/copy.js @@ -0,0 +1,9 @@ +document.addEventListener('DOMContentLoaded', function() { + let elements = document.querySelectorAll('.copy'); + elements.forEach(function(el) { + el.addEventListener('click', function(e) { + e.preventDefault(); + navigator.clipboard.writeText(e.target.text); + }); + }); +}); diff --git a/static/js/typeahead-home.js b/static/js/typeahead-home.js new file mode 100644 index 00000000..5af51c53 --- /dev/null +++ b/static/js/typeahead-home.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + const input = document.getElementById('pkgsearch-field'); + const form = document.getElementById('pkgsearch-form'); + const type = 'suggest'; + typeahead.init(type, input, form); +}); diff --git a/static/js/typeahead-pkgbase-merge.js b/static/js/typeahead-pkgbase-merge.js new file mode 100644 index 00000000..a8c87e4f --- /dev/null +++ b/static/js/typeahead-pkgbase-merge.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + const input = document.getElementById('merge_into'); + const form = document.getElementById('merge-form'); + const type = "suggest-pkgbase"; + typeahead.init(type, input, form, false); +}); diff --git a/static/js/typeahead-pkgbase-request.js b/static/js/typeahead-pkgbase-request.js new file mode 100644 index 00000000..e012d55f --- /dev/null +++ b/static/js/typeahead-pkgbase-request.js @@ -0,0 +1,36 @@ +function showHideMergeSection() { + const elem = document.getElementById('id_type'); + const merge_section = document.getElementById('merge_section'); + if (elem.value == 'merge') { + merge_section.style.display = ''; + } else { + merge_section.style.display = 'none'; + } +} + +function showHideRequestHints() { + document.getElementById('deletion_hint').style.display = 'none'; + document.getElementById('merge_hint').style.display = 'none'; + document.getElementById('orphan_hint').style.display = 'none'; + + const elem = document.getElementById('id_type'); + document.getElementById(elem.value + '_hint').style.display = ''; +} + +document.addEventListener('DOMContentLoaded', function() { + showHideMergeSection(); + showHideRequestHints(); + + const input = document.getElementById('id_merge_into'); + const form = document.getElementById('request-form'); + const type = "suggest-pkgbase"; + + typeahead.init(type, input, form, false); +}); + +// Bind the change event here, otherwise we have to inline javascript, +// which angers CSP (Content Security Policy). +document.getElementById("id_type").addEventListener("change", function() { + showHideMergeSection(); + showHideRequestHints(); +}); diff --git a/static/js/typeahead.js b/static/js/typeahead.js new file mode 100644 index 00000000..bfd3d156 --- /dev/null +++ b/static/js/typeahead.js @@ -0,0 +1,151 @@ +"use strict"; + +const typeahead = (function() { + var input; + var form; + var suggest_type; + var list; + var submit = true; + + function resetResults() { + if (!list) return; + list.style.display = "none"; + list.innerHTML = ""; + } + + function getCompleteList() { + if (!list) { + list = document.createElement("UL"); + list.setAttribute("class", "pkgsearch-typeahead"); + form.appendChild(list); + setListLocation(); + } + return list; + } + + function onListClick(e) { + let target = e.target; + while (!target.getAttribute('data-value')) { + target = target.parentNode; + } + input.value = target.getAttribute('data-value'); + if (submit) { + form.submit(); + } + } + + function setListLocation() { + if (!list) return; + const rects = input.getClientRects()[0]; + list.style.top = (rects.top + rects.height) + "px"; + list.style.left = rects.left + "px"; + } + + function loadData(letter, data) { + const pkgs = data.slice(0, 10); // Show maximum of 10 results + + resetResults(); + + if (pkgs.length === 0) { + return; + } + + const ul = getCompleteList(); + ul.style.display = "block"; + const fragment = document.createDocumentFragment(); + + for (let i = 0; i < pkgs.length; i++) { + const item = document.createElement("li"); + const text = pkgs[i].replace(letter, '' + letter + ''); + item.innerHTML = '' + text + ''; + item.setAttribute('data-value', pkgs[i]); + fragment.appendChild(item); + } + + ul.appendChild(fragment); + ul.addEventListener('click', onListClick); + } + + function fetchData(letter) { + const url = '/rpc?v=5&type=' + suggest_type + '&arg=' + letter; + fetch(url).then(function(response) { + return response.json(); + }).then(function(data) { + loadData(letter, data); + }); + } + + function onInputClick() { + if (input.value === "") { + resetResults(); + return; + } + fetchData(input.value); + } + + function onKeyDown(e) { + if (!list) return; + + const elem = document.querySelector(".pkgsearch-typeahead li.active"); + switch(e.keyCode) { + case 13: // enter + if (!submit) { + return; + } + if (elem) { + input.value = elem.getAttribute('data-value'); + form.submit(); + } else { + form.submit(); + } + e.preventDefault(); + break; + case 38: // up + if (elem && elem.previousElementSibling) { + elem.className = ""; + elem.previousElementSibling.className = "active"; + } + e.preventDefault(); + break; + case 40: // down + if (elem && elem.nextElementSibling) { + elem.className = ""; + elem.nextElementSibling.className = "active"; + } else if (!elem && list.childElementCount !== 0) { + list.children[0].className = "active"; + } + e.preventDefault(); + break; + } + } + + // debounce https://davidwalsh.name/javascript-debounce-function + function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + } + + return { + init: function(type, inputfield, formfield, submitdata = true) { + suggest_type = type; + input = inputfield; + form = formfield; + submit = submitdata; + + input.addEventListener("input", onInputClick); + input.addEventListener("keydown", onKeyDown); + window.addEventListener('resize', debounce(setListLocation, 150)); + document.addEventListener("click", resetResults); + } + } +}()); From 8ca63075e907609ad6c81decff3bc7d027e1cde2 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Fri, 28 Apr 2023 16:10:32 +0200 Subject: [PATCH 1294/1451] housekeep: remove PHP implementation removal of the PHP codebase Signed-off-by: moson-mo --- .editorconfig | 3 - .env | 1 - .gitlab-ci.yml | 1 - CONTRIBUTING.md | 1 - INSTALL | 3 +- README.md | 1 - TESTING | 1 - aurweb/db.py | 4 +- aurweb/spawn.py | 59 +- conf/config.defaults | 16 +- conf/config.dev | 11 - doc/docker.md | 20 +- doc/maintenance.txt | 2 +- docker-compose.aur-dev.yml | 16 - docker-compose.override.yml | 14 - docker-compose.yml | 76 +- docker/README.md | 3 +- docker/config/nginx.conf | 52 - docker/health/memcached.sh | 2 - docker/health/php.sh | 2 - docker/mariadb-entrypoint.sh | 2 +- docker/php-entrypoint.sh | 32 - docker/scripts/install-deps.sh | 5 +- docker/scripts/run-memcached.sh | 2 - docker/scripts/run-nginx.sh | 2 - docker/scripts/run-php.sh | 4 - po/Makefile | 16 +- test/test_spawn.py | 35 +- web/html/404.php | 47 - web/html/503.php | 14 - web/html/account.php | 223 --- web/html/addvote.php | 116 -- web/html/comaintainers.php | 16 - web/html/commentedit.php | 18 - web/html/css/archnavbar/archlogo.png | Bin 5359 -> 0 bytes web/html/css/archnavbar/archnavbar.css | 26 - web/html/css/archnavbar/aurlogo.png | Bin 5997 -> 0 bytes web/html/css/archweb.css | 1255 ---------------- web/html/css/aurweb.css | 292 ---- web/html/css/cgit.css | 866 ----------- web/html/home.php | 215 --- web/html/images/ICON-LICENSE | 26 - web/html/images/action-undo.min.svg | 3 - web/html/images/action-undo.svg | 32 - web/html/images/ajax-loader.gif | Bin 723 -> 0 bytes web/html/images/favicon.ico | Bin 575 -> 0 bytes web/html/images/pencil.min.svg | 3 - web/html/images/pencil.svg | 55 - web/html/images/pin.min.svg | 1 - web/html/images/pin.svg | 3 - web/html/images/rss.svg | 3 - web/html/images/unpin.min.svg | 1 - web/html/images/unpin.svg | 4 - web/html/images/x.min.svg | 3 - web/html/images/x.svg | 31 - web/html/index.php | 205 --- web/html/js/comment-edit.js | 61 - web/html/js/copy.js | 9 - web/html/js/typeahead-home.js | 6 - web/html/js/typeahead-pkgbase-merge.js | 6 - web/html/js/typeahead-pkgbase-request.js | 36 - web/html/js/typeahead.js | 151 -- web/html/login.php | 68 - web/html/logout.php | 31 - web/html/modified-rss.php | 62 - web/html/packages.php | 173 --- web/html/passreset.php | 100 -- web/html/pkgbase.php | 195 --- web/html/pkgdel.php | 45 - web/html/pkgdisown.php | 59 - web/html/pkgflag.php | 91 -- web/html/pkgflagcomment.php | 16 - web/html/pkgmerge.php | 58 - web/html/pkgreq.php | 83 -- web/html/register.php | 87 -- web/html/rpc.php | 17 - web/html/rss.php | 61 - web/html/tos.php | 50 - web/html/tu.php | 122 -- web/html/voters.php | 34 - web/lib/DB.class.php | 59 - web/lib/acctfuncs.inc.php | 1522 ------------------- web/lib/aur.inc.php | 774 ---------- web/lib/aurjson.class.php | 710 --------- web/lib/cachefuncs.inc.php | 99 -- web/lib/confparser.inc.php | 59 - web/lib/credentials.inc.php | 91 -- web/lib/feedcreator.class.php | 1546 -------------------- web/lib/gettext.php | 432 ------ web/lib/pkgbasefuncs.inc.php | 1253 ---------------- web/lib/pkgfuncs.inc.php | 957 ------------ web/lib/pkgreqfuncs.inc.php | 260 ---- web/lib/routing.inc.php | 85 -- web/lib/stats.inc.php | 108 -- web/lib/streams.php | 167 --- web/lib/timezone.inc.php | 63 - web/lib/translator.inc.php | 139 -- web/lib/version.inc.php | 2 - web/locale/README | 5 - web/template/account_delete.php | 29 - web/template/account_details.php | 93 -- web/template/account_edit_form.php | 222 --- web/template/account_search_results.php | 87 -- web/template/cgit/footer.html | 6 - web/template/cgit/header.html | 15 - web/template/comaintainers_form.php | 19 - web/template/flag_comment.php | 26 - web/template/footer.php | 13 - web/template/header.php | 82 -- web/template/pkg_comment_box.php | 4 - web/template/pkg_comment_form.php | 28 - web/template/pkg_comments.php | 239 --- web/template/pkg_details.php | 317 ---- web/template/pkg_search_form.php | 120 -- web/template/pkg_search_results.php | 153 -- web/template/pkgbase_actions.php | 49 - web/template/pkgbase_details.php | 146 -- web/template/pkgreq_close_form.php | 31 - web/template/pkgreq_form.php | 81 - web/template/pkgreq_results.php | 129 -- web/template/search_accounts_form.php | 52 - web/template/stats/general_stats_table.php | 36 - web/template/stats/updates_table.php | 19 - web/template/stats/user_table.php | 21 - web/template/template.phps | 19 - web/template/tu_details.php | 123 -- web/template/tu_last_votes_list.php | 36 - web/template/tu_list.php | 82 -- 128 files changed, 27 insertions(+), 16046 deletions(-) delete mode 100755 docker/health/memcached.sh delete mode 100755 docker/health/php.sh delete mode 100755 docker/php-entrypoint.sh delete mode 100755 docker/scripts/run-memcached.sh delete mode 100755 docker/scripts/run-php.sh delete mode 100644 web/html/404.php delete mode 100644 web/html/503.php delete mode 100644 web/html/account.php delete mode 100644 web/html/addvote.php delete mode 100644 web/html/comaintainers.php delete mode 100644 web/html/commentedit.php delete mode 100644 web/html/css/archnavbar/archlogo.png delete mode 100644 web/html/css/archnavbar/archnavbar.css delete mode 100644 web/html/css/archnavbar/aurlogo.png delete mode 100644 web/html/css/archweb.css delete mode 100644 web/html/css/aurweb.css delete mode 100644 web/html/css/cgit.css delete mode 100644 web/html/home.php delete mode 100644 web/html/images/ICON-LICENSE delete mode 100644 web/html/images/action-undo.min.svg delete mode 100644 web/html/images/action-undo.svg delete mode 100644 web/html/images/ajax-loader.gif delete mode 100644 web/html/images/favicon.ico delete mode 100644 web/html/images/pencil.min.svg delete mode 100644 web/html/images/pencil.svg delete mode 100644 web/html/images/pin.min.svg delete mode 100644 web/html/images/pin.svg delete mode 100644 web/html/images/rss.svg delete mode 100644 web/html/images/unpin.min.svg delete mode 100644 web/html/images/unpin.svg delete mode 100644 web/html/images/x.min.svg delete mode 100644 web/html/images/x.svg delete mode 100644 web/html/index.php delete mode 100644 web/html/js/comment-edit.js delete mode 100644 web/html/js/copy.js delete mode 100644 web/html/js/typeahead-home.js delete mode 100644 web/html/js/typeahead-pkgbase-merge.js delete mode 100644 web/html/js/typeahead-pkgbase-request.js delete mode 100644 web/html/js/typeahead.js delete mode 100644 web/html/login.php delete mode 100644 web/html/logout.php delete mode 100644 web/html/modified-rss.php delete mode 100644 web/html/packages.php delete mode 100644 web/html/passreset.php delete mode 100644 web/html/pkgbase.php delete mode 100644 web/html/pkgdel.php delete mode 100644 web/html/pkgdisown.php delete mode 100644 web/html/pkgflag.php delete mode 100644 web/html/pkgflagcomment.php delete mode 100644 web/html/pkgmerge.php delete mode 100644 web/html/pkgreq.php delete mode 100644 web/html/register.php delete mode 100644 web/html/rpc.php delete mode 100644 web/html/rss.php delete mode 100644 web/html/tos.php delete mode 100644 web/html/tu.php delete mode 100644 web/html/voters.php delete mode 100644 web/lib/DB.class.php delete mode 100644 web/lib/acctfuncs.inc.php delete mode 100644 web/lib/aur.inc.php delete mode 100644 web/lib/aurjson.class.php delete mode 100644 web/lib/cachefuncs.inc.php delete mode 100644 web/lib/confparser.inc.php delete mode 100644 web/lib/credentials.inc.php delete mode 100644 web/lib/feedcreator.class.php delete mode 100644 web/lib/gettext.php delete mode 100644 web/lib/pkgbasefuncs.inc.php delete mode 100644 web/lib/pkgfuncs.inc.php delete mode 100644 web/lib/pkgreqfuncs.inc.php delete mode 100644 web/lib/routing.inc.php delete mode 100644 web/lib/stats.inc.php delete mode 100644 web/lib/streams.php delete mode 100644 web/lib/timezone.inc.php delete mode 100644 web/lib/translator.inc.php delete mode 100644 web/lib/version.inc.php delete mode 100644 web/locale/README delete mode 100644 web/template/account_delete.php delete mode 100644 web/template/account_details.php delete mode 100644 web/template/account_edit_form.php delete mode 100644 web/template/account_search_results.php delete mode 100644 web/template/cgit/footer.html delete mode 100644 web/template/cgit/header.html delete mode 100644 web/template/comaintainers_form.php delete mode 100644 web/template/flag_comment.php delete mode 100644 web/template/footer.php delete mode 100644 web/template/header.php delete mode 100644 web/template/pkg_comment_box.php delete mode 100644 web/template/pkg_comment_form.php delete mode 100644 web/template/pkg_comments.php delete mode 100644 web/template/pkg_details.php delete mode 100644 web/template/pkg_search_form.php delete mode 100644 web/template/pkg_search_results.php delete mode 100644 web/template/pkgbase_actions.php delete mode 100644 web/template/pkgbase_details.php delete mode 100644 web/template/pkgreq_close_form.php delete mode 100644 web/template/pkgreq_form.php delete mode 100644 web/template/pkgreq_results.php delete mode 100644 web/template/search_accounts_form.php delete mode 100644 web/template/stats/general_stats_table.php delete mode 100644 web/template/stats/updates_table.php delete mode 100644 web/template/stats/user_table.php delete mode 100644 web/template/template.phps delete mode 100644 web/template/tu_details.php delete mode 100644 web/template/tu_last_votes_list.php delete mode 100644 web/template/tu_list.php diff --git a/.editorconfig b/.editorconfig index 5a751aad..95f2c7dd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,3 @@ root = true end_of_line = lf insert_final_newline = true charset = utf-8 - -[*.{php,t}] -indent_style = tab diff --git a/.env b/.env index 22846cb4..bf6c48c4 100644 --- a/.env +++ b/.env @@ -1,7 +1,6 @@ FASTAPI_BACKEND="uvicorn" FASTAPI_WORKERS=2 MARIADB_SOCKET_DIR="/var/run/mysqld/" -AURWEB_PHP_PREFIX=https://localhost:8443 AURWEB_FASTAPI_PREFIX=https://localhost:8444 AURWEB_SSHD_PREFIX=ssh://aur@localhost:2222 GIT_DATA_DIR="./aur.git/" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index af722d99..10dd1787 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -71,7 +71,6 @@ deploy: variables: FASTAPI_BACKEND: gunicorn FASTAPI_WORKERS: 5 - AURWEB_PHP_PREFIX: https://aur-dev.archlinux.org AURWEB_FASTAPI_PREFIX: https://aur-dev.archlinux.org AURWEB_SSHD_PREFIX: ssh://aur@aur-dev.archlinux.org:2222 COMMIT_HASH: $CI_COMMIT_SHA diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8d4f90d..a91e3eec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,6 @@ browser if desired. Accessible services (on the host): - https://localhost:8444 (python via nginx) -- https://localhost:8443 (php via nginx) - localhost:13306 (mariadb) - localhost:16379 (redis) diff --git a/INSTALL b/INSTALL index 03459726..107fab4b 100644 --- a/INSTALL +++ b/INSTALL @@ -14,8 +14,7 @@ read the instructions below. $ cd aurweb $ poetry install -2) Setup a web server with PHP and MySQL. Configure the web server to redirect - all URLs to /index.php/foo/bar/. The following block can be used with nginx: +2) Setup a web server with MySQL. The following block can be used with nginx: server { # https is preferred and can be done easily with LetsEncrypt diff --git a/README.md b/README.md index 2741efa2..4d732bb2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ Directory Layout * `schema`: schema for the SQL database * `test`: test suite and test cases * `upgrading`: instructions for upgrading setups from one release to another -* `web`: PHP-based web interface for the AUR Documentation ------------- diff --git a/TESTING b/TESTING index cb34c0e9..078d330b 100644 --- a/TESTING +++ b/TESTING @@ -29,7 +29,6 @@ docker-compose 4) Browse to local aurweb development server. Python: https://localhost:8444/ - PHP: https://localhost:8443/ 5) [Optionally] populate the database with dummy data: diff --git a/aurweb/db.py b/aurweb/db.py index ab0f80b8..8311f2be 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -364,7 +364,7 @@ class ConnectionExecutor: def execute(self, query, params=()): # pragma: no cover # TODO: SQLite support has been removed in FastAPI. It remains - # here to fund its support for PHP until it is removed. + # here to fund its support for the Sharness testsuite. if self._paramstyle in ("format", "pyformat"): query = query.replace("%", "%%").replace("?", "%s") elif self._paramstyle == "qmark": @@ -410,7 +410,7 @@ class Connection: ) elif aur_db_backend == "sqlite": # pragma: no cover # TODO: SQLite support has been removed in FastAPI. It remains - # here to fund its support for PHP until it is removed. + # here to fund its support for Sharness testsuite. import math import sqlite3 diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 29162f33..442d89a9 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -20,7 +20,6 @@ from typing import Iterable import aurweb.config import aurweb.schema -from aurweb.exceptions import AurwebException children = [] temporary_dir = None @@ -28,9 +27,6 @@ verbosity = 0 asgi_backend = "" workers = 1 -PHP_BINARY = os.environ.get("PHP_BINARY", "php") -PHP_MODULES = ["pdo_mysql", "pdo_sqlite"] -PHP_NGINX_PORT = int(os.environ.get("PHP_NGINX_PORT", 8001)) FASTAPI_NGINX_PORT = int(os.environ.get("FASTAPI_NGINX_PORT", 8002)) @@ -47,42 +43,12 @@ class ProcessExceptions(Exception): super().__init__("\n- ".join(messages)) -def validate_php_config() -> None: - """ - Perform a validation check against PHP_BINARY's configuration. - - AurwebException is raised here if checks fail to pass. We require - the 'pdo_mysql' and 'pdo_sqlite' modules to be enabled. - - :raises: AurwebException - :return: None - """ - try: - proc = subprocess.Popen( - [PHP_BINARY, "-m"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - out, _ = proc.communicate() - except FileNotFoundError: - raise AurwebException(f"Unable to locate the '{PHP_BINARY}' " "executable.") - - assert proc.returncode == 0, ( - "Received non-zero error code " f"{proc.returncode} from '{PHP_BINARY}'." - ) - - modules = out.decode().splitlines() - for module in PHP_MODULES: - if module not in modules: - raise AurwebException(f"PHP does not have the '{module}' module enabled.") - - def generate_nginx_config(): """ Generate an nginx configuration based on aurweb's configuration. The file is generated under `temporary_dir`. Returns the path to the created configuration file. """ - php_bind = aurweb.config.get("php", "bind_address") - php_host = php_bind.split(":")[0] fastapi_bind = aurweb.config.get("fastapi", "bind_address") fastapi_host = fastapi_bind.split(":")[0] config_path = os.path.join(temporary_dir, "nginx.conf") @@ -101,12 +67,6 @@ def generate_nginx_config(): fastcgi_temp_path {os.path.join(temporary_dir, "fastcgi")}1 2; uwsgi_temp_path {os.path.join(temporary_dir, "uwsgi")}; scgi_temp_path {os.path.join(temporary_dir, "scgi")}; - server {{ - listen {php_host}:{PHP_NGINX_PORT}; - location / {{ - proxy_pass http://{php_bind}; - }} - }} server {{ listen {fastapi_host}:{FASTAPI_NGINX_PORT}; location / {{ @@ -154,7 +114,7 @@ def start(): terminal_width = 80 print( "{ruler}\n" - "Spawing PHP and FastAPI, then nginx as a reverse proxy.\n" + "Spawing FastAPI, then nginx as a reverse proxy.\n" "Check out {aur_location}\n" "Hit ^C to terminate everything.\n" "{ruler}".format( @@ -163,12 +123,6 @@ def start(): ) ) - # PHP - php_address = aurweb.config.get("php", "bind_address") - php_host = php_address.split(":")[0] - htmldir = aurweb.config.get("php", "htmldir") - spawn_child(["php", "-S", php_address, "-t", htmldir]) - # FastAPI fastapi_host, fastapi_port = aurweb.config.get("fastapi", "bind_address").rsplit( ":", 1 @@ -210,10 +164,7 @@ def start(): f""" > Started nginx. > - > PHP backend: http://{php_address} - > FastAPI backend: http://{fastapi_host}:{fastapi_port} - > - > PHP frontend: http://{php_host}:{PHP_NGINX_PORT} + > FastAPI backend: http://{fastapi_host}:{fastapi_port} > FastAPI frontend: http://{fastapi_host}:{FASTAPI_NGINX_PORT} > > Frontends are hosted via nginx and should be preferred. @@ -307,12 +258,6 @@ if __name__ == "__main__": ) args = parser.parse_args() - try: - validate_php_config() - except AurwebException as exc: - print(f"error: {str(exc)}") - sys.exit(1) - verbosity = args.verbose asgi_backend = args.backend workers = args.workers diff --git a/conf/config.defaults b/conf/config.defaults index 06e73afe..0cd4b9d4 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -38,11 +38,9 @@ enable-maintenance = 1 maintenance-exceptions = 127.0.0.1 render-comment-cmd = /usr/bin/aurweb-rendercomment localedir = /srv/http/aurweb/web/locale/ -; memcache, apc, or redis -; memcache/apc are supported in PHP, redis is supported in Python. +; cache: redis is supported in Python. cache = none cache_pkginfo_ttl = 86400 -memcache_servers = 127.0.0.1:11211 salt_rounds = 12 redis_address = redis://localhost ; Toggles traceback display in templates/errors/500.html. @@ -125,12 +123,12 @@ sync-dbs = core extra community multilib testing community-testing server = https://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] -archivedir = /srv/http/aurweb/web/html -packagesfile = /srv/http/aurweb/web/html/packages.gz -packagesmetafile = /srv/http/aurweb/web/html/packages-meta-v1.json.gz -packagesmetaextfile = /srv/http/aurweb/web/html/packages-meta-ext-v1.json.gz -pkgbasefile = /srv/http/aurweb/web/html/pkgbase.gz -userfile = /srv/http/aurweb/web/html/users.gz +archivedir = /srv/http/aurweb/archives +packagesfile = /srv/http/aurweb/archives/packages.gz +packagesmetafile = /srv/http/aurweb/archives/packages-meta-v1.json.gz +packagesmetaextfile = /srv/http/aurweb/archives/packages-meta-ext-v1.json.gz +pkgbasefile = /srv/http/aurweb/archives/pkgbase.gz +userfile = /srv/http/aurweb/archives/users.gz [git-archive] author = git_archive.py diff --git a/conf/config.dev b/conf/config.dev index b36bfe77..f3b0ee21 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -6,7 +6,6 @@ ; development-specific options too. [database] -; PHP options: mysql, sqlite. ; FastAPI options: mysql. backend = mysql @@ -31,9 +30,6 @@ localedir = YOUR_AUR_ROOT/web/locale salt_rounds = 4 ; See config.defaults comment about cache. cache = none -; In docker, the memcached host is available. On a user's system, -; this should be set to localhost (most likely). -memcache_servers = memcached:11211 ; If cache = 'redis' this address is used to connect to Redis. redis_address = redis://127.0.0.1 aur_request_ml = aur-requests@localhost @@ -51,13 +47,6 @@ openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/open client_id = aurweb client_secret = -[php] -; Address PHP should bind when spawned in development mode by aurweb.spawn. -bind_address = 127.0.0.1:8081 - -; Directory containing aurweb's PHP code, required by aurweb.spawn. -htmldir = YOUR_AUR_ROOT/web/html - [fastapi] ; Address uvicorn should bind when spawned in development mode by aurweb.spawn. bind_address = 127.0.0.1:8082 diff --git a/doc/docker.md b/doc/docker.md index 22505f7a..c54184b8 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -65,12 +65,9 @@ Services | [mariadb](#mariadb) | 127.0.0.1:13306 | | [git](#git) | 127.0.0.1:2222 | | redis | 127.0.0.1:16379 | -| [php-fpm](#php-fpm) | 127.0.0.1:19000 | -| cgit-php | | | [fastapi](#fastapi) | 127.0.0.1:18000 | | cgit-fastapi | | | [nginx](#nginx) (fastapi) | 127.0.0.1:8444 | -| [nginx](#nginx) (php) | 127.0.0.1:8443 | There are more services which have not been referred to here; the services listed above encompass all notable services. Some @@ -113,16 +110,6 @@ to be used for the AUR. This service will perform setup in either case if the repository is not yet initialized. -#### php-fpm - -When running any services which use the _php-fpm_ backend or other -php-related services, users should define: - -- `AURWEB_PHP_PREFIX` - - Default: `https://localhost:8443` -- `AURWEB_SSHD_PREFIX` - - Default: `ssh://aur@localhost:2222` - #### fastapi The _fastapi_ service hosts a `gunicorn`, `uvicorn` or `hypercorn` @@ -145,20 +132,17 @@ backend or other fastapi-related services, users should define: #### nginx -The _nginx_ service binds to two host endpoints: 127.0.0.1:8444 (fastapi) -and 127.0.0.1:8443 (php). Each instance is available over the `https` +The _nginx_ service binds to host endpoint: 127.0.0.1:8444 (fastapi). +The instance is available over the `https` protocol as noted in the table below. | Impl | Host Binding | URL | |--------|----------------|------------------------| | Python | 127.0.0.1:8444 | https://localhost:8444 | -| PHP | 127.0.0.1:8443 | https://localhost:8443 | When running this service, the following variables should be defined: - `AURWEB_FASTAPI_PREFIX` - Default: `https://localhost:8444` -- `AURWEB_PHP_PREFIX` - - Default: `https://localhost:8443` - `AURWEB_SSHD_PREFIX` - Default: `ssh://aur@localhost:2222` diff --git a/doc/maintenance.txt b/doc/maintenance.txt index dacf2b60..39642f21 100644 --- a/doc/maintenance.txt +++ b/doc/maintenance.txt @@ -21,7 +21,7 @@ The RPC interface can be used to query package information via HTTP. Installation ------------ -The web backend requires a web server with PHP and an SQL database. The Git/SSH +The web backend requires a web server and an SQL database. The Git/SSH interface requires Python, several Python modules and an up-to-date version of Git. APCu or memcached can be used to reduce load on the database server. diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index 0b91dd93..1763f427 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -6,9 +6,6 @@ services: - data:/data - step:/root/.step - memcached: - restart: always - redis: restart: always @@ -32,11 +29,6 @@ services: - data:/data - smartgit_run:/var/run/smartgit - cgit-php: - restart: always - volumes: - - ${GIT_DATA_DIR}:/aurweb/aur.git - cgit-fastapi: restart: always volumes: @@ -48,14 +40,6 @@ services: - mariadb_run:/var/run/mysqld - archives:/var/lib/aurweb/archives - php-fpm: - restart: always - environment: - - AURWEB_PHP_PREFIX=${AURWEB_PHP_PREFIX} - - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} - volumes: - - data:/data - fastapi: restart: always environment: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 1e466730..6580de30 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -25,26 +25,12 @@ services: mariadb: condition: service_healthy - php-fpm: - volumes: - - ./data:/data - - ./aurweb:/aurweb/aurweb - - ./migrations:/aurweb/migrations - - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - - ./templates:/aurweb/templates - fastapi: volumes: - ./data:/data - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates nginx: diff --git a/docker-compose.yml b/docker-compose.yml index a1c2bb42..0973fc0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,9 @@ # - `ca` - Certificate Authority generation # - `git` - `port 2222` - Git over SSH server # - `fastapi` - hypercorn service for aurweb's FastAPI app -# - `php-fpm` - Execution server for PHP aurweb -# - `nginx` - `ports 8444 (FastAPI), 8443 (PHP)` - Everything -# - You can reach `nginx` via FastAPI at `https://localhost:8444/` -# or via PHP at `https://localhost:8443/`. CGit can be reached -# via the `/cgit/` request uri on either server. +# - `nginx` - `port 8444 (FastAPI) +# - You can reach `nginx` via FastAPI at `https://localhost:8444/`. +# CGit can be reached via the `/cgit/` request uri on either server. # # Copyright (C) 2021 aurweb Development # All Rights Reserved. @@ -36,14 +34,6 @@ services: volumes: - step:/root/.step - memcached: - image: aurweb:latest - init: true - command: /docker/scripts/run-memcached.sh - healthcheck: - test: "bash /docker/health/memcached.sh" - interval: 3s - redis: image: aurweb:latest init: true @@ -133,26 +123,6 @@ services: test: "bash /docker/health/smartgit.sh" interval: 3s - cgit-php: - image: aurweb:latest - init: true - environment: - - AUR_CONFIG=/aurweb/conf/config - - CGIT_CLONE_PREFIX=${AURWEB_PHP_PREFIX} - - CGIT_CSS=/css/cgit.css - entrypoint: /docker/cgit-entrypoint.sh - command: /docker/scripts/run-cgit.sh 3000 - healthcheck: - test: "bash /docker/health/cgit.sh 3000" - interval: 3s - depends_on: - git: - condition: service_healthy - ports: - - "127.0.0.1:13000:3000" - volumes: - - git_data:/aurweb/aur.git - cgit-fastapi: image: aurweb:latest init: true @@ -189,32 +159,6 @@ services: - mariadb_run:/var/run/mysqld - archives:/var/lib/aurweb/archives - php-fpm: - image: aurweb:latest - init: true - environment: - - AUR_CONFIG=/aurweb/conf/config - - AURWEB_PHP_PREFIX=${AURWEB_PHP_PREFIX} - - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} - - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} - entrypoint: /docker/php-entrypoint.sh - command: /docker/scripts/run-php.sh - healthcheck: - test: "bash /docker/health/php.sh" - interval: 3s - depends_on: - git: - condition: service_healthy - memcached: - condition: service_healthy - cron: - condition: service_started - volumes: - - mariadb_run:/var/run/mysqld - - archives:/var/lib/aurweb/archives - ports: - - "127.0.0.1:19000:9000" - fastapi: image: aurweb:latest init: true @@ -252,7 +196,6 @@ services: entrypoint: /docker/nginx-entrypoint.sh command: /docker/scripts/run-nginx.sh ports: - - "127.0.0.1:8443:8443" # PHP - "127.0.0.1:8444:8444" # FastAPI healthcheck: test: "bash /docker/health/nginx.sh" @@ -260,16 +203,12 @@ services: depends_on: ca: condition: service_healthy - cgit-php: - condition: service_healthy cgit-fastapi: condition: service_healthy smartgit: condition: service_healthy fastapi: condition: service_healthy - php-fpm: - condition: service_healthy sharness: image: aurweb:latest @@ -290,9 +229,6 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates pytest-mysql: @@ -319,9 +255,6 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates test: @@ -346,9 +279,6 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates volumes: diff --git a/docker/README.md b/docker/README.md index cc1f5df0..51e485f6 100644 --- a/docker/README.md +++ b/docker/README.md @@ -55,8 +55,7 @@ can proceed. ### Querying the RPC -The Fast (Python) API runs on Port 8444, while the legacy PHP version runs -on 8443. You can query one like so: +The Fast (Python) API runs on Port 8444. You can query one like so: ```sh curl -k "https://localhost:8444/rpc/?v=5&type=search&arg=python" diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index 99804d1d..9b167553 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -27,10 +27,6 @@ http { server fastapi:8000; } - upstream cgit-php { - server cgit-php:3000; - } - upstream cgit-fastapi { server cgit-fastapi:3000; } @@ -39,54 +35,6 @@ http { server unix:/var/run/smartgit/smartgit.sock; } - server { - listen 8443 ssl http2; - server_name localhost default_server; - - ssl_certificate /etc/ssl/certs/web.cert.pem; - ssl_certificate_key /etc/ssl/private/web.key.pem; - - root /aurweb/web/html; - index index.php; - - location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { - include uwsgi_params; - uwsgi_pass smartgit; - uwsgi_modifier1 9; - uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; - uwsgi_param PATH_INFO /aur.git/$3; - uwsgi_param GIT_HTTP_EXPORT_ALL ""; - uwsgi_param GIT_NAMESPACE $1; - uwsgi_param GIT_PROJECT_ROOT /aurweb; - } - - location ~ ^/cgit { - include uwsgi_params; - rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=$1&$2 last; - uwsgi_modifier1 9; - uwsgi_param CGIT_CONFIG /etc/cgitrc; - uwsgi_pass uwsgi://cgit-php; - } - - location ~ ^/[^/]+\.php($|/) { - fastcgi_pass php-fpm:9000; - fastcgi_index index.php; - fastcgi_split_path_info ^(/[^/]+\.php)(/.*)$; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - include fastcgi_params; - } - - location ~ .+\.(css|js?|jpe?g|png|svg|ico)/?$ { - try_files $uri =404; - } - - location ~ .* { - rewrite ^/(.*)$ /index.php/$1 last; - } - - } - server { listen 8444 ssl http2; server_name localhost default_server; diff --git a/docker/health/memcached.sh b/docker/health/memcached.sh deleted file mode 100755 index 00f8cd98..00000000 --- a/docker/health/memcached.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec pgrep memcached diff --git a/docker/health/php.sh b/docker/health/php.sh deleted file mode 100755 index 7325946b..00000000 --- a/docker/health/php.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec printf "" >>/dev/tcp/127.0.0.1/9000 diff --git a/docker/mariadb-entrypoint.sh b/docker/mariadb-entrypoint.sh index a00f6106..a6fb9a76 100755 --- a/docker/mariadb-entrypoint.sh +++ b/docker/mariadb-entrypoint.sh @@ -12,7 +12,7 @@ while ! mysqladmin ping 2>/dev/null; do done # Configure databases. -DATABASE="aurweb" # Persistent database for fastapi/php-fpm. +DATABASE="aurweb" # Persistent database for fastapi. echo "Taking care of primary database '${DATABASE}'..." mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'localhost' IDENTIFIED BY 'aur';" diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh deleted file mode 100755 index dc1a91de..00000000 --- a/docker/php-entrypoint.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -set -eou pipefail - -for archive in packages pkgbase users packages-meta-v1.json packages-meta-ext-v1.json; do - ln -vsf /var/lib/aurweb/archives/${archive}.gz /aurweb/web/html/${archive}.gz -done - -# Setup database. -NO_INITDB=1 /docker/mariadb-init-entrypoint.sh - -# Setup some other options. -aurweb-config set options cache 'memcache' -aurweb-config set options aur_location "$AURWEB_PHP_PREFIX" -aurweb-config set options git_clone_uri_anon "${AURWEB_PHP_PREFIX}/%s.git" -aurweb-config set options git_clone_uri_priv "${AURWEB_SSHD_PREFIX}/%s.git" - -# Listen on :9000. -sed -ri 's/^(listen).*/\1 = 0.0.0.0:9000/' /etc/php/php-fpm.d/www.conf -sed -ri 's/^;?(clear_env).*/\1 = no/' /etc/php/php-fpm.d/www.conf - -# Log to stderr. View logs via `docker-compose logs php-fpm`. -sed -ri 's|^(error_log) = .*$|\1 = /proc/self/fd/2|g' /etc/php/php-fpm.conf -sed -ri 's|^;?(access\.log) = .*$|\1 = /proc/self/fd/2|g' \ - /etc/php/php-fpm.d/www.conf - -sed -ri 's/^;?(extension=pdo_mysql)/\1/' /etc/php/php.ini -sed -ri 's/^;?(open_basedir).*$/\1 = \//' /etc/php/php.ini - -# Use the sqlite3 extension line for memcached. -sed -ri 's/^;(extension)=sqlite3$/\1=memcached/' /etc/php/php.ini - -exec "$@" diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index 85403969..7aa225fa 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -15,9 +15,8 @@ pacman -Sy --noconfirm --noprogressbar archlinux-keyring pacman -Syu --noconfirm --noprogressbar \ --cachedir .pkg-cache git gpgme nginx redis openssh \ mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ - php php-fpm memcached php-memcached python-pip pyalpm \ - python-srcinfo curl libeatmydata cronie python-poetry \ - python-poetry-core step-cli step-ca asciidoc \ + python-pip pyalpm python-srcinfo curl libeatmydata cronie \ + python-poetry python-poetry-core step-cli step-ca asciidoc \ python-virtualenv python-pre-commit exec "$@" diff --git a/docker/scripts/run-memcached.sh b/docker/scripts/run-memcached.sh deleted file mode 100755 index 90784b0f..00000000 --- a/docker/scripts/run-memcached.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec /usr/bin/memcached -u memcached -m 64 -c 1024 -l 0.0.0.0 diff --git a/docker/scripts/run-nginx.sh b/docker/scripts/run-nginx.sh index 6ece3303..e976f67d 100755 --- a/docker/scripts/run-nginx.sh +++ b/docker/scripts/run-nginx.sh @@ -5,8 +5,6 @@ echo echo " Services:" echo " - FastAPI : https://localhost:8444/" echo " (cgit) : https://localhost:8444/cgit/" -echo " - PHP : https://localhost:8443/" -echo " (cgit) : https://localhost:8443/cgit/" echo echo " Note: Copy root CA (./data/ca.root.pem) to ca-certificates or browser." echo diff --git a/docker/scripts/run-php.sh b/docker/scripts/run-php.sh deleted file mode 100755 index b86f8ce5..00000000 --- a/docker/scripts/run-php.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -eou pipefail - -exec php-fpm --fpm-config /etc/php/php-fpm.conf --nodaemonize diff --git a/po/Makefile b/po/Makefile index 0b579f48..8fd17515 100644 --- a/po/Makefile +++ b/po/Makefile @@ -48,20 +48,12 @@ all: ${MOFILES} lang=`echo $@ | sed -e 's/\.po-update$$//'`; \ msgmerge -U --no-location --lang="$$lang" $< aurweb.pot -POTFILES-php: - find ../web -type f -name '*.php' -printf '%P\n' | sort >POTFILES-php - POTFILES-py: find ../aurweb -type f -name '*.py' -printf '%P\n' | sort >POTFILES-py -update-pot: POTFILES-php POTFILES-py +update-pot: POTFILES-py pkgname=AURWEB; \ - pkgver=`sed -n 's/.*"AURWEB_VERSION", "\(.*\)".*/\1/p' ../web/lib/version.inc.php`; \ - xgettext --default-domain=aurweb -L php --keyword=__ --keyword=_n:1,2 \ - --add-location=file --add-comments=TRANSLATORS: \ - --package-name="$$pkgname" --package-version="$$pkgver" \ - --msgid-bugs-address='${MSGID_BUGS_ADDRESS}' \ - --directory ../web --files-from POTFILES-php -o aurweb.pot; \ + pkgver=`sed -n 's/version\s*=\s*"\(.*\)"/\1/p' ../pyproject.toml`; \ xgettext --default-domain=aurweb -L python --join-existing \ --keyword=translate \ --add-location=file --add-comments=TRANSLATORS: \ @@ -73,7 +65,7 @@ update-po: ${MAKE} ${UPDATEPOFILES} clean: - rm -f *.mo *.po\~ POTFILES-php POTFILES-py + rm -f *.mo *.po\~ POTFILES-py install: all for l in ${LOCALES}; do mkdir -p ${DESTDIR}${PREFIX}/$$l/LC_MESSAGES/; done @@ -82,4 +74,4 @@ install: all uninstall: for l in ${LOCALES}; do rm -rf ${DESTDIR}${PREFIX}/$$l/LC_MESSAGES/; done -.PHONY: all update-pot update-po clean install uninstall POTFILES-php POTFILES-py +.PHONY: all update-pot update-po clean install uninstall POTFILES-py diff --git a/test/test_spawn.py b/test/test_spawn.py index 25b9ebfc..c57c9b52 100644 --- a/test/test_spawn.py +++ b/test/test_spawn.py @@ -7,10 +7,9 @@ import pytest import aurweb.config import aurweb.spawn -from aurweb.exceptions import AurwebException # Some os.environ overrides we use in this suite. -TEST_ENVIRONMENT = {"PHP_NGINX_PORT": "8001", "FASTAPI_NGINX_PORT": "8002"} +TEST_ENVIRONMENT = {"FASTAPI_NGINX_PORT": "8002"} class FakeProcess: @@ -49,34 +48,6 @@ class MockFakeProcess: return proc -@mock.patch("aurweb.spawn.PHP_BINARY", "does-not-exist") -def test_spawn(): - match = r"^Unable to locate the '.*' executable\.$" - with pytest.raises(AurwebException, match=match): - aurweb.spawn.validate_php_config() - - -@mock.patch("subprocess.Popen", side_effect=MockFakeProcess(1).process) -def test_spawn_non_zero_php_binary(fake_process: FakeProcess): - match = r"^Received non-zero error code.*$" - with pytest.raises(AssertionError, match=match): - aurweb.spawn.validate_php_config() - - -def test_spawn_missing_modules(): - side_effect = MockFakeProcess(stdout=b"pdo_sqlite").process - with mock.patch("subprocess.Popen", side_effect=side_effect): - match = r"PHP does not have the 'pdo_mysql' module enabled\.$" - with pytest.raises(AurwebException, match=match): - aurweb.spawn.validate_php_config() - - side_effect = MockFakeProcess(stdout=b"pdo_mysql").process - with mock.patch("subprocess.Popen", side_effect=side_effect): - match = r"PHP does not have the 'pdo_sqlite' module enabled\.$" - with pytest.raises(AurwebException, match=match): - aurweb.spawn.validate_php_config() - - @mock.patch.dict("os.environ", TEST_ENVIRONMENT) def test_spawn_generate_nginx_config(): ctx = tempfile.TemporaryDirectory() @@ -86,13 +57,9 @@ def test_spawn_generate_nginx_config(): with open(nginx_config_path) as f: nginx_config = f.read().rstrip() - php_address = aurweb.config.get("php", "bind_address") - php_host = php_address.split(":")[0] fastapi_address = aurweb.config.get("fastapi", "bind_address") fastapi_host = fastapi_address.split(":")[0] expected_content = [ - f'listen {php_host}:{TEST_ENVIRONMENT.get("PHP_NGINX_PORT")}', - f"proxy_pass http://{php_address}", f'listen {fastapi_host}:{TEST_ENVIRONMENT.get("FASTAPI_NGINX_PORT")}', f"proxy_pass http://{fastapi_address}", ] diff --git a/web/html/404.php b/web/html/404.php deleted file mode 100644 index 9f81d115..00000000 --- a/web/html/404.php +++ /dev/null @@ -1,47 +0,0 @@ - - -
    -

    404 -

    -

    - -
      -
    • - : - -
    • -
    • - ' . htmlspecialchars($gitpkg) . '', - '' . htmlspecialchars($gitcmd) . '') ?> -
    • -
    • - ', '', - '' . htmlspecialchars($gitpkg) . '') ?> -
    • -
    - -
    - - - -
    -

    503 -

    -

    -
    - -\n"; -echo "

    ".__("Accounts")."

    \n"; - -if (isset($_COOKIE["AURSID"])) { - if ($action == "SearchAccounts") { - - # security check - # - if (has_credential(CRED_ACCOUNT_SEARCH)) { - # the user has entered search criteria, find any matching accounts - # - search_results_page(in_request("O"), in_request("SB"), - in_request("U"), in_request("T"), in_request("S"), - in_request("E"), in_request("R"), in_request("I"), - in_request("K")); - - } else { - # a non-privileged user is trying to access the search page - # - print __("You are not allowed to access this area.")."
    \n"; - } - - } elseif ($action == "DisplayAccount") { - # the user has clicked 'edit', display the account details in a form - # - if (empty($row)) { - print __("Could not retrieve information for the specified user."); - } else { - /* Verify user has permission to edit the account */ - if (can_edit_account($row)) { - display_account_form("UpdateAccount", - $row["Username"], - $row["AccountTypeID"], - $row["Suspended"], - $row["Email"], - $row["BackupEmail"], - $row["HideEmail"], - "", - "", - $row["RealName"], - $row["LangPreference"], - $row["Timezone"], - $row["Homepage"], - $row["IRCNick"], - $row["PGPKey"], - $PK, - $row["InactivityTS"] ? 1 : 0, - $row["CommentNotify"], - $row["UpdateNotify"], - $row["OwnershipNotify"], - $row["ID"], - $row["Username"]); - } else { - print __("You do not have permission to edit this account."); - } - } - - } elseif ($action == "DeleteAccount") { - /* Details for account being deleted. */ - if ($row && can_edit_account($row)) { - $uid_removal = $row['ID']; - $uid_session = uid_from_sid($_COOKIE['AURSID']); - $username = $row['Username']; - - if (in_request('confirm') && check_token()) { - if (check_passwd($uid_session, $_REQUEST['passwd']) == 1) { - user_delete($uid_removal); - header('Location: /'); - } else { - echo "
    • "; - echo __("Invalid password."); - echo "
    "; - include("account_delete.php"); - } - } else { - include("account_delete.php"); - } - } else { - print __("You do not have permission to edit this account."); - } - } elseif ($action == "AccountInfo") { - # no editing, just looking up user info - # - if (empty($row)) { - print __("Could not retrieve information for the specified user."); - } else { - include("account_details.php"); - } - - } elseif ($action == "UpdateAccount") { - print $update_account_message; - - if ($row && !$success) { - display_account_form("UpdateAccount", - in_request("U"), - in_request("T"), - in_request("S"), - in_request("E"), - in_request("BE"), - in_request("H"), - in_request("P"), - in_request("C"), - in_request("R"), - in_request("L"), - in_request("TZ"), - in_request("HP"), - in_request("I"), - in_request("K"), - in_request("PK"), - in_request("J"), - in_request("CN"), - in_request("UN"), - in_request("ON"), - in_request("ID"), - $row["Username"]); - } - - } elseif ($action == "ListComments") { - if ($row && has_credential(CRED_ACCOUNT_LIST_COMMENTS, array($row["ID"]))) { - # display the comment list if they're a TU/dev - - $total_comment_count = account_comments_count($row["ID"]); - list($pagination_templs, $per_page, $offset) = calculate_pagination($total_comment_count); - - $username = $row["Username"]; - $uid = $row["ID"]; - $comments = account_comments($uid, $per_page, $offset); - - $comment_section = "account"; - include('pkg_comments.php'); - - } else { - print __("You are not allowed to access this area."); - } - - } else { - if (has_credential(CRED_ACCOUNT_SEARCH)) { - # display the search page if they're a TU/dev - # - print __("Use this form to search existing accounts.")."
    \n"; - include('search_accounts_form.php'); - - } else { - print __("You are not allowed to access this area."); - } - } - -} else { - # visitor is not logged in - # - print __("You must log in to view user information."); -} - -echo ""; - -html_footer(AURWEB_VERSION); - -?> diff --git a/web/html/addvote.php b/web/html/addvote.php deleted file mode 100644 index 4b52a3bb..00000000 --- a/web/html/addvote.php +++ /dev/null @@ -1,116 +0,0 @@ -" . __("New proposal submitted.") . "

    \n"; - } else { -?> - - -

    - - -
    -

    - -
    -

    - - - -

    -

    - - -

    -

    -
    -
    - - - " /> -

    -
    -
    ->10<3$laUEN!Q#6008Pn zbXOpBG|m5lHpl75-vVhpC(a!4x$h*`V2lakNUt?{-fw^e?ODm zN2l+hb*}S!PafO;yvB^)?EDQ@iLzn>_b!b9fT^^f*w@{?%2`uO4TKQ2Smxg5ikKW2 zQ*A?S8C;#@KdJ0k753J9XG*F&MOl%LT4?kK&;(>rVhi9U1?;<+SJ-zbGm)>gYnJLH z%w}?U0WeGe;1*6<3W^4axh(3Rm06q~vz;20p9m)iop2}sG)ey@z5ZEzGBay7^^(hv zOFvOQpz-HtC(no`dwszCljyn5{FA6S0{1JalqoLl^-)5KLk=qdBTC!pRYl~Zu4~eY z`jm=y&G7{n7Ek2g)hM=KE?#h6uBwFA4#F~l!gQh1QhEZ zVRX6TH*Dt7a2hD-{!i`M-_>j)z6g%ca~xCE?`N$AK|HXr$u$Q&P^B$#;&T%7mJM zuo8+h+l1P5KqsF_FkI5xbvE#m)@W=uI^t;J3`eFie;MX?$(u}bW!*u-&#nH?kt^%d zx|YaA-ExCV9U&V&;SIrCEb(@465tV50>|q>SR zoU@zOxr=q~2E8QP8G}nzeV$%*DuQQq|Tc`gx!LmYDeG= zxKY+=D~=fjPC4|=pTYudRG(20hFb0uLqD#-tI}K=Ka;(+@i-pI>=l)p6i$ z&9-~lq-@(Seb}1YT!Fy^x8xB-=ujz?L)W!@=ax`=LLfUDA2ZdD#!`(EzFdkzMFYWBytXn;~HGS8jwD~p^JH9 z(yY!Xaf-vSL#cK0=xru5#$e>dlTyIaB33aiIEI-;90sCX`viJ zA0i;5`@2n-HiiSnJS{XdI4WF@Dm#>@7=~DhfH|<5T9m?3X2M$XEY_)Boxvz8R1=d+ zB`^*2^JpfC?3HnEWQFUvg3{}9;A~0gk4%RM(^LTtAnciRWj)K8EL1z=nCL`vFJ0_8 z!lUO+=S6*pjK~uS5pwKHtY(>*-;1v^W&;MohRce{DXIOkviW#BfZjD`@$7ik~xbg$=6paDPdGZM^={xaJ@@q2g3F_z3%7tZk`C zur_-ZmzZiRoB!k5KDqsu&zw`4dhk;}>?d+hUXaSqbmfQjyn1xNf=oQU%Ay=+y!NUKi@<_oc32aa9U%msvJaVpldpOC0k^?27Mr#?k`zim@2j!tQM8L z>5NktPROk;Vy)csIG|uk19*2kOGxmT0O-PQd40OA%-8}209F~HZf=Td#(;;6XTF|h zGG5=G!a^xRAc~u#SS3Xwrx^tTPhb|}T=HW#cvOeJbg_ZtCNaD3TxyjDGOH`2F&N~i zK7%H?&d0K(b}N}2LKXZU=#8)o@P$KnIRtT-efg^}&%OhchdWNi^uZJJ5~ zsROEr60OfOob6dz%ll^ zw`BJj(gPB=SuLcx+ZjhamX2)_&8oO;QyKpP14>$%p2EHQWqkkVu`xg(V}oVi}Ri1z*S+I zg&vwS4$uun2<^C{X&aMF^DN3nO)lxHR>1oQ{Z?T`8iK%gj1M1j_B(}M{>u?&qPqCf z@uD>bS$InQJq`cN+P@oOa_&vEnx|U=u1hPu{XyV*CC>MBy&6G%>l26W*t=CBlXDDG z6=*#pihOP#lTE22?q`sZa3sigQ^t;7EsE0h^Jrtf7+{UX`CfGHD~LprH&0G2mS><_ zyZVj0jd5Wx@9sBtbgeo`9e}YJXj8`SWrhXbgC%E!X(`q$mp5gTKS>%Vl39a^@Fnh> zrQ}ro+g6k_&$ppZg>ZU`g8U#2p%uT|JE^pGL@X@KOl6nXY1 zV10N_gQU6nR({-gHmuQc?o@7=>Vm(tjfji_s`T&NmFf5I7ZHuF&zOH)4_MS*J1a5m zj^ydAYi5p=oW5!qyL@3mnsD;>k10{nJhlr|3H{Ut`O+P9Pj*qF5;xUngQOxl>`5OT z&)Tz_Gd^M50N7Kn(8`Ba!k$|~2>J){l{t}mhqLb8Xs!=Mz(vm42#U*y-ogL-AGoQQ8XB4ez$vAk@nHI82! z8d9IJr16mXG{~R@PZ>x}nY`_B1tCNdc4powbO1}zff z9u12=;9MC{dh4p~e6-0|uF6$`Pj{xAt0Jr+{39w6dq}am5a(PV>Um-HRfEPPS(G*8 z$>LlpE%mSB?%8W)=@7LK^+u-e%_XBp-gtU|1On9feMMGxLNVz3l0l#Z3wFJUrNZNq z=)&0bq%9%iA`=U@3afIyi?UKitiEN7$7@|MhLB9j`y0^mougfz51B1v%7Z-$^ZfI} zwQvKT$?R|7l%b=L+n|c)ZRmSslV|r|ir*yXkw!=j;o4)`)&9i&vQr`V;o?N7*4H~GRuFf>EYM8 z75NO70;8S&Z%M)#lxvZ-#&%;@_oFzzNuu?^L#dErg-H6l?T#=CR>*qr2Eb5oAS$-@ zoE>VjIJo&!`uPMVZlxmZYekd=eC_k5UPV}Oh21VDP6mFwYue2SrxK|*7%OfVUP{Lv z7Bo(pkXOYoC0`Q$Sho|f75Vm$&E>1!MS3I*0bA3=k`Ptq=` zl*jVdmYB(g`_Havu^{x)3A!sO&}>gg5Ju%aw8 z(+7dG`wX8zE`;xWtj@Ubij)aeaF&b*7q5Q`Z~xCB_jWZqY}f9 zM}Pd%k?O0iHVeBp^{Oh54+%Wb2-_%_+x|9(b-|WRit}c1S4wB}MUelnUs-3h^6Frg zO7;Vl?oT<|9WI z)L({xmKW)r8PZaTJv->_F7ufCkdUk_BqyGHn~#)mOu!*2f>+oWe;qhvDVW%|*AkrR zayWjz;XR8b51uDI6FGJC4NKRFx5m9Y>Jzus5nG5Cf@hoBqcE(*=Ir#pclmAOe)M@2 zY!S#sdBv{GRi1=+VAzIO^2(O7+;kBPd+Z|GuYN*qfJY~^SL6Ev(d0jSf^2IT>)CNT zZOqG06m_S5M@gi7+;62DdcmB@|KU<9sJEIwx(XDPyb?&a1H>~(pjJx$s`>iHwm@^- zh!zD$RqscY>gW+nnFhN?DKq<;laZ)~N3zETIFxNm7^d7(t?+14rhU(;bbM*VOZ%#q zn(GTJgf2XNsNRAaXk)G===ivdH|CQ1DJhk#!=yN9+%k33o3K-4GdEp*XQ|{0J)n)t zTUfA*VW55-_bEY^pleU?%!MJBBgK8ZE+d*_Yk`vF+dT5^v^R!j_S}SPDTDiJ4?&(a zdc)daUl3HUDW{@wn4x6v>y}V|;WQ1pVny=fX%=Tx=-I7HeC4#0q!Cd0sM3e7iim?E zr7WdCd-HrNeH@ZKjYy)>XB@q|?n>!{etU;K*_GiBSDEmy0qk1e`FXOHeiKL%&%M+) z84ChG>x!ZQ%&yHHk!p)30$b1nh^Uz~G}Q0kz?9BQmgBy=9b#6FI6KVC%gc#Lgdfj8 zlf8g_RGi3+ql39R3HP<(a1aLVeycB!uGTgx@KHD+5XB+kEmcJeuV>BU-?lNBnVz=>k(&9qn^S z7K2hxxY;uFiO@8m`**ob>EWLwtBYq-B*Bhvd_c|b{kywN<`Ue${v}YPZI;N(=AMsp zzt1^8&0lz`)WB6GE>KH_W~`CJ@i=Fd0~3=aTlTL?Hczl?@$ z@BcaT9p-r6)8HIU)FCvs6%bfX!F*8Cl4U^mgjr53K-SB@l%^OGPh^%ysA(2-InjRn z$L4ZSPF6O)%j7mrM<%UT_P5z%%R(~R3>Bb?TCPj$qsIG3j1jdM5^PKT%K)ZkYU%22 z1FY>E0pB-&9$NH}opdQ%zkIw&ZP?oUs>HGwsnzl+n&H;4zXZ4K8iH(%P-W&KX`Mvp4;W6k)w(@UKYq)zF%33UvR9^Vdk;p ze$x>&*Dolow}vb2^iK)dv>3j$e1f8)W_(v<&u&_R%F8QT-6et_4ugjT&e^(?)N74! zr2@lX=dG9+Sa7}CgE(~DW}Y>ntw@+ubsO7+u?M(0S$DLMecn?v9e*-+#A%EEo@0ha zx1FYSOT>&~{e@o~9eQl@oW}S|=?ihQX2$xo?Hbho4LBM{?XUEs)~MWMDzsd_Ps+)0 zm$z=F_RZ-tIx4Az(0(TKH&Tul@?vg`BH_PBN0ag(jz+7MvRw$+cbVT)b^MMkF%N^0 zn5`K-mQgtGmis_K@Q}hlM3J)?0H`$WM2|iBb-}+ajxO6#NnsY(sC%|`T~uY-SX@1z zc^lz?q7d#;BCtY4ota^k=$|s_$$Mft&vLi!yeM$eeL2pU1KwfN)Z!}^#Df}xSuXc8 zTfCh8LxwYVEYH_>0H3#aLxqFxW-=j4V+dh50`aTcMAjU=$Hd*a56>BsM}a|~4`SbF z%r@~aN+OVEgjMv158JDSyJK%X8vjgDdw4FyHQamUm-I~e{67C1Q|Jw27am{KRPd4G z&e!7)(6SD2Mg+JhIQhBU0YDlm^*|gda~Civ83m}cf~>44^q~S2+5z&A{(l5sKF;p0 zq5pruh}}fmoq+j&9V~oY1A-BLE`Z?RUR4J?A{`Jpl*=BG%MU(F5+Sz{!V)16=3lFeiZt5(_0ZH-#a$v|q>+9uvDnZ60#RRUswf)x%7pUU@TJ6OOWbM)&`B92~4tQuME0y@Gmst2;XK94s^n zOY~5t;;Ej-k{P77BRBGneohUNW?QAO@!S)DzanT>G*-OhvoI?qWZM!lNM3EPB9`nD zyJBe`KY7x%L9a*_I#up|5gPmq^ZIK(9<@pGQpA!ffzE%TS%+q{ei-rjQdEk0!ZJry zTXd3-a8k#059OQ@U)ANA+YCd-I$O~0cy00o9b_mb!drm{T2yvd!cFcj(d*vdh^4#I zu;)d>&D~98cZtN-UmX|;Z--uuZ^PUt;@HIcP-(d@hIQ3!NfmFS-C@YgXA^P-DA@QQ z?v7PKM|sozZ~EcQ59IXp1f>Q9RmMcX5tBbMHy4wdO8x1}mx7|AuDW>38n(|`qaz~< zO+D6CtQ64-$%TX0aIG>dYrM;sjFr0S}w*ye$Eo(&BRtdf#+HTHu_Q0Vh8B{sZ} z_G0bK#O!QmJ{CHOHfts^3zoV1WD|7Ss=4Kg`htw{mel`go zHR0&!DEjCq_{$d>2}Tk}M@Q(xeSpF1*NB3GPUd3aDDp6{B3(q#ODyV0($~+0zZj7+ z5F>kOvd(cyoPV^n$y?jlAWtNce>b?TlhM#bw)kJH)eOrCH$EH>JhR#8P2!wxZfU8q z=^+7w!D(+@ot?v9aAGNvrHMJyQ}dgIp-_;Tvxu-T+CYnX7bX%Wsj#>>B1o-Cje*0% z{asXieE0eO{Oamz2ZYww*LS8ZP~K(nXBYT&4fFEK29e#22ws_1`vK&jvD;J8VdZ2b zR%XQzHbRS`7r~{pE{JJz0Ya%Pv5GE29g&zNMHC@1?9jR0ceOi>0;qEatn6Nzd|vhU z2}+W&h;tI$wgdWJm!hMeYK)+gx|=ED6x-nEs9d?8L!xwWOlPeOy9$Tog2_$jWGcD! zY_*++R`mk4xVZS1kMfy?DTeUb)*P+N$~Hpvh+y`$kHNogH($By;T7&W@vsii%}- z939qJEp$~0hVH-(eIk?~Ti4E{eX+XRWRpF^Z;t;Kxv^o`U@l8iT|-P2 z8CmyR6vZqSp*RUzonU|mh|qC?Bx2 z2s%1Ce1R{7wIJd@4x6hzYq|SXS8inM;CxN(RZfBAH>*hPEYQq*gy-di5tyH$ppfz%#vb zjQ0L^OTOUycM_aX3^q14^uA%zw-l0$arB}D zMiu6wyGI|Dp~_fw&hruaB|3D9KLc*OYKFbMy&nq;Q#uUir~*V0p(ifXdrCn;k(io_ zBQGyc_2|)RLf~z|tfYp;Y>U5m>*X3DM1($N*Z<-`RZR^iObO9<*50lVArg7NJYX5Y za|(NgHT=l&5bD!=Posl;9TzaPW7vLoyF79kJ2;{lp}f0;9iKlsv7YjYser^vh>H!R zL2!{LxETOx?i`Ujc->%jT{a90PpIn~0EL7N4C=3(iMDeQ!(KyWJPtbZ+*u;jhRGW4 z(fkWBLPzLEgCN`VZBY`beDkoIE}}?KTT2U*Yx5D#-#_W0D=U^Y!yB8Mh}PB|wb*O= z5J_M33Wnh~V=Ia&4^L0kw{IEuXMxy$d)r9y@#9C%X;TvuD3AiZwnBVws=)wvL(N@% zDZGHp-~mt)VsP>71D4&!3yE3-1>HB8ARmcU`8@BW5dMmA)EJ_KW)h(x zMkRQ{V#@k5Dw2{q*q}DlG?3CU7cZ#Ly=7`*KX>!;|# zZTmU#_kN3;9)Y7kR_JTzH7Jb2S)b6uum3Nwr*n%&QO*rt;#dl@2+Jdg$MrDLIQ9nU zx7R%y#8uyqrUAZsL^%_ae4xOH9!uOpYe1&g;J`qv z*M9$J-pivN7A8YiBcm^mUXX&`hIy8a=XMhrzu2+FsnkY0aR1eV)8EEe>oN+Mm>U!O z-mc}K<~J)ke2vAw-ZPV-i5Z!JO0R7adOdP7^7CrW48Ga_nN|)aVUyKVT-=RQqA_a+ zz1#ddm`Nfk;j}MA`#xPkXq%N!?!Vb-OWYSO$mu=!>VK6iQ_6=s#4)=>=7XXQpK_T! z0!gEGJmPYQv0g#~ox%93e_$Fz@9WEbK70_`H}UnAtgfzx9xOHG%P zvbw$fuIt_GU&Qq|BS^Q@z$e2xw&VQx{GNGYZ)cbFB>bzKK_p6)@0a<4Fb#*NW5dFq z&dy75zkIoAcgC??F@Kos@FZJP`A zvfL;WJ64u?`7L}1HCm9%9ffNYA;Qp6WhoR|Aa%EPgr(LRZtd5K7lX2(eSlvs?B;2l zk7Kp6^K|X-AdyVPQ31Z~U4J$=vg8AqwTi~}PCkGB+~9xlU0K0Fz>S}Wr{6IQRjQw@ z4&q80SOX#={gGsbJbG-llHppUYEqJK1 zl*GSp5pDw@j7~TG`r41aH~kX+?4w}M;66gCtE=mWim;K9(Km~>CC6Eym6$Z&_a#t- z5&y7HH0b#TpXb*A!XR_J@S;MIE+_23sz4E90DX4LI2N+VI+2ZvOVhECJ$Nj`OzPs| z(oGuKDL6aMM@8U_1^3^B**s=?5|b9}e|H}%y55t}1GL*gxhAiDlZK`y=|UtE6B9QN zkC61%FL#7MD_{%%i+AzF(7DMV&{3DwfU@SCFRlWR#RPYgO`> zD_DHSl^{t;$**N)N-&sA@r(2jfS>d};YtYnAe`!9&w*6_sF;|1WKMB$vGwsrpM={Q z*k!qu__J1cW#w}YPELBt+2rKpcKezt_?=%jm7Th7wt2te*6AnDIiVGdykW=kgHbKv zSmIiByoNz<{`wSKMM=kqt1C>ushBy=3eXHA-ho+I-f`9Vs*y#+iXn!9mHtR-CDE9^ z={x)KNym>YpS7@oAbvIA!2@GTNJ}dtbd7O=2w;|{{wcr0NEV*|NJ#K&R}zw$hBMjw zG_?i32SeXmNuv`OB|~T|U`bw7>IMc>`T6;@1KHBvBtWq0GuJQlkQd8=ET;7?MRakw zxw(_CC*O@1uZp)_5zF18m78|z`Lp_dTQadsZZ`#WB~71)iQrR|C2AT5S48<2^$70N z%HmR;h8p=V@Q!HRcI-eROh@5@bTC_&0v*0dFuRVumvKYm2vonSz9J#bd$!;-NDGv(0aUB8_Q2oJ%KsgfT!q* z@9bl2Z0z3N%4hreWn~2Z=X+I|J2MjGmT4yx_Cfy<#~)k>!o7^7FB9|#q&kc9ZGQM( zmp;16d-9G+iXE^tvS6i(BAX%cYA!#>bA`8+427vVL24)wG8z~Ts#fuwqDv1&Vk$}(M_$J2ZEpkH~B=sEdsrq+A67-u)z<0VG`JxxCr=u?Gb;gaMbk;H%m zw#Tp^PGZi5Uq2liA9uU^xi391lp`CvIq;>Rw6xFV#r)b4KIgii6U4HRzOKqoKRcgy zIjCq)vmC-wPj*HqueLkk@uTM!6RDqya9i?+z_v;MX<&ei63bHm6=>spC!C{L!o&I+CglBl+hVD*AY+J32r|rH z1lsB!pN=_cj#cUysmN*#Q{yF3n2tIkGcc=e9B;ec;RX6@^VQ$XG)ebj!N{C^?)B8R zZ{PR~jEvSNO7$U*j!J+K;o;?dWoD+$fa$vU)9$#6^UE^}H4_v1Vx8<5-;=*=tgIk7 z9Nzt^$opm$NU_B9^uDk9CF2MnT;7LEU4TDw#%VP{s@d5+Sxh{2gl6z|?8}eo`BiRu zvWl83$1Fzgc=#OBg(0H6y?8V=@7*F_Yr>D`?%<#Y z4(|RUro3hE8`7UT{p;rPkWX$rwUP?R=+v_x)1A9BYW4|ss+TZ1SU|ul3kz2vibM|l zV1;(tU7FY~h?A4M#zrJ7o{z7uV#Rbpnkw2~h<4i=jt zGcqzT+~5S@eXwYZ<(Vy*6uO&}!v!vVC74BYlZ4O{|%qlSPK(q;obF zh^-4<9O#CpW$DI0v>Cr|$ekzLYT6RyP(Bt3>ppr)J^zB{Y;2tQ=~+(x_T9U8+1SVp@nyl70Pk=u;Pzy zV|_Y<4c{j<sl&c$9-HNKd(mT zg$D8m!2IyJ`1+KFkKWuv3=zZSy@tr zGW6qmW-%&mclblGnsLQf41bV1Ry+s#wt_=1N6?v%Wbhh#lw5Qe(UWvru5lPLAbr>z zT3s!)v9+a)G-Md(hrwXzDO1y0^SnbVJL0k{<4H#v7$N*Q>J2ce2Gg(efN7pxV-kO}7OPc2p82W-!_+6}IHYu}A zUTrojo`|TZ)%o6RgZoyzoAi!CI5Efs!6l{4l^Mox^{9)?OqRq;J*Cs)x#Fu8(NCe% zx+JT$l)5Tyc(2hM>u@tWWA?Z3KUk)-w||10yf+)XjlO7{`nXl(<$>Dn&ng7pjdA?$ z>av>p_67(_w-6GE1gI?%@~x?fVQ6Rw^yiQc#T#%qpYNJiKKH`;ut{@i=||kXvpk?Z z#>U3bJLMHh<&$ft+rh4?9Kkt-Y>9k9`Rx?~;nPRUih2z<(Lp)(zS&5!+y_ka9x)7q`OqmVRAgktG+dcw001Puhs9E#IcjKHk zJDs&o)4ImSQA0yIw`UcBUB{aPA8+CZ`uq1>Ist9t`q~0;4pS8tq`*c+O})0!m-3vJ zXr|GfD^MaV5?a!5!PV zm#>VBA^|H81dUBxd|GabJmZ|X%eg^y`0z4Xb)Otu>n-tipw*vKwoQdSSPAj`Z^|jD zVA-Uj$vuf?G8&BzQbJVvF-9Y;ChxBSsfs?Ewpecu#`=d)6?XFd7cA7V*wHFV1COdF zyqta&rf7=2p9z|+aj*eM>ve{Gvqiw?;@%~E_wXT|Bx-E;ERL$%^7)Lm>)vR|`1YSV z{fg;K?5{<2^Awxa*r#AcR&j9}>nd6c_*Ky&Nf2~H-iJ9kFktn_2bkQc1q8_Qr<`rE zpZpzE1}siYZ0tU1B@srT#ywLw<4y?MmG`FyT-<}Z*hdaU3&3C;q^YW-@?FU? a.headerlink { - visibility: visible; -} - -/* headings */ -h2 { - font-size: 1.5em; - margin-bottom: 0.5em; - border-bottom: 1px solid #888; -} - -h3 { - font-size: 1.25em; - margin-top: .5em; -} - -h4 { - font-size: 1.15em; - margin-top: 1em; -} - -h5 { - font-size: 1em; - margin-top: 1em; -} - -/* general layout */ -[dir="rtl"] #content { - text-align: right; -} - -#content { - width: 95%; - margin: 0 auto; - text-align: left; -} - -[dir="rtl"] #content-left-wrapper { - float: right; -} - -#content-left-wrapper { - float: left; - width: 100%; /* req to keep content above sidebar in source code */ -} - -[dir="rtl"] #content-left { - margin: 0 0 0 340px; -} - -#content-left { - margin: 0 340px 0 0; -} - -[dir="rtl"] #content-right { - float: right; - margin-right: -300px; -} - -#content-right { - float: left; - width: 300px; - margin-left: -300px; -} - -div.box { - margin-bottom: 1.5em; - padding: 0.65em; - background: #ecf2f5; - border: 1px solid #bcd; -} - -#footer { - clear: both; - margin: 2em 0 1em; -} - - #footer p { - margin: 0; - text-align: center; - font-size: 0.85em; - } - -/* alignment */ -div.center, -table.center, -img.center { - width: auto; - margin-left: auto; - margin-right: auto; -} - -p.center, -td.center, -th.center { - text-align: center; -} - -/* table generics */ -table { - width: 100%; - border-collapse: collapse; -} - - table .wrap { - white-space: normal; - } - -[dir="rtl"] th, -[dir="rtl"] td { - text-align: right; -} - -th, -td { - white-space: nowrap; - text-align: left; -} - - th { - vertical-align: middle; - font-weight: bold; - } - - td { - vertical-align: top; - } - -/* table pretty styles */ -table.pretty2 { - width: auto; - margin-top: 0.25em; - margin-bottom: 0.5em; - border-collapse: collapse; - border: 1px solid #bbb; -} - - .pretty2 th { - padding: 0.35em; - background: #eee; - border: 1px solid #bbb; - } - - .pretty2 td { - padding: 0.35em; - border: 1px dotted #bbb; - } - -table.compact { - width: auto; -} - - .compact td { - padding: 0.25em 0 0.25em 1.5em; - } - - -/* definition lists */ -dl { - clear: both; -} - - dl dt, - dl dd { - margin-bottom: 4px; - padding: 8px 0 4px; - font-weight: bold; - border-top: 1px dotted #bbb; - } - - [dir="rtl"] dl dt { - float: right; - padding-left: 15px; - } - dl dt { - color: #333; - float: left; - padding-right: 15px; - } - -/* forms and input styling */ -form p { - margin: 0.5em 0; -} - -fieldset { - border: 0; -} - -label { - width: 12em; - vertical-align: top; - display: inline-block; - font-weight: bold; -} - -input[type=text], -input[type=password], -input[type=email], -textarea { - padding: 0.10em; -} - -form.general-form label, -form.general-form .form-help { - width: 10em; - vertical-align: top; - display: inline-block; -} - -form.general-form input[type=text], -form.general-form textarea { - width: 45%; -} - -/* archdev navbar */ -#archdev-navbar { - margin: 1.5em 0; -} - - #archdev-navbar ul { - list-style: none; - margin: -0.5em 0; - padding: 0; - } - - #archdev-navbar li { - display: inline; - margin: 0; - padding: 0; - font-size: 0.9em; - } - - #archdev-navbar li a { - padding: 0 0.5em; - color: #07b; - } - -/* error/info messages (x pkg is already flagged out-of-date, etc) */ -#sys-message { - width: 35em; - text-align: center; - margin: 1em auto; - padding: 0.5em; - background: #fff; - border: 1px solid #f00; -} - - #sys-message p { - margin: 0; - } - -ul.errorlist { - color: red; -} - -form ul.errorlist { - margin: 0.5em 0; -} - -/* JS sorting via tablesorter */ -[dir="rtl"] table th.tablesorter-header { - padding-left: 20px; - background-position: center left ; -} -table th.tablesorter-header { - padding-right: 20px; - background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); - background-repeat: no-repeat; - background-position: center right; - cursor: pointer; -} - -table thead th.tablesorter-headerAsc { - background-color: #e4eeff; - background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); -} - -table thead th.tablesorter-headerDesc { - background-color: #e4eeff; - background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); -} - -table thead th.sorter-false { - background-image: none; - cursor: default; -} - -.tablesorter-header:focus { - outline: none; -} - -/** - * PAGE SPECIFIC STYLES - */ - -/* home: introduction */ -[dir="rtl"] #intro p.readmore { - text-align: left; -} -#intro p.readmore { - margin: -0.5em 0 0 0; - font-size: .9em; - text-align: right; -} - -/* home: news */ -#news { - margin-top: 1.5em; -} - - [dir="rtl"] #news h3 { - float: right; - } - #news h3 { - float: left; - padding-bottom: .5em - } - - #news div { - margin-bottom: 1em; - } - - #news div p { - margin-bottom: 0.5em; - } - - #news .more { - font-weight: normal; - } - [dir="rtl"] #news .rss-icon { - float: left; - } - #news .rss-icon { - float: right; - margin-top: 1em; - } - - #news h4 { - clear: both; - font-size: 1em; - margin-top: 1.5em; - border-bottom: 1px dotted #bbb; - } - [dir="rtl"] #news .timestamp { - float: left; - margin: -1.8em 0 0 0.5em; - } - #news .timestamp { - float: right; - font-size: 0.85em; - margin: -1.8em 0.5em 0 0; - } - -/* home: arrowed headings */ -#news h3 a { - display: block; - background: #1794D1; - font-size: 15px; - padding: 2px 10px; - color: white; -} - - #news a:active { - color: white; - } - -h3 span.arrow { - display: block; - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: 6px solid #1794D1; - margin: 0 auto; - font-size: 0; - line-height: 0px; -} - -/* home: pkgsearch box */ -#pkgsearch { - padding: 1em 0.75em; - background: #3ad; - color: #fff; - border: 1px solid #08b; -} - - #pkgsearch label { - width: auto; - padding: 0.1em 0; - } - - [dir="rtl"] #pkgsearch input { - float: left; - } - #pkgsearch input { - width: 10em; - float: right; - font-size: 1em; - color: #000; - background: #fff; - border: 1px solid #09c; - } - - [dir="rtl"] .pkgsearch-typeahead { - right: 0; - float: right; - text-align: right; - } - - .pkgsearch-typeahead { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - padding: 0.15em 0.1em; - margin: 0; - min-width: 10em; - font-size: 1em; - text-align: left; - list-style: none; - background-color: #f6f9fc; - border: 1px solid #09c; - } - - .pkgsearch-typeahead li a { - color: #000; - } - - .pkgsearch-typeahead li:hover a, - .pkgsearch-typeahead li.active a { - color: #07b; - } - -/* home: recent pkg updates */ -#pkg-updates h3 { - margin: 0 0 0.3em; -} - - #pkg-updates .more { - font-weight: normal; - } - [dir="rtl"] #pkg-updates .rss-icon { - float: left; - } - #pkg-updates .rss-icon { - float: right; - margin: -2em 0 0 0; - } - - [dir="rtl"] #pkg-updates .rss-icon.latest { - margin-left: 1em; - } - #pkg-updates .rss-icon.latest { - margin-right: 1em; - } - - #pkg-updates table { - margin: 0; - direction: ltr; - } - - #pkg-updates td.pkg-name { - white-space: normal; - text-align: left; - } - - [dir="rtl"] #pkg-updates td.pkg-arch { - text-align: left; - } - #pkg-updates td.pkg-arch { - text-align: right; - } - - #pkg-updates span.testing { - font-style: italic; - } - - #pkg-updates span.staging { - font-style: italic; - color: #ff8040; - } - -/* home: sidebar navigation */ -[dir="rtl"] #nav-sidebar ul { - margin: 0.5em 1em 0.5em 0; -} - -#nav-sidebar ul { - list-style: none; - margin: 0.5em 0 0.5em 1em; - padding: 0; -} - -/* home: sponsor banners */ -#arch-sponsors img { - padding: 0.3em 0; -} - -/* home: sidebar components (navlist, sponsors, pkgsearch, etc) */ -div.widget { - margin-bottom: 1.5em; -} - -/* feeds page */ -[dir="rtl"] #rss-feeds .rss { - padding-left: 20px; - background: url(rss.png) top left no-repeat; -} - -#rss-feeds .rss { - padding-right: 20px; - background: url(rss.png) top right no-repeat; -} - -/* artwork: logo images */ -#artwork img.inverted { - background: #333; - padding: 0; -} - -#artwork div.imagelist img { - display: inline; - margin: 0.75em; -} - -/* news: article list */ -[dir="rtl"] .news-nav { - float: left; -} -.news-nav { - float: right; - margin-top: -2.2em; -} - - .news-nav .prev, - .news-nav .next { - margin: 0 1em; - } - -/* news: article pages */ -div.news-article .article-info { - margin: 0; - color: #999; -} - -/* news: add/edit article */ -#newsform { - width: 60em; -} - - #newsform input[type=text], - #newsform textarea { - width: 75%; - } - -#news-preview { - display: none; -} - -/* todolists: list */ -[dir="rtl"] .todolist-nav { - float: left; -} -.todolist-nav { - float: right; - margin-top: -2.2em; -} - - .todolist-nav .prev, - .todolist-nav .next { - margin: 0 1em; - } - -/* donate: donor list */ -#donor-list ul { - width: 100%; -} - /* max 4 columns, but possibly fewer if screen size doesn't allow for more */ - [dir="rtl"] #donor-list li { - float: right; - } - #donor-list li { - float: left; - width: 25%; - min-width: 20em; - } - -/* download page */ -#arch-downloads h3 { - border-bottom: 1px dotted #bbb; -} - -/* pkglists/devlists */ -table.results { - font-size: 0.846em; - border-top: 1px dotted #999; - border-bottom: 1px dotted #999; - direction: ltr; -} - - [dir="rtl"] .results th {text-align: center; direction:rtl;} - .results th { - padding: 0.5em 1em 0.25em 0.25em; - border-bottom: 1px solid #999; - white-space: nowrap; - background-color:#fff; - } - - .results td { - padding: .3em 1em .3em 3px; - text-align: left; - } - - .results .flagged { - color: red; - } - - .results tr.empty td { - text-align: center; - } - -/* pkglist: layout */ -#pkglist-about { - margin-top: 1.5em; -} - -/* pkglist: results navigation */ -.pkglist-stats { - font-size: 0.85em; -} - -[dir="rtl"] #pkglist-results .pkglist-nav { - float: left; -} -#pkglist-results .pkglist-nav { - float: right; - margin-top: -2.2em; -} - -[dir="rtl"] .pkglist-nav .prev { - margin-left: 1em; -} - -.pkglist-nav .prev { - margin-right: 1em; -} - -[dir="rtl"] .pkglist-nav .next { - margin-left: 1em; -} -.pkglist-nav .next { - margin-right: 1em; -} - -/* search fields and other filter selections */ -.filter-criteria { - margin-bottom: 1em; -} - -.filter-criteria h3 { - font-size: 1em; - margin-top: 0; -} -[dir="rtl"] .filter-criteria div { - float: right; - margin-left: 1.65em; -} -.filter-criteria div { - float: left; - margin-right: 1.65em; - font-size: 0.85em; -} - -.filter-criteria legend { - display: none; -} - -.filter-criteria label { - width: auto; - display: block; - font-weight: normal; -} - -/* pkgdetails: details links that float on the right */ -[dir="rtl"] #pkgdetails #detailslinks { - float: left; -} -#pkgdetails #detailslinks { - float: right; -} - - #pkgdetails #detailslinks h4 { - margin-top: 0; - margin-bottom: 0.25em; - } - - #pkgdetails #detailslinks ul { - list-style: none; - padding: 0; - margin-bottom: 0; - font-size: 0.846em; - } - - #pkgdetails #detailslinks > div { - padding: 0.5em; - margin-bottom: 1em; - background: #eee; - border: 1px solid #bbb; - } - -#pkgdetails #actionlist .flagged { - color: red; - font-size: 0.9em; - font-style: italic; -} - -/* pkgdetails: pkg info */ -#pkgdetails #pkginfo { - width: auto; -} - -[dir="rtl"] #pkgdetails td { - padding: 0.25em 1.5em 0.25em 0; - } - - #pkgdetails #pkginfo td { - padding: 0.25em 0 0.25em 1.5em; - } - - #pkgdetails #pkginfo .userdata { - font-size: 0.85em; - padding: 0.5em; - } - -/* pkgdetails: flag package */ -#flag-pkg-form label { - width: 10em; -} - -#flag-pkg-form textarea, -#flag-pkg-form input[type=text] { - width: 45%; -} - -#flag-pkg-form #id_website { - display: none; -} - -/* pkgdetails: deps, required by and file lists */ -#pkgdetails #metadata { - clear: both; -} - -#pkgdetails #metadata h3 { - background: #555; - color: #fff; - font-size: 1em; - margin-bottom: 0.5em; - padding: 0.2em 0.35em; -} - -#pkgdetails #metadata ul { - list-style: none; - margin: 0; - padding: 0; -} - -[dir="rtl"] #pkgdetails #metadata li { - padding-right: 0.5em; -} - -#pkgdetails #metadata li { - padding-left: 0.5em; -} - -[dir="rtl"] #pkgdetails #metadata p { - padding-right: 0.5em; -} -#pkgdetails #metadata p { - padding-left: 0.5em; -} - -#pkgdetails #metadata .message { - font-style: italic; -} - -#pkgdetails #metadata br { - clear: both; -} - -[dir="rtl"] #pkgdetails #pkgdeps { - float: right; - width: 48%; - margin-left: 2%; - -} -#pkgdetails #pkgdeps { - float: left; - width: 48%; - margin-right: 2%; -} - -#pkgdetails #metadata .virtual-dep, -#pkgdetails #metadata .testing-dep, -#pkgdetails #metadata .staging-dep, -#pkgdetails #metadata .opt-dep, -#pkgdetails #metadata .make-dep, -#pkgdetails #metadata .check-dep, -#pkgdetails #metadata .dep-desc { - font-style: italic; -} - -[dir="rtl"] #pkgdetails #pkgreqs { - float: right; - width: 48%; -} - -#pkgdetails #pkgreqs { - float: left; - width: 50%; -} - -#pkgdetails #pkgfiles { - clear: both; - padding-top: 1em; -} - -#pkgfilelist li.d { - color: #666; -} - -#pkgfilelist li.f { -} - -/* mirror stuff */ -table td.country { - white-space: normal; -} - -#list-generator div ul { - list-style: none; - display: inline; - padding-left: 0; -} - - #list-generator div ul li { - display: inline; - } - -.visualize-mirror .axis path, -.visualize-mirror .axis line { - fill: none; - stroke: #000; - stroke-width: 3px; - shape-rendering: crispEdges; -} - -.visualize-mirror .url-dot { - stroke: #000; -} - -.visualize-mirror .url-line { - fill: none; - stroke-width: 1.5px; -} - -/* dev/TU biographies */ -#arch-bio-toc { - width: 75%; - margin: 0 auto; - text-align: center; -} - - #arch-bio-toc a { - white-space: nowrap; - } - -.arch-bio-entry { - width: 75%; - min-width: 640px; - margin: 0 auto; -} - .arch-bio-entry td.pic { - padding-left: 15px; - } - .arch-bio-entry td.pic { - vertical-align: top; - padding-right: 15px; - padding-top: 2.25em; - } - - .arch-bio-entry td.pic img { - padding: 4px; - border: 1px solid #ccc; - } - - .arch-bio-entry td h3 { - border-bottom: 1px dotted #ccc; - margin-bottom: 0.5em; - } - - .arch-bio-entry table.bio { - margin-bottom: 2em; - } - [dir="rtl"] .arch-bio-entry table.bio th { - text-align: left; - padding-left: 0.5em; - } - - .arch-bio-entry table.bio th { - color: #666; - font-weight: normal; - text-align: right; - padding-right: 0.5em; - vertical-align: top; - white-space: nowrap; - } - - .arch-bio-entry table.bio td { - width: 100%; - padding-bottom: 0.25em; - white-space: normal; - } - -/* dev: login/out */ -#dev-login { - width: auto; -} - -/* tables rows: highlight on mouse-vover */ -#article-list tr:hover, -#clocks-table tr:hover, -#dev-dashboard tr:hover, -#dev-todo-lists tr:hover, -#dev-todo-pkglist tr:hover, -#pkglist-results tr:hover, -#stats-area tr:hover { - background: #ffd; -} - -.results tr:nth-child(even), -#article-list tr:nth-child(even) { - background: #e4eeff; -} - -.results tr:nth-child(odd), -#article-list tr:nth-child(odd) { - background: #fff; -} - -/* dev dashboard: */ -table.dash-stats .key { - width: 50%; -} - -/* dev dashboard: admin actions (add news items, todo list, etc) */ -[dir="rtl"] ul.admin-actions { - float: left; -} -ul.admin-actions { - float: right; - list-style: none; - margin-top: -2.5em; -} - - ul.admin-actions li { - display: inline; - padding-left: 1.5em; - } - -/* colored yes/no type values */ -.todo-table .complete, -.signoff-yes, -#key-status .signed-yes, -#release-list .available-yes { - color: green; -} - -.todo-table .incomplete, -.signoff-no, -#key-status .signed-no, -#release-list .available-no { - color: red; -} - -.todo-table .inprogress, -.signoff-bad { - color: darkorange; -} - - -/* todo lists (public and private) */ -.todo-info { - color: #999; - border-bottom: 1px dotted #bbb; -} - -.todo-description { - margin-top: 1em; - padding-left: 2em; - max-width: 900px; -} - -.todo-pkgbases { - border-top: 1px dotted #bbb; -} - -.todo-list h4 { - margin-top: 0; - margin-bottom: 0.4em; -} - -/* dev: signoff page */ -#dev-signoffs tr:hover { - background: #ffd; -} - -ul.signoff-list { - list-style: none; - margin: 0; - padding: 0; -} - -.signoff-yes { - font-weight: bold; -} - -.signoff-disabled { - color: gray; -} - -/* highlight current website in the navbar */ -#archnavbar.anb-home ul li#anb-home a, -#archnavbar.anb-packages ul li#anb-packages a, -#archnavbar.anb-download ul li#anb-download a { - color: white !important; -} - -/* visualizations page */ -.visualize-buttons { - margin: 0.5em 0.33em; -} - -.visualize-chart { - position: relative; - height: 500px; - margin: 0.33em; -} - -#visualize-archrepo .treemap-cell { - border: solid 1px white; - overflow: hidden; - position: absolute; -} - - #visualize-archrepo .treemap-cell span { - padding: 3px; - font-size: 0.85em; - line-height: 1em; - } - -#visualize-keys svg { - width: 100%; - height: 100%; -} - -/* releases */ -#release-table th:first-of-type { - width: 30px; -} - -/* itemprops */ -.itemprop { - display: none; -} diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css deleted file mode 100644 index 64a65742..00000000 --- a/web/html/css/aurweb.css +++ /dev/null @@ -1,292 +0,0 @@ -/* aurweb-specific customizations to archweb.css */ - -#archnavbar.anb-aur ul li#anb-aur a { - color: white !important; -} - -#archnavbarlogo { - background: url('archnavbar/aurlogo.png') !important; -} - -[dir="rtl"] #lang_sub { - float: left; - } -#lang_sub { - float: right; -} - -.pkglist-nav .page { - margin: 0 .25em; -} - -#pkg-stats td.stat-desc { - white-space: normal; -} - -#actionlist form { - margin: 0; - padding: 0; -} - -.arch-bio-entry ul { - list-style: none; - padding: 0; -} - -#pkg-updates table { - table-layout: fixed; - width:100%; -} - -#pkg-updates td.pkg-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -[dir="rtl"] #pkg-updates td.pkg-date { - text-align:left; -} -#pkg-updates td.pkg-date { - text-align:right; -} - -[dir="rtl"] .keyword:link, .keyword:visited { - float: right; -} - -.keyword:link, .keyword:visited { - float: left; - margin: 1px .5ex 1px 0; - padding: 0 1em; - color: white; - background-color: #36a; - border: 1px solid transparent; - border-radius: 2px; -} - -.keyword:hover { - cursor: pointer; -} - -.keyword:focus { - border: 1px dotted #000; -} - -.text-button { - background: transparent; - border: none !important; - margin: 0 !important; - padding: 0 !important; - font: normal 100% sans-serif; - text-decoration: none; - color: #07b; - cursor: pointer; -} - -.text-button:hover { - text-decoration: underline; - color: #666; -} - -.text-button::-moz-focus-inner { - padding: 0; - border: none; -} - -.comment-deleted { - color: #999; -} - -.edited { - font-size: 0.9em; - color: #999; -} - -[dir="rtl"] .delete-comment-form, .undelete-comment-form, .pin-comment-form, .edit-comment { - float: left; - margin-right: 8px; -} - -.delete-comment-form, .undelete-comment-form, .pin-comment-form, .edit-comment { - float: right; - margin-left: 8px; -} - -.edit-comment { - height: 11px; - position: relative; - top: 1px; -} - -.comment-enable-notifications { - display: inline-block; - margin-left: 1em; -} - -.rss-icon, .delete-comment, .undelete-comment, .edit-comment, .pin-comment { - filter: grayscale(100%); - opacity: 0.6; -} - -.rss-icon:hover, .delete-comment:hover, .undelete-comment:hover, .edit-comment:hover, .pin-comment:hover { - filter: none; - opacity: 1; -} - -[dir="rtl"] .ajax-loader { - float: left; -} - -.ajax-loader { - float: right; - position: relative; - top: 4px; -} - -.flagged a { - color: inherit; -} - -legend { - padding: 1em 0; -} - -p.important { - font-weight: bold; -} - -span.hover-help { - border-bottom: 1px dotted black; - cursor:help; -} - -label.confirmation { - width: auto; -} - -#pkgdepslist .broken { - color: red; - font-weight: bold; -} - -.package-comments { - margin-top: 1.5em; -} - -.comments-header { - display: flex; - justify-content: space-between; - align-items: flex-start; -} - -/* arrowed headings */ -.comments-header h3 span.text { - display: block; - background: #1794D1; - font-size: 15px; - padding: 2px 10px; - color: white; -} - -.comments-header .comments-header-nav { - align-self: flex-end; -} - -.comments-footer { - display: flex; - justify-content: flex-end; -} - -.comment-header { - clear: both; - font-size: 1em; - margin-top: 1.5em; - border-bottom: 1px dotted #bbb; -} - -.comments div { - margin-bottom: 1em; -} - -.comments div p { - margin-bottom: 0.5em; -} - -.comments .more { - font-weight: normal; -} - -.error { - color: red; -} - -.article-content > div { - overflow: hidden; - transition: height 1s; -} - -.proposal.details { - margin: .33em 0 1em; -} - -button[type="submit"], -button[type="reset"] { - padding: 0 0.6em; -} - -.results tr td[align="left"] fieldset { - text-align: left; -} - -.results tr td[align="right"] fieldset { - text-align: right; -} - -input#search-action-submit { - width: 80px; -} - -.success { - color: green; -} - -/* Styling used to clone styles for a form.link button. */ -form.link, form.link button { - display: inline; - font-family: sans-serif; -} -form.link button { - padding: 0 0.5em; - color: #07b; - background: none; - border: none; - font-family: inherit; - font-size: inherit; -} -form.link button:hover { - cursor: pointer; - text-decoration: underline; -} - -/* Customize form.link when used inside of a page. */ -div.box form.link p { - margin: .33em 0 1em; -} -div.box form.link button { - padding: 0; -} - -pre.traceback { - /* https://css-tricks.com/snippets/css/make-pre-text-wrap/ */ - white-space: pre-wrap; - word-wrap: break-all; -} - -/* By default, tables use 100% width, which we do not always want. */ -table.no-width { - width: auto; -} -table.no-width > tbody > tr > td { - padding-right: 2px; -} diff --git a/web/html/css/cgit.css b/web/html/css/cgit.css deleted file mode 100644 index 429b5f54..00000000 --- a/web/html/css/cgit.css +++ /dev/null @@ -1,866 +0,0 @@ -/* - * ARCH GLOBAL NAVBAR - * We're forcing all generic selectors with !important - * to help prevent other stylesheets from interfering. - */ - -/* container for the entire bar */ -#archnavbar { height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } -#archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; background: url('archnavbar/archlogo.png') no-repeat !important; } - -/* move the heading text offscreen */ -#archnavbarlogo h1 { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } - -/* make the link the same size as the logo */ -#archnavbarlogo a { display: block !important; height: 40px !important; width: 190px !important; } - -/* display the list inline, float it to the right and style it */ -#archnavbarlist { display: inline !important; float: right !important; list-style: none !important; margin: 0 !important; padding: 0 !important; } -#archnavbarlist li { float: left !important; font-size: 14px !important; font-family: sans-serif !important; line-height: 45px !important; padding-right: 15px !important; padding-left: 15px !important; } - -/* style the links */ -#archnavbarlist li a { color: #999; font-weight: bold !important; text-decoration: none !important; } -#archnavbarlist li a:hover { color: white !important; text-decoration: underline !important; } - -/* END ARCH GLOBAL NAVBAR */ - -#footer { - clear: both; - margin: 0; -} - -#footer p { - margin: 1em; -} - -#archnavbar.anb-aur ul li#anb-aur a { - color: white !important; -} - -#archnavbarlogo { - background: url('archnavbar/aurlogo.png') !important; -} - -body { - padding: 0; - margin: 0; - font-family: sans-serif; - font-size: 10pt; - color: #333; - background: white; -} - -div#cgit a { - color: blue; - text-decoration: none; -} - -div#cgit a:hover { - text-decoration: underline; -} - -div#cgit table { - border-collapse: collapse; -} - -div#cgit table#header { - width: 100%; - margin-bottom: 1em; -} - -div#cgit table#header td.logo { - width: 96px; - vertical-align: top; -} - -div#cgit table#header td.main { - font-size: 250%; - padding-left: 10px; - white-space: nowrap; -} - -div#cgit table#header td.main a { - color: #000; -} - -div#cgit table#header td.form { - text-align: right; - vertical-align: bottom; - padding-right: 1em; - padding-bottom: 2px; - white-space: nowrap; -} - -div#cgit table#header td.form form, -div#cgit table#header td.form input, -div#cgit table#header td.form select { - font-size: 90%; -} - -div#cgit table#header td.sub { - color: #777; - border-top: solid 1px #ccc; - padding-left: 10px; -} - -div#cgit table.tabs { - border-bottom: solid 3px #ccc; - border-collapse: collapse; - margin-top: 2em; - margin-bottom: 0px; - width: 100%; -} - -div#cgit table.tabs td { - padding: 0px 1em; - vertical-align: bottom; -} - -div#cgit table.tabs td a { - padding: 2px 0.75em; - color: #777; - font-size: 110%; -} - -div#cgit table.tabs td a.active { - color: #000; - background-color: #ccc; -} - -div#cgit table.tabs td.form { - text-align: right; -} - -div#cgit table.tabs td.form form { - padding-bottom: 2px; - font-size: 90%; - white-space: nowrap; -} - -div#cgit table.tabs td.form input, -div#cgit table.tabs td.form select { - font-size: 90%; -} - -div#cgit div.path { - margin: 0px; - padding: 5px 2em 2px 2em; - color: #000; - background-color: #eee; -} - -div#cgit div.content { - margin: 0px; - padding: 2em; - border-bottom: solid 3px #ccc; -} - - -div#cgit table.list { - width: 100%; - border: none; - border-collapse: collapse; -} - -div#cgit table.list tr { - background: white; -} - -div#cgit table.list tr.logheader { - background: #eee; -} - -div#cgit table.list tr:hover { - background: #eee; -} - -div#cgit table.list tr.nohover:hover { - background: white; -} - -div#cgit table.list th { - font-weight: bold; - /* color: #888; - border-top: dashed 1px #888; - border-bottom: dashed 1px #888; - */ - padding: 0.1em 0.5em 0.05em 0.5em; - vertical-align: baseline; -} - -div#cgit table.list td { - border: none; - padding: 0.1em 0.5em 0.1em 0.5em; -} - -div#cgit table.list td.commitgraph { - font-family: monospace; - white-space: pre; -} - -div#cgit table.list td.commitgraph .column1 { - color: #a00; -} - -div#cgit table.list td.commitgraph .column2 { - color: #0a0; -} - -div#cgit table.list td.commitgraph .column3 { - color: #aa0; -} - -div#cgit table.list td.commitgraph .column4 { - color: #00a; -} - -div#cgit table.list td.commitgraph .column5 { - color: #a0a; -} - -div#cgit table.list td.commitgraph .column6 { - color: #0aa; -} - -div#cgit table.list td.logsubject { - font-family: monospace; - font-weight: bold; -} - -div#cgit table.list td.logmsg { - font-family: monospace; - white-space: pre; - padding: 0 0.5em; -} - -div#cgit table.list td a { - color: black; -} - -div#cgit table.list td a.ls-dir { - font-weight: bold; - color: #00f; -} - -div#cgit table.list td a:hover { - color: #00f; -} - -div#cgit img { - border: none; -} - -div#cgit input#switch-btn { - margin: 2px 0px 0px 0px; -} - -div#cgit td#sidebar input.txt { - width: 100%; - margin: 2px 0px 0px 0px; -} - -div#cgit table#grid { - margin: 0px; -} - -div#cgit td#content { - vertical-align: top; - padding: 1em 2em 1em 1em; - border: none; -} - -div#cgit div#summary { - vertical-align: top; - margin-bottom: 1em; -} - -div#cgit table#downloads { - float: right; - border-collapse: collapse; - border: solid 1px #777; - margin-left: 0.5em; - margin-bottom: 0.5em; -} - -div#cgit table#downloads th { - background-color: #ccc; -} - -div#cgit div#blob { - border: solid 1px black; -} - -div#cgit div.error { - color: red; - font-weight: bold; - margin: 1em 2em; -} - -div#cgit a.ls-blob, div#cgit a.ls-dir, div#cgit a.ls-mod { - font-family: monospace; -} - -div#cgit td.ls-size { - text-align: right; - font-family: monospace; - width: 10em; -} - -div#cgit td.ls-mode { - font-family: monospace; - width: 10em; -} - -div#cgit table.blob { - margin-top: 0.5em; - border-top: solid 1px black; -} - -div#cgit table.blob td.lines { - margin: 0; padding: 0 0 0 0.5em; - vertical-align: top; - color: black; -} - -div#cgit table.blob td.linenumbers { - margin: 0; padding: 0 0.5em 0 0.5em; - vertical-align: top; - text-align: right; - border-right: 1px solid gray; -} - -div#cgit table.blob pre { - padding: 0; margin: 0; -} - -div#cgit table.blob a.no, div#cgit table.ssdiff a.no { - color: gray; - text-align: right; - text-decoration: none; -} - -div#cgit table.blob a.no a:hover { - color: black; -} - -div#cgit table.bin-blob { - margin-top: 0.5em; - border: solid 1px black; -} - -div#cgit table.bin-blob th { - font-family: monospace; - white-space: pre; - border: solid 1px #777; - padding: 0.5em 1em; -} - -div#cgit table.bin-blob td { - font-family: monospace; - white-space: pre; - border-left: solid 1px #777; - padding: 0em 1em; -} - -div#cgit table.nowrap td { - white-space: nowrap; -} - -div#cgit table.commit-info { - border-collapse: collapse; - margin-top: 1.5em; -} - -div#cgit div.cgit-panel { - float: right; - margin-top: 1.5em; -} - -div#cgit div.cgit-panel table { - border-collapse: collapse; - border: solid 1px #aaa; - background-color: #eee; -} - -div#cgit div.cgit-panel th { - text-align: center; -} - -div#cgit div.cgit-panel td { - padding: 0.25em 0.5em; -} - -div#cgit div.cgit-panel td.label { - padding-right: 0.5em; -} - -div#cgit div.cgit-panel td.ctrl { - padding-left: 0.5em; -} - -div#cgit table.commit-info th { - text-align: left; - font-weight: normal; - padding: 0.1em 1em 0.1em 0.1em; - vertical-align: top; -} - -div#cgit table.commit-info td { - font-weight: normal; - padding: 0.1em 1em 0.1em 0.1em; -} - -div#cgit div.commit-subject { - font-weight: bold; - font-size: 125%; - margin: 1.5em 0em 0.5em 0em; - padding: 0em; -} - -div#cgit div.commit-msg { - white-space: pre; - font-family: monospace; -} - -div#cgit div.notes-header { - font-weight: bold; - padding-top: 1.5em; -} - -div#cgit div.notes { - white-space: pre; - font-family: monospace; - border: solid 1px #ee9; - background-color: #ffd; - padding: 0.3em 2em 0.3em 1em; - float: left; -} - -div#cgit div.notes-footer { - clear: left; -} - -div#cgit div.diffstat-header { - font-weight: bold; - padding-top: 1.5em; -} - -div#cgit table.diffstat { - border-collapse: collapse; - border: solid 1px #aaa; - background-color: #eee; -} - -div#cgit table.diffstat th { - font-weight: normal; - text-align: left; - text-decoration: underline; - padding: 0.1em 1em 0.1em 0.1em; - font-size: 100%; -} - -div#cgit table.diffstat td { - padding: 0.2em 0.2em 0.1em 0.1em; - font-size: 100%; - border: none; -} - -div#cgit table.diffstat td.mode { - white-space: nowrap; -} - -div#cgit table.diffstat td span.modechange { - padding-left: 1em; - color: red; -} - -div#cgit table.diffstat td.add a { - color: green; -} - -div#cgit table.diffstat td.del a { - color: red; -} - -div#cgit table.diffstat td.upd a { - color: blue; -} - -div#cgit table.diffstat td.graph { - width: 500px; - vertical-align: middle; -} - -div#cgit table.diffstat td.graph table { - border: none; -} - -div#cgit table.diffstat td.graph td { - padding: 0px; - border: 0px; - height: 7pt; -} - -div#cgit table.diffstat td.graph td.add { - background-color: #5c5; -} - -div#cgit table.diffstat td.graph td.rem { - background-color: #c55; -} - -div#cgit div.diffstat-summary { - color: #888; - padding-top: 0.5em; -} - -div#cgit table.diff { - width: 100%; -} - -div#cgit table.diff td { - font-family: monospace; - white-space: pre; -} - -div#cgit table.diff td div.head { - font-weight: bold; - margin-top: 1em; - color: black; -} - -div#cgit table.diff td div.hunk { - color: #009; -} - -div#cgit table.diff td div.add { - color: green; -} - -div#cgit table.diff td div.del { - color: red; -} - -div#cgit .sha1 { - font-family: monospace; - font-size: 90%; -} - -div#cgit .left { - text-align: left; -} - -div#cgit .right { - text-align: right; - float: none !important; - width: auto !important; - padding: 0 !important; -} - -div#cgit table.list td.reposection { - font-style: italic; - color: #888; -} - -div#cgit a.button { - font-size: 80%; - padding: 0em 0.5em; -} - -div#cgit a.primary { - font-size: 100%; -} - -div#cgit a.secondary { - font-size: 90%; -} - -div#cgit td.toplevel-repo { - -} - -div#cgit table.list td.sublevel-repo { - padding-left: 1.5em; -} - -div#cgit ul.pager { - list-style-type: none; - text-align: center; - margin: 1em 0em 0em 0em; - padding: 0; -} - -div#cgit ul.pager li { - display: inline-block; - margin: 0.25em 0.5em; -} - -div#cgit ul.pager a { - color: #777; -} - -div#cgit ul.pager .current { - font-weight: bold; -} - -div#cgit span.age-mins { - font-weight: bold; - color: #080; -} - -div#cgit span.age-hours { - color: #080; -} - -div#cgit span.age-days { - color: #040; -} - -div#cgit span.age-weeks { - color: #444; -} - -div#cgit span.age-months { - color: #888; -} - -div#cgit span.age-years { - color: #bbb; -} -div#cgit div.footer { - margin-top: 0.5em; - text-align: center; - font-size: 80%; - color: #ccc; -} -div#cgit a.branch-deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #88ff88; - border: solid 1px #007700; -} -div#cgit a.tag-deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #ffff88; - border: solid 1px #777700; -} -div#cgit a.remote-deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #ccccff; - border: solid 1px #000077; -} -div#cgit a.deco { - color: #000; - margin: 0px 0.5em; - padding: 0px 0.25em; - background-color: #ff8888; - border: solid 1px #770000; -} - -div#cgit div.commit-subject a.branch-deco, -div#cgit div.commit-subject a.tag-deco, -div#cgit div.commit-subject a.remote-deco, -div#cgit div.commit-subject a.deco { - margin-left: 1em; - font-size: 75%; -} - -div#cgit table.stats { - border: solid 1px black; - border-collapse: collapse; -} - -div#cgit table.stats th { - text-align: left; - padding: 1px 0.5em; - background-color: #eee; - border: solid 1px black; -} - -div#cgit table.stats td { - text-align: right; - padding: 1px 0.5em; - border: solid 1px black; -} - -div#cgit table.stats td.total { - font-weight: bold; - text-align: left; -} - -div#cgit table.stats td.sum { - color: #c00; - font-weight: bold; -/* background-color: #eee; */ -} - -div#cgit table.stats td.left { - text-align: left; -} - -div#cgit table.vgraph { - border-collapse: separate; - border: solid 1px black; - height: 200px; -} - -div#cgit table.vgraph th { - background-color: #eee; - font-weight: bold; - border: solid 1px white; - padding: 1px 0.5em; -} - -div#cgit table.vgraph td { - vertical-align: bottom; - padding: 0px 10px; -} - -div#cgit table.vgraph div.bar { - background-color: #eee; -} - -div#cgit table.hgraph { - border: solid 1px black; - width: 800px; -} - -div#cgit table.hgraph th { - background-color: #eee; - font-weight: bold; - border: solid 1px black; - padding: 1px 0.5em; -} - -div#cgit table.hgraph td { - vertical-align: middle; - padding: 2px 2px; -} - -div#cgit table.hgraph div.bar { - background-color: #eee; - height: 1em; -} - -div#cgit table.ssdiff { - width: 100%; -} - -div#cgit table.ssdiff td { - font-size: 75%; - font-family: monospace; - white-space: pre; - padding: 1px 4px 1px 4px; - border-left: solid 1px #aaa; - border-right: solid 1px #aaa; -} - -div#cgit table.ssdiff td.add { - color: black; - background: #cfc; - min-width: 50%; -} - -div#cgit table.ssdiff td.add_dark { - color: black; - background: #aca; - min-width: 50%; -} - -div#cgit table.ssdiff span.add { - background: #cfc; - font-weight: bold; -} - -div#cgit table.ssdiff td.del { - color: black; - background: #fcc; - min-width: 50%; -} - -div#cgit table.ssdiff td.del_dark { - color: black; - background: #caa; - min-width: 50%; -} - -div#cgit table.ssdiff span.del { - background: #fcc; - font-weight: bold; -} - -div#cgit table.ssdiff td.changed { - color: black; - background: #ffc; - min-width: 50%; -} - -div#cgit table.ssdiff td.changed_dark { - color: black; - background: #cca; - min-width: 50%; -} - -div#cgit table.ssdiff td.lineno { - color: black; - background: #eee; - text-align: right; - width: 3em; - min-width: 3em; -} - -div#cgit table.ssdiff td.hunk { - color: black; - background: #ccf; - border-top: solid 1px #aaa; - border-bottom: solid 1px #aaa; -} - -div#cgit table.ssdiff td.head { - border-top: solid 1px #aaa; - border-bottom: solid 1px #aaa; -} - -div#cgit table.ssdiff td.head div.head { - font-weight: bold; - color: black; -} - -div#cgit table.ssdiff td.foot { - border-top: solid 1px #aaa; - border-left: none; - border-right: none; - border-bottom: none; -} - -div#cgit table.ssdiff td.space { - border: none; -} - -div#cgit table.ssdiff td.space div { - min-height: 3em; -} - -/* - * Style definitions generated by highlight 3.14, http://www.andre-simon.de/ - * Highlighting theme: Kwrite Editor - */ -div#cgit table.blob .num { color:#b07e00; } -div#cgit table.blob .esc { color:#ff00ff; } -div#cgit table.blob .str { color:#bf0303; } -div#cgit table.blob .pps { color:#818100; } -div#cgit table.blob .slc { color:#838183; font-style:italic; } -div#cgit table.blob .com { color:#838183; font-style:italic; } -div#cgit table.blob .ppc { color:#008200; } -div#cgit table.blob .opt { color:#000000; } -div#cgit table.blob .ipl { color:#0057ae; } -div#cgit table.blob .lin { color:#555555; } -div#cgit table.blob .kwa { color:#000000; font-weight:bold; } -div#cgit table.blob .kwb { color:#0057ae; } -div#cgit table.blob .kwc { color:#000000; font-weight:bold; } -div#cgit table.blob .kwd { color:#010181; } diff --git a/web/html/home.php b/web/html/home.php deleted file mode 100644 index 5ea79ee9..00000000 --- a/web/html/home.php +++ /dev/null @@ -1,215 +0,0 @@ - - -
    -
    - -
    -

    -

    - 50, - 'SeB' => 'M', - 'K' => username_from_sid($_COOKIE["AURSID"]), - 'outdated' => 'on', - 'SB' => 'l', - 'SO' => 'a' - ); - pkg_search_page($params, false, $_COOKIE["AURSID"]); - ?> -

    - -
    -
    -

    -

    ">

    - 50, - 'SeB' => 'm', - 'K' => username_from_sid($_COOKIE["AURSID"]), - 'SB' => 'l', - 'SO' => 'd' - ); - pkg_search_page($params, false, $_COOKIE["AURSID"]); - ?> -
    -
    -

    -

    ">

    - 50, - 'SeB' => 'c', - 'K' => username_from_sid($_COOKIE["AURSID"]), - 'SB' => 'l', - 'SO' => 'd' - ); - pkg_search_page($params, false, $_COOKIE["AURSID"]); - ?> -
    - -
    -

    AUR

    -

    - ', - '', - '', - '' - ); - ?> - ', '', - '', - '' - ); - ?> - - -

    -

    - : - -

    -

    -
    -
    -

    -

    -
    -

    - ', - '' - ); - ?> -

    -
      -
    • :
    • -
    • :
    • -
    • :
    • -
    -

    - ', - '' - ); - ?> -

    -
    -

    -
    -

    - ', - '' - ); - ?> -

    - -

    - -

    -
      - $fingerprint): ?> -
    • :
    • - -
    - -
    -

    -
    -

    - ', - '', - '', - '' - ); - ?> -

    -
    -

    -
    -

    - ', - '', - '', - '' - ); - ?> -

    -
    -
    - -
    -
    -
    -
    -
    -
    - - - " maxlength="35" autocomplete="off"/> -
    -
    -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - - - - - diff --git a/web/html/images/action-undo.svg b/web/html/images/action-undo.svg deleted file mode 100644 index b93ebb78..00000000 --- a/web/html/images/action-undo.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/web/html/images/ajax-loader.gif b/web/html/images/ajax-loader.gif deleted file mode 100644 index df07e7ec2076177c99b53d4d29a45f0db6b06a9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmZ?wbhEHb6kyWo5uik&V|NP^(AHU+;_dI&}>C3lYM=m|vboAbp zdv88|`N04KivPL&TtkAL9RpmA^bD98f#Qn)q@0UV6H8K46v{J8G87WC5-W1@6I1ju z^V0Ge6o0aCasyTAfJ^{6l7UrML7^`tbKa5#T#rsMt#c4)wm4&2aJl;4?H%*^*q;ct zZ+YZ!f=91--8C-PwbPuinV^!8D8ZUAZ$+j|`^0?*ZXH_r=F;-s=Wq7D-W{Q@F^9F$ zTCh`s37bYUpw-=pI*&V4IF+P$l9wbc(l{x7eoOCbBdG(^nGZDWjsAGTTd?u$#mhT{ z{bn8t<<=6J=66T{n^C4fqn2>E3WhNCJ~l~G@x1uTreFAcY2|b4S-i`cPqf%2ZE*i3 z+J9zZu_cRCJ%=P)K~y-6jgvo!6G0Tle=|GDx_9PaSMatt8xuJ=jueWZ*0zF( zjc8@z38oPY2!}RWYjKN}g`kaCi1-H-uCNkOgxf0BeA-5$w9LxMMtXHz@2@3 zyz+UJPuh|vi*mtJavK=cNwf1dpEbZ!a<``>o|1IZ?Bv;J>;8WS9J=%2sOyMVt`f_h zlJvCko4lXZH(|7*(w!f`Tnu;_ptK?Jqw7?)K+hZAu%R^ukDjFp75q4Pbjddz93wNAlTi;8fmk1LdSvP5vdgJg^M# zaG-vgp9c5>)Q7GRMsXQ9!>~RL)NlL5fD7Ck3IMJGg}hz^t^pqh0-C^eU>&Fc%V89s z01(qlD|><0z!Ts~QmejXjKU~B2rL4Jfq5~#v~m%6p46(=A2%lGz#nlao8kSq3cLUS N002ovPDHLkV1na%{Dc4i diff --git a/web/html/images/pencil.min.svg b/web/html/images/pencil.min.svg deleted file mode 100644 index 06125ae0..00000000 --- a/web/html/images/pencil.min.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/html/images/pencil.svg b/web/html/images/pencil.svg deleted file mode 100644 index 91f08991..00000000 --- a/web/html/images/pencil.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - diff --git a/web/html/images/pin.min.svg b/web/html/images/pin.min.svg deleted file mode 100644 index ac08903d..00000000 --- a/web/html/images/pin.min.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web/html/images/pin.svg b/web/html/images/pin.svg deleted file mode 100644 index b4ee9eb7..00000000 --- a/web/html/images/pin.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/html/images/rss.svg b/web/html/images/rss.svg deleted file mode 100644 index 3c7f6ba1..00000000 --- a/web/html/images/rss.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/html/images/unpin.min.svg b/web/html/images/unpin.min.svg deleted file mode 100644 index 3cf2413c..00000000 --- a/web/html/images/unpin.min.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web/html/images/unpin.svg b/web/html/images/unpin.svg deleted file mode 100644 index de897152..00000000 --- a/web/html/images/unpin.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web/html/images/x.min.svg b/web/html/images/x.min.svg deleted file mode 100644 index 833d4f22..00000000 --- a/web/html/images/x.min.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/html/images/x.svg b/web/html/images/x.svg deleted file mode 100644 index e323fe19..00000000 --- a/web/html/images/x.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/web/html/index.php b/web/html/index.php deleted file mode 100644 index dc435162..00000000 --- a/web/html/index.php +++ /dev/null @@ -1,205 +0,0 @@ - '1'); - } - } - - include get_route('/' . $tokens[1]); -} elseif (!empty($tokens[1]) && '/' . $tokens[1] == get_pkgreq_route()) { - if (!empty($tokens[2])) { - /* TODO: Create a proper data structure to pass variables from - * the routing framework to the individual pages instead of - * initializing arbitrary variables here. */ - if (!empty($tokens[3]) && $tokens[3] == 'close') { - $pkgreq_id = $tokens[2]; - } else { - $pkgreq_id = null; - } - - if (!$pkgreq_id) { - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - return; - } - } - - include get_route('/' . $tokens[1]); -} elseif (!empty($tokens[1]) && '/' . $tokens[1] == get_user_route()) { - if (!empty($tokens[2])) { - $_REQUEST['ID'] = uid_from_username($tokens[2]); - - if (!$_REQUEST['ID']) { - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - return; - } - - if (!empty($tokens[3])) { - if ($tokens[3] == 'edit') { - $_REQUEST['Action'] = "DisplayAccount"; - } elseif ($tokens[3] == 'update') { - $_REQUEST['Action'] = "UpdateAccount"; - } elseif ($tokens[3] == 'delete') { - $_REQUEST['Action'] = "DeleteAccount"; - } elseif ($tokens[3] == 'comments') { - $_REQUEST['Action'] = "ListComments"; - } else { - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - return; - } - } else { - $_REQUEST['Action'] = "AccountInfo"; - } - } - include get_route('/' . $tokens[1]); -} elseif (get_route($path) !== NULL) { - include get_route($path); -} else { - switch ($path) { - case "/css/archweb.css": - case "/css/aurweb.css": - case "/css/cgit.css": - case "/css/archnavbar/archnavbar.css": - header("Content-Type: text/css"); - readfile("./$path"); - break; - case "/images/ajax-loader.gif": - header("Content-Type: image/gif"); - readfile("./$path"); - break; - case "/css/archnavbar/archlogo.png": - case "/css/archnavbar/aurlogo.png": - case "/images/favicon.ico": - header("Content-Type: image/png"); - readfile("./$path"); - break; - case "/images/x.min.svg": - case "/images/action-undo.min.svg": - case "/images/pencil.min.svg": - case "/images/pin.min.svg": - case "/images/unpin.min.svg": - case "/images/rss.svg": - header("Content-Type: image/svg+xml"); - readfile("./$path"); - break; - case "/js/typeahead.js": - header("Content-Type: application/javascript"); - readfile("./$path"); - break; - case "/packages.gz": - case "/packages-meta-v1.json.gz": - case "/packages-meta-ext-v1.json.gz": - case "/pkgbase.gz": - case "/users.gz": - header("Content-Type: text/plain"); - header("Content-Encoding: gzip"); - readfile("./$path"); - break; - default: - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - break; - } -} diff --git a/web/html/js/comment-edit.js b/web/html/js/comment-edit.js deleted file mode 100644 index 23ffdd34..00000000 --- a/web/html/js/comment-edit.js +++ /dev/null @@ -1,61 +0,0 @@ -function add_busy_indicator(sibling) { - const img = document.createElement('img'); - img.src = "/static/images/ajax-loader.gif"; - img.classList.add('ajax-loader'); - img.style.height = 11; - img.style.width = 16; - img.alt = "Busy…"; - - sibling.insertAdjacentElement('afterend', img); -} - -function remove_busy_indicator(sibling) { - const elem = sibling.nextElementSibling; - elem.parentNode.removeChild(elem); -} - -function getParentsUntil(elem, className) { - // Limit to 10 depth - for ( ; elem && elem !== document; elem = elem.parentNode) { - if (elem.matches(className)) { - break; - } - } - - return elem; -} - -function handleEditCommentClick(event, pkgbasename) { - event.preventDefault(); - const parent_element = getParentsUntil(event.target, '.comment-header'); - const parent_id = parent_element.id; - const comment_id = parent_id.substr(parent_id.indexOf('-') + 1); - // The div class="article-content" which contains the comment - const edit_form = parent_element.nextElementSibling; - - const url = "/pkgbase/" + pkgbasename + "/comments/" + comment_id + "/form?"; - - add_busy_indicator(event.target); - - fetch(url + new URLSearchParams({ next: window.location.pathname }), { - method: 'GET', - credentials: 'same-origin' - }) - .then(function(response) { - if (!response.ok) { - throw Error(response.statusText); - } - return response.json(); - }) - .then(function(data) { - remove_busy_indicator(event.target); - edit_form.innerHTML = data.form; - edit_form.querySelector('textarea').focus(); - }) - .catch(function(error) { - remove_busy_indicator(event.target); - console.error(error); - }); - - return false; -} diff --git a/web/html/js/copy.js b/web/html/js/copy.js deleted file mode 100644 index 3b659270..00000000 --- a/web/html/js/copy.js +++ /dev/null @@ -1,9 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - let elements = document.querySelectorAll('.copy'); - elements.forEach(function(el) { - el.addEventListener('click', function(e) { - e.preventDefault(); - navigator.clipboard.writeText(e.target.text); - }); - }); -}); diff --git a/web/html/js/typeahead-home.js b/web/html/js/typeahead-home.js deleted file mode 100644 index 5af51c53..00000000 --- a/web/html/js/typeahead-home.js +++ /dev/null @@ -1,6 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - const input = document.getElementById('pkgsearch-field'); - const form = document.getElementById('pkgsearch-form'); - const type = 'suggest'; - typeahead.init(type, input, form); -}); diff --git a/web/html/js/typeahead-pkgbase-merge.js b/web/html/js/typeahead-pkgbase-merge.js deleted file mode 100644 index a8c87e4f..00000000 --- a/web/html/js/typeahead-pkgbase-merge.js +++ /dev/null @@ -1,6 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - const input = document.getElementById('merge_into'); - const form = document.getElementById('merge-form'); - const type = "suggest-pkgbase"; - typeahead.init(type, input, form, false); -}); diff --git a/web/html/js/typeahead-pkgbase-request.js b/web/html/js/typeahead-pkgbase-request.js deleted file mode 100644 index e012d55f..00000000 --- a/web/html/js/typeahead-pkgbase-request.js +++ /dev/null @@ -1,36 +0,0 @@ -function showHideMergeSection() { - const elem = document.getElementById('id_type'); - const merge_section = document.getElementById('merge_section'); - if (elem.value == 'merge') { - merge_section.style.display = ''; - } else { - merge_section.style.display = 'none'; - } -} - -function showHideRequestHints() { - document.getElementById('deletion_hint').style.display = 'none'; - document.getElementById('merge_hint').style.display = 'none'; - document.getElementById('orphan_hint').style.display = 'none'; - - const elem = document.getElementById('id_type'); - document.getElementById(elem.value + '_hint').style.display = ''; -} - -document.addEventListener('DOMContentLoaded', function() { - showHideMergeSection(); - showHideRequestHints(); - - const input = document.getElementById('id_merge_into'); - const form = document.getElementById('request-form'); - const type = "suggest-pkgbase"; - - typeahead.init(type, input, form, false); -}); - -// Bind the change event here, otherwise we have to inline javascript, -// which angers CSP (Content Security Policy). -document.getElementById("id_type").addEventListener("change", function() { - showHideMergeSection(); - showHideRequestHints(); -}); diff --git a/web/html/js/typeahead.js b/web/html/js/typeahead.js deleted file mode 100644 index bfd3d156..00000000 --- a/web/html/js/typeahead.js +++ /dev/null @@ -1,151 +0,0 @@ -"use strict"; - -const typeahead = (function() { - var input; - var form; - var suggest_type; - var list; - var submit = true; - - function resetResults() { - if (!list) return; - list.style.display = "none"; - list.innerHTML = ""; - } - - function getCompleteList() { - if (!list) { - list = document.createElement("UL"); - list.setAttribute("class", "pkgsearch-typeahead"); - form.appendChild(list); - setListLocation(); - } - return list; - } - - function onListClick(e) { - let target = e.target; - while (!target.getAttribute('data-value')) { - target = target.parentNode; - } - input.value = target.getAttribute('data-value'); - if (submit) { - form.submit(); - } - } - - function setListLocation() { - if (!list) return; - const rects = input.getClientRects()[0]; - list.style.top = (rects.top + rects.height) + "px"; - list.style.left = rects.left + "px"; - } - - function loadData(letter, data) { - const pkgs = data.slice(0, 10); // Show maximum of 10 results - - resetResults(); - - if (pkgs.length === 0) { - return; - } - - const ul = getCompleteList(); - ul.style.display = "block"; - const fragment = document.createDocumentFragment(); - - for (let i = 0; i < pkgs.length; i++) { - const item = document.createElement("li"); - const text = pkgs[i].replace(letter, '' + letter + ''); - item.innerHTML = '' + text + ''; - item.setAttribute('data-value', pkgs[i]); - fragment.appendChild(item); - } - - ul.appendChild(fragment); - ul.addEventListener('click', onListClick); - } - - function fetchData(letter) { - const url = '/rpc?v=5&type=' + suggest_type + '&arg=' + letter; - fetch(url).then(function(response) { - return response.json(); - }).then(function(data) { - loadData(letter, data); - }); - } - - function onInputClick() { - if (input.value === "") { - resetResults(); - return; - } - fetchData(input.value); - } - - function onKeyDown(e) { - if (!list) return; - - const elem = document.querySelector(".pkgsearch-typeahead li.active"); - switch(e.keyCode) { - case 13: // enter - if (!submit) { - return; - } - if (elem) { - input.value = elem.getAttribute('data-value'); - form.submit(); - } else { - form.submit(); - } - e.preventDefault(); - break; - case 38: // up - if (elem && elem.previousElementSibling) { - elem.className = ""; - elem.previousElementSibling.className = "active"; - } - e.preventDefault(); - break; - case 40: // down - if (elem && elem.nextElementSibling) { - elem.className = ""; - elem.nextElementSibling.className = "active"; - } else if (!elem && list.childElementCount !== 0) { - list.children[0].className = "active"; - } - e.preventDefault(); - break; - } - } - - // debounce https://davidwalsh.name/javascript-debounce-function - function debounce(func, wait, immediate) { - var timeout; - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - if (!immediate) func.apply(context, args); - }; - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - }; - } - - return { - init: function(type, inputfield, formfield, submitdata = true) { - suggest_type = type; - input = inputfield; - form = formfield; - submit = submitdata; - - input.addEventListener("input", onInputClick); - input.addEventListener("keydown", onKeyDown); - window.addEventListener('resize', debounce(setListLocation, 150)); - document.addEventListener("click", resetResults); - } - } -}()); diff --git a/web/html/login.php b/web/html/login.php deleted file mode 100644 index 3f3d66cc..00000000 --- a/web/html/login.php +++ /dev/null @@ -1,68 +0,0 @@ - -
    -

    AUR

    - -

    - ' . username_from_sid($_COOKIE["AURSID"]) . ''); ?> - [] -

    - -
    -
    - - -
    - -

    - - -

    -

    - - -

    -

    - - -

    -

    - " /> - [] - - [] - - - - -

    -
    -
    - -

    - ', ''); ?> -

    - -
    -query("SELECT SSOAccountID FROM Users WHERE ID = " . $dbh->quote($uid)) - ->fetchColumn(); - if ($sso_account_id) - $redirect_uri = '/sso/logout'; - } -} - -header("Location: $redirect_uri"); diff --git a/web/html/modified-rss.php b/web/html/modified-rss.php deleted file mode 100644 index 4c5c47e0..00000000 --- a/web/html/modified-rss.php +++ /dev/null @@ -1,62 +0,0 @@ -cssStyleSheet = false; -$rss->xslStyleSheet = false; - -# Use UTF-8 (fixes FS#10706). -$rss->encoding = "UTF-8"; - -#All the general RSS setup -$rss->title = "AUR Latest Modified Packages"; -$rss->description = "The latest modified packages in the AUR"; -$rss->link = "${protocol}://{$host}"; -$rss->syndicationURL = "{$protocol}://{$host}" . get_uri('/rss/'); -$image = new FeedImage(); -$image->title = "AUR Latest Modified Packages"; -$image->url = "{$protocol}://{$host}/css/archnavbar/aurlogo.png"; -$image->link = $rss->link; -$image->description = "AUR Latest Modified Packages Feed"; -$rss->image = $image; - -#Get the latest packages and add items for them -$packages = latest_modified_pkgs(100); - -foreach ($packages as $indx => $row) { - $item = new FeedItem(); - $item->title = $row["Name"]; - $item->link = "{$protocol}://{$host}" . get_pkg_uri($row["Name"]); - $item->description = $row["Description"]; - $item->date = intval($row["ModifiedTS"]); - $item->source = "{$protocol}://{$host}"; - $item->author = username_from_id($row["MaintainerUID"]); - $item->guidIsPermaLink = true; - $item->guid = $row["Name"] . "-" . $row["ModifiedTS"]; - $rss->addItem($item); -} - -#save it so that useCached() can find it -$feedContent = $rss->createFeed(); -set_cache_value($feed_key, $feedContent, 600); -echo $feedContent; -?> diff --git a/web/html/packages.php b/web/html/packages.php deleted file mode 100644 index 24d1f82e..00000000 --- a/web/html/packages.php +++ /dev/null @@ -1,173 +0,0 @@ - - - - -\n"; - } -} else { - if (!isset($_GET['K']) && !isset($_GET['SB'])) { - $_GET['SB'] = 'p'; - $_GET['SO'] = 'd'; - } - echo '
    '; - if (isset($_COOKIE["AURSID"])) { - pkg_search_page($_GET, true, $_COOKIE["AURSID"]); - } else { - pkg_search_page($_GET, true); - } - echo '
    '; -} - -html_footer(AURWEB_VERSION); diff --git a/web/html/passreset.php b/web/html/passreset.php deleted file mode 100644 index 26b9bbbb..00000000 --- a/web/html/passreset.php +++ /dev/null @@ -1,100 +0,0 @@ - - -
    -

    - - -

    - -

    - - -
    - -
    - - - - - - - - - - - - - -
    -
    - -
    - -

    ', - ''); ?>

    - -
    - -
    -

    -

    - -
    - -
    - - $i) { - $id = intval($id); - if ($id > 0) { - $ids[] = $id; - } - } -} - -/* Perform package base actions. */ -$via = isset($_POST['via']) ? $_POST['via'] : NULL; -$return_to = isset($_POST['return_to']) ? $_POST['return_to'] : NULL; -$ret = false; -$output = ""; -$fragment = ""; -if (check_token()) { - if (current_action("do_Flag")) { - list($ret, $output) = pkgbase_flag($ids, $_POST['comments']); - } elseif (current_action("do_UnFlag")) { - list($ret, $output) = pkgbase_unflag($ids); - } elseif (current_action("do_Adopt")) { - list($ret, $output) = pkgbase_adopt($ids, true, NULL); - } elseif (current_action("do_Disown")) { - if (isset($_POST['confirm'])) { - list($ret, $output) = pkgbase_adopt($ids, false, $via); - } else { - $output = __("The selected packages have not been disowned, check the confirmation checkbox."); - $ret = false; - } - } elseif (current_action("do_DisownComaintainer")) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - list($ret, $output) = pkgbase_remove_comaintainer($base_id, $uid); - } elseif (current_action("do_Vote")) { - list($ret, $output) = pkgbase_vote($ids, true); - } elseif (current_action("do_UnVote")) { - list($ret, $output) = pkgbase_vote($ids, false); - } elseif (current_action("do_Delete")) { - if (isset($_POST['confirm'])) { - if (!isset($_POST['merge_Into']) || empty($_POST['merge_Into'])) { - list($ret, $output) = pkgbase_delete($ids, NULL, $via); - unset($_GET['ID']); - unset($base_id); - } - else { - $merge_base_id = pkgbase_from_name($_POST['merge_Into']); - if (!$merge_base_id) { - $output = __("Cannot find package to merge votes and comments into."); - $ret = false; - } elseif (in_array($merge_base_id, $ids)) { - $output = __("Cannot merge a package base with itself."); - $ret = false; - } else { - list($ret, $output) = pkgbase_delete($ids, $merge_base_id, $via); - unset($_GET['ID']); - unset($base_id); - } - } - } - else { - $output = __("The selected packages have not been deleted, check the confirmation checkbox."); - $ret = false; - } - } elseif (current_action("do_Notify")) { - list($ret, $output) = pkgbase_notify($ids); - } elseif (current_action("do_UnNotify")) { - list($ret, $output) = pkgbase_notify($ids, false); - } elseif (current_action("do_DeleteComment")) { - list($ret, $output) = pkgbase_delete_comment(); - } elseif (current_action("do_UndeleteComment")) { - list($ret, $output) = pkgbase_delete_comment(true); - if ($ret && isset($_POST["comment_id"])) { - $fragment = '#comment-' . intval($_POST["comment_id"]); - } - } elseif (current_action("do_PinComment")) { - list($ret, $output) = pkgbase_pin_comment(); - } elseif (current_action("do_UnpinComment")) { - list($ret, $output) = pkgbase_pin_comment(true); - } elseif (current_action("do_SetKeywords")) { - list($ret, $output) = pkgbase_set_keywords($base_id, preg_split("/[\s,;]+/", $_POST['keywords'], -1, PREG_SPLIT_NO_EMPTY)); - } elseif (current_action("do_FileRequest")) { - list($ret, $output) = pkgreq_file($ids, $_POST['type'], $_POST['merge_into'], $_POST['comments']); - } elseif (current_action("do_CloseRequest")) { - list($ret, $output) = pkgreq_close($_POST['reqid'], $_POST['reason'], $_POST['comments']); - } elseif (current_action("do_EditComaintainers")) { - list($ret, $output) = pkgbase_set_comaintainers($base_id, explode("\n", $_POST['users'])); - } elseif (current_action("do_AddComment")) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - list($ret, $output) = pkgbase_add_comment($base_id, $uid, $_REQUEST['comment']); - if ($ret && isset($_REQUEST['enable_notifications'])) { - list($ret, $output) = pkgbase_notify(array($base_id)); - } - $fragment = '#news'; - } elseif (current_action("do_EditComment")) { - list($ret, $output) = pkgbase_edit_comment($_REQUEST['comment']); - if ($ret && isset($_POST["comment_id"])) { - $fragment = '#comment-' . intval($_POST["comment_id"]); - } - } - - if ($ret) { - if (current_action("do_CloseRequest") || - (current_action("do_Delete") && $via)) { - /* Redirect back to package request page on success. */ - header('Location: ' . get_pkgreq_route()); - exit(); - } elseif ((current_action("do_DeleteComment") || - current_action("do_UndeleteComment")) && $return_to) { - header('Location: ' . $return_to); - exit(); - } elseif (current_action("do_PinComment") && $return_to) { - header('Location: ' . $return_to); - exit(); - } elseif (isset($base_id)) { - /* Redirect back to package base page on success. */ - header('Location: ' . get_pkgbase_uri($pkgbase_name) . $fragment); - exit(); - } else { - /* Redirect back to package search page. */ - header('Location: ' . get_pkg_route()); - exit(); - } - } -} - -if (isset($base_id)) { - $pkgs = pkgbase_get_pkgnames($base_id); - if (!$output && count($pkgs) == 1) { - /* Not a split package. Redirect to the package page. */ - if (empty($_SERVER['QUERY_STRING'])) { - header('Location: ' . get_pkg_uri($pkgs[0]) . $fragment); - } else { - header('Location: ' . get_pkg_uri($pkgs[0]) . '?' . $_SERVER['QUERY_STRING'] . $fragment); - } - } - - $details = pkgbase_get_details($base_id); -} else { - $details = array(); -} -html_header($title, $details); -?> - - - -

    - -
    - - - - -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    -

    - - -

    -
    -
    - - - - - - -

    -

    " />

    -
    -
    -
    - - -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    -

    - - - - 0 && !has_credential(CRED_PKGBASE_DISOWN)): ?> - ', $comaintainers[0], ''); ?> - - - -

    -
    -
    - - - - - - -

    " />

    -
    -
    -
    - - $i) { - $id = intval($id); - if ($id > 0) { - $ids[] = $id; - } - } -} - -/* Perform package base actions. */ -$ret = false; -$output = ""; -if (check_token()) { - if (current_action("do_Flag")) { - list($ret, $output) = pkgbase_flag($ids, $_POST['comments']); - } - - if ($ret) { - header('Location: ' . get_pkgbase_uri($pkgbase_name)); - exit(); - } -} - -/* Get default comment. */ -$comment = ''; -if (isset($_POST['comments'])) { - $comment = $_POST['comments']; -} - -html_header(__("Flag Package Out-Of-Date")); - -if (has_credential(CRED_PKGBASE_FLAG)): ?> -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    - -

    - This seems to be a VCS package. Please do not - flag it out-of-date if the package version in the AUR does not - match the most recent commit. Flagging this package should only - be done if the sources moved or changes in the PKGBUILD are - required because of recent upstream changes. -

    - -

    - ', ''); ?> - -

    - - -
    - - -
    -
    - - - -

    - - -

    -

    " />

    -
    -
    -
    - - -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> - -

    -
      - -
    • - -
    -

    - - - -

    -
    -
    - - - - - - - - -

    -

    -

    -

    " />

    -
    -
    -
    - - 0) ? $_GET['PP'] : 50; - $current = ceil($first / $per_page); - $pages = ceil($total / $per_page); - $templ_pages = array(); - - if ($current > 1) { - $templ_pages['« ' . __('First')] = 0; - $templ_pages['‹ ' . __('Previous')] = ($current - 2) * $per_page; - } - - if ($current - 5 > 1) - $templ_pages["..."] = false; - - for ($i = max($current - 5, 1); $i <= min($pages, $current + 5); $i++) { - $templ_pages[$i] = ($i - 1) * $per_page; - } - - if ($current + 5 < $pages) - $templ_pages["... "] = false; - - if ($current < $pages) { - $templ_pages[__('Next') . ' ›'] = $current * $per_page; - $templ_pages[__('Last') . ' »'] = ($pages - 1) * $per_page; - } - - $SID = $_COOKIE['AURSID']; - - html_header(__("Requests")); - echo '
    '; - $show_headers = true; - include('pkgreq_results.php'); - echo '
    '; -} - -html_footer(AURWEB_VERSION); diff --git a/web/html/register.php b/web/html/register.php deleted file mode 100644 index fee0a68f..00000000 --- a/web/html/register.php +++ /dev/null @@ -1,87 +0,0 @@ -'; -echo '

    ' . __('Register') . '

    '; - -if (in_request("Action") == "NewAccount") { - list($success, $message) = process_account_form( - "new", - "NewAccount", - in_request("U"), - 1, - 0, - in_request("E"), - in_request("BE"), - in_request("H"), - '', - '', - in_request("R"), - in_request("L"), - in_request("TZ"), - in_request("HP"), - in_request("I"), - in_request("K"), - in_request("PK"), - 0, - in_request("CN"), - in_request("UN"), - in_request("ON"), - 0, - "", - '', - in_request("captcha_salt"), - in_request("captcha"), - ); - - print $message; - - if (!$success) { - display_account_form("NewAccount", - in_request("U"), - 1, - 0, - in_request("E"), - in_request("BE"), - in_request("H"), - '', - '', - in_request("R"), - in_request("L"), - in_request("TZ"), - in_request("HP"), - in_request("I"), - in_request("K"), - in_request("PK"), - 0, - in_request("CN"), - in_request("UN"), - in_request("ON"), - 0, - "", - '', - in_request("captcha_salt"), - in_request("captcha") - ); - } -} else { - print '

    ' . __("Use this form to create an account.") . '

    '; - display_account_form("NewAccount", "", "", "", "", "", "", "", "", "", $LANG); -} - -echo ''; - -html_footer(AURWEB_VERSION); - -?> diff --git a/web/html/rpc.php b/web/html/rpc.php deleted file mode 100644 index 64c95622..00000000 --- a/web/html/rpc.php +++ /dev/null @@ -1,17 +0,0 @@ -handle($_GET); -} -else { - echo file_get_contents('../../doc/rpc.html'); -} -?> diff --git a/web/html/rss.php b/web/html/rss.php deleted file mode 100644 index 1e6335cf..00000000 --- a/web/html/rss.php +++ /dev/null @@ -1,61 +0,0 @@ -cssStyleSheet = false; -$rss->xslStyleSheet = false; - -# Use UTF-8 (fixes FS#10706). -$rss->encoding = "UTF-8"; - -#All the general RSS setup -$rss->title = "AUR Newest Packages"; -$rss->description = "The latest and greatest packages in the AUR"; -$rss->link = "${protocol}://{$host}"; -$rss->syndicationURL = "{$protocol}://{$host}" . get_uri('/rss/'); -$image = new FeedImage(); -$image->title = "AUR Newest Packages"; -$image->url = "{$protocol}://{$host}/css/archnavbar/aurlogo.png"; -$image->link = $rss->link; -$image->description = "AUR Newest Packages Feed"; -$rss->image = $image; - -#Get the latest packages and add items for them -$packages = latest_pkgs(100); - -foreach ($packages as $indx => $row) { - $item = new FeedItem(); - $item->title = $row["Name"]; - $item->link = "{$protocol}://{$host}" . get_pkg_uri($row["Name"]); - $item->description = $row["Description"]; - $item->date = intval($row["SubmittedTS"]); - $item->source = "{$protocol}://{$host}"; - $item->author = username_from_id($row["MaintainerUID"]); - $item->guid = $item->link; - $rss->addItem($item); -} - -#save it so that useCached() can find it -$feedContent = $rss->createFeed(); -set_cache_value($feed_key, $feedContent, 600); -echo $feedContent; -?> diff --git a/web/html/tos.php b/web/html/tos.php deleted file mode 100644 index fc5d8765..00000000 --- a/web/html/tos.php +++ /dev/null @@ -1,50 +0,0 @@ - -
    -

    AUR

    - -
    -
    -

    - ' . username_from_sid($_COOKIE["AURSID"]) . ''); ?> -

    -

    - -

    -
      - -
    • "> ()
    • - -
    -

    - - ]" value="" /> - - -

    -

    - " /> -

    -
    -
    - -
    - time() ? 1 : 0; - - # List voters of a proposal. - $whovoted = voter_list($row['ID']); - - $canvote = 1; - $hasvoted = 0; - $errorvote = ""; - if ($isrunning == 0) { - $canvote = 0; - $errorvote = __("Voting is closed for this proposal."); - } else if (!has_credential(CRED_TU_VOTE)) { - $canvote = 0; - $errorvote = __("Only Trusted Users are allowed to vote."); - } else if ($row['User'] == username_from_sid($_COOKIE["AURSID"])) { - $canvote = 0; - $errorvote = __("You cannot vote in an proposal about you."); - } - if (tu_voted($row['ID'], uid_from_sid($_COOKIE["AURSID"]))) { - $canvote = 0; - $hasvoted = 1; - if ($isrunning) { - $errorvote = __("You've already voted for this proposal."); - } - } - - if ($canvote == 1) { - if (isset($_POST['doVote']) && check_token()) { - if (isset($_POST['voteYes'])) { - $myvote = "Yes"; - } else if (isset($_POST['voteNo'])) { - $myvote = "No"; - } else if (isset($_POST['voteAbstain'])) { - $myvote = "Abstain"; - } - - cast_proposal_vote($row['ID'], uid_from_sid($_COOKIE["AURSID"]), $myvote, $row[$myvote] + 1); - - # Can't vote anymore - # - $canvote = 0; - $errorvote = __("You've already voted for this proposal."); - - # Update if they voted - if (tu_voted($row['ID'], uid_from_sid($_COOKIE["AURSID"]))) { - $hasvoted = 1; - } - $row = vote_details($_GET['id']); - } - } - include("tu_details.php"); - } - } else { - print __("Vote ID not valid."); - } - - } else { - $limit = $pp; - if (isset($_GET['off'])) - $offset = $_GET['off']; - - if (isset($_GET['by'])) - $by = $_GET['by']; - else - $by = 'desc'; - - if (!empty($offset) && is_numeric($offset)) { - if ($offset >= 1) { - $off = $offset; - } else { - $off = 0; - } - } else { - $off = 0; - } - - $order = ($by == 'asc') ? 'ASC' : 'DESC'; - $lim = ($limit > 0) ? " LIMIT $limit OFFSET $off" : ""; - $by_next = ($by == 'desc') ? 'asc' : 'desc'; - - $result = current_proposal_list($order); - $type = __("Current Votes"); - $nextresult = 0; - include("tu_list.php"); - - $result = past_proposal_list($order, $lim); - $type = __("Past Votes"); - $nextresult = proposal_count(); - include("tu_list.php"); - - $result = last_votes_list(); - include("tu_last_votes_list.php"); - } -} -else { - header('Location: /'); -} - -html_footer(AURWEB_VERSION); diff --git a/web/html/voters.php b/web/html/voters.php deleted file mode 100644 index bacbcfc8..00000000 --- a/web/html/voters.php +++ /dev/null @@ -1,34 +0,0 @@ - - -
    -

    Votes for

    -
    -
      - $row): ?> -
    • - - 0): ?> - () - -
    • - -
    -
    -
    - -exec("SET NAMES 'utf8' COLLATE 'utf8_general_ci';"); - } else if ($backend == "sqlite") { - $dsn = $backend . - ":" . $name; - - self::$dbh = new PDO($dsn, null, null); - } else { - die("Error - " . $backend . " is not supported by aurweb"); - } - - } catch (PDOException $e) { - die('Error - Could not connect to AUR database'); - } - } - - return self::$dbh; - } -} diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php deleted file mode 100644 index 0d021f99..00000000 --- a/web/lib/acctfuncs.inc.php +++ /dev/null @@ -1,1522 +0,0 @@ -\n" - . "
  • " . __("It must be between %s and %s characters long", $length_min, $length_max) - . "
  • " - . "
  • " . __("Start and end with a letter or number") . "
  • " - . "
  • " . __("Can contain only one period, underscore or hyphen.") - . "
  • \n"; - } - - if (!$error && $P && !$C) { - $error = __("Please confirm your new password."); - } - if (!$error && $P && $P != $C) { - $error = __("Password fields do not match."); - } - if (!$error && $P != '' && !good_passwd($P)) { - $length_min = config_get_int('options', 'passwd_min_len'); - $error = __("Your password must be at least %s characters.", - $length_min); - } - - if (!$error && !valid_email($E)) { - $error = __("The email address is invalid."); - } - if (!$error && $BE && !valid_email($BE)) { - $error = __("The backup email address is invalid."); - } - - if (!$error && !empty($HP) && !valid_homepage($HP)) { - $error = __("The home page is invalid, please specify the full HTTP(s) URL."); - } - - if (!$error && $K != '' && !valid_pgp_fingerprint($K)) { - $error = __("The PGP key fingerprint is invalid."); - } - - if (!$error && !empty($PK)) { - $ssh_keys = array_filter(array_map('trim', explode("\n", $PK))); - $ssh_fingerprints = array(); - - foreach ($ssh_keys as &$ssh_key) { - if (!valid_ssh_pubkey($ssh_key)) { - $error = __("The SSH public key is invalid."); - break; - } - - $ssh_fingerprint = ssh_key_fingerprint($ssh_key); - if (!$ssh_fingerprint) { - $error = __("The SSH public key is invalid."); - break; - } - - $tokens = explode(" ", $ssh_key); - $ssh_key = $tokens[0] . " " . $tokens[1]; - - $ssh_fingerprints[] = $ssh_fingerprint; - } - - /* - * Destroy last reference to prevent accidentally overwriting - * an array element. - */ - unset($ssh_key); - } - - if (isset($_COOKIE['AURSID'])) { - $atype = account_from_sid($_COOKIE['AURSID']); - if (($atype == "User" && $T > 1) || ($atype == "Trusted User" && $T > 2)) { - $error = __("Cannot increase account permissions."); - } - } - - if (!$error && !array_key_exists($L, $SUPPORTED_LANGS)) { - $error = __("Language is not currently supported."); - } - if (!$error && !array_key_exists($TZ, generate_timezone_list())) { - $error = __("Timezone is not currently supported."); - } - if (!$error) { - /* - * Check whether the user name is available. - * TODO: Fix race condition. - */ - $q = "SELECT COUNT(*) AS CNT FROM Users "; - $q.= "WHERE Username = " . $dbh->quote($U); - if ($TYPE == "edit") { - $q.= " AND ID != ".intval($UID); - } - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row[0]) { - $error = __("The username, %s%s%s, is already in use.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - } - } - if (!$error) { - /* - * Check whether the e-mail address is available. - * TODO: Fix race condition. - */ - $q = "SELECT COUNT(*) AS CNT FROM Users "; - $q.= "WHERE Email = " . $dbh->quote($E); - if ($TYPE == "edit") { - $q.= " AND ID != ".intval($UID); - } - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row[0]) { - $error = __("The address, %s%s%s, is already in use.", - "", htmlspecialchars($E,ENT_QUOTES), ""); - } - } - if (!$error && isset($ssh_keys) && count($ssh_keys) > 0) { - /* - * Check whether any of the SSH public keys is already in use. - * TODO: Fix race condition. - */ - $q = "SELECT Fingerprint FROM SSHPubKeys "; - $q.= "WHERE Fingerprint IN ("; - $q.= implode(',', array_map(array($dbh, 'quote'), $ssh_fingerprints)); - $q.= ")"; - if ($TYPE == "edit") { - $q.= " AND UserID != " . intval($UID); - } - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - $error = __("The SSH public key, %s%s%s, is already in use.", - "", htmlspecialchars($row[0], ENT_QUOTES), ""); - } - } - - if (!$error && $TYPE == "new" && empty($captcha)) { - $error = __("The CAPTCHA is missing."); - } - - if (!$error && $TYPE == "new" && !in_array($captcha_salt, get_captcha_salts())) { - $error = __("This CAPTCHA has expired. Please try again."); - } - - if (!$error && $TYPE == "new" && $captcha != get_captcha_answer($captcha_salt)) { - $error = __("The entered CAPTCHA answer is invalid."); - } - - if ($error) { - $message = "
    • ".$error."
    \n"; - return array(false, $message); - } - - if ($TYPE == "new") { - /* Create an unprivileged user. */ - if (empty($P)) { - $send_resetkey = true; - $email = $E; - } else { - $send_resetkey = false; - $P = password_hash($P, PASSWORD_DEFAULT); - } - $U = $dbh->quote($U); - $E = $dbh->quote($E); - $BE = $dbh->quote($BE); - $P = $dbh->quote($P); - $R = $dbh->quote($R); - $L = $dbh->quote($L); - $TZ = $dbh->quote($TZ); - $HP = $dbh->quote($HP); - $I = $dbh->quote($I); - $K = $dbh->quote(str_replace(" ", "", $K)); - $q = "INSERT INTO Users (AccountTypeID, Suspended, "; - $q.= "InactivityTS, Username, Email, BackupEmail, Passwd , "; - $q.= "RealName, LangPreference, Timezone, Homepage, IRCNick, PGPKey) "; - $q.= "VALUES (1, 0, 0, $U, $E, $BE, $P, $R, $L, $TZ, "; - $q.= "$HP, $I, $K)"; - $result = $dbh->exec($q); - if (!$result) { - $message = __("Error trying to create account, %s%s%s.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - return array(false, $message); - } - - $uid = $dbh->lastInsertId(); - if (isset($ssh_keys) && count($ssh_keys) > 0) { - account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints); - } - - $message = __("The account, %s%s%s, has been successfully created.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - $message .= "

    \n"; - - if ($send_resetkey) { - send_resetkey($email, true); - $message .= __("A password reset key has been sent to your e-mail address."); - $message .= "

    \n"; - } else { - $message .= __("Click on the Login link above to use your account."); - $message .= "

    \n"; - } - } else { - /* Modify an existing account. */ - $q = "SELECT InactivityTS FROM Users WHERE "; - $q.= "ID = " . intval($UID); - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - if ($row[0] && $J) { - $inactivity_ts = $row[0]; - } elseif ($J) { - $inactivity_ts = time(); - } else { - $inactivity_ts = 0; - } - - $q = "UPDATE Users SET "; - $q.= "Username = " . $dbh->quote($U); - if ($T) { - $q.= ", AccountTypeID = ".intval($T); - } - if ($S) { - /* Ensure suspended users can't keep an active session */ - delete_user_sessions($UID); - $q.= ", Suspended = 1"; - } else { - $q.= ", Suspended = 0"; - } - $q.= ", Email = " . $dbh->quote($E); - $q.= ", BackupEmail = " . $dbh->quote($BE); - if ($H) { - $q.= ", HideEmail = 1"; - } else { - $q.= ", HideEmail = 0"; - } - if ($P) { - $hash = password_hash($P, PASSWORD_DEFAULT); - $q .= ", Passwd = " . $dbh->quote($hash); - } - $q.= ", RealName = " . $dbh->quote($R); - $q.= ", LangPreference = " . $dbh->quote($L); - $q.= ", Timezone = " . $dbh->quote($TZ); - $q.= ", Homepage = " . $dbh->quote($HP); - $q.= ", IRCNick = " . $dbh->quote($I); - $q.= ", PGPKey = " . $dbh->quote(str_replace(" ", "", $K)); - $q.= ", InactivityTS = " . $inactivity_ts; - $q.= ", CommentNotify = " . ($CN ? "1" : "0"); - $q.= ", UpdateNotify = " . ($UN ? "1" : "0"); - $q.= ", OwnershipNotify = " . ($ON ? "1" : "0"); - $q.= " WHERE ID = ".intval($UID); - $result = $dbh->exec($q); - - if (isset($ssh_keys) && count($ssh_keys) > 0) { - $ssh_key_result = account_set_ssh_keys($UID, $ssh_keys, $ssh_fingerprints); - } else { - $ssh_key_result = true; - } - - if (isset($_COOKIE["AURTZ"]) && ($_COOKIE["AURTZ"] != $TZ)) { - /* set new cookie for timezone */ - $timeout = intval(config_get("options", "persistent_cookie_timeout")); - $cookie_time = time() + $timeout; - setcookie("AURTZ", $TZ, $cookie_time, "/"); - } - - if (isset($_COOKIE["AURLANG"]) && ($_COOKIE["AURLANG"] != $L)) { - /* set new cookie for language */ - $timeout = intval(config_get("options", "persistent_cookie_timeout")); - $cookie_time = time() + $timeout; - setcookie("AURLANG", $L, $cookie_time, "/"); - } - - if ($result === false || $ssh_key_result === false) { - $message = __("No changes were made to the account, %s%s%s.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - } else { - $message = __("The account, %s%s%s, has been successfully modified.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - } - } - - return array(true, $message); -} - -/** - * Display the search results page - * - * @param string $O The offset for the results page - * @param string $SB The column to sort the results page by - * @param string $U The username search criteria - * @param string $T The account type search criteria - * @param string $S Whether the account is suspended search criteria - * @param string $E The e-mail address search criteria - * @param string $R The real name search criteria - * @param string $I The IRC nickname search criteria - * @param string $K The PGP key fingerprint search criteria - * - * @return void - */ -function search_results_page($O=0,$SB="",$U="",$T="", - $S="",$E="",$R="",$I="",$K="") { - - $HITS_PER_PAGE = 50; - if ($O) { - $OFFSET = intval($O); - } else { - $OFFSET = 0; - } - if ($OFFSET < 0) { - $OFFSET = 0; - } - $search_vars = array(); - - $dbh = DB::connect(); - - $q = "SELECT Users.*, AccountTypes.AccountType "; - $q.= "FROM Users, AccountTypes "; - $q.= "WHERE AccountTypes.ID = Users.AccountTypeID "; - if ($T == "u") { - $q.= "AND AccountTypes.ID = 1 "; - $search_vars[] = "T"; - } elseif ($T == "t") { - $q.= "AND AccountTypes.ID = 2 "; - $search_vars[] = "T"; - } elseif ($T == "d") { - $q.= "AND AccountTypes.ID = 3 "; - $search_vars[] = "T"; - } elseif ($T == "td") { - $q.= "AND AccountTypes.ID = 4 "; - $search_vars[] = "T"; - } - if ($S) { - $q.= "AND Users.Suspended = 1 "; - $search_vars[] = "S"; - } - if ($U) { - $U = "%" . addcslashes($U, '%_') . "%"; - $q.= "AND Username LIKE " . $dbh->quote($U) . " "; - $search_vars[] = "U"; - } - if ($E) { - $E = "%" . addcslashes($E, '%_') . "%"; - $q.= "AND Email LIKE " . $dbh->quote($E) . " "; - $search_vars[] = "E"; - } - if ($R) { - $R = "%" . addcslashes($R, '%_') . "%"; - $q.= "AND RealName LIKE " . $dbh->quote($R) . " "; - $search_vars[] = "R"; - } - if ($I) { - $I = "%" . addcslashes($I, '%_') . "%"; - $q.= "AND IRCNick LIKE " . $dbh->quote($I) . " "; - $search_vars[] = "I"; - } - if ($K) { - $K = "%" . addcslashes(str_replace(" ", "", $K), '%_') . "%"; - $q.= "AND PGPKey LIKE " . $dbh->quote($K) . " "; - $search_vars[] = "K"; - } - switch ($SB) { - case 't': - $q.= "ORDER BY AccountTypeID, Username "; - break; - case 'r': - $q.= "ORDER BY RealName, AccountTypeID "; - break; - case 'i': - $q.= "ORDER BY IRCNick, AccountTypeID "; - break; - default: - $q.= "ORDER BY Username, AccountTypeID "; - break; - } - $search_vars[] = "SB"; - $q.= "LIMIT " . $HITS_PER_PAGE . " OFFSET " . $OFFSET; - - $dbh = DB::connect(); - - $result = $dbh->query($q); - - $userinfo = array(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $userinfo[] = $row; - } - } - - include("account_search_results.php"); - return; -} - -/** - * Attempt to login and generate a session - * - * @return array Session ID for user, error message if applicable - */ -function try_login() { - $login_error = ""; - $new_sid = ""; - $userID = null; - - if (!isset($_REQUEST['user']) && !isset($_REQUEST['passwd'])) { - return array('SID' => '', 'error' => null); - } - - if (is_ipbanned()) { - $login_error = __('The login form is currently disabled ' . - 'for your IP address, probably due ' . - 'to sustained spam attacks. Sorry for the ' . - 'inconvenience.'); - return array('SID' => '', 'error' => $login_error); - } - - $dbh = DB::connect(); - $userID = uid_from_loginname($_REQUEST['user']); - - if (user_suspended($userID)) { - $login_error = __('Account suspended'); - return array('SID' => '', 'error' => $login_error); - } - - switch (check_passwd($userID, $_REQUEST['passwd'])) { - case -1: - $login_error = __('Your password has been reset. ' . - 'If you just created a new account, please ' . - 'use the link from the confirmation email ' . - 'to set an initial password. Otherwise, ' . - 'please request a reset key on the %s' . - 'Password Reset%s page.', '', - ''); - return array('SID' => '', 'error' => $login_error); - case 0: - $login_error = __("Bad username or password."); - return array('SID' => '', 'error' => $login_error); - case 1: - break; - } - - $logged_in = 0; - $num_tries = 0; - - /* Generate a session ID and store it. */ - while (!$logged_in && $num_tries < 5) { - $new_sid = new_sid(); - $q = "INSERT INTO Sessions (UsersID, SessionID, LastUpdateTS)" - ." VALUES (" . $userID . ", '" . $new_sid . "', " . strval(time()) . ")"; - $result = $dbh->exec($q); - - /* Query will fail if $new_sid is not unique. */ - if ($result) { - $logged_in = 1; - break; - } - - $num_tries++; - } - - if (!$logged_in) { - $login_error = __('An error occurred trying to generate a user session.'); - return array('SID' => $new_sid, 'error' => $login_error); - } - - $q = "UPDATE Users SET LastLogin = " . strval(time()) . ", "; - $q.= "LastLoginIPAddress = " . $dbh->quote($_SERVER['REMOTE_ADDR']) . " "; - $q.= "WHERE ID = $userID"; - $dbh->exec($q); - - /* Set the SID cookie. */ - if (isset($_POST['remember_me']) && $_POST['remember_me'] == "on") { - /* Set cookies for 30 days. */ - $timeout = config_get_int('options', 'persistent_cookie_timeout'); - $cookie_time = time() + $timeout; - - /* Set session for 30 days. */ - $q = "UPDATE Sessions SET LastUpdateTS = $cookie_time "; - $q.= "WHERE SessionID = '$new_sid'"; - $dbh->exec($q); - } else { - $cookie_time = 0; - } - - setcookie("AURSID", $new_sid, $cookie_time, "/", null, !empty($_SERVER['HTTPS']), true); - - $referer = in_request('referer'); - if (strpos($referer, aur_location()) !== 0) { - $referer = '/'; - } - header("Location: " . get_uri($referer)); - $login_error = ""; - return array('SID' => $new_sid, 'error' => null); -} - -/** - * Determine if the user is using a banned IP address - * - * @return bool True if IP address is banned, otherwise false - */ -function is_ipbanned() { - $dbh = DB::connect(); - - $q = "SELECT * FROM Bans WHERE IPAddress = " . $dbh->quote($_SERVER['REMOTE_ADDR']); - $result = $dbh->query($q); - - return ($result->fetchColumn() ? true : false); -} - -/** - * Validate a username against a collection of rules - * - * The username must be longer or equal to the configured minimum length. It - * must be shorter or equal to the configured maximum length. It must start and - * end with either a letter or a number. It can contain one period, hypen, or - * underscore. Returns boolean of whether name is valid. - * - * @param string $user Username to validate - * - * @return bool True if username meets criteria, otherwise false - */ -function valid_username($user) { - $length_min = config_get_int('options', 'username_min_len'); - $length_max = config_get_int('options', 'username_max_len'); - - if (strlen($user) < $length_min || strlen($user) > $length_max) { - return false; - } else if (!preg_match("/^[a-z0-9]+[.\-_]?[a-z0-9]+$/Di", $user)) { - return false; - } - - return true; -} - -/** - * Determine if a user already has a proposal open about themselves - * - * @param string $user Username to checkout for open proposal - * - * @return bool True if there is an open proposal about the user, otherwise false - */ -function open_user_proposals($user) { - $dbh = DB::connect(); - $q = "SELECT * FROM TU_VoteInfo WHERE User = " . $dbh->quote($user) . " "; - $q.= "AND End > " . strval(time()); - $result = $dbh->query($q); - - return ($result->fetchColumn() ? true : false); -} - -/** - * Add a new Trusted User proposal to the database - * - * @param string $agenda The agenda of the vote - * @param string $user The use the vote is about - * @param int $votelength The length of time for the vote to last - * @param string $submitteruid The user ID of the individual who submitted the proposal - * - * @return void - */ -function add_tu_proposal($agenda, $user, $votelength, $quorum, $submitteruid) { - $dbh = DB::connect(); - - $q = "SELECT COUNT(*) FROM Users WHERE (AccountTypeID = 2 OR AccountTypeID = 4)"; - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - $active_tus = $row[0]; - - $q = "INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End, Quorum, "; - $q.= "SubmitterID, ActiveTUs) VALUES "; - $q.= "(" . $dbh->quote($agenda) . ", " . $dbh->quote($user) . ", "; - $q.= strval(time()) . ", " . strval(time()) . " + " . $dbh->quote($votelength); - $q.= ", " . $dbh->quote($quorum) . ", " . $submitteruid . ", "; - $q.= $active_tus . ")"; - $result = $dbh->exec($q); -} - -/** - * Add a reset key to the database for a specified user - * - * @param string $resetkey A password reset key to be stored in database - * @param string $uid The user ID to store the reset key for - * - * @return void - */ -function create_resetkey($resetkey, $uid) { - $dbh = DB::connect(); - $q = "UPDATE Users "; - $q.= "SET ResetKey = '" . $resetkey . "' "; - $q.= "WHERE ID = " . $uid; - $dbh->exec($q); -} - -/** - * Send a reset key to a specific e-mail address - * - * @param string $user User name or email address of the user - * @param bool $welcome Whether to use the welcome message - * - * @return void - */ -function send_resetkey($user, $welcome=false) { - $uid = uid_from_loginname($user); - if ($uid == null) { - return; - } - - /* We (ab)use new_sid() to get a random 32 characters long string. */ - $resetkey = new_sid(); - create_resetkey($resetkey, $uid); - - /* Send e-mail with confirmation link. */ - notify(array($welcome ? 'welcome' : 'send-resetkey', $uid)); -} - -/** - * Change a user's password in the database if reset key and e-mail are correct - * - * @param string $password The new password - * @param string $resetkey Code e-mailed to a user to reset a password - * @param string $user User name or email address of the user - * - * @return string|void Redirect page if successful, otherwise return error message - */ -function password_reset($password, $resetkey, $user) { - $hash = password_hash($password, PASSWORD_DEFAULT); - - $dbh = DB::connect(); - $q = "UPDATE Users SET "; - $q.= "Passwd = " . $dbh->quote($hash) . ", "; - $q.= "ResetKey = '' "; - $q.= "WHERE ResetKey != '' "; - $q.= "AND ResetKey = " . $dbh->quote($resetkey) . " "; - $q.= "AND (Email = " . $dbh->quote($user) . " OR "; - $q.= "UserName = " . $dbh->quote($user) . ")"; - $result = $dbh->exec($q); - - if (!$result) { - $error = __('Invalid e-mail and reset key combination.'); - return $error; - } else { - header('Location: ' . get_uri('/passreset/') . '?step=complete'); - exit(); - } -} - -/** - * Determine if the password is longer than the minimum length - * - * @param string $passwd The password to check - * - * @return bool True if longer than minimum length, otherwise false - */ -function good_passwd($passwd) { - $length_min = config_get_int('options', 'passwd_min_len'); - return (strlen($passwd) >= $length_min); -} - -/** - * Determine if the password is correct and salt it if it hasn't been already - * - * @param int $user_id The user ID to check the password against - * @param string $passwd The password the visitor sent - * - * @return int Positive if password is correct, negative if password is unset - */ -function check_passwd($user_id, $passwd) { - $dbh = DB::connect(); - - /* Get password hash and salt. */ - $q = "SELECT Passwd, Salt FROM Users WHERE ID = " . intval($user_id); - $result = $dbh->query($q); - if (!$result) { - return 0; - } - $row = $result->fetch(PDO::FETCH_ASSOC); - if (!$row) { - return 0; - } - $hash = $row['Passwd']; - $salt = $row['Salt']; - if (!$hash) { - return -1; - } - - /* Verify the password hash. */ - if (!password_verify($passwd, $hash)) { - /* Invalid password, fall back to MD5. */ - if (md5($salt . $passwd) != $hash) { - return 0; - } - } - - /* Password correct, migrate the hash if necessary. */ - if (password_needs_rehash($hash, PASSWORD_DEFAULT)) { - $hash = password_hash($passwd, PASSWORD_DEFAULT); - - $q = "UPDATE Users SET Passwd = " . $dbh->quote($hash) . " "; - $q.= "WHERE ID = " . intval($user_id); - $dbh->query($q); - } - - return 1; -} - -/** - * Determine if the PGP key fingerprint is valid (must be 40 hexadecimal digits) - * - * @param string $fingerprint PGP fingerprint to check if valid - * - * @return bool True if the fingerprint is 40 hexadecimal digits, otherwise false - */ -function valid_pgp_fingerprint($fingerprint) { - $fingerprint = str_replace(" ", "", $fingerprint); - return (strlen($fingerprint) == 40 && ctype_xdigit($fingerprint)); -} - -/** - * Determine if the SSH public key is valid - * - * @param string $pubkey SSH public key to check - * - * @return bool True if the SSH public key is valid, otherwise false - */ -function valid_ssh_pubkey($pubkey) { - $valid_prefixes = explode(' ', config_get('auth', 'valid-keytypes')); - - $has_valid_prefix = false; - foreach ($valid_prefixes as $prefix) { - if (strpos($pubkey, $prefix . " ") === 0) { - $has_valid_prefix = true; - break; - } - } - if (!$has_valid_prefix) { - return false; - } - - $tokens = explode(" ", $pubkey); - if (empty($tokens[1])) { - return false; - } - - return (base64_encode(base64_decode($tokens[1], true)) == $tokens[1]); -} - -/** - * Determine if the user account has been suspended - * - * @param string $id The ID of user to check if suspended - * - * @return bool True if the user is suspended, otherwise false - */ -function user_suspended($id) { - $dbh = DB::connect(); - if (!$id) { - return false; - } - $q = "SELECT Suspended FROM Users WHERE ID = " . $id; - $result = $dbh->query($q); - if ($result) { - $row = $result->fetch(PDO::FETCH_NUM); - if ($row[0]) { - return true; - } - } - return false; -} - -/** - * Delete a specified user account from the database - * - * @param int $id The user ID of the account to be deleted - * - * @return void - */ -function user_delete($id) { - $dbh = DB::connect(); - $id = intval($id); - - /* - * These are normally already taken care of by propagation constraints - * but it is better to be explicit here. - */ - $fields_delete = array( - array("Sessions", "UsersID"), - array("PackageVotes", "UsersID"), - array("PackageNotifications", "UserID") - ); - - $fields_set_null = array( - array("PackageBases", "SubmitterUID"), - array("PackageBases", "MaintainerUID"), - array("PackageBases", "PackagerUID"), - array("PackageComments", "UsersID"), - array("PackageComments", "DelUsersID"), - array("PackageRequests", "UsersID"), - array("TU_VoteInfo", "SubmitterID"), - array("TU_Votes", "UserID") - ); - - foreach($fields_delete as list($table, $field)) { - $q = "DELETE FROM " . $table . " "; - $q.= "WHERE " . $field . " = " . $id; - $dbh->query($q); - } - - foreach($fields_set_null as list($table, $field)) { - $q = "UPDATE " . $table . " SET " . $field . " = NULL "; - $q.= "WHERE " . $field . " = " . $id; - $dbh->query($q); - } - - $q = "DELETE FROM Users WHERE ID = " . $id; - $dbh->query($q); - return; -} - -/** - * Remove the session from the database on logout - * - * @param string $sid User's session ID - * - * @return void - */ -function delete_session_id($sid) { - $dbh = DB::connect(); - - $q = "DELETE FROM Sessions WHERE SessionID = " . $dbh->quote($sid); - $dbh->query($q); -} - -/** - * Remove all sessions belonging to a particular user - * - * @param int $uid ID of user to remove all sessions for - * - * @return void - */ -function delete_user_sessions($uid) { - $dbh = DB::connect(); - - $q = "DELETE FROM Sessions WHERE UsersID = " . intval($uid); - $dbh->exec($q); -} - -/** - * Remove sessions from the database that have exceed the timeout - * - * @return void - */ -function clear_expired_sessions() { - $dbh = DB::connect(); - - $timeout = config_get_int('options', 'login_timeout'); - $q = "DELETE FROM Sessions WHERE LastUpdateTS < (" . strval(time()) . " - " . $timeout . ")"; - $dbh->query($q); - - return; -} - -/** - * Get account details for a specific user - * - * @param string $uid The User ID of account to get information for - * @param string $username The username of the account to get for - * - * @return array Account details for the specified user - */ -function account_details($uid, $username) { - $dbh = DB::connect(); - $q = "SELECT Users.*, AccountTypes.AccountType "; - $q.= "FROM Users, AccountTypes "; - $q.= "WHERE AccountTypes.ID = Users.AccountTypeID "; - if (!empty($uid)) { - $q.= "AND Users.ID = ".intval($uid); - } else { - $q.= "AND Users.Username = " . $dbh->quote($username); - } - $result = $dbh->query($q); - - if ($result) { - $row = $result->fetch(PDO::FETCH_ASSOC); - } - - return $row; -} - -/** - * Determine if a user has already voted on a specific proposal - * - * @param string $voteid The ID of the Trusted User proposal - * @param string $uid The ID to check if the user already voted - * - * @return bool True if the user has already voted, otherwise false - */ -function tu_voted($voteid, $uid) { - $dbh = DB::connect(); - - $q = "SELECT COUNT(*) FROM TU_Votes "; - $q.= "WHERE VoteID = " . intval($voteid) . " AND UserID = " . intval($uid); - $result = $dbh->query($q); - if ($result->fetchColumn() > 0) { - return true; - } - else { - return false; - } -} - -/** - * Get all current Trusted User proposals from the database - * - * @param string $order Ascending or descending order for the proposal listing - * - * @return array The details for all current Trusted User proposals - */ -function current_proposal_list($order) { - $dbh = DB::connect(); - - $q = "SELECT * FROM TU_VoteInfo WHERE End > " . time() . " ORDER BY Submitted " . $order; - $result = $dbh->query($q); - - $details = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $details[] = $row; - } - - return $details; -} - -/** - * Get a subset of all past Trusted User proposals from the database - * - * @param string $order Ascending or descending order for the proposal listing - * @param string $lim The number of proposals to list with the offset - * - * @return array The details for the subset of past Trusted User proposals - */ -function past_proposal_list($order, $lim) { - $dbh = DB::connect(); - - $q = "SELECT * FROM TU_VoteInfo WHERE End < " . time() . " ORDER BY Submitted " . $order . $lim; - $result = $dbh->query($q); - - $details = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $details[] = $row; - } - - return $details; -} - -/** - * Get the vote ID of the last vote of all Trusted Users - * - * @return array The vote ID of the last vote of each Trusted User - */ -function last_votes_list() { - $dbh = DB::connect(); - - $q = "SELECT UserID, MAX(VoteID) AS LastVote FROM TU_Votes, "; - $q .= "TU_VoteInfo, Users WHERE TU_VoteInfo.ID = TU_Votes.VoteID AND "; - $q .= "TU_VoteInfo.End < " . strval(time()) . " AND "; - $q .= "Users.ID = TU_Votes.UserID AND (Users.AccountTypeID = 2 OR Users.AccountTypeID = 4) "; - $q .= "GROUP BY UserID ORDER BY LastVote DESC, UserName ASC"; - $result = $dbh->query($q); - - $details = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $details[] = $row; - } - - return $details; -} - -/** - * Determine the total number of Trusted User proposals - * - * @return string The total number of Trusted User proposals - */ -function proposal_count() { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) FROM TU_VoteInfo"; - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - return $row[0]; -} - -/** - * Get all details related to a specific vote from the database - * - * @param string $voteid The ID of the Trusted User proposal - * - * @return array All stored details for a specific vote - */ -function vote_details($voteid) { - $dbh = DB::connect(); - - $q = "SELECT * FROM TU_VoteInfo "; - $q.= "WHERE ID = " . intval($voteid); - - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_ASSOC); - - return $row; -} - -/** - * Get an alphabetical list of users who voted for a proposal with HTML links - * - * @param string $voteid The ID of the Trusted User proposal - * - * @return array All users who voted for a specific proposal - */ -function voter_list($voteid) { - $dbh = DB::connect(); - - $whovoted = array(); - - $q = "SELECT tv.UserID,U.Username "; - $q.= "FROM TU_Votes tv, Users U "; - $q.= "WHERE tv.VoteID = " . intval($voteid); - $q.= " AND tv.UserID = U.ID "; - $q.= "ORDER BY Username"; - - $result = $dbh->query($q); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $whovoted[] = $row['Username']; - } - } - return $whovoted; -} - -/** - * Cast a vote for a specific user proposal - * - * @param string $voteid The ID of the proposal being voted on - * @param string $uid The user ID of the individual voting - * @param string $vote Vote position, either "Yes", "No", or "Abstain" - * @param int $newtotal The total number of votes after the user has voted - * - * @return void - */ -function cast_proposal_vote($voteid, $uid, $vote, $newtotal) { - $dbh = DB::connect(); - - $q = "UPDATE TU_VoteInfo SET " . $vote . " = (" . $newtotal . ") WHERE ID = " . $voteid; - $result = $dbh->exec($q); - - $q = "INSERT INTO TU_Votes (VoteID, UserID) VALUES (" . intval($voteid) . ", " . intval($uid) . ")"; - $result = $dbh->exec($q); -} - -/** - * Verify a user has the proper permissions to edit an account - * - * @param array $acctinfo User account information for edited account - * - * @return bool True if permission to edit the account, otherwise false - */ -function can_edit_account($acctinfo) { - if ($acctinfo['AccountType'] == 'Developer' || - $acctinfo['AccountType'] == 'Trusted User & Developer') { - return has_credential(CRED_ACCOUNT_EDIT_DEV); - } - - $uid = $acctinfo['ID']; - return has_credential(CRED_ACCOUNT_EDIT, array($uid)); -} - -/* - * Compute the fingerprint of an SSH key. - * - * @param string $ssh_key The SSH public key to retrieve the fingerprint for - * - * @return string The SSH key fingerprint - */ -function ssh_key_fingerprint($ssh_key) { - $tmpfile = tempnam(sys_get_temp_dir(), "aurweb"); - file_put_contents($tmpfile, $ssh_key); - - /* - * The -l option of ssh-keygen can be used to show the fingerprint of - * the specified public key file. Expected output format: - * - * 2048 SHA256:uBBTXmCNjI2CnLfkuz9sG8F+e9/T4C+qQQwLZWIODBY user@host (RSA) - * - * ... where 2048 is the key length, the second token is the actual - * fingerprint, followed by the key comment and the key type. - */ - - $cmd = "/usr/bin/ssh-keygen -l -f " . escapeshellarg($tmpfile); - exec($cmd, $out, $ret); - if ($ret !== 0 || count($out) !== 1) { - return false; - } - - unlink($tmpfile); - - $tokens = explode(' ', $out[0]); - if (count($tokens) < 4) { - return false; - } - - $tokens = explode(':', $tokens[1]); - if (count($tokens) != 2 || $tokens[0] != 'SHA256') { - return false; - } - - return $tokens[1]; -} - -/* - * Get the SSH public keys associated with an account. - * - * @param int $uid The user ID of the account to retrieve the keys for. - * - * @return array An array representing the keys - */ -function account_get_ssh_keys($uid) { - $dbh = DB::connect(); - $q = "SELECT PubKey FROM SSHPubKeys WHERE UserID = " . intval($uid); - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/* - * Set the SSH public keys associated with an account. - * - * @param int $uid The user ID of the account to assign the keys to. - * @param array $ssh_keys The SSH public keys. - * @param array $ssh_fingerprints The corresponding SSH key fingerprints. - * - * @return bool Boolean flag indicating success or failure. - */ -function account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints) { - $dbh = DB::connect(); - - $q = sprintf("DELETE FROM SSHPubKeys WHERE UserID = %d", $uid); - $dbh->exec($q); - - $ssh_fingerprint = reset($ssh_fingerprints); - foreach ($ssh_keys as $ssh_key) { - $q = sprintf( - "INSERT INTO SSHPubKeys (UserID, Fingerprint, PubKey) " . - "VALUES (%d, %s, %s)", $uid, - $dbh->quote($ssh_fingerprint), $dbh->quote($ssh_key) - ); - $dbh->exec($q); - $ssh_fingerprint = next($ssh_fingerprints); - } - - return true; -} - -/* - * Invoke the email notification script. - * - * @param string $params Command line parameters for the script. - * - * @return void - */ -function notify($params) { - $cmd = config_get('notifications', 'notify-cmd'); - foreach ($params as $param) { - $cmd .= ' ' . escapeshellarg($param); - } - - $descspec = array( - 0 => array('pipe', 'r'), - 1 => array('pipe', 'w'), - ); - - $p = proc_open($cmd, $descspec, $pipes); - - if (!is_resource($p)) { - return false; - } - - fclose($pipes[0]); - fclose($pipes[1]); - - return proc_close($p); -} - -/* - * Obtain a list of terms a given user has not yet accepted. - * - * @param int $uid The ID of the user to obtain terms for. - * - * @return array A list of terms the user has not yet accepted. - */ -function fetch_updated_terms($uid) { - $dbh = DB::connect(); - - $q = "SELECT ID, Terms.Revision, Description, URL "; - $q .= "FROM Terms LEFT JOIN AcceptedTerms "; - $q .= "ON AcceptedTerms.TermsID = Terms.ID "; - $q .= "AND AcceptedTerms.UsersID = " . intval($uid) . " "; - $q .= "WHERE AcceptedTerms.Revision IS NULL OR "; - $q .= "AcceptedTerms.Revision < Terms.Revision"; - - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(); - } else { - return array(); - } -} - -/* - * Accept a list of given terms. - * - * @param int $uid The ID of the user to accept the terms. - * @param array $termrev An array mapping each term to the accepted revision. - * - * @return void - */ -function accept_terms($uid, $termrev) { - $dbh = DB::connect(); - - $q = "SELECT TermsID, Revision FROM AcceptedTerms "; - $q .= "WHERE UsersID = " . intval($uid); - - $result = $dbh->query($q); - - if (!$result) { - return; - } - - $termrev_update = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $id = $row['TermsID']; - if (!array_key_exists($id, $termrev)) { - continue; - } - if ($row['Revision'] < $termrev[$id]) { - $termrev_update[$id] = $termrev[$id]; - } - } - $termrev_add = array_diff_key($termrev, $termrev_update); - - foreach ($termrev_add as $id => $rev) { - $q = "INSERT INTO AcceptedTerms (TermsID, UsersID, Revision) "; - $q .= "VALUES (" . intval($id) . ", " . intval($uid) . ", "; - $q .= intval($rev) . ")"; - $dbh->exec($q); - } - - foreach ($termrev_update as $id => $rev) { - $q = "UPDATE AcceptedTerms "; - $q .= "SET Revision = " . intval($rev) . " "; - $q .= "WHERE TermsID = " . intval($id) . " AND "; - $q .= "UsersID = " . intval($uid); - $dbh->exec($q); - } -} - -function account_comments($uid, $limit, $offset=0) { - $dbh = DB::connect(); - $q = "SELECT PackageComments.ID, Comments, UsersID, "; - $q.= "PackageBaseId, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, "; - $q.= "PinnedTS, "; - $q.= "C.UserName as DelUserName, RenderedComment, "; - $q.= "PB.ID as PackageBaseID, PB.Name as PackageBaseName "; - $q.= "FROM PackageComments "; - $q.= "LEFT JOIN PackageBases PB ON PackageComments.PackageBaseID = PB.ID "; - $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; - $q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID "; - $q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID "; - $q.= "WHERE A.ID = " . $dbh->quote($uid) . " "; - $q.= "ORDER BY CommentTS DESC"; - - if ($limit > 0) { - $q.=" LIMIT " . intval($limit); - } - - if ($offset > 0) { - $q.=" OFFSET " . intval($offset); - } - - $result = $dbh->query($q); - if (!$result) { - return null; - } - - return $result->fetchAll(); -} - -function account_comments_count($uid) { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) "; - $q.= "FROM PackageComments "; - $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; - $q.= "WHERE A.ID = " . $dbh->quote($uid); - - $result = $dbh->query($q); - return $result->fetchColumn(); -} - -/* - * Compute the list of active CAPTCHA salts. The salt changes based on the - * number of registered users. This ensures that new users always use a - * different salt and protects against hardcoding the CAPTCHA response. - * - * The first CAPTCHA in the list is the most recent one and should be used for - * new CAPTCHA challenges. The other ones are slightly outdated but may still - * be valid for recent challenges that were created before the number of users - * increased. The current implementation ensures that we can still use our - * CAPTCHA salt, even if five new users registered since the CAPTCHA challenge - * was created. - * - * @return string The list of active salts, the first being the most recent - * one. - */ -function get_captcha_salts() { - $dbh = DB::connect(); - $q = "SELECT count(*) FROM Users"; - $result = $dbh->query($q); - $user_count = $result->fetchColumn(); - - $ret = array(); - for ($i = 0; $i <= 5; $i++) { - array_push($ret, 'aurweb-' . ($user_count - $i)); - } - return $ret; -} - -/* - * Return the CAPTCHA challenge for a given salt. - * - * @param string $salt The salt to be used for the CAPTCHA computation. - * - * @return string The challenge as a string. - */ -function get_captcha_challenge($salt) { - $token = substr(md5($salt), 0, 3); - return "LC_ALL=C pacman -V|sed -r 's#[0-9]+#" . $token . "#g'|md5sum|cut -c1-6"; -} - -/* - * Compute CAPTCHA answer for a given salt. - * - * @param string $salt The salt to be used for the CAPTCHA computation. - * - * @return string The correct answer as a string. - */ -function get_captcha_answer($salt) { - $token = substr(md5($salt), 0, 3); - $text = <<quote($_COOKIE["AURSID"]); - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if (!$row) { - # Invalid SessionID - hacker alert! - # - $failed = 1; - } else { - $last_update = $row[0]; - if ($last_update + $timeout <= $row[1]) { - $failed = 2; - } - } - - if ($failed == 1) { - # clear out the hacker's cookie, and send them to a naughty page - # why do you have to be so harsh on these people!? - # - setcookie("AURSID", "", 1, "/", null, !empty($_SERVER['HTTPS']), true); - unset($_COOKIE['AURSID']); - } elseif ($failed == 2) { - # session id timeout was reached and they must login again. - # - delete_session_id($_COOKIE["AURSID"]); - - setcookie("AURSID", "", 1, "/", null, !empty($_SERVER['HTTPS']), true); - unset($_COOKIE['AURSID']); - } else { - # still logged in and haven't reached the timeout, go ahead - # and update the idle timestamp - - # Only update the timestamp if it is less than the - # current time plus $timeout. - # - # This keeps 'remembered' sessions from being - # overwritten. - if ($last_update < time() + $timeout) { - $q = "UPDATE Sessions SET LastUpdateTS = " . strval(time()) . " "; - $q.= "WHERE SessionID = " . $dbh->quote($_COOKIE["AURSID"]); - $dbh->exec($q); - } - } - } - return; -} - -/** - * Redirect user to the Terms of Service agreement if there are updated terms. - * - * @return void - */ -function check_tos() { - if (!isset($_COOKIE["AURSID"])) { - return; - } - - $path = $_SERVER['PATH_INFO']; - $route = get_route($path); - if (!$route || $route == "tos.php") { - return; - } - - if (count(fetch_updated_terms(uid_from_sid($_COOKIE["AURSID"]))) > 0) { - header('Location: ' . get_uri('/tos')); - exit(); - } -} - -/** - * Verify the supplied CSRF token matches expected token - * - * @return bool True if the CSRF token is the same as the cookie SID, otherwise false - */ -function check_token() { - if (isset($_POST['token']) && isset($_COOKIE['AURSID'])) { - return ($_POST['token'] == $_COOKIE['AURSID']); - } else { - return false; - } -} - -/** - * Verify a user supplied e-mail against RFC 3696 and DNS records - * - * @param string $addy E-mail address being validated in foo@example.com format - * - * @return bool True if e-mail passes validity checks, otherwise false - */ -function valid_email($addy) { - // check against RFC 3696 - if (filter_var($addy, FILTER_VALIDATE_EMAIL) === false) { - return false; - } - - // check dns for mx, a, aaaa records - list($local, $domain) = explode('@', $addy); - if (!(checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA'))) { - return false; - } - - return true; -} - -/** - * Verify that a given URL is valid and uses the HTTP(s) protocol - * - * @param string $url URL of the home page to be validated - * - * @return bool True if URL passes validity checks, false otherwise - */ -function valid_homepage($url) { - if (filter_var($url, FILTER_VALIDATE_URL) === false) { - return false; - } - - $url_components = parse_url($url); - if (!in_array($url_components['scheme'], array('http', 'https'))) { - return false; - } - - return true; -} - -/** - * Generate a unique session ID - * - * @return string MD5 hash of the concatenated user IP, random number, and current time - */ -function new_sid() { - return md5($_SERVER['REMOTE_ADDR'] . uniqid(mt_rand(), true)); -} - -/** - * Determine the user's username in the database using a user ID - * - * @param string $id User's ID - * - * @return string Username if it exists, otherwise null - */ -function username_from_id($id) { - $id = intval($id); - - $dbh = DB::connect(); - $q = "SELECT Username FROM Users WHERE ID = " . $dbh->quote($id); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's username in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string Username of the visitor - */ -function username_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT Username "; - $q.= "FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return ""; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Format a user name for inclusion in HTML data - * - * @param string $username The user name to format - * - * @return string The generated HTML code for the account link - */ -function html_format_username($username) { - $username_fmt = $username ? htmlspecialchars($username, ENT_QUOTES) : __("None"); - - if ($username && isset($_COOKIE["AURSID"])) { - $link = '' . $username_fmt . ''; - return $link; - } else { - return $username_fmt; - } -} - -/** - * Format the maintainer and co-maintainers for inclusion in HTML data - * - * @param string $maintainer The user name of the maintainer - * @param array $comaintainers The list of co-maintainer user names - * - * @return string The generated HTML code for the account links - */ -function html_format_maintainers($maintainer, $comaintainers) { - $code = html_format_username($maintainer); - - if (count($comaintainers) > 0) { - $code .= ' ('; - foreach ($comaintainers as $comaintainer) { - $code .= html_format_username($comaintainer); - if ($comaintainer !== end($comaintainers)) { - $code .= ', '; - } - } - $code .= ')'; - } - - return $code; -} - -/** - * Format a link in the package actions box - * - * @param string $uri The link target - * @param string $inner The HTML code to use for the link label - * - * @return string The generated HTML code for the action link - */ -function html_action_link($uri, $inner) { - if (isset($_COOKIE["AURSID"])) { - $code = ''; - } else { - $code = ''; - } - $code .= $inner . ''; - - return $code; -} - -/** - * Format a form in the package actions box - * - * @param string $uri The link target - * @param string $action The action name (passed as HTTP POST parameter) - * @param string $inner The HTML code to use for the link label - * - * @return string The generated HTML code for the action link - */ -function html_action_form($uri, $action, $inner) { - if (isset($_COOKIE["AURSID"])) { - $code = '
    '; - $code .= ''; - $code .= '
    '; - } else { - $code = ''; - $code .= $inner . ''; - } - - return $code; -} - -/** - * Determine the user's e-mail address in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string User's e-mail address as given during registration - */ -function email_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT Email "; - $q.= "FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return ""; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's account type in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string Account type of user ("User", "Trusted User", or "Developer") - */ -function account_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT AccountType "; - $q.= "FROM Users, AccountTypes, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND AccountTypes.ID = Users.AccountTypeID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return ""; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's ID in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string|int The user's name, 0 on query failure - */ -function uid_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT Users.ID "; - $q.= "FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return 0; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Common AUR header displayed on all pages - * - * @global string $LANG Language selected by the visitor - * @global array $SUPPORTED_LANGS Languages that are supported by the AUR - * @param string $title Name of the AUR page to be displayed on browser - * - * @return void - */ -function html_header($title="", $details=array()) { - global $LANG; - global $SUPPORTED_LANGS; - - include('header.php'); - return; -} - -/** - * Common AUR footer displayed on all pages - * - * @param string $ver The AUR version - * - * @return void - */ -function html_footer($ver="") { - include('footer.php'); - return; -} - -/** - * Determine if a user has permission to submit a package - * - * @param string $name Name of the package to be submitted - * @param string $sid User's session ID - * - * @return int 0 if the user can't submit, 1 if the user can submit - */ -function can_submit_pkgbase($name="", $sid="") { - if (!$name || !$sid) {return 0;} - $dbh = DB::connect(); - $q = "SELECT MaintainerUID "; - $q.= "FROM PackageBases WHERE Name = " . $dbh->quote($name); - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if (!$row[0]) { - return 1; - } - $my_uid = uid_from_sid($sid); - - if ($row[0] === NULL || $row[0] == $my_uid) { - return 1; - } - - return 0; -} - -/** - * Determine if a package can be overwritten by some package base - * - * @param string $name Name of the package to be submitted - * @param int $base_id The ID of the package base - * - * @return bool True if the package can be overwritten, false if not - */ -function can_submit_pkg($name, $base_id) { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) FROM Packages WHERE "; - $q.= "Name = " . $dbh->quote($name) . " AND "; - $q.= "PackageBaseID <> " . intval($base_id); - $result = $dbh->query($q); - - if (!$result) return false; - return ($result->fetchColumn() == 0); -} - -/** - * Recursively delete a directory - * - * @param string $dirname Name of the directory to be removed - * - * @return void - */ -function rm_tree($dirname) { - if (empty($dirname) || !is_dir($dirname)) return; - - foreach (scandir($dirname) as $item) { - if ($item != '.' && $item != '..') { - $path = $dirname . '/' . $item; - if (is_file($path) || is_link($path)) { - unlink($path); - } - else { - rm_tree($path); - } - } - } - - rmdir($dirname); - - return; -} - - /** - * Determine the user's ID in the database using a username - * - * @param string $username The username of an account - * - * @return string Return user ID if exists for username, otherwise null - */ -function uid_from_username($username) { - $dbh = DB::connect(); - $q = "SELECT ID FROM Users WHERE Username = " . $dbh->quote($username); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's ID in the database using a username or email address - * - * @param string $username The username or email address of an account - * - * @return string Return user ID if exists, otherwise null - */ -function uid_from_loginname($loginname) { - $uid = uid_from_username($loginname); - if (!$uid) { - $uid = uid_from_email($loginname); - } - return $uid; -} - -/** - * Determine the user's ID in the database using an e-mail address - * - * @param string $email An e-mail address in foo@example.com format - * - * @return string The user's ID - */ -function uid_from_email($email) { - $dbh = DB::connect(); - $q = "SELECT ID FROM Users WHERE Email = " . $dbh->quote($email); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Generate clean url with edited/added user values - * - * Makes a clean string of variables for use in URLs based on current $_GET and - * list of values to edit/add to that. Any empty variables are discarded. - * - * @example print "http://example.com/test.php?" . mkurl("foo=bar&bar=baz") - * - * @param string $append string of variables and values formatted as in URLs - * - * @return string clean string of variables to append to URL, urlencoded - */ -function mkurl($append) { - $get = $_GET; - $append = explode('&', $append); - $uservars = array(); - $out = ''; - - foreach ($append as $i) { - $ex = explode('=', $i); - $uservars[$ex[0]] = $ex[1]; - } - - foreach ($uservars as $k => $v) { $get[$k] = $v; } - - foreach ($get as $k => $v) { - if ($v !== '') { - $out .= '&' . urlencode($k) . '=' . urlencode($v); - } - } - - return substr($out, 5); -} - -/** - * Get a package comment - * - * @param int $comment_id The ID of the comment - * - * @return array The user ID and comment OR null, null in case of an error - */ -function comment_by_id($comment_id) { - $dbh = DB::connect(); - $q = "SELECT UsersID, Comments FROM PackageComments "; - $q.= "WHERE ID = " . intval($comment_id); - $result = $dbh->query($q); - if (!$result) { - return array(null, null); - } - - return $result->fetch(PDO::FETCH_NUM); -} - -/** - * Process submitted comments so any links can be followed - * - * @param string $comment Raw user submitted package comment - * - * @return string User comment with links printed in HTML - */ -function parse_comment($comment) { - $url_pattern = '/(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' . - '(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))/iS'; - - $matches = preg_split($url_pattern, $comment, -1, - PREG_SPLIT_DELIM_CAPTURE); - - $html = ''; - for ($i = 0; $i < count($matches); $i++) { - if ($i % 2) { - # convert links - $html .= '' . htmlspecialchars($matches[$i]) . ''; - } - else { - # convert everything else - $html .= nl2br(htmlspecialchars($matches[$i])); - } - } - - return $html; -} - -/** - * Wrapper for beginning a database transaction - */ -function begin_atomic_commit() { - $dbh = DB::connect(); - $dbh->beginTransaction(); -} - -/** - * Wrapper for committing a database transaction - */ -function end_atomic_commit() { - $dbh = DB::connect(); - $dbh->commit(); -} - -/** - * Merge pkgbase and package options - * - * Merges entries of the first and the second array. If any key appears in both - * arrays and the corresponding value in the second array is either a non-array - * type or a non-empty array, the value from the second array replaces the - * value from the first array. If the value from the second array is an array - * containing a single empty string, the value in the resulting array becomes - * an empty array instead. If the value in the second array is empty, the - * resulting array contains the value from the first array. - * - * @param array $pkgbase_info Options from the pkgbase section - * @param array $section_info Options from the package section - * - * @return array Merged information from both sections - */ -function array_pkgbuild_merge($pkgbase_info, $section_info) { - $pi = $pkgbase_info; - foreach ($section_info as $opt_key => $opt_val) { - if (is_array($opt_val)) { - if ($opt_val == array('')) { - $pi[$opt_key] = array(); - } elseif (count($opt_val) > 0) { - $pi[$opt_key] = $opt_val; - } - } else { - $pi[$opt_key] = $opt_val; - } - } - return $pi; -} - -/** - * Bound an integer value between two values - * - * @param int $n Integer value to bound - * @param int $min Lower bound - * @param int $max Upper bound - * - * @return int Bounded integer value - */ -function bound($n, $min, $max) { - return min(max($n, $min), $max); -} - -/** - * Return the URL of the AUR root - * - * @return string The URL of the AUR root - */ -function aur_location() { - $location = config_get('options', 'aur_location'); - if (substr($location, -1) != '/') { - $location .= '/'; - } - return $location; -} - -/** - * Calculate pagination templates - * - * @return array The array of pagination templates, per page, and offset values - */ -function calculate_pagination($total_comment_count) { - /* Sanitize paging variables. */ - if (isset($_GET["O"])) { - $_GET["O"] = max(intval($_GET["O"]), 0); - } else { - $_GET["O"] = 0; - } - $offset = $_GET["O"]; - - if (isset($_GET["PP"])) { - $_GET["PP"] = bound(intval($_GET["PP"]), 1, 250); - } else { - $_GET["PP"] = 10; - } - $per_page = $_GET["PP"]; - - // Page offsets start at zero, so page 2 has offset 1, which means that we - // need to add 1 to the offset to get the current page. - $current_page = ceil($offset / $per_page) + 1; - $num_pages = ceil($total_comment_count / $per_page); - $pagination_templs = array(); - - if ($current_page > 1) { - $previous_page = $current_page - 1; - $previous_offset = ($previous_page - 1) * $per_page; - $pagination_templs['« ' . __('First')] = 0; - $pagination_templs['‹ ' . __('Previous')] = $previous_offset; - } - - if ($current_page - 5 > 1) { - $pagination_templs["..."] = false; - } - - for ($i = max($current_page - 5, 1); $i <= min($num_pages, $current_page + 5); $i++) { - $pagination_templs[$i] = ($i - 1) * $per_page; - } - - if ($current_page + 5 < $num_pages) - $pagination_templs["... "] = false; - - if ($current_page < $num_pages) { - $pagination_templs[__('Next') . ' ›'] = $current_page * $per_page; - $pagination_templs[__('Last') . ' »'] = ($num_pages - 1) * $per_page; - } - - return array($pagination_templs, $per_page, $offset); -} diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php deleted file mode 100644 index 81c27bd9..00000000 --- a/web/lib/aurjson.class.php +++ /dev/null @@ -1,710 +0,0 @@ -version = intval($http_data['v']); - } - if ($this->version < 1 || $this->version > 6) { - return $this->json_error('Invalid version specified.'); - } - - if (!isset($http_data['type']) || !isset($http_data['arg'])) { - return $this->json_error('No request type/data specified.'); - } - if (!in_array($http_data['type'], self::$exposed_methods)) { - return $this->json_error('Incorrect request type specified.'); - } - - if (isset($http_data['search_by']) && !isset($http_data['by'])) { - $http_data['by'] = $http_data['search_by']; - } - if (isset($http_data['by']) && !in_array($http_data['by'], self::$exposed_fields)) { - return $this->json_error('Incorrect by field specified.'); - } - - $this->dbh = DB::connect(); - - if ($this->check_ratelimit($_SERVER['REMOTE_ADDR'])) { - header("HTTP/1.1 429 Too Many Requests"); - return $this->json_error('Rate limit reached'); - } - - $type = str_replace('-', '_', $http_data['type']); - if ($type == 'info' && $this->version >= 5) { - $type = 'multiinfo'; - } - $json = call_user_func(array(&$this, $type), $http_data); - - $etag = md5($json); - header("Etag: \"$etag\""); - /* - * Make sure to strip a few things off the - * if-none-match header. Stripping whitespace may not - * be required, but removing the quote on the incoming - * header is required to make the equality test. - */ - $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? - trim($_SERVER['HTTP_IF_NONE_MATCH'], "\t\n\r\" ") : false; - if ($if_none_match && $if_none_match == $etag) { - header('HTTP/1.1 304 Not Modified'); - return; - } - - if (isset($http_data['callback'])) { - $callback = $http_data['callback']; - if (!preg_match('/^[a-zA-Z0-9()_.]{1,128}$/D', $callback)) { - return $this->json_error('Invalid callback name.'); - } - header('content-type: text/javascript'); - return '/**/' . $callback . '(' . $json . ')'; - } else { - header('content-type: application/json'); - return $json; - } - } - - /* - * Check if an IP needs to be rate limited. - * - * @param $ip IP of the current request - * - * @return true if IP needs to be rate limited, false otherwise. - */ - private function check_ratelimit($ip) { - $limit = config_get("ratelimit", "request_limit"); - if ($limit == 0) { - return false; - } - - $this->update_ratelimit($ip); - - $status = false; - $value = get_cache_value('ratelimit:' . $ip, $status); - if (!$status) { - $stmt = $this->dbh->prepare(" - SELECT Requests FROM ApiRateLimit - WHERE IP = :ip"); - $stmt->bindParam(":ip", $ip); - $result = $stmt->execute(); - - if (!$result) { - return false; - } - - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $value = $row['Requests']; - } - - return $value > $limit; - } - - /* - * Update a rate limit for an IP by increasing it's requests value by one. - * - * @param $ip IP of the current request - * - * @return void - */ - private function update_ratelimit($ip) { - $window_length = config_get("ratelimit", "window_length"); - $db_backend = config_get("database", "backend"); - $time = time(); - $deletion_time = $time - $window_length; - - /* Try to use the cache. */ - $status = false; - $value = get_cache_value('ratelimit-ws:' . $ip, $status); - if (!$status || ($status && $value < $deletion_time)) { - if (set_cache_value('ratelimit-ws:' . $ip, $time, $window_length) && - set_cache_value('ratelimit:' . $ip, 1, $window_length)) { - return; - } - } else { - $value = get_cache_value('ratelimit:' . $ip, $status); - if ($status && set_cache_value('ratelimit:' . $ip, $value + 1, $window_length)) - return; - } - - /* Clean up old windows. */ - $stmt = $this->dbh->prepare(" - DELETE FROM ApiRateLimit - WHERE WindowStart < :time"); - $stmt->bindParam(":time", $deletion_time); - $stmt->execute(); - - if ($db_backend == "mysql") { - $stmt = $this->dbh->prepare(" - INSERT INTO ApiRateLimit - (IP, Requests, WindowStart) - VALUES (:ip, 1, :window_start) - ON DUPLICATE KEY UPDATE Requests=Requests+1"); - $stmt->bindParam(":ip", $ip); - $stmt->bindParam(":window_start", $time); - $stmt->execute(); - } elseif ($db_backend == "sqlite") { - $stmt = $this->dbh->prepare(" - INSERT OR IGNORE INTO ApiRateLimit - (IP, Requests, WindowStart) - VALUES (:ip, 0, :window_start);"); - $stmt->bindParam(":ip", $ip); - $stmt->bindParam(":window_start", $time); - $stmt->execute(); - - $stmt = $this->dbh->prepare(" - UPDATE ApiRateLimit - SET Requests = Requests + 1 - WHERE IP = :ip"); - $stmt->bindParam(":ip", $ip); - $stmt->execute(); - } else { - throw new RuntimeException("Unknown database backend"); - } - } - - /* - * Returns a JSON formatted error string. - * - * @param $msg The error string to return - * - * @return mixed A json formatted error response. - */ - private function json_error($msg) { - header('content-type: application/json'); - if ($this->version < 3) { - return $this->json_results('error', 0, $msg, NULL); - } elseif ($this->version >= 3) { - return $this->json_results('error', 0, array(), $msg); - } - } - - /* - * Returns a JSON formatted result data. - * - * @param $type The response method type. - * @param $count The number of results to return - * @param $data The result data to return - * @param $error An error message to include in the response - * - * @return mixed A json formatted result response. - */ - private function json_results($type, $count, $data, $error) { - $json_array = array( - 'version' => $this->version, - 'type' => $type, - 'resultcount' => $count, - 'results' => $data - ); - - if ($this->version != 5) { - $json_array['warning'] = 'The use of versions lower than 5 is ' - . 'now deprecated and will soon be unsupported. To ensure ' - . 'your API client supports the change without issue, it ' - . 'should use version 5 and adjust for any changes in the ' - . 'API interface. See https://aur.archlinux.org/rpc for ' - . 'documentation related to v5.'; - } - - if ($error) { - $json_array['error'] = $error; - } - - return json_encode($json_array); - } - - /* - * Get extended package details (for info and multiinfo queries). - * - * @param $pkgid The ID of the package to retrieve details for. - * @param $base_id The ID of the package base to retrieve details for. - * - * @return array An array containing package details. - */ - private function get_extended_fields($pkgid, $base_id) { - $query = "SELECT DependencyTypes.Name AS Type, " . - "PackageDepends.DepName AS Name, " . - "PackageDepends.DepCondition AS Cond " . - "FROM PackageDepends " . - "LEFT JOIN DependencyTypes " . - "ON DependencyTypes.ID = PackageDepends.DepTypeID " . - "WHERE PackageDepends.PackageID = " . $pkgid . " " . - "UNION SELECT RelationTypes.Name AS Type, " . - "PackageRelations.RelName AS Name, " . - "PackageRelations.RelCondition AS Cond " . - "FROM PackageRelations " . - "LEFT JOIN RelationTypes " . - "ON RelationTypes.ID = PackageRelations.RelTypeID " . - "WHERE PackageRelations.PackageID = " . $pkgid . " " . - "UNION SELECT 'groups' AS Type, `Groups`.`Name`, '' AS Cond " . - "FROM `Groups` INNER JOIN PackageGroups " . - "ON PackageGroups.PackageID = " . $pkgid . " " . - "AND PackageGroups.GroupID = `Groups`.ID " . - "UNION SELECT 'license' AS Type, Licenses.Name, '' AS Cond " . - "FROM Licenses INNER JOIN PackageLicenses " . - "ON PackageLicenses.PackageID = " . $pkgid . " " . - "AND PackageLicenses.LicenseID = Licenses.ID"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($query, 'extended-fields:' . $pkgid, PDO::FETCH_ASSOC, $ttl); - - $type_map = array( - 'depends' => 'Depends', - 'makedepends' => 'MakeDepends', - 'checkdepends' => 'CheckDepends', - 'optdepends' => 'OptDepends', - 'conflicts' => 'Conflicts', - 'provides' => 'Provides', - 'replaces' => 'Replaces', - 'groups' => 'Groups', - 'license' => 'License', - ); - $data = array(); - foreach ($rows as $row) { - $type = $type_map[$row['Type']]; - $data[$type][] = $row['Name'] . $row['Cond']; - } - - if ($this->version >= 5) { - $query = "SELECT Keyword FROM PackageKeywords " . - "WHERE PackageBaseID = " . intval($base_id) . " " . - "ORDER BY Keyword ASC"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($query, 'keywords:' . intval($base_id), PDO::FETCH_NUM, $ttl); - $data['Keywords'] = array_map(function ($x) { return $x[0]; }, $rows); - } - - return $data; - } - - /* - * Retrieve package information (used in info, multiinfo, search and - * depends requests). - * - * @param $type The request type. - * @param $where_condition An SQL WHERE-condition to filter packages. - * - * @return mixed Returns an array of package matches. - */ - private function process_query($type, $where_condition) { - $max_results = config_get_int('options', 'max_rpc_results'); - - if ($this->version == 1) { - $fields = implode(',', self::$fields_v1); - $query = "SELECT {$fields} " . - "FROM Packages LEFT JOIN PackageBases " . - "ON PackageBases.ID = Packages.PackageBaseID " . - "LEFT JOIN Users " . - "ON PackageBases.MaintainerUID = Users.ID " . - "LEFT JOIN PackageLicenses " . - "ON PackageLicenses.PackageID = Packages.ID " . - "LEFT JOIN Licenses " . - "ON Licenses.ID = PackageLicenses.LicenseID " . - "WHERE ${where_condition} " . - "AND PackageBases.PackagerUID IS NOT NULL " . - "LIMIT $max_results"; - } elseif ($this->version >= 2) { - if ($this->version == 2 || $this->version == 3) { - $fields = implode(',', self::$fields_v2); - } else if ($this->version >= 4 && $this->version <= 6) { - $fields = implode(',', self::$fields_v4); - } - $query = "SELECT {$fields} " . - "FROM Packages LEFT JOIN PackageBases " . - "ON PackageBases.ID = Packages.PackageBaseID " . - "LEFT JOIN Users " . - "ON PackageBases.MaintainerUID = Users.ID " . - "WHERE ${where_condition} " . - "AND PackageBases.PackagerUID IS NOT NULL " . - "LIMIT $max_results"; - } - $result = $this->dbh->query($query); - - if ($result) { - $resultcount = 0; - $search_data = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $resultcount++; - $row['URLPath'] = sprintf(config_get('options', 'snapshot_uri'), urlencode($row['PackageBase'])); - if ($this->version < 4) { - $row['CategoryID'] = 1; - } - - /* - * Unfortunately, mysql_fetch_assoc() returns - * all fields as strings. We need to coerce - * numeric values into integers to provide - * proper data types in the JSON response. - */ - foreach (self::$numeric_fields as $field) { - if (isset($row[$field])) { - $row[$field] = intval($row[$field]); - } - } - - foreach (self::$decimal_fields as $field) { - if (isset($row[$field])) { - $row[$field] = floatval($row[$field]); - } - } - - if ($this->version >= 2 && ($type == 'info' || $type == 'multiinfo')) { - $extfields = $this->get_extended_fields($row['ID'], $row['PackageBaseID']); - if ($extfields) { - $row = array_merge($row, $extfields); - } - } - - if ($this->version < 3) { - if ($type == 'info') { - $search_data = $row; - break; - } else { - array_push($search_data, $row); - } - } elseif ($this->version >= 3) { - array_push($search_data, $row); - } - } - - if ($resultcount === $max_results) { - return $this->json_error('Too many package results.'); - } - - return $this->json_results($type, $resultcount, $search_data, NULL); - } else { - return $this->json_results($type, 0, array(), NULL); - } - } - - /* - * Parse the args to the multiinfo function. We may have a string or an - * array, so do the appropriate thing. Within the elements, both * package - * IDs and package names are valid; sort them into the relevant arrays and - * escape/quote the names. - * - * @param array $args Query parameters. - * - * @return mixed An array containing 'ids' and 'names'. - */ - private function parse_multiinfo_args($args) { - if (!is_array($args)) { - $args = array($args); - } - - $id_args = array(); - $name_args = array(); - foreach ($args as $arg) { - if (!$arg) { - continue; - } - if ($this->version < 5 && is_numeric($arg)) { - $id_args[] = intval($arg); - } else { - $name_args[] = $this->dbh->quote($arg); - } - } - - return array('ids' => $id_args, 'names' => $name_args); - } - - /* - * Performs a fulltext mysql search of the package database. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of package matches. - */ - private function search($http_data) { - $keyword_string = $http_data['arg']; - - if (isset($http_data['by'])) { - $search_by = $http_data['by']; - } else { - $search_by = 'name-desc'; - } - - if ($search_by === 'name' || $search_by === 'name-desc') { - if (strlen($keyword_string) < 2) { - return $this->json_error('Query arg too small.'); - } - - if ($this->version >= 6 && $search_by === 'name-desc') { - $where_condition = construct_keyword_search($this->dbh, - $keyword_string, true, false); - } else { - $keyword_string = $this->dbh->quote( - "%" . addcslashes($keyword_string, '%_') . "%"); - - if ($search_by === 'name') { - $where_condition = "(Packages.Name LIKE $keyword_string)"; - } else if ($search_by === 'name-desc') { - $where_condition = "(Packages.Name LIKE $keyword_string "; - $where_condition .= "OR Description LIKE $keyword_string)"; - } - - } - } else if ($search_by === 'maintainer') { - if (empty($keyword_string)) { - $where_condition = "Users.ID is NULL"; - } else { - $keyword_string = $this->dbh->quote($keyword_string); - $where_condition = "Users.Username = $keyword_string "; - } - } else if (in_array($search_by, self::$exposed_depfields)) { - if (empty($keyword_string)) { - return $this->json_error('Query arg is empty.'); - } else { - $keyword_string = $this->dbh->quote($keyword_string); - $search_by = $this->dbh->quote($search_by); - $subquery = "SELECT PackageDepends.DepName FROM PackageDepends "; - $subquery .= "LEFT JOIN DependencyTypes "; - $subquery .= "ON PackageDepends.DepTypeID = DependencyTypes.ID "; - $subquery .= "WHERE PackageDepends.PackageID = Packages.ID "; - $subquery .= "AND DependencyTypes.Name = $search_by"; - $where_condition = "$keyword_string IN ($subquery)"; - } - } - - return $this->process_query('search', $where_condition); - } - - /* - * Returns the info on a specific package. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of value data containing the package data - */ - private function info($http_data) { - $pqdata = $http_data['arg']; - if ($this->version < 5 && is_numeric($pqdata)) { - $where_condition = "Packages.ID = $pqdata"; - } else { - $where_condition = "Packages.Name = " . $this->dbh->quote($pqdata); - } - - return $this->process_query('info', $where_condition); - } - - /* - * Returns the info on multiple packages. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of results containing the package data - */ - private function multiinfo($http_data) { - $pqdata = $http_data['arg']; - $args = $this->parse_multiinfo_args($pqdata); - $ids = $args['ids']; - $names = $args['names']; - - if (!$ids && !$names) { - return $this->json_error('Invalid query arguments.'); - } - - $where_condition = ""; - if ($ids) { - $ids_value = implode(',', $args['ids']); - $where_condition .= "Packages.ID IN ($ids_value) "; - } - if ($ids && $names) { - $where_condition .= "OR "; - } - if ($names) { - /* - * Individual names were quoted in - * parse_multiinfo_args(). - */ - $names_value = implode(',', $args['names']); - $where_condition .= "Packages.Name IN ($names_value) "; - } - - return $this->process_query('multiinfo', $where_condition); - } - - /* - * Returns all the packages for a specific maintainer. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of value data containing the package data - */ - private function msearch($http_data) { - $http_data['by'] = 'maintainer'; - return $this->search($http_data); - } - - /* - * Get all package names that start with $search. - * - * @param array $http_data Query parameters. - * - * @return string The JSON formatted response data. - */ - private function suggest($http_data) { - $search = $http_data['arg']; - $query = "SELECT Packages.Name FROM Packages "; - $query.= "LEFT JOIN PackageBases "; - $query.= "ON PackageBases.ID = Packages.PackageBaseID "; - $query.= "WHERE Packages.Name LIKE "; - $query.= $this->dbh->quote(addcslashes($search, '%_') . '%'); - $query.= " AND PackageBases.PackagerUID IS NOT NULL "; - $query.= "ORDER BY Name ASC LIMIT 20"; - - $result = $this->dbh->query($query); - $result_array = array(); - - if ($result) { - $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); - } - - return json_encode($result_array); - } - - /* - * Get all package base names that start with $search. - * - * @param array $http_data Query parameters. - * - * @return string The JSON formatted response data. - */ - private function suggest_pkgbase($http_data) { - $search = $http_data['arg']; - $query = "SELECT Name FROM PackageBases WHERE Name LIKE "; - $query.= $this->dbh->quote(addcslashes($search, '%_') . '%'); - $query.= " AND PackageBases.PackagerUID IS NOT NULL "; - $query.= "ORDER BY Name ASC LIMIT 20"; - - $result = $this->dbh->query($query); - $result_array = array(); - - if ($result) { - $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); - } - - return json_encode($result_array); - } - - /** - * Get the HTML markup of the comment form. - * - * @param array $http_data Query parameters. - * - * @return string The JSON formatted response data. - */ - private function get_comment_form($http_data) { - if (!isset($http_data['base_id']) || !isset($http_data['pkgbase_name'])) { - $output = array( - 'success' => 0, - 'error' => __('Package base ID or package base name missing.') - ); - return json_encode($output); - } - - $comment_id = intval($http_data['arg']); - $base_id = intval($http_data['base_id']); - $pkgbase_name = $http_data['pkgbase_name']; - - list($user_id, $comment) = comment_by_id($comment_id); - - if (!has_credential(CRED_COMMENT_EDIT, array($user_id))) { - $output = array( - 'success' => 0, - 'error' => __('You are not allowed to edit this comment.') - ); - return json_encode($output); - } elseif (is_null($comment)) { - $output = array( - 'success' => 0, - 'error' => __('Comment does not exist.') - ); - return json_encode($output); - } - - ob_start(); - include('pkg_comment_form.php'); - $html = ob_get_clean(); - $output = array( - 'success' => 1, - 'form' => $html - ); - - return json_encode($output); - } -} diff --git a/web/lib/cachefuncs.inc.php b/web/lib/cachefuncs.inc.php deleted file mode 100644 index b2b96c24..00000000 --- a/web/lib/cachefuncs.inc.php +++ /dev/null @@ -1,99 +0,0 @@ -addServer($mcserver[0], intval($mcserver[1])); - } -} - -# Set a value in the cache (currently APC) if cache is available for use. If -# not available, this becomes effectively a no-op (return value is -# false). Accepts an optional TTL (defaults to 600 seconds). -function set_cache_value($key, $value, $ttl=600) { - $status = false; - if (defined('EXTENSION_LOADED_APC')) { - $status = apc_store(CACHE_PREFIX.$key, $value, $ttl); - } - if (defined('EXTENSION_LOADED_MEMCACHE')) { - global $memcache; - $status = $memcache->set(CACHE_PREFIX.$key, $value, $ttl); - } - return $status; -} - -# Get a value from the cache (currently APC) if cache is available for use. If -# not available, this returns false (optionally sets passed in variable $status -# to false, much like apc_fetch() behaves). This allows for testing the fetch -# result appropriately even in the event that a 'false' value was the value in -# the cache. -function get_cache_value($key, &$status=false) { - if(defined('EXTENSION_LOADED_APC')) { - $ret = apc_fetch(CACHE_PREFIX.$key, $status); - if ($status) { - return $ret; - } - } - if (defined('EXTENSION_LOADED_MEMCACHE')) { - global $memcache; - $ret = $memcache->get(CACHE_PREFIX.$key); - if (!$ret) { - $status = false; - } - else { - $status = true; - } - return $ret; - } - return $status; -} - -# Run a simple db query, retrieving and/or caching the value if APC is -# available for use. Accepts an optional TTL value (defaults to 600 seconds). -function db_cache_value($dbq, $key, $ttl=600) { - $dbh = DB::connect(); - $status = false; - $value = get_cache_value($key, $status); - if (!$status) { - $result = $dbh->query($dbq); - if (!$result) { - return false; - } - $row = $result->fetch(PDO::FETCH_NUM); - $value = $row[0]; - set_cache_value($key, $value, $ttl); - } - return $value; -} - -# Run a simple db query, retrieving and/or caching the result set if APC is -# available for use. Accepts an optional TTL value (defaults to 600 seconds). -function db_cache_result($dbq, $key, $fetch_style=PDO::FETCH_NUM, $ttl=600) { - $dbh = DB::connect(); - $status = false; - $value = get_cache_value($key, $status); - if (!$status) { - $result = $dbh->query($dbq); - if (!$result) { - return false; - } - $value = $result->fetchAll($fetch_style); - set_cache_value($key, $value, $ttl); - } - return $value; -} - -?> diff --git a/web/lib/confparser.inc.php b/web/lib/confparser.inc.php deleted file mode 100644 index fdd2b78e..00000000 --- a/web/lib/confparser.inc.php +++ /dev/null @@ -1,59 +0,0 @@ -useCached(); // use cached version if age<1 hour -$rss->title = "PHP news"; -$rss->description = "daily news from the PHP scripting world"; - -//optional -$rss->descriptionTruncSize = 500; -$rss->descriptionHtmlSyndicated = true; - -$rss->link = "http://www.dailyphp.net/news"; -$rss->syndicationURL = "http://www.dailyphp.net/".$_SERVER["PHP_SELF"]; - -$image = new FeedImage(); -$image->title = "dailyphp.net logo"; -$image->url = "http://www.dailyphp.net/images/logo.gif"; -$image->link = "http://www.dailyphp.net"; -$image->description = "Feed provided by dailyphp.net. Click to visit."; - -//optional -$image->descriptionTruncSize = 500; -$image->descriptionHtmlSyndicated = true; - -$rss->image = $image; - -// get your news items from somewhere, e.g. your database: -mysql_select_db($dbHost, $dbUser, $dbPass); -$res = mysql_query("SELECT * FROM news ORDER BY newsdate DESC"); -while ($data = mysql_fetch_object($res)) { - $item = new FeedItem(); - $item->title = $data->title; - $item->link = $data->url; - $item->description = $data->short; - - //optional - item->descriptionTruncSize = 500; - item->descriptionHtmlSyndicated = true; - - $item->date = $data->newsdate; - $item->source = "http://www.dailyphp.net"; - $item->author = "John Doe"; - - $rss->addItem($item); -} - -// valid format strings are: RSS0.91, RSS1.0, RSS2.0, PIE0.1 (deprecated), -// MBOX, OPML, ATOM, ATOM0.3, HTML, JS -echo $rss->saveFeed("RSS1.0", "news/feed.xml"); - - -*************************************************************************** -* A little setup * -**************************************************************************/ - -// your local timezone, set to "" to disable or for GMT -define("TIME_ZONE","+01:00"); - - - - -/** - * Version string. - **/ -define("FEEDCREATOR_VERSION", "FeedCreator 1.7.2"); - - - -/** - * A FeedItem is a part of a FeedCreator feed. - * - * @author Kai Blankenhorn - * @since 1.3 - */ -class FeedItem extends HtmlDescribable { - /** - * Mandatory attributes of an item. - */ - var $title, $description, $link; - - /** - * Optional attributes of an item. - */ - var $author, $authorEmail, $image, $category, $comments, $guid, $guidIsPermaLink, $source, $creator; - - /** - * Publishing date of an item. May be in one of the following formats: - * - * RFC 822: - * "Mon, 20 Jan 03 18:05:41 +0400" - * "20 Jan 03 18:05:41 +0000" - * - * ISO 8601: - * "2003-01-20T18:05:41+04:00" - * - * Unix: - * 1043082341 - */ - var $date; - - /** - * Any additional elements to include as an assiciated array. All $key => $value pairs - * will be included unencoded in the feed item in the form - * <$key>$value - * Again: No encoding will be used! This means you can invalidate or enhance the feed - * if $value contains markup. This may be abused to embed tags not implemented by - * the FeedCreator class used. - */ - var $additionalElements = Array(); - - // on hold - // var $source; -} - - - -/** - * An FeedImage may be added to a FeedCreator feed. - * @author Kai Blankenhorn - * @since 1.3 - */ -class FeedImage extends HtmlDescribable { - /** - * Mandatory attributes of an image. - */ - var $title, $url, $link; - - /** - * Optional attributes of an image. - */ - var $width, $height, $description; -} - - - -/** - * An HtmlDescribable is an item within a feed that can have a description that may - * include HTML markup. - */ -class HtmlDescribable { - /** - * Indicates whether the description field should be rendered in HTML. - */ - var $descriptionHtmlSyndicated; - - /** - * Indicates whether and to how many characters a description should be truncated. - */ - var $descriptionTruncSize; - - /** - * Returns a formatted description field, depending on descriptionHtmlSyndicated and - * $descriptionTruncSize properties - * @return string the formatted description - */ - function getDescription() { - $descriptionField = new FeedHtmlField($this->description); - $descriptionField->syndicateHtml = $this->descriptionHtmlSyndicated; - $descriptionField->truncSize = $this->descriptionTruncSize; - return $descriptionField->output(); - } - -} - - -/** - * An FeedHtmlField describes and generates - * a feed, item or image html field (probably a description). Output is - * generated based on $truncSize, $syndicateHtml properties. - * @author Pascal Van Hecke - * @version 1.6 - */ -class FeedHtmlField { - /** - * Mandatory attributes of a FeedHtmlField. - */ - var $rawFieldContent; - - /** - * Optional attributes of a FeedHtmlField. - * - */ - var $truncSize, $syndicateHtml; - - /** - * Creates a new instance of FeedHtmlField. - * @param $string: if given, sets the rawFieldContent property - */ - function FeedHtmlField($parFieldContent) { - if ($parFieldContent) { - $this->rawFieldContent = $parFieldContent; - } - } - - - /** - * Creates the right output, depending on $truncSize, $syndicateHtml properties. - * @return string the formatted field - */ - function output() { - // when field available and syndicated in html we assume - // - valid html in $rawFieldContent and we enclose in CDATA tags - // - no truncation (truncating risks producing invalid html) - if (!$this->rawFieldContent) { - $result = ""; - } elseif ($this->syndicateHtml) { - $result = "rawFieldContent."]]>"; - } else { - if ($this->truncSize and is_int($this->truncSize)) { - $result = FeedCreator::iTrunc(htmlspecialchars($this->rawFieldContent),$this->truncSize); - } else { - $result = htmlspecialchars($this->rawFieldContent); - } - } - return $result; - } - -} - - - -/** - * UniversalFeedCreator lets you choose during runtime which - * format to build. - * For general usage of a feed class, see the FeedCreator class - * below or the example above. - * - * @since 1.3 - * @author Kai Blankenhorn - */ -class UniversalFeedCreator extends FeedCreator { - var $_feed; - - function _setFormat($format) { - switch (strtoupper($format)) { - - case "2.0": - // fall through - case "RSS2.0": - $this->_feed = new RSSCreator20(); - break; - - case "1.0": - // fall through - case "RSS1.0": - $this->_feed = new RSSCreator10(); - break; - - case "0.91": - // fall through - case "RSS0.91": - $this->_feed = new RSSCreator091(); - break; - - case "PIE0.1": - $this->_feed = new PIECreator01(); - break; - - case "MBOX": - $this->_feed = new MBOXCreator(); - break; - - case "OPML": - $this->_feed = new OPMLCreator(); - break; - - case "ATOM": - // fall through: always the latest ATOM version - - case "ATOM0.3": - $this->_feed = new AtomCreator03(); - break; - - case "HTML": - $this->_feed = new HTMLCreator(); - break; - - case "JS": - // fall through - case "JAVASCRIPT": - $this->_feed = new JSCreator(); - break; - - default: - $this->_feed = new RSSCreator091(); - break; - } - - $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - // prevent overwriting of properties "contentType", "encoding"; do not copy "_feed" itself - if (!in_array($key, array("_feed", "contentType", "encoding"))) { - $this->_feed->{$key} = $this->{$key}; - } - } - } - - /** - * Creates a syndication feed based on the items previously added. - * - * @see FeedCreator::addItem() - * @param string format format the feed should comply to. Valid values are: - * "PIE0.1", "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML", "ATOM0.3", "HTML", "JS" - * @return string the contents of the feed. - */ - function createFeed($format = "RSS0.91") { - $this->_setFormat($format); - return $this->_feed->createFeed(); - } - - - - /** - * Saves this feed as a file on the local disk. After the file is saved, an HTTP redirect - * header may be sent to redirect the use to the newly created file. - * @since 1.4 - * - * @param string format format the feed should comply to. Valid values are: - * "PIE0.1" (deprecated), "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML", "ATOM", "ATOM0.3", "HTML", "JS" - * @param string filename optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param boolean displayContents optional send the content of the file or not. If true, the file will be sent in the body of the response. - */ - function saveFeed($format="RSS0.91", $filename="", $displayContents=true) { - $this->_setFormat($format); - $this->_feed->saveFeed($filename, $displayContents); - } - - - /** - * Turns on caching and checks if there is a recent version of this feed in the cache. - * If there is, an HTTP redirect header is sent. - * To effectively use caching, you should create the FeedCreator object and call this method - * before anything else, especially before you do the time consuming task to build the feed - * (web fetching, for example). - * - * @param string format format the feed should comply to. Valid values are: - * "PIE0.1" (deprecated), "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML", "ATOM0.3". - * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param timeout int optional the timeout in seconds before a cached version is refreshed (defaults to 3600 = 1 hour) - */ - function useCached($format="RSS0.91", $filename="", $timeout=3600) { - $this->_setFormat($format); - $this->_feed->useCached($filename, $timeout); - } - -} - - -/** - * FeedCreator is the abstract base implementation for concrete - * implementations that implement a specific format of syndication. - * - * @abstract - * @author Kai Blankenhorn - * @since 1.4 - */ -class FeedCreator extends HtmlDescribable { - - /** - * Mandatory attributes of a feed. - */ - var $title, $description, $link; - - - /** - * Optional attributes of a feed. - */ - var $syndicationURL, $image, $language, $copyright, $pubDate, $lastBuildDate, $editor, $editorEmail, $webmaster, $category, $docs, $ttl, $rating, $skipHours, $skipDays; - - /** - * The url of the external xsl stylesheet used to format the naked rss feed. - * Ignored in the output when empty. - */ - var $xslStyleSheet = ""; - - - /** - * @access private - */ - var $items = Array(); - - - /** - * This feed's MIME content type. - * @since 1.4 - * @access private - */ - var $contentType = "application/xml"; - - - /** - * This feed's character encoding. - * @since 1.6.1 - **/ - var $encoding = "ISO-8859-1"; - - - /** - * Any additional elements to include as an assiciated array. All $key => $value pairs - * will be included unencoded in the feed in the form - * <$key>$value - * Again: No encoding will be used! This means you can invalidate or enhance the feed - * if $value contains markup. This may be abused to embed tags not implemented by - * the FeedCreator class used. - */ - var $additionalElements = Array(); - - - /** - * Adds an FeedItem to the feed. - * - * @param object FeedItem $item The FeedItem to add to the feed. - * @access public - */ - function addItem($item) { - $this->items[] = $item; - } - - - /** - * Truncates a string to a certain length at the most sensible point. - * First, if there's a '.' character near the end of the string, the string is truncated after this character. - * If there is no '.', the string is truncated after the last ' ' character. - * If the string is truncated, " ..." is appended. - * If the string is already shorter than $length, it is returned unchanged. - * - * @static - * @param string string A string to be truncated. - * @param int length the maximum length the string should be truncated to - * @return string the truncated string - */ - function iTrunc($string, $length) { - if (strlen($string)<=$length) { - return $string; - } - - $pos = strrpos($string,"."); - if ($pos>=$length-4) { - $string = substr($string,0,$length-4); - $pos = strrpos($string,"."); - } - if ($pos>=$length*0.4) { - return substr($string,0,$pos+1)." ..."; - } - - $pos = strrpos($string," "); - if ($pos>=$length-4) { - $string = substr($string,0,$length-4); - $pos = strrpos($string," "); - } - if ($pos>=$length*0.4) { - return substr($string,0,$pos)." ..."; - } - - return substr($string,0,$length-4)." ..."; - - } - - - /** - * Creates a comment indicating the generator of this feed. - * The format of this comment seems to be recognized by - * Syndic8.com. - */ - function _createGeneratorComment() { - return "\n"; - } - - - /** - * Creates a string containing all additional elements specified in - * $additionalElements. - * @param elements array an associative array containing key => value pairs - * @param indentString string a string that will be inserted before every generated line - * @return string the XML tags corresponding to $additionalElements - */ - function _createAdditionalElements($elements, $indentString="") { - $ae = ""; - if (is_array($elements)) { - foreach($elements AS $key => $value) { - $ae.= $indentString."<$key>$value\n"; - } - } - return $ae; - } - - function _createStylesheetReferences() { - $xml = ""; - if ($this->cssStyleSheet) $xml .= "cssStyleSheet."\" type=\"text/css\"?>\n"; - if ($this->xslStyleSheet) $xml .= "xslStyleSheet."\" type=\"text/xsl\"?>\n"; - return $xml; - } - - - /** - * Builds the feed's text. - * @abstract - * @return string the feed's complete text - */ - function createFeed() { - } - - /** - * Generate a filename for the feed cache file. The result will be $_SERVER["PHP_SELF"] with the extension changed to .xml. - * For example: - * - * echo $_SERVER["PHP_SELF"]."\n"; - * echo FeedCreator::_generateFilename(); - * - * would produce: - * - * /rss/latestnews.php - * latestnews.xml - * - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".xml"; - } - - - /** - * @since 1.4 - * @access private - */ - function _redirect($filename) { - // attention, heavily-commented-out-area - - // maybe use this in addition to file time checking - //Header("Expires: ".date("r",time()+$this->_timeout)); - - /* no caching at all, doesn't seem to work as good: - Header("Cache-Control: no-cache"); - Header("Pragma: no-cache"); - */ - - // HTTP redirect, some feed readers' simple HTTP implementations don't follow it - //Header("Location: ".$filename); - - Header("Content-Type: ".$this->contentType."; charset=".$this->encoding."; filename=".basename($filename)); - Header("Content-Disposition: inline; filename=".basename($filename)); - readfile($filename, "r"); - die(); - } - - /** - * Turns on caching and checks if there is a recent version of this feed in the cache. - * If there is, an HTTP redirect header is sent. - * To effectively use caching, you should create the FeedCreator object and call this method - * before anything else, especially before you do the time consuming task to build the feed - * (web fetching, for example). - * @since 1.4 - * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param timeout int optional the timeout in seconds before a cached version is refreshed (defaults to 3600 = 1 hour) - */ - function useCached($filename="", $timeout=3600) { - $this->_timeout = $timeout; - if ($filename=="") { - $filename = $this->_generateFilename(); - } - if (file_exists($filename) AND (time()-filemtime($filename) < $timeout)) { - $this->_redirect($filename); - } - } - - - /** - * Saves this feed as a file on the local disk. After the file is saved, a redirect - * header may be sent to redirect the user to the newly created file. - * @since 1.4 - * - * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param redirect boolean optional send an HTTP redirect header or not. If true, the user will be automatically redirected to the created file. - */ - function saveFeed($filename="", $displayContents=true) { - if ($filename=="") { - $filename = $this->_generateFilename(); - } - $feedFile = fopen($filename, "w+"); - if ($feedFile) { - fputs($feedFile,$this->createFeed()); - fclose($feedFile); - if ($displayContents) { - $this->_redirect($filename); - } - } else { - echo "
    Error creating feed file, please check write permissions.
    "; - } - } - -} - - -/** - * FeedDate is an internal class that stores a date for a feed or feed item. - * Usually, you won't need to use this. - */ -class FeedDate { - var $unix; - - /** - * Creates a new instance of FeedDate representing a given date. - * Accepts RFC 822, ISO 8601 date formats as well as unix time stamps. - * @param mixed $dateString optional the date this FeedDate will represent. If not specified, the current date and time is used. - */ - function FeedDate($dateString="") { - if ($dateString=="") $dateString = date("r"); - - if (is_integer($dateString)) { - $this->unix = $dateString; - return; - } - if (preg_match("~(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s+)?(\\d{1,2})\\s+([a-zA-Z]{3})\\s+(\\d{4})\\s+(\\d{2}):(\\d{2}):(\\d{2})\\s+(.*)~",$dateString,$matches)) { - $months = Array("Jan"=>1,"Feb"=>2,"Mar"=>3,"Apr"=>4,"May"=>5,"Jun"=>6,"Jul"=>7,"Aug"=>8,"Sep"=>9,"Oct"=>10,"Nov"=>11,"Dec"=>12); - $this->unix = mktime($matches[4],$matches[5],$matches[6],$months[$matches[2]],$matches[1],$matches[3]); - if (substr($matches[7],0,1)=='+' OR substr($matches[7],0,1)=='-') { - $tzOffset = (substr($matches[7],0,3) * 60 + substr($matches[7],-2)) * 60; - } else { - if (strlen($matches[7])==1) { - $oneHour = 3600; - $ord = ord($matches[7]); - if ($ord < ord("M")) { - $tzOffset = (ord("A") - $ord - 1) * $oneHour; - } elseif ($ord >= ord("M") AND $matches[7]!="Z") { - $tzOffset = ($ord - ord("M")) * $oneHour; - } elseif ($matches[7]=="Z") { - $tzOffset = 0; - } - } - switch ($matches[7]) { - case "UT": - case "GMT": $tzOffset = 0; - } - } - $this->unix += $tzOffset; - return; - } - if (preg_match("~(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(.*)~",$dateString,$matches)) { - $this->unix = mktime($matches[4],$matches[5],$matches[6],$matches[2],$matches[3],$matches[1]); - if (substr($matches[7],0,1)=='+' OR substr($matches[7],0,1)=='-') { - $tzOffset = (substr($matches[7],0,3) * 60 + substr($matches[7],-2)) * 60; - } else { - if ($matches[7]=="Z") { - $tzOffset = 0; - } - } - $this->unix += $tzOffset; - return; - } - $this->unix = 0; - } - - /** - * Gets the date stored in this FeedDate as an RFC 822 date. - * - * @return a date in RFC 822 format - */ - function rfc822() { - //return gmdate("r",$this->unix); - $date = gmdate("D, d M Y H:i:s", $this->unix); - if (TIME_ZONE!="") $date .= " ".str_replace(":","",TIME_ZONE); - return $date; - } - - /** - * Gets the date stored in this FeedDate as an ISO 8601 date. - * - * @return a date in ISO 8601 format - */ - function iso8601() { - $date = gmdate("Y-m-d\TH:i:sO",$this->unix); - $date = substr($date,0,22) . ':' . substr($date,-2); - if (TIME_ZONE!="") $date = str_replace("+00:00",TIME_ZONE,$date); - return $date; - } - - /** - * Gets the date stored in this FeedDate as unix time stamp. - * - * @return a date as a unix time stamp - */ - function unix() { - return $this->unix; - } -} - - -/** - * RSSCreator10 is a FeedCreator that implements RDF Site Summary (RSS) 1.0. - * - * @see http://www.purl.org/rss/1.0/ - * @since 1.3 - * @author Kai Blankenhorn - */ -class RSSCreator10 extends FeedCreator { - - /** - * Builds the RSS feed's text. The feed will be compliant to RDF Site Summary (RSS) 1.0. - * The feed will contain all items previously added in the same order. - * @return string the feed's complete text - */ - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - if ($this->cssStyleSheet=="") { - $cssStyleSheet = "http://www.w3.org/2000/08/w3c-synd/style.css"; - } - $feed.= $this->_createStylesheetReferences(); - $feed.= "\n"; - $feed.= " syndicationURL."\">\n"; - $feed.= " ".htmlspecialchars($this->title)."\n"; - $feed.= " ".htmlspecialchars($this->description)."\n"; - $feed.= " ".$this->link."\n"; - if ($this->image!=null) { - $feed.= " image->url."\" />\n"; - } - $now = new FeedDate(); - $feed.= " ".htmlspecialchars($now->iso8601())."\n"; - $feed.= " \n"; - $feed.= " \n"; - for ($i=0;$iitems);$i++) { - $feed.= " items[$i]->link)."\"/>\n"; - } - $feed.= " \n"; - $feed.= " \n"; - $feed.= " \n"; - if ($this->image!=null) { - $feed.= " image->url."\">\n"; - $feed.= " ".$this->image->title."\n"; - $feed.= " ".$this->image->link."\n"; - $feed.= " ".$this->image->url."\n"; - $feed.= " \n"; - } - $feed.= $this->_createAdditionalElements($this->additionalElements, " "); - - for ($i=0;$iitems);$i++) { - $feed.= " items[$i]->link)."\">\n"; - //$feed.= " Posting\n"; - $feed.= " text/html\n"; - if ($this->items[$i]->date!=null) { - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - } - if ($this->items[$i]->source!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->source)."\n"; - } - if ($this->items[$i]->author!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - } - $feed.= " ".htmlspecialchars(strip_tags(strtr($this->items[$i]->title,"\n\r"," ")))."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->description)."\n"; - $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); - $feed.= " \n"; - } - $feed.= "\n"; - return $feed; - } -} - - - -/** - * RSSCreator091 is a FeedCreator that implements RSS 0.91 Spec, revision 3. - * - * @see http://my.netscape.com/publish/formats/rss-spec-0.91.html - * @since 1.3 - * @author Kai Blankenhorn - */ -class RSSCreator091 extends FeedCreator { - - /** - * Stores this RSS feed's version number. - * @access private - */ - var $RSSVersion; - - function RSSCreator091() { - $this->_setRSSVersion("0.91"); - $this->contentType = "application/rss+xml"; - } - - /** - * Sets this RSS feed's version number. - * @access private - */ - function _setRSSVersion($version) { - $this->RSSVersion = $version; - } - - /** - * Builds the RSS feed's text. The feed will be compliant to RDF Site Summary (RSS) 1.0. - * The feed will contain all items previously added in the same order. - * @return string the feed's complete text - */ - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - $feed.= $this->_createStylesheetReferences(); - $feed.= "RSSVersion."\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n"; - $feed.= " \n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."\n"; - $this->descriptionTruncSize = 500; - $feed.= " ".$this->getDescription()."\n"; - $feed.= " ".$this->link."\n"; - $feed.= " syndicationURL."\" rel=\"self\" type=\"application/rss+xml\" />\n"; - $now = new FeedDate(); - $feed.= " ".htmlspecialchars($now->rfc822())."\n"; - $feed.= " ".FEEDCREATOR_VERSION."\n"; - - if ($this->image!=null) { - $feed.= " \n"; - $feed.= " ".$this->image->url."\n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->image->title),100)."\n"; - $feed.= " ".$this->image->link."\n"; - if ($this->image->width!="") { - $feed.= " ".$this->image->width."\n"; - } - if ($this->image->height!="") { - $feed.= " ".$this->image->height."\n"; - } - if ($this->image->description!="") { - $feed.= " ".$this->image->getDescription()."\n"; - } - $feed.= " \n"; - } - if ($this->language!="") { - $feed.= " ".$this->language."\n"; - } - if ($this->copyright!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->copyright),100)."\n"; - } - if ($this->editor!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->editor),100)."\n"; - } - if ($this->webmaster!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->webmaster),100)."\n"; - } - if ($this->pubDate!="") { - $pubDate = new FeedDate($this->pubDate); - $feed.= " ".htmlspecialchars($pubDate->rfc822())."\n"; - } - if ($this->category!="") { - $feed.= " ".htmlspecialchars($this->category)."\n"; - } - if ($this->docs!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->docs),500)."\n"; - } - if ($this->ttl!="") { - $feed.= " ".htmlspecialchars($this->ttl)."\n"; - } - if ($this->rating!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->rating),500)."\n"; - } - if ($this->skipHours!="") { - $feed.= " ".htmlspecialchars($this->skipHours)."\n"; - } - if ($this->skipDays!="") { - $feed.= " ".htmlspecialchars($this->skipDays)."\n"; - } - $feed.= $this->_createAdditionalElements($this->additionalElements, " "); - - for ($i=0;$iitems);$i++) { - $feed.= " \n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100)."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $feed.= " ".$this->items[$i]->getDescription()."\n"; - - if ($this->items[$i]->author!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - } - /* - // on hold - if ($this->items[$i]->source!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->source)."\n"; - } - */ - if ($this->items[$i]->category!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->category)."\n"; - } - if ($this->items[$i]->comments!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->comments)."\n"; - } - if ($this->items[$i]->date!="") { - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->rfc822())."\n"; - } - if ($this->items[$i]->guid!="") { - $feed.= " items[$i]->guidIsPermaLink == false) { - $feed.= " isPermaLink=\"false\""; - } - $feed.= ">".htmlspecialchars($this->items[$i]->guid)."\n"; - } - $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); - $feed.= " \n"; - } - $feed.= " \n"; - $feed.= "\n"; - return $feed; - } -} - - - -/** - * RSSCreator20 is a FeedCreator that implements RDF Site Summary (RSS) 2.0. - * - * @see http://backend.userland.com/rss - * @since 1.3 - * @author Kai Blankenhorn - */ -class RSSCreator20 extends RSSCreator091 { - - function RSSCreator20() { - parent::_setRSSVersion("2.0"); - } - -} - - -/** - * PIECreator01 is a FeedCreator that implements the emerging PIE specification, - * as in http://intertwingly.net/wiki/pie/Syntax. - * - * @deprecated - * @since 1.3 - * @author Scott Reynen and Kai Blankenhorn - */ -class PIECreator01 extends FeedCreator { - - function PIECreator01() { - $this->encoding = "utf-8"; - } - - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createStylesheetReferences(); - $feed.= "\n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."\n"; - $this->truncSize = 500; - $feed.= " ".$this->getDescription()."\n"; - $feed.= " ".$this->link."\n"; - for ($i=0;$iitems);$i++) { - $feed.= " \n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100)."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->guid)."\n"; - if ($this->items[$i]->author!="") { - $feed.= " \n"; - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - if ($this->items[$i]->authorEmail!="") { - $feed.= " ".$this->items[$i]->authorEmail."\n"; - } - $feed.=" \n"; - } - $feed.= " \n"; - $feed.= "
    ".$this->items[$i]->getDescription()."
    \n"; - $feed.= "
    \n"; - $feed.= "
    \n"; - } - $feed.= "
    \n"; - return $feed; - } -} - - -/** - * AtomCreator03 is a FeedCreator that implements the atom specification, - * as in http://www.intertwingly.net/wiki/pie/FrontPage. - * Please note that just by using AtomCreator03 you won't automatically - * produce valid atom files. For example, you have to specify either an editor - * for the feed or an author for every single feed item. - * - * Some elements have not been implemented yet. These are (incomplete list): - * author URL, item author's email and URL, item contents, alternate links, - * other link content types than text/html. Some of them may be created with - * AtomCreator03::additionalElements. - * - * @see FeedCreator#additionalElements - * @since 1.6 - * @author Kai Blankenhorn , Scott Reynen - */ -class AtomCreator03 extends FeedCreator { - - function AtomCreator03() { - $this->contentType = "application/atom+xml"; - $this->encoding = "utf-8"; - } - - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - $feed.= $this->_createStylesheetReferences(); - $feed.= "language!="") { - $feed.= " xml:lang=\"".$this->language."\""; - } - $feed.= ">\n"; - $feed.= " ".htmlspecialchars($this->title)."\n"; - $feed.= " ".htmlspecialchars($this->description)."\n"; - $feed.= " link)."\"/>\n"; - $feed.= " ".htmlspecialchars($this->link)."\n"; - $now = new FeedDate(); - $feed.= " ".htmlspecialchars($now->iso8601())."\n"; - if ($this->editor!="") { - $feed.= " \n"; - $feed.= " ".$this->editor."\n"; - if ($this->editorEmail!="") { - $feed.= " ".$this->editorEmail."\n"; - } - $feed.= " \n"; - } - $feed.= " ".FEEDCREATOR_VERSION."\n"; - $feed.= $this->_createAdditionalElements($this->additionalElements, " "); - for ($i=0;$iitems);$i++) { - $feed.= " \n"; - $feed.= " ".htmlspecialchars(strip_tags($this->items[$i]->title))."\n"; - $feed.= " items[$i]->link)."\"/>\n"; - if ($this->items[$i]->date=="") { - $this->items[$i]->date = time(); - } - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); - if ($this->items[$i]->author!="") { - $feed.= " \n"; - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - $feed.= " \n"; - } - if ($this->items[$i]->description!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->description)."\n"; - } - $feed.= " \n"; - } - $feed.= "\n"; - return $feed; - } -} - - -/** - * MBOXCreator is a FeedCreator that implements the mbox format - * as described in http://www.qmail.org/man/man5/mbox.html - * - * @since 1.3 - * @author Kai Blankenhorn - */ -class MBOXCreator extends FeedCreator { - - function MBOXCreator() { - $this->contentType = "text/plain"; - $this->encoding = "ISO-8859-15"; - } - - function qp_enc($input = "", $line_max = 76) { - $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'); - $lines = preg_split("/(?:\r\n|\r|\n)/", $input); - $eol = "\r\n"; - $escape = "="; - $output = ""; - while( list(, $line) = each($lines) ) { - //$line = rtrim($line); // remove trailing white space -> no =20\r\n necessary - $linlen = strlen($line); - $newline = ""; - for($i = 0; $i < $linlen; $i++) { - $c = substr($line, $i, 1); - $dec = ord($c); - if ( ($dec == 32) && ($i == ($linlen - 1)) ) { // convert space at eol only - $c = "=20"; - } elseif ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) ) { // always encode "\t", which is *not* required - $h2 = floor($dec/16); $h1 = floor($dec%16); - $c = $escape.$hex["$h2"].$hex["$h1"]; - } - if ( (strlen($newline) + strlen($c)) >= $line_max ) { // CRLF is not counted - $output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay - $newline = ""; - } - $newline .= $c; - } // end of for - $output .= $newline.$eol; - } - return trim($output); - } - - - /** - * Builds the MBOX contents. - * @return string the feed's complete text - */ - function createFeed() { - for ($i=0;$iitems);$i++) { - if ($this->items[$i]->author!="") { - $from = $this->items[$i]->author; - } else { - $from = $this->title; - } - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= "From ".strtr(MBOXCreator::qp_enc($from)," ","_")." ".date("D M d H:i:s Y",$itemDate->unix())."\n"; - $feed.= "Content-Type: text/plain;\n"; - $feed.= " charset=\"".$this->encoding."\"\n"; - $feed.= "Content-Transfer-Encoding: quoted-printable\n"; - $feed.= "Content-Type: text/plain\n"; - $feed.= "From: \"".MBOXCreator::qp_enc($from)."\"\n"; - $feed.= "Date: ".$itemDate->rfc822()."\n"; - $feed.= "Subject: ".MBOXCreator::qp_enc(FeedCreator::iTrunc($this->items[$i]->title,100))."\n"; - $feed.= "\n"; - $body = chunk_split(MBOXCreator::qp_enc($this->items[$i]->description)); - $feed.= preg_replace("~\nFrom ([^\n]*)(\n?)~","\n>From $1$2\n",$body); - $feed.= "\n"; - $feed.= "\n"; - } - return $feed; - } - - /** - * Generate a filename for the feed cache file. Overridden from FeedCreator to prevent XML data types. - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".mbox"; - } -} - - -/** - * OPMLCreator is a FeedCreator that implements OPML 1.0. - * - * @see http://opml.scripting.com/spec - * @author Dirk Clemens, Kai Blankenhorn - * @since 1.5 - */ -class OPMLCreator extends FeedCreator { - - function OPMLCreator() { - $this->encoding = "utf-8"; - } - - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - $feed.= $this->_createStylesheetReferences(); - $feed.= "\n"; - $feed.= " \n"; - $feed.= " ".htmlspecialchars($this->title)."\n"; - if ($this->pubDate!="") { - $date = new FeedDate($this->pubDate); - $feed.= " ".$date->rfc822()."\n"; - } - if ($this->lastBuildDate!="") { - $date = new FeedDate($this->lastBuildDate); - $feed.= " ".$date->rfc822()."\n"; - } - if ($this->editor!="") { - $feed.= " ".$this->editor."\n"; - } - if ($this->editorEmail!="") { - $feed.= " ".$this->editorEmail."\n"; - } - $feed.= " \n"; - $feed.= " \n"; - for ($i=0;$iitems);$i++) { - $feed.= " items[$i]->title,"\n\r"," "))); - $feed.= " title=\"".$title."\""; - $feed.= " text=\"".$title."\""; - //$feed.= " description=\"".htmlspecialchars($this->items[$i]->description)."\""; - $feed.= " url=\"".htmlspecialchars($this->items[$i]->link)."\""; - $feed.= "/>\n"; - } - $feed.= " \n"; - $feed.= "\n"; - return $feed; - } -} - - - -/** - * HTMLCreator is a FeedCreator that writes an HTML feed file to a specific - * location, overriding the createFeed method of the parent FeedCreator. - * The HTML produced can be included over http by scripting languages, or serve - * as the source for an IFrame. - * All output by this class is embedded in
    tags to enable formatting - * using CSS. - * - * @author Pascal Van Hecke - * @since 1.7 - */ -class HTMLCreator extends FeedCreator { - - var $contentType = "text/html"; - - /** - * Contains HTML to be output at the start of the feed's html representation. - */ - var $header; - - /** - * Contains HTML to be output at the end of the feed's html representation. - */ - var $footer ; - - /** - * Contains HTML to be output between entries. A separator is only used in - * case of multiple entries. - */ - var $separator; - - /** - * Used to prefix the stylenames to make sure they are unique - * and do not clash with stylenames on the users' page. - */ - var $stylePrefix; - - /** - * Determines whether the links open in a new window or not. - */ - var $openInNewWindow = true; - - var $imageAlign ="right"; - - /** - * In case of very simple output you may want to get rid of the style tags, - * hence this variable. There's no equivalent on item level, but of course you can - * add strings to it while iterating over the items ($this->stylelessOutput .= ...) - * and when it is non-empty, ONLY the styleless output is printed, the rest is ignored - * in the function createFeed(). - */ - var $stylelessOutput =""; - - /** - * Writes the HTML. - * @return string the scripts's complete text - */ - function createFeed() { - // if there is styleless output, use the content of this variable and ignore the rest - if ($this->stylelessOutput!="") { - return $this->stylelessOutput; - } - - //if no stylePrefix is set, generate it yourself depending on the script name - if ($this->stylePrefix=="") { - $this->stylePrefix = str_replace(".", "_", $this->_generateFilename())."_"; - } - - //set an openInNewWindow_token_to be inserted or not - if ($this->openInNewWindow) { - $targetInsert = " target='_blank'"; - } - - // use this array to put the lines in and implode later with "document.write" javascript - $feedArray = array(); - if ($this->image!=null) { - $imageStr = "". - "".
-							FeedCreator::iTrunc(htmlspecialchars($this->image->title),100).
-							"image->width) { - $imageStr .=" width='".$this->image->width. "' "; - } - if ($this->image->height) { - $imageStr .=" height='".$this->image->height."' "; - } - $imageStr .="/>"; - $feedArray[] = $imageStr; - } - - if ($this->title) { - $feedArray[] = ""; - } - if ($this->getDescription()) { - $feedArray[] = "
    ". - str_replace("]]>", "", str_replace("getDescription())). - "
    "; - } - - if ($this->header) { - $feedArray[] = "
    ".$this->header."
    "; - } - - for ($i=0;$iitems);$i++) { - if ($this->separator and $i > 0) { - $feedArray[] = "
    ".$this->separator."
    "; - } - - if ($this->items[$i]->title) { - if ($this->items[$i]->link) { - $feedArray[] = - ""; - } else { - $feedArray[] = - "
    ". - FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100). - "
    "; - } - } - if ($this->items[$i]->getDescription()) { - $feedArray[] = - "
    ". - str_replace("]]>", "", str_replace("items[$i]->getDescription())). - "
    "; - } - } - if ($this->footer) { - $feedArray[] = "
    ".$this->footer."
    "; - } - - $feed= "".join($feedArray, "\r\n"); - return $feed; - } - - /** - * Overrrides parent to produce .html extensions - * - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".html"; - } -} - - -/** - * JSCreator is a class that writes a js file to a specific - * location, overriding the createFeed method of the parent HTMLCreator. - * - * @author Pascal Van Hecke - */ -class JSCreator extends HTMLCreator { - var $contentType = "text/javascript"; - - /** - * writes the javascript - * @return string the scripts's complete text - */ - function createFeed() - { - $feed = parent::createFeed(); - $feedArray = explode("\n",$feed); - - $jsFeed = ""; - foreach ($feedArray as $value) { - $jsFeed .= "document.write('".trim(addslashes($value))."');\n"; - } - return $jsFeed; - } - - /** - * Overrrides parent to produce .js extensions - * - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".js"; - } - -} - - - -/*** TEST SCRIPT ********************************************************* - -//include("feedcreator.class.php"); - -$rss = new UniversalFeedCreator(); -$rss->useCached(); -$rss->title = "PHP news"; -$rss->description = "daily news from the PHP scripting world"; - -//optional -//$rss->descriptionTruncSize = 500; -//$rss->descriptionHtmlSyndicated = true; -//$rss->xslStyleSheet = "http://feedster.com/rss20.xsl"; - -$rss->link = "http://www.dailyphp.net/news"; -$rss->feedURL = "http://www.dailyphp.net/".$PHP_SELF; - -$image = new FeedImage(); -$image->title = "dailyphp.net logo"; -$image->url = "http://www.dailyphp.net/images/logo.gif"; -$image->link = "http://www.dailyphp.net"; -$image->description = "Feed provided by dailyphp.net. Click to visit."; - -//optional -$image->descriptionTruncSize = 500; -$image->descriptionHtmlSyndicated = true; - -$rss->image = $image; - -// get your news items from somewhere, e.g. your database: -//mysql_select_db($dbHost, $dbUser, $dbPass); -//$res = mysql_query("SELECT * FROM news ORDER BY newsdate DESC"); -//while ($data = mysql_fetch_object($res)) { - $item = new FeedItem(); - $item->title = "This is an the test title of an item"; - $item->link = "http://localhost/item/"; - $item->description = "description in
    HTML"; - - //optional - //item->descriptionTruncSize = 500; - $item->descriptionHtmlSyndicated = true; - - $item->date = time(); - $item->source = "http://www.dailyphp.net"; - $item->author = "John Doe"; - - $rss->addItem($item); -//} - -// valid format strings are: RSS0.91, RSS1.0, RSS2.0, PIE0.1, MBOX, OPML, ATOM0.3, HTML, JS -echo $rss->saveFeed("RSS0.91", "feed.xml"); - - - -***************************************************************************/ - -?> diff --git a/web/lib/gettext.php b/web/lib/gettext.php deleted file mode 100644 index 098f0e5e..00000000 --- a/web/lib/gettext.php +++ /dev/null @@ -1,432 +0,0 @@ -. - Copyright (c) 2005 Nico Kaiser - - This file is part of PHP-gettext. - - PHP-gettext 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 2 of the License, or - (at your option) any later version. - - PHP-gettext 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 PHP-gettext; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -*/ - -/** - * Provides a simple gettext replacement that works independently from - * the system's gettext abilities. - * It can read MO files and use them for translating strings. - * The files are passed to gettext_reader as a Stream (see streams.php) - * - * This version has the ability to cache all strings and translations to - * speed up the string lookup. - * While the cache is enabled by default, it can be switched off with the - * second parameter in the constructor (e.g. whenusing very large MO files - * that you don't want to keep in memory) - */ -class gettext_reader { - //public: - var $error = 0; // public variable that holds error code (0 if no error) - - //private: - var $BYTEORDER = 0; // 0: low endian, 1: big endian - var $STREAM = NULL; - var $short_circuit = false; - var $enable_cache = false; - var $originals = NULL; // offset of original table - var $translations = NULL; // offset of translation table - var $pluralheader = NULL; // cache header field for plural forms - var $total = 0; // total string count - var $table_originals = NULL; // table for original strings (offsets) - var $table_translations = NULL; // table for translated strings (offsets) - var $cache_translations = NULL; // original -> translation mapping - - - /* Methods */ - - - /** - * Reads a 32bit Integer from the Stream - * - * @access private - * @return Integer from the Stream - */ - function readint() { - if ($this->BYTEORDER == 0) { - // low endian - $input=unpack('V', $this->STREAM->read(4)); - return array_shift($input); - } else { - // big endian - $input=unpack('N', $this->STREAM->read(4)); - return array_shift($input); - } - } - - function read($bytes) { - return $this->STREAM->read($bytes); - } - - /** - * Reads an array of Integers from the Stream - * - * @param int count How many elements should be read - * @return Array of Integers - */ - function readintarray($count) { - if ($this->BYTEORDER == 0) { - // low endian - return unpack('V'.$count, $this->STREAM->read(4 * $count)); - } else { - // big endian - return unpack('N'.$count, $this->STREAM->read(4 * $count)); - } - } - - /** - * Constructor - * - * @param object Reader the StreamReader object - * @param boolean enable_cache Enable or disable caching of strings (default on) - */ - function __construct($Reader, $enable_cache = true) { - // If there isn't a StreamReader, turn on short circuit mode. - if (! $Reader || isset($Reader->error) ) { - $this->short_circuit = true; - return; - } - - // Caching can be turned off - $this->enable_cache = $enable_cache; - - $MAGIC1 = "\x95\x04\x12\xde"; - $MAGIC2 = "\xde\x12\x04\x95"; - - $this->STREAM = $Reader; - $magic = $this->read(4); - if ($magic == $MAGIC1) { - $this->BYTEORDER = 1; - } elseif ($magic == $MAGIC2) { - $this->BYTEORDER = 0; - } else { - $this->error = 1; // not MO file - return false; - } - - // FIXME: Do we care about revision? We should. - $revision = $this->readint(); - - $this->total = $this->readint(); - $this->originals = $this->readint(); - $this->translations = $this->readint(); - } - - /** - * Loads the translation tables from the MO file into the cache - * If caching is enabled, also loads all strings into a cache - * to speed up translation lookups - * - * @access private - */ - function load_tables() { - if (is_array($this->cache_translations) && - is_array($this->table_originals) && - is_array($this->table_translations)) - return; - - /* get original and translations tables */ - if (!is_array($this->table_originals)) { - $this->STREAM->seekto($this->originals); - $this->table_originals = $this->readintarray($this->total * 2); - } - if (!is_array($this->table_translations)) { - $this->STREAM->seekto($this->translations); - $this->table_translations = $this->readintarray($this->total * 2); - } - - if ($this->enable_cache) { - $this->cache_translations = array (); - /* read all strings in the cache */ - for ($i = 0; $i < $this->total; $i++) { - $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); - $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); - $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); - $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); - $this->cache_translations[$original] = $translation; - } - } - } - - /** - * Returns a string from the "originals" table - * - * @access private - * @param int num Offset number of original string - * @return string Requested string if found, otherwise '' - */ - function get_original_string($num) { - $length = $this->table_originals[$num * 2 + 1]; - $offset = $this->table_originals[$num * 2 + 2]; - if (! $length) - return ''; - $this->STREAM->seekto($offset); - $data = $this->STREAM->read($length); - return (string)$data; - } - - /** - * Returns a string from the "translations" table - * - * @access private - * @param int num Offset number of original string - * @return string Requested string if found, otherwise '' - */ - function get_translation_string($num) { - $length = $this->table_translations[$num * 2 + 1]; - $offset = $this->table_translations[$num * 2 + 2]; - if (! $length) - return ''; - $this->STREAM->seekto($offset); - $data = $this->STREAM->read($length); - return (string)$data; - } - - /** - * Binary search for string - * - * @access private - * @param string string - * @param int start (internally used in recursive function) - * @param int end (internally used in recursive function) - * @return int string number (offset in originals table) - */ - function find_string($string, $start = -1, $end = -1) { - if (($start == -1) or ($end == -1)) { - // find_string is called with only one parameter, set start end end - $start = 0; - $end = $this->total; - } - if (abs($start - $end) <= 1) { - // We're done, now we either found the string, or it doesn't exist - $txt = $this->get_original_string($start); - if ($string == $txt) - return $start; - else - return -1; - } else if ($start > $end) { - // start > end -> turn around and start over - return $this->find_string($string, $end, $start); - } else { - // Divide table in two parts - $half = (int)(($start + $end) / 2); - $cmp = strcmp($string, $this->get_original_string($half)); - if ($cmp == 0) - // string is exactly in the middle => return it - return $half; - else if ($cmp < 0) - // The string is in the upper half - return $this->find_string($string, $start, $half); - else - // The string is in the lower half - return $this->find_string($string, $half, $end); - } - } - - /** - * Translates a string - * - * @access public - * @param string string to be translated - * @return string translated string (or original, if not found) - */ - function translate($string) { - if ($this->short_circuit) - return $string; - $this->load_tables(); - - if ($this->enable_cache) { - // Caching enabled, get translated string from cache - if (array_key_exists($string, $this->cache_translations)) - return $this->cache_translations[$string]; - else - return $string; - } else { - // Caching not enabled, try to find string - $num = $this->find_string($string); - if ($num == -1) - return $string; - else - return $this->get_translation_string($num); - } - } - - /** - * Sanitize plural form expression for use in PHP eval call. - * - * @access private - * @return string sanitized plural form expression - */ - function sanitize_plural_expression($expr) { - // Get rid of disallowed characters. - $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); - - // Add parenthesis for tertiary '?' operator. - $expr .= ';'; - $res = ''; - $p = 0; - for ($i = 0; $i < strlen($expr); $i++) { - $ch = $expr[$i]; - switch ($ch) { - case '?': - $res .= ' ? ('; - $p++; - break; - case ':': - $res .= ') : ('; - break; - case ';': - $res .= str_repeat( ')', $p) . ';'; - $p = 0; - break; - default: - $res .= $ch; - } - } - return $res; - } - - /** - * Parse full PO header and extract only plural forms line. - * - * @access private - * @return string verbatim plural form header field - */ - function extract_plural_forms_header_from_po_header($header) { - if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) - $expr = $regs[2]; - else - $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; - return $expr; - } - - /** - * Get possible plural forms from MO header - * - * @access private - * @return string plural form header - */ - function get_plural_forms() { - // lets assume message number 0 is header - // this is true, right? - $this->load_tables(); - - // cache header field for plural forms - if (! is_string($this->pluralheader)) { - if ($this->enable_cache) { - $header = $this->cache_translations[""]; - } else { - $header = $this->get_translation_string(0); - } - $expr = $this->extract_plural_forms_header_from_po_header($header); - $this->pluralheader = $this->sanitize_plural_expression($expr); - } - return $this->pluralheader; - } - - /** - * Detects which plural form to take - * - * @access private - * @param n count - * @return int array index of the right plural form - */ - function select_string($n) { - $string = $this->get_plural_forms(); - $string = str_replace('nplurals',"\$total",$string); - $string = str_replace("n",$n,$string); - $string = str_replace('plural',"\$plural",$string); - - $total = 0; - $plural = 0; - - eval("$string"); - if ($plural >= $total) $plural = $total - 1; - return $plural; - } - - /** - * Plural version of gettext - * - * @access public - * @param string single - * @param string plural - * @param string number - * @return translated plural form - */ - function ngettext($single, $plural, $number) { - if ($this->short_circuit) { - if ($number != 1) - return $plural; - else - return $single; - } - - // find out the appropriate form - $select = $this->select_string($number); - - // this should contains all strings separated by NULLs - $key = $single . chr(0) . $plural; - - - if ($this->enable_cache) { - if (! array_key_exists($key, $this->cache_translations)) { - return ($number != 1) ? $plural : $single; - } else { - $result = $this->cache_translations[$key]; - $list = explode(chr(0), $result); - return $list[$select]; - } - } else { - $num = $this->find_string($key); - if ($num == -1) { - return ($number != 1) ? $plural : $single; - } else { - $result = $this->get_translation_string($num); - $list = explode(chr(0), $result); - return $list[$select]; - } - } - } - - function pgettext($context, $msgid) { - $key = $context . chr(4) . $msgid; - $ret = $this->translate($key); - if (strpos($ret, "\004") !== false) { - return $msgid; - } else { - return $ret; - } - } - - function npgettext($context, $singular, $plural, $number) { - $key = $context . chr(4) . $singular; - $ret = $this->ngettext($key, $plural, $number); - if (strpos($ret, "\004") !== false) { - return $singular; - } else { - return $ret; - } - - } -} - -?> diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php deleted file mode 100644 index a053962e..00000000 --- a/web/lib/pkgbasefuncs.inc.php +++ /dev/null @@ -1,1253 +0,0 @@ -query($q); - if (!$result) { - return null; - } - - return $result->fetchColumn(0); -} - -/** - * Get all package comment information for a specific package base - * - * @param int $base_id The package base ID to get comments for - * @param int $limit Maximum number of comments to return (0 means unlimited) - * @param bool $include_deleted True if deleted comments should be included - * @param bool $only_pinned True when only pinned comments are to be included - * - * @return array All package comment information for a specific package base - */ -function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false, $offset=0) { - $base_id = intval($base_id); - $limit = intval($limit); - if (!$base_id) { - return null; - } - - $dbh = DB::connect(); - $q = "SELECT PackageComments.ID, A.UserName AS UserName, UsersID, Comments, "; - $q.= "PackageBaseID, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, "; - $q.= "DelUsersID, C.UserName AS DelUserName, RenderedComment, "; - $q.= "PinnedTS FROM PackageComments "; - $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; - $q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID "; - $q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID "; - $q.= "WHERE PackageBaseID = " . $base_id . " "; - - if (!$include_deleted) { - $q.= "AND DelTS IS NULL "; - } - if ($only_pinned) { - $q.= "AND NOT PinnedTS = 0 "; - } - $q.= "ORDER BY CommentTS DESC"; - if ($limit > 0) { - $q.=" LIMIT " . $limit; - } - if ($offset > 0) { - $q.=" OFFSET " . $offset; - } - $result = $dbh->query($q); - if (!$result) { - return null; - } - - return $result->fetchAll(); -} - -/* - * Invoke the comment rendering script. - * - * @param int $id ID of the comment to render - * - * @return void - */ -function render_comment($id) { - $cmd = config_get('options', 'render-comment-cmd'); - $cmd .= ' ' . intval($id); - - $descspec = array( - 0 => array('pipe', 'r'), - 1 => array('pipe', 'w'), - ); - - $p = proc_open($cmd, $descspec, $pipes); - - if (!is_resource($p)) { - return false; - } - - fclose($pipes[0]); - fclose($pipes[1]); - - return proc_close($p); -} - -/** - * Add a comment to a package page and send out appropriate notifications - * - * @param string $base_id The package base ID to add the comment on - * @param string $uid The user ID of the individual who left the comment - * @param string $comment The comment left on a package page - * - * @return void - */ -function pkgbase_add_comment($base_id, $uid, $comment) { - $dbh = DB::connect(); - - if (trim($comment) == '') { - return array(false, __('Comment cannot be empty.')); - } - - $q = "INSERT INTO PackageComments "; - $q.= "(PackageBaseID, UsersID, Comments, RenderedComment, CommentTS) "; - $q.= "VALUES (" . intval($base_id) . ", " . $uid . ", "; - $q.= $dbh->quote($comment) . ", '', " . strval(time()) . ")"; - $dbh->exec($q); - $comment_id = $dbh->lastInsertId(); - - render_comment($comment_id); - - notify(array('comment', $uid, $base_id, $comment_id)); - - return array(true, __('Comment has been added.')); -} - -/** - * Pin/unpin a package comment - * - * @param bool $unpin True if unpinning rather than pinning - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_pin_comment($unpin=false) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - - if (!$uid) { - return array(false, __("You must be logged in before you can edit package information.")); - } - - if (isset($_POST["comment_id"])) { - $comment_id = $_POST["comment_id"]; - } else { - return array(false, __("Missing comment ID.")); - } - - if (!$unpin) { - if (pkgbase_comments_count($_POST['package_base'], false, true) >= 5){ - return array(false, __("No more than 5 comments can be pinned.")); - } - } - - if (!can_pin_comment($comment_id)) { - if (!$unpin) { - return array(false, __("You are not allowed to pin this comment.")); - } else { - return array(false, __("You are not allowed to unpin this comment.")); - } - } - - $dbh = DB::connect(); - $q = "UPDATE PackageComments "; - if (!$unpin) { - $q.= "SET PinnedTS = " . strval(time()) . " "; - } else { - $q.= "SET PinnedTS = 0 "; - } - $q.= "WHERE ID = " . intval($comment_id); - $dbh->exec($q); - - if (!$unpin) { - return array(true, __("Comment has been pinned.")); - } else { - return array(true, __("Comment has been unpinned.")); - } -} - -/** - - * Get a list of all packages a logged-in user has voted for - * - * @param string $sid The session ID of the visitor - * - * @return array All packages the visitor has voted for - */ -function pkgbase_votes_from_sid($sid="") { - $pkgs = array(); - if (!$sid) {return $pkgs;} - $dbh = DB::connect(); - $q = "SELECT PackageBaseID "; - $q.= "FROM PackageVotes, Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Users.ID = PackageVotes.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_NUM)) { - $pkgs[$row[0]] = 1; - } - } - return $pkgs; -} - -/** - * Get the package base details - * - * @param string $id The package base ID to get description for - * - * @return array The package base's details OR error message - **/ -function pkgbase_get_details($base_id) { - $dbh = DB::connect(); - - $q = "SELECT PackageBases.ID, PackageBases.Name, "; - $q.= "PackageBases.NumVotes, PackageBases.Popularity, "; - $q.= "PackageBases.OutOfDateTS, PackageBases.SubmittedTS, "; - $q.= "PackageBases.ModifiedTS, PackageBases.SubmitterUID, "; - $q.= "PackageBases.MaintainerUID, PackageBases.PackagerUID, "; - $q.= "PackageBases.FlaggerUID, "; - $q.= "(SELECT COUNT(*) FROM PackageRequests "; - $q.= " WHERE PackageRequests.PackageBaseID = PackageBases.ID "; - $q.= " AND PackageRequests.Status = 0) AS RequestCount "; - $q.= "FROM PackageBases "; - $q.= "WHERE PackageBases.ID = " . intval($base_id); - $result = $dbh->query($q); - - $row = array(); - - if (!$result) { - $row['error'] = __("Error retrieving package details."); - } - else { - $row = $result->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - $row['error'] = __("Package details could not be found."); - } - } - - return $row; -} - -/** - * Display the package base details page - * - * @param string $id The package base ID to get details page for - * @param array $row Package base details retrieved by pkgbase_get_details() - * @param string $SID The session ID of the visitor - * - * @return void - */ -function pkgbase_display_details($base_id, $row, $SID="") { - if (isset($row['error'])) { - print "

    " . $row['error'] . "

    \n"; - } - else { - $pkgbase_name = pkgbase_name_from_id($base_id); - - include('pkgbase_details.php'); - - if ($SID) { - $comment_section = "package"; - include('pkg_comment_box.php'); - } - - $include_deleted = has_credential(CRED_COMMENT_VIEW_DELETED); - - $limit_pinned = isset($_GET['pinned']) ? 0 : 5; - $pinned = pkgbase_comments($base_id, $limit_pinned, false, true); - if (!empty($pinned)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - unset($pinned); - - - $total_comment_count = pkgbase_comments_count($base_id, $include_deleted); - list($pagination_templs, $per_page, $offset) = calculate_pagination($total_comment_count); - - $comments = pkgbase_comments($base_id, $per_page, $include_deleted, false, $offset); - if (!empty($comments)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - } -} - -/** - * Convert a list of package IDs into a list of corresponding package bases. - * - * @param array|int $ids Array of package IDs to convert - * - * @return array|int List of package base IDs - */ -function pkgbase_from_pkgid($ids) { - $dbh = DB::connect(); - - if (is_array($ids)) { - $q = "SELECT PackageBaseID FROM Packages "; - $q.= "WHERE ID IN (" . implode(",", $ids) . ")"; - $result = $dbh->query($q); - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - $q = "SELECT PackageBaseID FROM Packages "; - $q.= "WHERE ID = " . $ids; - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); - } -} - -/** - * Retrieve ID of a package base by name - * - * @param string $name The package base name to retrieve the ID for - * - * @return int The ID of the package base - */ -function pkgbase_from_name($name) { - $dbh = DB::connect(); - $q = "SELECT ID FROM PackageBases WHERE Name = " . $dbh->quote($name); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Retrieve the name of a package base given its ID - * - * @param int $base_id The ID of the package base to query - * - * @return string The name of the package base - */ -function pkgbase_name_from_id($base_id) { - $dbh = DB::connect(); - $q = "SELECT Name FROM PackageBases WHERE ID = " . intval($base_id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Get the names of all packages belonging to a package base - * - * @param int $base_id The ID of the package base - * - * @return array The names of all packages belonging to the package base - */ -function pkgbase_get_pkgnames($base_id) { - $dbh = DB::connect(); - $q = "SELECT Name FROM Packages WHERE PackageBaseID = " . intval($base_id); - $result = $dbh->query($q); - return $result->fetchAll(PDO::FETCH_COLUMN, 0); -} - -/** - * Determine whether a package base is (or contains a) VCS package - * - * @param int $base_id The ID of the package base - * - * @return bool True if the package base is/contains a VCS package - */ -function pkgbase_is_vcs($base_id) { - $suffixes = array("-cvs", "-svn", "-git", "-hg", "-bzr", "-darcs"); - $haystack = pkgbase_get_pkgnames($base_id); - array_push($haystack, pkgbase_name_from_id($base_id)); - foreach ($haystack as $pkgname) { - foreach ($suffixes as $suffix) { - if (substr_compare($pkgname, $suffix, -strlen($suffix)) === 0) { - return true; - } - } - } - return false; -} - -/** - * Delete all packages belonging to a package base - * - * @param int $base_id The ID of the package base - * - * @return void - */ -function pkgbase_delete_packages($base_id) { - $dbh = DB::connect(); - $q = "DELETE FROM Packages WHERE PackageBaseID = " . intval($base_id); - $dbh->exec($q); -} - -/** - * Retrieve the maintainer of a package base given its ID - * - * @param int $base_id The ID of the package base to query - * - * @return int The user ID of the current package maintainer - */ -function pkgbase_maintainer_uid($base_id) { - $dbh = DB::connect(); - $q = "SELECT MaintainerUID FROM PackageBases WHERE ID = " . intval($base_id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Retrieve the maintainers of an array of package bases given by their ID - * - * @param int $base_ids The array of IDs of the package bases to query - * - * @return int The user ID of the current package maintainer - */ -function pkgbase_maintainer_uids($base_ids) { - $dbh = DB::connect(); - $q = "SELECT MaintainerUID FROM PackageBases WHERE ID IN (" . implode(",", $base_ids) . ")"; - $result = $dbh->query($q); - return $result->fetchAll(PDO::FETCH_COLUMN, 0); -} - -/** - * Flag package(s) as out-of-date - * - * @param array $base_ids Array of package base IDs to flag/unflag - * @param string $comment The comment to add - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_flag($base_ids, $comment) { - if (!has_credential(CRED_PKGBASE_FLAG)) { - return array(false, __("You must be logged in before you can flag packages.")); - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("You did not select any packages to flag.")); - } - - if (strlen($comment) < 3) { - return array(false, __("The selected packages have not been flagged, please enter a comment.")); - } - - $uid = uid_from_sid($_COOKIE['AURSID']); - $dbh = DB::connect(); - - $q = "UPDATE PackageBases SET "; - $q.= "OutOfDateTS = " . strval(time()) . ", FlaggerUID = " . $uid . ", "; - $q.= "FlaggerComment = " . $dbh->quote($comment) . " "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - $q.= "AND OutOfDateTS IS NULL"; - $dbh->exec($q); - - foreach ($base_ids as $base_id) { - notify(array('flag', $uid, $base_id)); - } - - return array(true, __("The selected packages have been flagged out-of-date.")); -} - -/** - * Unflag package(s) as out-of-date - * - * @param array $base_ids Array of package base IDs to flag/unflag - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_unflag($base_ids) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - return array(false, __("You must be logged in before you can unflag packages.")); - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("You did not select any packages to unflag.")); - } - - $dbh = DB::connect(); - - $q = "UPDATE PackageBases SET "; - $q.= "OutOfDateTS = NULL "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - - $maintainers = array_merge(pkgbase_maintainer_uids($base_ids), pkgbase_get_comaintainer_uids($base_ids)); - if (!has_credential(CRED_PKGBASE_UNFLAG, $maintainers)) { - $q.= "AND (MaintainerUID = " . $uid . " OR FlaggerUID = " . $uid. ")"; - } - - $result = $dbh->exec($q); - - if ($result) { - return array(true, __("The selected packages have been unflagged.")); - } -} - -/** - * Get package flag OOD comment - * - * @param int $base_id - * - * @return array Tuple of pkgbase ID, reason for OOD, and user who flagged - */ -function pkgbase_get_flag_comment($base_id) { - $base_id = intval($base_id); - $dbh = DB::connect(); - - $q = "SELECT FlaggerComment,OutOfDateTS,Username FROM PackageBases "; - $q.= "LEFT JOIN Users ON FlaggerUID = Users.ID "; - $q.= "WHERE PackageBases.ID = " . $base_id . " "; - $q.= "AND PackageBases.OutOfDateTS IS NOT NULL"; - $result = $dbh->query($q); - - $row = array(); - - if (!$result) { - $row['error'] = __("Error retrieving package details."); - } - else { - $row = $result->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - $row['error'] = __("Package details could not be found."); - } - } - - return $row; -} - -/** - * Delete package bases - * - * @param array $base_ids Array of package base IDs to delete - * @param int $merge_base_id Package base to merge the deleted ones into - * @param int $via Package request to close upon deletion - * @param bool $grant Allow anyone to delete the package base - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_delete ($base_ids, $merge_base_id, $via, $grant=false) { - if (!$grant && !has_credential(CRED_PKGBASE_DELETE)) { - return array(false, __("You do not have permission to delete packages.")); - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("You did not select any packages to delete.")); - } - - $dbh = DB::connect(); - - if ($merge_base_id) { - $merge_base_name = pkgbase_name_from_id($merge_base_id); - } - - $uid = uid_from_sid($_COOKIE['AURSID']); - foreach ($base_ids as $base_id) { - if ($merge_base_id) { - notify(array('delete', $uid, $base_id, $merge_base_id)); - } else { - notify(array('delete', $uid, $base_id)); - } - } - - /* - * Close package request if the deletion was initiated through the - * request interface. NOTE: This needs to happen *before* the actual - * deletion. Otherwise, the former maintainer will not be included in - * the Cc list of the request notification email. - */ - if ($via) { - pkgreq_close(intval($via), 'accepted', ''); - } - - /* Scan through pending deletion requests and close them. */ - $username = username_from_sid($_COOKIE['AURSID']); - foreach ($base_ids as $base_id) { - $pkgreq_ids = array_merge(pkgreq_by_pkgbase($base_id)); - foreach ($pkgreq_ids as $pkgreq_id) { - pkgreq_close(intval($pkgreq_id), 'accepted', - 'The user ' . $username . - ' deleted the package.', true); - } - } - - if ($merge_base_id) { - /* Merge comments */ - $q = "UPDATE PackageComments "; - $q.= "SET PackageBaseID = " . intval($merge_base_id) . " "; - $q.= "WHERE PackageBaseID IN (" . implode(",", $base_ids) . ")"; - $dbh->exec($q); - - /* Merge notifications */ - $q = "SELECT DISTINCT UserID FROM PackageNotifications cn "; - $q.= "WHERE PackageBaseID IN (" . implode(",", $base_ids) . ") "; - $q.= "AND NOT EXISTS (SELECT * FROM PackageNotifications cn2 "; - $q.= "WHERE cn2.PackageBaseID = " . intval($merge_base_id) . " "; - $q.= "AND cn2.UserID = cn.UserID)"; - $result = $dbh->query($q); - - while ($notify_uid = $result->fetch(PDO::FETCH_COLUMN, 0)) { - $q = "INSERT INTO PackageNotifications (UserID, PackageBaseID) "; - $q.= "VALUES (" . intval($notify_uid) . ", " . intval($merge_base_id) . ")"; - $dbh->exec($q); - } - - /* Merge votes */ - foreach ($base_ids as $base_id) { - $q = "UPDATE PackageVotes "; - $q.= "SET PackageBaseID = " . intval($merge_base_id) . " "; - $q.= "WHERE PackageBaseID = " . $base_id . " "; - $q.= "AND UsersID NOT IN ("; - $q.= "SELECT * FROM (SELECT UsersID "; - $q.= "FROM PackageVotes "; - $q.= "WHERE PackageBaseID = " . intval($merge_base_id); - $q.= ") temp)"; - $dbh->exec($q); - } - - $q = "UPDATE PackageBases "; - $q.= "SET NumVotes = (SELECT COUNT(*) FROM PackageVotes "; - $q.= "WHERE PackageBaseID = " . intval($merge_base_id) . ") "; - $q.= "WHERE ID = " . intval($merge_base_id); - $dbh->exec($q); - } - - $q = "DELETE FROM Packages WHERE PackageBaseID IN (" . implode(",", $base_ids) . ")"; - $dbh->exec($q); - - $q = "DELETE FROM PackageBases WHERE ID IN (" . implode(",", $base_ids) . ")"; - $dbh->exec($q); - - return array(true, __("The selected packages have been deleted.")); -} - -/** - * Adopt or disown packages - * - * @param array $base_ids Array of package base IDs to adopt/disown - * @param bool $action Adopts if true, disowns if false. Adopts by default - * @param int $via Package request to close upon adoption - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_adopt ($base_ids, $action=true, $via) { - $dbh = DB::connect(); - - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - if ($action) { - return array(false, __("You must be logged in before you can adopt packages.")); - } else { - return array(false, __("You must be logged in before you can disown packages.")); - } - } - - /* Verify package ownership. */ - $base_ids = sanitize_ids($base_ids); - - $q = "SELECT ID FROM PackageBases "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - - if ($action && !has_credential(CRED_PKGBASE_ADOPT)) { - /* Regular users may only adopt orphan packages. */ - $q.= "AND MaintainerUID IS NULL"; - } - if (!$action && !has_credential(CRED_PKGBASE_DISOWN)) { - /* Regular users may only disown their own packages. */ - $q.= "AND MaintainerUID = " . $uid; - } - - $result = $dbh->query($q); - $base_ids = $result->fetchAll(PDO::FETCH_COLUMN, 0); - - /* Error out if the list of remaining packages is empty. */ - if (empty($base_ids)) { - if ($action) { - return array(false, __("You did not select any packages to adopt.")); - } else { - return array(false, __("You did not select any packages to disown.")); - } - } - - /* - * Close package request if the disownment was initiated through the - * request interface. NOTE: This needs to happen *before* the actual - * disown operation. Otherwise, the former maintainer will not be - * included in the Cc list of the request notification email. - */ - if ($via) { - pkgreq_close(intval($via), 'accepted', ''); - } - - /* Scan through pending orphan requests and close them. */ - if (!$action) { - $username = username_from_sid($_COOKIE['AURSID']); - foreach ($base_ids as $base_id) { - $pkgreq_ids = pkgreq_by_pkgbase($base_id, 'orphan'); - foreach ($pkgreq_ids as $pkgreq_id) { - pkgreq_close(intval($pkgreq_id), 'accepted', - 'The user ' . $username . - ' disowned the package.', true); - } - } - } - - /* Adopt or disown the package. */ - if ($action) { - $q = "UPDATE PackageBases "; - $q.= "SET MaintainerUID = $uid "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - $dbh->exec($q); - - /* Add the new maintainer to the notification list. */ - pkgbase_notify($base_ids); - } else { - /* Update the co-maintainer list when disowning a package. */ - if (has_credential(CRED_PKGBASE_DISOWN)) { - foreach ($base_ids as $base_id) { - pkgbase_set_comaintainers($base_id, array()); - } - - $q = "UPDATE PackageBases "; - $q.= "SET MaintainerUID = NULL "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - $dbh->exec($q); - } else { - foreach ($base_ids as $base_id) { - $comaintainers = pkgbase_get_comaintainers($base_id); - - if (count($comaintainers) > 0) { - $comaintainer_uid = uid_from_username($comaintainers[0]); - $comaintainers = array_diff($comaintainers, array($comaintainers[0])); - pkgbase_set_comaintainers($base_id, $comaintainers); - } else { - $comaintainer_uid = "NULL"; - } - - $q = "UPDATE PackageBases "; - $q.= "SET MaintainerUID = " . $comaintainer_uid . " "; - $q.= "WHERE ID = " . $base_id; - $dbh->exec($q); - } - } - } - - foreach ($base_ids as $base_id) { - notify(array($action ? 'adopt' : 'disown', $uid, $base_id)); - } - - if ($action) { - return array(true, __("The selected packages have been adopted.")); - } else { - return array(true, __("The selected packages have been disowned.")); - } -} - -/** - * Vote and un-vote for packages - * - * @param array $base_ids Array of package base IDs to vote/un-vote - * @param bool $action Votes if true, un-votes if false. Votes by default - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_vote ($base_ids, $action=true) { - if (!has_credential(CRED_PKGBASE_VOTE)) { - if ($action) { - return array(false, __("You must be logged in before you can vote for packages.")); - } else { - return array(false, __("You must be logged in before you can un-vote for packages.")); - } - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - if ($action) { - return array(false, __("You did not select any packages to vote for.")); - } else { - return array(false, __("Your votes have been removed from the selected packages.")); - } - } - - $dbh = DB::connect(); - $my_votes = pkgbase_votes_from_sid($_COOKIE["AURSID"]); - $uid = uid_from_sid($_COOKIE["AURSID"]); - - $first = 1; - $vote_ids = ""; - $vote_clauses = ""; - foreach ($base_ids as $pid) { - if ($action) { - $check = !isset($my_votes[$pid]); - } else { - $check = isset($my_votes[$pid]); - } - - if ($check) { - if ($first) { - $first = 0; - $vote_ids = $pid; - if ($action) { - $vote_clauses = "($uid, $pid, " . strval(time()) . ")"; - } - } else { - $vote_ids .= ", $pid"; - if ($action) { - $vote_clauses .= ", ($uid, $pid, " . strval(time()) . ")"; - } - } - } - } - - if (!empty($vote_ids)) { - /* Only add votes for packages the user hasn't already voted for. */ - $op = $action ? "+" : "-"; - $q = "UPDATE PackageBases SET NumVotes = NumVotes $op 1 "; - $q.= "WHERE ID IN ($vote_ids)"; - - $dbh->exec($q); - - if ($action) { - $q = "INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS) VALUES "; - $q.= $vote_clauses; - } else { - $q = "DELETE FROM PackageVotes WHERE UsersID = $uid "; - $q.= "AND PackageBaseID IN ($vote_ids)"; - } - - $dbh->exec($q); - } - - if ($action) { - return array(true, __("Your votes have been cast for the selected packages.")); - } else { - return array(true, __("Your votes have been removed from the selected packages.")); - } -} - -/** - * Get all usernames and IDs that voted for a specific package base - * - * @param string $pkgbase_name The package base to retrieve votes for - * - * @return array User IDs and usernames that voted for a specific package base - */ -function pkgbase_votes_from_name($pkgbase_name) { - $dbh = DB::connect(); - - $q = "SELECT UsersID, Username, Name, VoteTS FROM PackageVotes "; - $q.= "LEFT JOIN Users ON UsersID = Users.ID "; - $q.= "LEFT JOIN PackageBases "; - $q.= "ON PackageVotes.PackageBaseID = PackageBases.ID "; - $q.= "WHERE PackageBases.Name = ". $dbh->quote($pkgbase_name) . " "; - $q.= "ORDER BY Username"; - $result = $dbh->query($q); - - if (!$result) { - return; - } - - $votes = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $votes[] = $row; - } - - return $votes; -} - -/** - * Determine if a user has already voted for a specific package base - * - * @param string $uid The user ID to check for an existing vote - * @param string $base_id The package base ID to check for an existing vote - * - * @return bool True if the user has already voted, otherwise false - */ -function pkgbase_user_voted($uid, $base_id) { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) FROM PackageVotes WHERE "; - $q.= "UsersID = ". $dbh->quote($uid) . " AND "; - $q.= "PackageBaseID = " . $dbh->quote($base_id); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - return ($result->fetch(PDO::FETCH_COLUMN, 0) > 0); -} - -/** - * Determine if a user wants notifications for a specific package base - * - * @param string $uid User ID to check in the database - * @param string $base_id Package base ID to check notifications for - * - * @return bool True if the user wants notifications, otherwise false - */ -function pkgbase_user_notify($uid, $base_id) { - $dbh = DB::connect(); - - $q = "SELECT * FROM PackageNotifications WHERE UserID = " . $dbh->quote($uid); - $q.= " AND PackageBaseID = " . $dbh->quote($base_id); - $result = $dbh->query($q); - - if (!$result) { - return false; - } - - return ($result->fetch(PDO::FETCH_NUM) > 0); -} - -/** - * Toggle notification of packages - * - * @param array $base_ids Array of package base IDs to toggle - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_notify ($base_ids, $action=true) { - if (!has_credential(CRED_PKGBASE_NOTIFY)) { - return; - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("Couldn't add to notification list.")); - } - - $dbh = DB::connect(); - $uid = uid_from_sid($_COOKIE["AURSID"]); - - $output = ""; - - $first = true; - - /* - * There currently shouldn't be multiple requests here, but the format - * in which it's sent requires this. - */ - foreach ($base_ids as $bid) { - $q = "SELECT Name FROM PackageBases WHERE ID = $bid"; - $result = $dbh->query($q); - if ($result) { - $row = $result->fetch(PDO::FETCH_NUM); - $basename = $row[0]; - } - else { - $basename = ''; - } - - if ($first) - $first = false; - else - $output .= ", "; - - - if ($action) { - $q = "SELECT COUNT(*) FROM PackageNotifications WHERE "; - $q .= "UserID = $uid AND PackageBaseID = $bid"; - - /* Notification already added. Don't add again. */ - $result = $dbh->query($q); - if ($result->fetchColumn() == 0) { - $q = "INSERT INTO PackageNotifications (PackageBaseID, UserID) VALUES ($bid, $uid)"; - $dbh->exec($q); - } - - $output .= $basename; - } - else { - $q = "DELETE FROM PackageNotifications WHERE PackageBaseID = $bid "; - $q .= "AND UserID = $uid"; - $dbh->exec($q); - - $output .= $basename; - } - } - - if ($action) { - $output = __("You have been added to the comment notification list for %s.", $output); - } - else { - $output = __("You have been removed from the comment notification list for %s.", $output); - } - - return array(true, $output); -} - -/** - * Delete a package comment - * - * @param boolean $undelete True if undeleting rather than deleting - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_delete_comment($undelete=false) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - return array(false, __("You must be logged in before you can edit package information.")); - } - - if (isset($_POST["comment_id"])) { - $comment_id = $_POST["comment_id"]; - } else { - return array(false, __("Missing comment ID.")); - } - - $dbh = DB::connect(); - if ($undelete) { - if (!has_credential(CRED_COMMENT_UNDELETE)) { - return array(false, __("You are not allowed to undelete this comment.")); - } - - $q = "UPDATE PackageComments "; - $q.= "SET DelUsersID = NULL, "; - $q.= "DelTS = NULL "; - $q.= "WHERE ID = ".intval($comment_id); - $dbh->exec($q); - return array(true, __("Comment has been undeleted.")); - } else { - if (!can_delete_comment($comment_id)) { - return array(false, __("You are not allowed to delete this comment.")); - } - - $q = "UPDATE PackageComments "; - $q.= "SET DelUsersID = ".$uid.", "; - $q.= "DelTS = " . strval(time()) . " "; - $q.= "WHERE ID = ".intval($comment_id); - $dbh->exec($q); - return array(true, __("Comment has been deleted.")); - } -} - -/** - * Edit a package comment - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_edit_comment($comment) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - return array(false, __("You must be logged in before you can edit package information.")); - } - - if (isset($_POST["comment_id"])) { - $comment_id = $_POST["comment_id"]; - } else { - return array(false, __("Missing comment ID.")); - } - - if (trim($comment) == '') { - return array(false, __('Comment cannot be empty.')); - } - - $dbh = DB::connect(); - if (can_edit_comment($comment_id)) { - $q = "UPDATE PackageComments "; - $q.= "SET EditedUsersID = ".$uid.", "; - $q.= "Comments = ".$dbh->quote($comment).", "; - $q.= "EditedTS = " . strval(time()) . " "; - $q.= "WHERE ID = ".intval($comment_id); - $dbh->exec($q); - - render_comment($comment_id); - - return array(true, __("Comment has been edited.")); - } else { - return array(false, __("You are not allowed to edit this comment.")); - } -} - -/** - * Get a list of package base keywords - * - * @param int $base_id The package base ID to retrieve the keywords for - * - * @return array An array of keywords - */ -function pkgbase_get_keywords($base_id) { - $dbh = DB::connect(); - $q = "SELECT Keyword FROM PackageKeywords "; - $q .= "WHERE PackageBaseID = " . intval($base_id) . " "; - $q .= "ORDER BY Keyword ASC"; - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/** - * Update the list of keywords of a package base - * - * @param int $base_id The package base ID to update the keywords of - * @param array $users Array of keywords - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_set_keywords($base_id, $keywords) { - $base_id = intval($base_id); - - $maintainers = array_merge(array(pkgbase_maintainer_uid($base_id)), pkgbase_get_comaintainer_uids(array($base_id))); - if (!has_credential(CRED_PKGBASE_SET_KEYWORDS, $maintainers)) { - return array(false, __("You are not allowed to edit the keywords of this package base.")); - } - - /* Remove empty and duplicate user names. */ - $keywords = array_unique(array_filter(array_map('trim', $keywords))); - - $dbh = DB::connect(); - - $q = sprintf("DELETE FROM PackageKeywords WHERE PackageBaseID = %d", $base_id); - $dbh->exec($q); - - $i = 0; - foreach ($keywords as $keyword) { - $q = sprintf("INSERT INTO PackageKeywords (PackageBaseID, Keyword) VALUES (%d, %s)", $base_id, $dbh->quote($keyword)); - $dbh->exec($q); - - $i++; - if ($i >= 20) { - break; - } - } - - return array(true, __("The package base keywords have been updated.")); -} - -/** - * Get a list of package base co-maintainers - * - * @param int $base_id The package base ID to retrieve the co-maintainers for - * - * @return array An array of co-maintainer user names - */ -function pkgbase_get_comaintainers($base_id) { - $dbh = DB::connect(); - $q = "SELECT UserName FROM PackageComaintainers "; - $q .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q .= "WHERE PackageComaintainers.PackageBaseID = " . intval($base_id) . " "; - $q .= "ORDER BY Priority ASC"; - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/** - * Get a list of package base co-maintainer IDs - * - * @param int $base_id The package base ID to retrieve the co-maintainers for - * - * @return array An array of co-maintainer user UDs - */ -function pkgbase_get_comaintainer_uids($base_ids) { - $dbh = DB::connect(); - $q = "SELECT UsersID FROM PackageComaintainers "; - $q .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q .= "WHERE PackageComaintainers.PackageBaseID IN (" . implode(",", $base_ids) . ") "; - $q .= "ORDER BY Priority ASC"; - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/** - * Update the list of co-maintainers of a package base - * - * @param int $base_id The package base ID to update the co-maintainers of - * @param array $users Array of co-maintainer user names - * @param boolean $override Override credential check if true - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_set_comaintainers($base_id, $users, $override=false) { - $maintainer_uid = pkgbase_maintainer_uid($base_id); - if (!$override && !has_credential(CRED_PKGBASE_EDIT_COMAINTAINERS, array($maintainer_uid))) { - return array(false, __("You are not allowed to manage co-maintainers of this package base.")); - } - - /* Remove empty and duplicate user names. */ - $users = array_unique(array_filter(array_map('trim', $users))); - - $dbh = DB::connect(); - - $uids_new = array(); - foreach($users as $user) { - $q = "SELECT ID FROM Users "; - $q .= "WHERE UserName = " . $dbh->quote($user); - $result = $dbh->query($q); - $uid = $result->fetchColumn(0); - - if (!$uid) { - return array(false, __("Invalid user name: %s", $user)); - } elseif ($uid == $maintainer_uid) { - // silently ignore when maintainer == co-maintainer - continue; - } else { - $uids_new[] = $uid; - } - } - - $q = sprintf("SELECT UsersID FROM PackageComaintainers WHERE PackageBaseID = %d", $base_id); - $result = $dbh->query($q); - $uids_old = $result->fetchAll(PDO::FETCH_COLUMN, 0); - - $uids_add = array_diff($uids_new, $uids_old); - $uids_rem = array_diff($uids_old, $uids_new); - - $i = 1; - foreach ($uids_new as $uid) { - if (in_array($uid, $uids_add)) { - $q = sprintf("INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (%d, %d, %d)", $base_id, $uid, $i); - notify(array('comaintainer-add', $uid, $base_id)); - } else { - $q = sprintf("UPDATE PackageComaintainers SET Priority = %d WHERE PackageBaseID = %d AND UsersID = %d", $i, $base_id, $uid); - } - - $dbh->exec($q); - $i++; - } - - foreach ($uids_rem as $uid) { - $q = sprintf("DELETE FROM PackageComaintainers WHERE PackageBaseID = %d AND UsersID = %d", $base_id, $uid); - $dbh->exec($q); - notify(array('comaintainer-remove', $uid, $base_id)); - } - - return array(true, __("The package base co-maintainers have been updated.")); -} - -function pkgbase_remove_comaintainer($base_id, $uid) { - $uname = username_from_id($uid); - $names = pkgbase_get_comaintainers($base_id); - $names = array_diff($names, array($uname)); - return pkgbase_set_comaintainers($base_id, $names, true); -} diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php deleted file mode 100644 index 140c7ec1..00000000 --- a/web/lib/pkgfuncs.inc.php +++ /dev/null @@ -1,957 +0,0 @@ -query($q); - - if (!$result) { - return false; - } - - $uid = $result->fetch(PDO::FETCH_COLUMN, 0); - - return has_credential(CRED_COMMENT_DELETE, array($uid)); -} - -/** - * Determine if the user can delete a specific package comment using an array - * - * Only the comment submitter, Trusted Users, and Developers can delete - * comments. This function is used for the frontend side of comment deletion. - * - * @param array $comment All database information relating a specific comment - * - * @return bool True if the user can delete the comment, otherwise false - */ -function can_delete_comment_array($comment) { - return has_credential(CRED_COMMENT_DELETE, array($comment['UsersID'])); -} - -/** - * Determine if the user can edit a specific package comment - * - * Only the comment submitter, Trusted Users, and Developers can edit - * comments. This function is used for the backend side of comment editing. - * - * @param string $comment_id The comment ID in the database - * - * @return bool True if the user can edit the comment, otherwise false - */ -function can_edit_comment($comment_id=0) { - $dbh = DB::connect(); - - $q = "SELECT UsersID FROM PackageComments "; - $q.= "WHERE ID = " . intval($comment_id); - $result = $dbh->query($q); - - if (!$result) { - return false; - } - - $uid = $result->fetch(PDO::FETCH_COLUMN, 0); - - return has_credential(CRED_COMMENT_EDIT, array($uid)); -} - -/** - * Determine if the user can edit a specific package comment using an array - * - * Only the comment submitter, Trusted Users, and Developers can edit - * comments. This function is used for the frontend side of comment editing. - * - * @param array $comment All database information relating a specific comment - * - * @return bool True if the user can edit the comment, otherwise false - */ -function can_edit_comment_array($comment) { - return has_credential(CRED_COMMENT_EDIT, array($comment['UsersID'])); -} - -/** - * Determine if the user can pin a specific package comment - * - * Only the Package Maintainer, Package Co-maintainers, Trusted Users, and - * Developers can pin comments. This function is used for the backend side of - * comment pinning. - * - * @param string $comment_id The comment ID in the database - * - * @return bool True if the user can pin the comment, otherwise false - */ -function can_pin_comment($comment_id=0) { - $dbh = DB::connect(); - - $q = "SELECT MaintainerUID FROM PackageBases AS pb "; - $q.= "LEFT JOIN PackageComments AS pc ON pb.ID = pc.PackageBaseID "; - $q.= "WHERE pc.ID = " . intval($comment_id) . " "; - $q.= "UNION "; - $q.= "SELECT pcm.UsersID FROM PackageComaintainers AS pcm "; - $q.= "LEFT JOIN PackageComments AS pc "; - $q.= "ON pcm.PackageBaseID = pc.PackageBaseID "; - $q.= "WHERE pc.ID = " . intval($comment_id); - $result = $dbh->query($q); - - if (!$result) { - return false; - } - - $uids = $result->fetchAll(PDO::FETCH_COLUMN, 0); - - return has_credential(CRED_COMMENT_PIN, $uids); -} - -/** - * Determine if the user can edit a specific package comment using an array - * - * Only the Package Maintainer, Package Co-maintainers, Trusted Users, and - * Developers can pin comments. This function is used for the frontend side of - * comment pinning. - * - * @param array $comment All database information relating a specific comment - * - * @return bool True if the user can edit the comment, otherwise false - */ -function can_pin_comment_array($comment) { - return can_pin_comment($comment['ID']); -} - -/** - * Check to see if the package name already exists in the database - * - * @param string $name The package name to check - * - * @return string|void Package name if it already exists - */ -function pkg_from_name($name="") { - if (!$name) {return NULL;} - $dbh = DB::connect(); - $q = "SELECT ID FROM Packages "; - $q.= "WHERE Name = " . $dbh->quote($name); - $result = $dbh->query($q); - if (!$result) { - return; - } - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Get licenses for a specific package - * - * @param int $pkgid The package to get licenses for - * - * @return array All licenses for the package - */ -function pkg_licenses($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT l.Name FROM Licenses l "; - $q.= "INNER JOIN PackageLicenses pl ON pl.LicenseID = l.ID "; - $q.= "WHERE pl.PackageID = ". $pkgid; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($q, 'licenses:' . $pkgid, PDO::FETCH_NUM, $ttl); - return array_map(function ($x) { return $x[0]; }, $rows); -} - -/** - * Get package groups for a specific package - * - * @param int $pkgid The package to get groups for - * - * @return array All package groups for the package - */ -function pkg_groups($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT g.Name FROM `Groups` g "; - $q.= "INNER JOIN PackageGroups pg ON pg.GroupID = g.ID "; - $q.= "WHERE pg.PackageID = ". $pkgid; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($q, 'groups:' . $pkgid, PDO::FETCH_NUM, $ttl); - return array_map(function ($x) { return $x[0]; }, $rows); -} - -/** - * Get providers for a specific package - * - * @param string $name The name of the "package" to get providers for - * - * @return array The IDs and names of all providers of the package - */ -function pkg_providers($name) { - $dbh = DB::connect(); - $q = "SELECT p.ID, p.Name FROM Packages p "; - $q.= "WHERE p.Name = " . $dbh->quote($name) . " "; - $q.= "UNION "; - $q.= "SELECT p.ID, p.Name FROM Packages p "; - $q.= "LEFT JOIN PackageRelations pr ON pr.PackageID = p.ID "; - $q.= "LEFT JOIN RelationTypes rt ON rt.ID = pr.RelTypeID "; - $q.= "WHERE (rt.Name = 'provides' "; - $q.= "AND pr.RelName = " . $dbh->quote($name) . ")"; - $q.= "UNION "; - $q.= "SELECT 0, Name FROM OfficialProviders "; - $q.= "WHERE Provides = " . $dbh->quote($name); - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'providers:' . $name, PDO::FETCH_NUM, $ttl); -} - -/** - * Get package dependencies for a specific package - * - * @param int $pkgid The package to get dependencies for - * @param int $limit An upper bound for the number of packages to retrieve - * - * @return array All package dependencies for the package - */ -function pkg_dependencies($pkgid, $limit) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT pd.DepName, dt.Name, pd.DepDesc, "; - $q.= "pd.DepCondition, pd.DepArch, p.ID "; - $q.= "FROM PackageDepends pd "; - $q.= "LEFT JOIN Packages p ON pd.DepName = p.Name "; - $q.= "LEFT JOIN DependencyTypes dt ON dt.ID = pd.DepTypeID "; - $q.= "WHERE pd.PackageID = ". $pkgid . " "; - $q.= "ORDER BY pd.DepName LIMIT " . intval($limit); - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'dependencies:' . $pkgid, PDO::FETCH_NUM, $ttl); -} - -/** - * Get package relations for a specific package - * - * @param int $pkgid The package to get relations for - * - * @return array All package relations for the package - */ -function pkg_relations($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT pr.RelName, rt.Name, pr.RelCondition, pr.RelArch, p.ID FROM PackageRelations pr "; - $q.= "LEFT JOIN Packages p ON pr.RelName = p.Name "; - $q.= "LEFT JOIN RelationTypes rt ON rt.ID = pr.RelTypeID "; - $q.= "WHERE pr.PackageID = ". $pkgid . " "; - $q.= "ORDER BY pr.RelName"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'relations:' . $pkgid, PDO::FETCH_NUM, $ttl); -} - -/** - * Get the HTML code to display a package dependency link annotation - * (dependency type, architecture, ...) - * - * @param string $type The name of the dependency type - * @param string $arch The package dependency architecture - * @param string $desc An optdepends description - * - * @return string The HTML code of the label to display - */ -function pkg_deplink_annotation($type, $arch, $desc=false) { - if ($type == 'depends' && !$arch) { - return ''; - } - - $link = ' ('; - - if ($type == 'makedepends') { - $link .= 'make'; - } elseif ($type == 'checkdepends') { - $link .= 'check'; - } elseif ($type == 'optdepends') { - $link .= 'optional'; - } - - if ($type != 'depends' && $arch) { - $link .= ', '; - } - - if ($arch) { - $link .= htmlspecialchars($arch); - } - - $link .= ')'; - if ($type == 'optdepends' && $desc) { - $link .= ' – ' . htmlspecialchars($desc) . ' '; - } - $link .= ''; - - return $link; -} - -/** - * Get the HTML code to display a package provider link - * - * @param string $name The name of the provider - * @param bool $official True if the package is in the official repositories - * - * @return string The HTML code of the link to display - */ -function pkg_provider_link($name, $official) { - $link = ''; - $link .= htmlspecialchars($name) . ''; - - return $link; -} - -/** - * Get the HTML code to display a package dependency link - * - * @param string $name The name of the dependency - * @param string $type The name of the dependency type - * @param string $desc The (optional) description of the dependency - * @param string $cond The package dependency condition string - * @param string $arch The package dependency architecture - * @param int $pkg_id The package of the package to display the dependency for - * - * @return string The HTML code of the label to display - */ -function pkg_depend_link($name, $type, $desc, $cond, $arch, $pkg_id) { - /* - * TODO: We currently perform one SQL query per nonexistent package - * dependency. It would be much better if we could annotate dependency - * data with providers so that we already know whether a dependency is - * a "provision name" or a package from the official repositories at - * this point. - */ - $providers = pkg_providers($name); - - if (count($providers) == 0) { - $link = ''; - $link .= htmlspecialchars($name); - $link .= ''; - $link .= htmlspecialchars($cond) . ' '; - $link .= pkg_deplink_annotation($type, $arch, $desc); - return $link; - } - - $link = htmlspecialchars($name); - foreach ($providers as $provider) { - if ($provider[1] == $name) { - $is_official = ($provider[0] == 0); - $name = $provider[1]; - $link = pkg_provider_link($name, $is_official); - break; - } - } - $link .= htmlspecialchars($cond) . ' '; - - foreach ($providers as $key => $provider) { - if ($provider[1] == $name) { - unset($providers[$key]); - } - } - - if (count($providers) > 0) { - $link .= '('; - foreach ($providers as $provider) { - $is_official = ($provider[0] == 0); - $name = $provider[1]; - $link .= pkg_provider_link($name, $is_official) . ', '; - } - $link = substr($link, 0, -2); - $link .= ')'; - } - - $link .= pkg_deplink_annotation($type, $arch, $desc); - - return $link; -} - -/** - * Get the HTML code to display a package requirement link - * - * @param string $name The name of the requirement - * @param string $depends The (literal) name of the dependency of $name - * @param string $type The name of the dependency type - * @param string $arch The package dependency architecture - * @param string $pkgname The name of dependant package - * - * @return string The HTML code of the link to display - */ -function pkg_requiredby_link($name, $depends, $type, $arch, $pkgname) { - $link = ''; - $link .= htmlspecialchars($name) . ''; - - if ($depends != $pkgname) { - $link .= ' ('; - $link .= __('requires %s', htmlspecialchars($depends)); - $link .= ')'; - } - - return $link . pkg_deplink_annotation($type, $arch); -} - -/** - * Get the HTML code to display a package relation - * - * @param string $name The name of the relation - * @param string $cond The package relation condition string - * @param string $arch The package relation architecture - * - * @return string The HTML code of the label to display - */ -function pkg_rel_html($name, $cond, $arch) { - $html = htmlspecialchars($name) . htmlspecialchars($cond); - - if ($arch) { - $html .= ' (' . htmlspecialchars($arch) . ')'; - } - - return $html; -} - -/** - * Get the HTML code to display a source link - * - * @param string $url The URL of the source - * @param string $arch The source architecture - * @param string $package The name of the package - * - * @return string The HTML code of the label to display - */ -function pkg_source_link($url, $arch, $package) { - $url = explode('::', $url); - $parsed_url = parse_url($url[0]); - - if (isset($parsed_url['scheme']) || isset($url[1])) { - $link = '' . htmlspecialchars($url[0]) . ''; - } else { - $file_url = sprintf(config_get('options', 'source_file_uri'), htmlspecialchars($url[0]), $package); - $link = '' . htmlspecialchars($url[0]) . ''; - } - - if ($arch) { - $link .= ' (' . htmlspecialchars($arch) . ')'; - } - - return $link; -} - -/** - * Determine packages that depend on a package - * - * @param string $name The package name for the dependency search - * @param array $provides A list of virtual provisions of the package - * @param int $limit An upper bound for the number of packages to retrieve - * - * @return array All packages that depend on the specified package name - */ -function pkg_required($name="", $provides, $limit) { - $deps = array(); - if ($name != "") { - $dbh = DB::connect(); - - $name_list = $dbh->quote($name); - foreach ($provides as $p) { - $name_list .= ',' . $dbh->quote($p[0]); - } - - $q = "SELECT p.Name, pd.DepName, dt.Name, pd.DepArch "; - $q.= "FROM PackageDepends pd "; - $q.= "LEFT JOIN Packages p ON p.ID = pd.PackageID "; - $q.= "LEFT JOIN DependencyTypes dt ON dt.ID = pd.DepTypeID "; - $q.= "WHERE pd.DepName IN (" . $name_list . ") "; - $q.= "ORDER BY p.Name LIMIT " . intval($limit); - /* Not invalidated by package updates. */ - return db_cache_result($q, 'required:' . $name, PDO::FETCH_NUM); - } - return $deps; -} - -/** - * Get all package sources for a specific package - * - * @param string $pkgid The package ID to get the sources for - * - * @return array All sources associated with a specific package - */ -function pkg_sources($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT Source, SourceArch FROM PackageSources "; - $q.= "WHERE PackageID = " . $pkgid; - $q.= " ORDER BY Source"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'required:' . $pkgid, PDO::FETCH_NUM, $ttl); -} - -/** - * Get the package details - * - * @param string $id The package ID to get description for - * - * @return array The package's details OR error message - **/ -function pkg_get_details($id=0) { - $dbh = DB::connect(); - - $q = "SELECT Packages.*, PackageBases.ID AS BaseID, "; - $q.= "PackageBases.Name AS BaseName, PackageBases.NumVotes, "; - $q.= "PackageBases.Popularity, PackageBases.OutOfDateTS, "; - $q.= "PackageBases.SubmittedTS, PackageBases.ModifiedTS, "; - $q.= "PackageBases.SubmitterUID, PackageBases.MaintainerUID, "; - $q.= "PackageBases.PackagerUID, PackageBases.FlaggerUID, "; - $q.= "(SELECT COUNT(*) FROM PackageRequests "; - $q.= " WHERE PackageRequests.PackageBaseID = Packages.PackageBaseID "; - $q.= " AND PackageRequests.Status = 0) AS RequestCount "; - $q.= "FROM Packages, PackageBases "; - $q.= "WHERE PackageBases.ID = Packages.PackageBaseID "; - $q.= "AND Packages.ID = " . intval($id); - $result = $dbh->query($q); - - $row = array(); - - if (!$result) { - $row['error'] = __("Error retrieving package details."); - } - else { - $row = $result->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - $row['error'] = __("Package details could not be found."); - } - } - - return $row; -} - -/** - * Display the package details page - * - * @param string $id The package ID to get details page for - * @param array $row Package details retrieved by pkg_get_details() - * @param string $SID The session ID of the visitor - * - * @return void - */ -function pkg_display_details($id=0, $row, $SID="") { - $dbh = DB::connect(); - - if (isset($row['error'])) { - print "

    " . $row['error'] . "

    \n"; - } - else { - $base_id = pkgbase_from_pkgid($id); - $pkgbase_name = pkgbase_name_from_id($base_id); - - include('pkg_details.php'); - - if ($SID) { - include('pkg_comment_box.php'); - } - - $include_deleted = has_credential(CRED_COMMENT_VIEW_DELETED); - - $limit_pinned = isset($_GET['pinned']) ? 0 : 5; - $pinned = pkgbase_comments($base_id, $limit_pinned, false, true); - if (!empty($pinned)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - unset($pinned); - - - $total_comment_count = pkgbase_comments_count($base_id, $include_deleted); - list($pagination_templs, $per_page, $offset) = calculate_pagination($total_comment_count); - - $comments = pkgbase_comments($base_id, $per_page, $include_deleted, false, $offset); - if (!empty($comments)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - } -} - -/** - * Output the body of the search results page - * - * @param array $params Search parameters - * @param bool $show_headers True if statistics should be included - * @param string $SID The session ID of the visitor - * - * @return int The total number of packages matching the query - */ -function pkg_search_page($params, $show_headers=true, $SID="") { - $dbh = DB::connect(); - - /* - * Get commonly used variables. - * TODO: Reduce the number of database queries! - */ - if ($SID) - $myuid = uid_from_sid($SID); - - /* Sanitize paging variables. */ - if (isset($params['O'])) { - $params['O'] = bound(intval($params['O']), 0, 2500); - } else { - $params['O'] = 0; - } - - if (isset($params["PP"])) { - $params["PP"] = bound(intval($params["PP"]), 50, 250); - } else { - $params["PP"] = 50; - } - - /* - * FIXME: Pull out DB-related code. All of it! This one's worth a - * choco-chip cookie, one of those nice big soft ones. - */ - - /* Build the package search query. */ - $q_select = "SELECT "; - if ($SID) { - $q_select .= "PackageNotifications.UserID AS Notify, - PackageVotes.UsersID AS Voted, "; - } - $q_select .= "Users.Username AS Maintainer, - Packages.Name, Packages.Version, Packages.Description, - PackageBases.NumVotes, PackageBases.Popularity, Packages.ID, - Packages.PackageBaseID, PackageBases.OutOfDateTS "; - - $q_from = "FROM Packages - LEFT JOIN PackageBases ON (PackageBases.ID = Packages.PackageBaseID) - LEFT JOIN Users ON (PackageBases.MaintainerUID = Users.ID) "; - if ($SID) { - /* This is not needed for the total row count query. */ - $q_from_extra = "LEFT JOIN PackageVotes - ON (PackageBases.ID = PackageVotes.PackageBaseID AND PackageVotes.UsersID = $myuid) - LEFT JOIN PackageNotifications - ON (PackageBases.ID = PackageNotifications.PackageBaseID AND PackageNotifications.UserID = $myuid) "; - } else { - $q_from_extra = ""; - } - - $q_where = 'WHERE PackageBases.PackagerUID IS NOT NULL '; - - if (isset($params['K'])) { - if (isset($params["SeB"]) && $params["SeB"] == "m") { - /* Search by maintainer. */ - $q_where .= "AND Users.Username = " . $dbh->quote($params['K']) . " "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "c") { - /* Search by co-maintainer. */ - $q_where .= "AND EXISTS (SELECT * FROM PackageComaintainers "; - $q_where .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q_where .= "WHERE PackageComaintainers.PackageBaseID = PackageBases.ID "; - $q_where .= "AND Users.Username = " . $dbh->quote($params['K']) . ")"; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "M") { - /* Search by maintainer and co-maintainer. */ - $q_where .= "AND (Users.Username = " . $dbh->quote($params['K']) . " "; - $q_where .= "OR EXISTS (SELECT * FROM PackageComaintainers "; - $q_where .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q_where .= "WHERE PackageComaintainers.PackageBaseID = PackageBases.ID "; - $q_where .= "AND Users.Username = " . $dbh->quote($params['K']) . "))"; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "s") { - /* Search by submitter. */ - $q_where .= "AND SubmitterUID = " . intval(uid_from_username($params['K'])) . " "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "n") { - /* Search by name. */ - $K = "%" . addcslashes($params['K'], '%_') . "%"; - $q_where .= "AND (Packages.Name LIKE " . $dbh->quote($K) . ") "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "b") { - /* Search by package base name. */ - $K = "%" . addcslashes($params['K'], '%_') . "%"; - $q_where .= "AND (PackageBases.Name LIKE " . $dbh->quote($K) . ") "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "k") { - /* Search by name. */ - $q_where .= construct_keyword_search($dbh, $params['K'], false, true); - } - elseif (isset($params["SeB"]) && $params["SeB"] == "N") { - /* Search by name (exact match). */ - $q_where .= "AND (Packages.Name = " . $dbh->quote($params['K']) . ") "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "B") { - /* Search by package base name (exact match). */ - $q_where .= "AND (PackageBases.Name = " . $dbh->quote($params['K']) . ") "; - } - else { - /* Keyword search (default). */ - $q_where .= construct_keyword_search($dbh, $params['K'], true, true); - } - } - - if (isset($params["do_Orphans"])) { - $q_where .= "AND MaintainerUID IS NULL "; - } - - if (isset($params['outdated'])) { - if ($params['outdated'] == 'on') { - $q_where .= "AND OutOfDateTS IS NOT NULL "; - } - elseif ($params['outdated'] == 'off') { - $q_where .= "AND OutOfDateTS IS NULL "; - } - } - - $order = (isset($params["SO"]) && $params["SO"] == 'd') ? 'DESC' : 'ASC'; - - $q_sort = "ORDER BY "; - $sort_by = isset($params["SB"]) ? $params["SB"] : ''; - switch ($sort_by) { - case 'v': - $q_sort .= "NumVotes " . $order . ", "; - break; - case 'p': - $q_sort .= "Popularity " . $order . ", "; - break; - case 'w': - if ($SID) { - $q_sort .= "Voted " . $order . ", "; - } - break; - case 'o': - if ($SID) { - $q_sort .= "Notify " . $order . ", "; - } - break; - case 'm': - $q_sort .= "Maintainer " . $order . ", "; - break; - case 'l': - $q_sort .= "ModifiedTS " . $order . ", "; - break; - case 'a': - /* For compatibility with old search links. */ - $q_sort .= "-ModifiedTS " . $order . ", "; - break; - default: - break; - } - $q_sort .= " Packages.Name " . $order . " "; - - $q_limit = "LIMIT ".$params["PP"]." OFFSET ".$params["O"]; - - $q = $q_select . $q_from . $q_from_extra . $q_where . $q_sort . $q_limit; - $q_total = "SELECT COUNT(*) " . $q_from . $q_where; - - $result = $dbh->query($q); - $result_t = $dbh->query($q_total); - if ($result_t) { - $row = $result_t->fetch(PDO::FETCH_NUM); - $total = min($row[0], 2500); - } else { - $total = 0; - } - - if ($result && $total > 0) { - if (isset($params["SO"]) && $params["SO"] == "d"){ - $SO_next = "a"; - } - else { - $SO_next = "d"; - } - } - - /* Calculate the results to use. */ - $first = $params['O'] + 1; - - /* Calculation of pagination links. */ - $per_page = ($params['PP'] > 0) ? $params['PP'] : 50; - $current = ceil($first / $per_page); - $pages = ceil($total / $per_page); - $templ_pages = array(); - - if ($current > 1) { - $templ_pages['« ' . __('First')] = 0; - $templ_pages['‹ ' . __('Previous')] = ($current - 2) * $per_page; - } - - if ($current - 5 > 1) - $templ_pages["..."] = false; - - for ($i = max($current - 5, 1); $i <= min($pages, $current + 5); $i++) { - $templ_pages[$i] = ($i - 1) * $per_page; - } - - if ($current + 5 < $pages) - $templ_pages["... "] = false; - - if ($current < $pages) { - $templ_pages[__('Next') . ' ›'] = $current * $per_page; - $templ_pages[__('Last') . ' »'] = ($pages - 1) * $per_page; - } - - $searchresults = array(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $searchresults[] = $row; - } - } - - include('pkg_search_results.php'); - - return $total; -} - -/** - * Construct the WHERE part of the sophisticated keyword search - * - * @param handle $dbh Database handle - * @param string $keywords The search term - * @param bool $namedesc Search name and description fields - * @param bool $keyword Search packages with a matching PackageBases.Keyword - * - * @return string WHERE part of the SQL clause - */ -function construct_keyword_search($dbh, $keywords, $namedesc, $keyword=false) { - $count = 0; - $where_part = ""; - $q_keywords = ""; - $op = ""; - - foreach (str_getcsv($keywords, ' ') as $term) { - if ($term == "") { - continue; - } - if ($count > 0 && strtolower($term) == "and") { - $op = "AND "; - continue; - } - if ($count > 0 && strtolower($term) == "or") { - $op = "OR "; - continue; - } - if ($count > 0 && strtolower($term) == "not") { - $op .= "NOT "; - continue; - } - - $term = "%" . addcslashes($term, '%_') . "%"; - $q_keywords .= $op . " ("; - $q_keywords .= "Packages.Name LIKE " . $dbh->quote($term) . " "; - if ($namedesc) { - $q_keywords .= "OR Description LIKE " . $dbh->quote($term) . " "; - } - - if ($keyword) { - $q_keywords .= "OR EXISTS (SELECT * FROM PackageKeywords WHERE "; - $q_keywords .= "PackageKeywords.PackageBaseID = Packages.PackageBaseID AND "; - $q_keywords .= "PackageKeywords.Keyword LIKE " . $dbh->quote($term) . ")) "; - } else { - $q_keywords .= ") "; - } - - $count++; - if ($count >= 20) { - break; - } - $op = "AND "; - } - - if (!empty($q_keywords)) { - $where_part = "AND (" . $q_keywords . ") "; - } - - return $where_part; -} - -/** - * Determine if a POST string has been sent by a visitor - * - * @param string $action String to check has been sent via POST - * - * @return bool True if the POST string was used, otherwise false - */ -function current_action($action) { - return (isset($_POST['action']) && $_POST['action'] == $action) || - isset($_POST[$action]); -} - -/** - * Determine if sent IDs are valid integers - * - * @param array $ids IDs to validate - * - * @return array All sent IDs that are valid integers - */ -function sanitize_ids($ids) { - $new_ids = array(); - foreach ($ids as $id) { - $id = intval($id); - if ($id > 0) { - $new_ids[] = $id; - } - } - return $new_ids; -} - -/** - * Determine package information for latest package - * - * @param int $numpkgs Number of packages to get information on - * - * @return array $packages Package info for the specified number of recent packages - */ -function latest_pkgs($numpkgs, $orderBy='SubmittedTS') { - $dbh = DB::connect(); - - $q = "SELECT Packages.*, MaintainerUID, SubmittedTS, ModifiedTS "; - $q.= "FROM Packages LEFT JOIN PackageBases ON "; - $q.= "PackageBases.ID = Packages.PackageBaseID "; - $q.= "ORDER BY " . $orderBy . " DESC "; - $q.= "LIMIT " . intval($numpkgs); - $result = $dbh->query($q); - - $packages = array(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $packages[] = $row; - } - } - - return $packages; -} - -/** - * Determine package information for latest modified packages - * - * @param int $numpkgs Number of packages to get information on - * - * @return array $packages Package info for the specified number of recently modified packages - */ -function latest_modified_pkgs($numpkgs) { - return latest_pkgs($numpkgs, 'ModifiedTS'); -} diff --git a/web/lib/pkgreqfuncs.inc.php b/web/lib/pkgreqfuncs.inc.php deleted file mode 100644 index 7fce307c..00000000 --- a/web/lib/pkgreqfuncs.inc.php +++ /dev/null @@ -1,260 +0,0 @@ -query($q)->fetchColumn(); -} - -/** - * Get a list of all package requests - * - * @param int $offset The index of the first request to return - * @param int $limit The maximum number of requests to return - * @param int $uid Only return packages affecting the given user - * @param int $from Do not return packages older than the given date - * - * @return array List of package requests with details - */ -function pkgreq_list($offset, $limit, $uid=false, $from=false) { - $dbh = DB::connect(); - - $q = "SELECT PackageRequests.ID, "; - $q.= "PackageRequests.PackageBaseID AS BaseID, "; - $q.= "PackageRequests.PackageBaseName AS Name, "; - $q.= "PackageRequests.MergeBaseName AS MergeInto, "; - $q.= "RequestTypes.Name AS Type, PackageRequests.Comments, "; - $q.= "Users.Username AS User, PackageRequests.RequestTS, "; - $q.= "PackageRequests.Status, PackageRequests.Status = 0 AS Open "; - $q.= "FROM PackageRequests INNER JOIN RequestTypes ON "; - $q.= "RequestTypes.ID = PackageRequests.ReqTypeID "; - $q.= "INNER JOIN Users ON Users.ID = PackageRequests.UsersID "; - - if ($uid || $from) { - $q.= "WHERE "; - if ($uid) { - $q.= "(PackageRequests.UsersID = " . intval($uid). " "; - $q.= "OR Users.ID = " . intval($uid) . ") AND "; - } - if ($from) { - $q.= "RequestTS >= " . intval($from). " "; - } - } - - $q.= "ORDER BY Open DESC, RequestTS DESC "; - $q.= "LIMIT " . $limit . " OFFSET " . $offset; - - return $dbh->query($q)->fetchAll(); -} - -/** - * Get a list of all open package requests belonging to a certain package base - * - * @param int $baseid The package base ID to retrieve requests for - * @param int $type The type of requests to obtain - * - * @return array List of package request IDs - */ -function pkgreq_by_pkgbase($baseid, $type=false) { - $dbh = DB::connect(); - - $q = "SELECT PackageRequests.ID "; - $q.= "FROM PackageRequests INNER JOIN RequestTypes ON "; - $q.= "RequestTypes.ID = PackageRequests.ReqTypeID "; - $q.= "WHERE PackageRequests.Status = 0 "; - $q.= "AND PackageRequests.PackageBaseID = " . intval($baseid); - - if ($type) { - $q .= " AND RequestTypes.Name = " . $dbh->quote($type); - } - - return $dbh->query($q)->fetchAll(PDO::FETCH_COLUMN, 0); -} - -/** - * Obtain the package base that belongs to a package request. - * - * @param int $id Package request ID to retrieve the package base for - * - * @return int The name of the corresponding package base - */ -function pkgreq_get_pkgbase_name($id) { - $dbh = DB::connect(); - - $q = "SELECT PackageBaseName FROM PackageRequests "; - $q.= "WHERE ID = " . intval($id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Obtain the email address of the creator of a package request - * - * @param int $id Package request ID to retrieve the creator for - * - * @return int The email address of the creator - */ -function pkgreq_get_creator_email($id) { - $dbh = DB::connect(); - - $q = "SELECT Email FROM Users INNER JOIN PackageRequests "; - $q.= "ON Users.ID = PackageRequests.UsersID "; - $q.= "WHERE PackageRequests.ID = " . intval($id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * File a deletion/orphan request against a package base - * - * @param string $ids The package base IDs to file the request against - * @param string $type The type of the request - * @param string $merge_into The target of a merge operation - * @param string $comments The comments to be added to the request - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgreq_file($ids, $type, $merge_into, $comments) { - if (!has_credential(CRED_PKGREQ_FILE)) { - return array(false, __("You must be logged in to file package requests.")); - } - - /* Ignore merge target for non-merge requests. */ - if ($type !== 'merge') { - $merge_into = ''; - } - - if (!empty($merge_into) && !preg_match("/^[a-z0-9][a-z0-9\.+_-]*$/D", $merge_into)) { - return array(false, __("Invalid name: only lowercase letters are allowed.")); - } - - if (!empty($merge_into) && !pkgbase_from_name($merge_into)) { - return array(false, __("Cannot find package to merge votes and comments into.")); - } - - if (empty($comments)) { - return array(false, __("The comment field must not be empty.")); - } - - $dbh = DB::connect(); - $uid = uid_from_sid($_COOKIE["AURSID"]); - - /* TODO: Allow for filing multiple requests at once. */ - $base_id = intval($ids[0]); - $pkgbase_name = pkgbase_name_from_id($base_id); - - if ($merge_into == $pkgbase_name) { - return array(false, __("Cannot merge a package base with itself.")); - } - - $q = "SELECT ID FROM RequestTypes WHERE Name = " . $dbh->quote($type); - $result = $dbh->query($q); - if ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $type_id = $row['ID']; - } else { - return array(false, __("Invalid request type.")); - } - - $q = "INSERT INTO PackageRequests "; - $q.= "(ReqTypeID, PackageBaseID, PackageBaseName, MergeBaseName, "; - $q.= "UsersID, Comments, ClosureComment, RequestTS) VALUES (" . $type_id . ", "; - $q.= $base_id . ", " . $dbh->quote($pkgbase_name) . ", "; - $q.= $dbh->quote($merge_into) . ", " . $uid . ", "; - $q.= $dbh->quote($comments) . ", '', " . strval(time()) . ")"; - $dbh->exec($q); - $request_id = $dbh->lastInsertId(); - - /* Send e-mail notifications. */ - $params = array('request-open', $uid, $request_id, $type, $base_id); - if ($type === 'merge') { - $params[] = $merge_into; - } - notify($params); - - $auto_orphan_age = config_get('options', 'auto_orphan_age'); - $auto_delete_age = config_get('options', 'auto_delete_age'); - $details = pkgbase_get_details($base_id); - if ($type == 'orphan' && $details['OutOfDateTS'] > 0 && - time() - $details['OutOfDateTS'] >= $auto_orphan_age && - $auto_orphan_age > 0) { - /* - * Close package request. NOTE: This needs to happen *before* - * the actual disown operation. Otherwise, the former - * maintainer will not be included in the Cc list of the - * request notification email. - */ - $out_of_date_time = date("Y-m-d", intval($details["OutOfDateTS"])); - pkgreq_close($request_id, "accepted", - "The package base has been flagged out-of-date " . - "since " . $out_of_date_time . ".", true); - $q = "UPDATE PackageBases SET MaintainerUID = NULL "; - $q.= "WHERE ID = " . $base_id; - $dbh->exec($q); - } else if ($type == 'deletion' && $details['MaintainerUID'] == $uid && - $details['SubmittedTS'] > 0 && $auto_delete_age > 0 && - time() - $details['SubmittedTS'] <= $auto_delete_age) { - /* - * Close package request. NOTE: This needs to happen *before* - * the actual deletion operation. Otherwise, the former - * maintainer will not be included in the Cc list of the - * request notification email. - */ - pkgreq_close($request_id, "accepted", - "Deletion of a fresh package requested by its " . - "current maintainer.", true); - pkgbase_delete(array($base_id), NULL, NULL, true); - } - - return array(true, __("Added request successfully.")); -} - -/** - * Close a deletion/orphan request - * - * @param int $id The package request to close - * @param string $reason Whether the request was accepted or rejected - * @param string $comments Comments to be added to the notification email - * @param boolean $auto_close (optional) Whether the request is auto-closed - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgreq_close($id, $reason, $comments, $auto_close=false) { - switch ($reason) { - case 'accepted': - $status = 2; - break; - case 'rejected': - $status = 3; - break; - default: - return array(false, __("Invalid reason.")); - } - - $dbh = DB::connect(); - $id = intval($id); - $uid = $auto_close ? 0 : uid_from_sid($_COOKIE["AURSID"]); - - if (!$auto_close && !has_credential(CRED_PKGREQ_CLOSE)) { - return array(false, __("Only TUs and developers can close requests.")); - } - - $q = "UPDATE PackageRequests SET Status = " . intval($status) . ", "; - $q.= "ClosedTS = " . strval(time()) . ", "; - $q.= "ClosedUID = " . ($uid == 0 ? "NULL" : intval($uid)) . ", "; - $q.= "ClosureComment = " . $dbh->quote($comments) . " "; - $q.= "WHERE ID = " . intval($id); - $dbh->exec($q); - - /* Send e-mail notifications. */ - notify(array('request-close', $uid, $id, $reason)); - - return array(true, __("Request closed successfully.")); -} diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php deleted file mode 100644 index 73c667d2..00000000 --- a/web/lib/routing.inc.php +++ /dev/null @@ -1,85 +0,0 @@ - 'home.php', - '/index.php' => 'home.php', - '/packages' => 'packages.php', - '/pkgbase' => 'pkgbase.php', - '/requests' => 'pkgreq.php', - '/register' => 'register.php', - '/account' => 'account.php', - '/accounts' => 'account.php', - '/login' => 'login.php', - '/logout' => 'logout.php', - '/passreset' => 'passreset.php', - '/rpc' => 'rpc.php', - '/rss/modified' => 'modified-rss.php', - '/rss' => 'rss.php', - '/tos' => 'tos.php', - '/tu' => 'tu.php', - '/addvote' => 'addvote.php', -); - -$PKG_PATH = '/packages'; -$PKGBASE_PATH = '/pkgbase'; -$PKGREQ_PATH = '/requests'; -$USER_PATH = '/account'; - -function get_route($path) { - global $ROUTES; - - $path = rtrim($path, '/'); - if (isset($ROUTES[$path])) { - return $ROUTES[$path]; - } else { - return NULL; - } -} - -function get_uri($path, $absolute=false) { - if ($absolute) { - return rtrim(aur_location(), '/') . $path; - } else { - return $path; - } -} - -function get_pkg_route() { - global $PKG_PATH; - return $PKG_PATH; -} - -function get_pkgbase_route() { - global $PKGBASE_PATH; - return $PKGBASE_PATH; -} - -function get_pkgreq_route() { - global $PKGREQ_PATH; - return $PKGREQ_PATH; -} - -function get_pkg_uri($pkgname, $absolute=false) { - global $PKG_PATH; - $path = $PKG_PATH . '/' . urlencode($pkgname) . '/'; - return get_uri($path, $absolute); -} - -function get_pkgbase_uri($pkgbase_name, $absolute=false) { - global $PKGBASE_PATH; - $path = $PKGBASE_PATH . '/' . urlencode($pkgbase_name) . '/'; - return get_uri($path, $absolute); -} - -function get_user_route() { - global $USER_PATH; - return $USER_PATH; -} - -function get_user_uri($username, $absolute=false) { - global $USER_PATH; - $path = $USER_PATH . '/' . urlencode($username) . '/'; - return get_uri($path, $absolute); -} diff --git a/web/lib/stats.inc.php b/web/lib/stats.inc.php deleted file mode 100644 index f5692f96..00000000 --- a/web/lib/stats.inc.php +++ /dev/null @@ -1,108 +0,0 @@ -query($q); - - $newest_packages = new ArrayObject(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $newest_packages->append($row); - } - set_cache_value($key, $newest_packages); - } - } - include('stats/updates_table.php'); -} - -/** - * Display a user's statistics table - * - * @param string $userid The user ID of the person to get package statistics for - * - * @return void - */ -function user_table($userid) { - $base_q = "SELECT COUNT(*) FROM PackageBases "; - $base_q.= "WHERE MaintainerUID = " . $userid . " "; - $base_q.= "AND PackagerUID IS NOT NULL"; - - $user_pkg_count = db_cache_value($base_q, 'user_pkg_count:' . $userid); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE OutOfDateTS IS NOT NULL "; - $q.= "AND MaintainerUID = " . $userid . " "; - $q.= "AND PackagerUID IS NOT NULL"; - - $flagged_outdated = db_cache_value($q, 'user_flagged_outdated:' . $userid); - - include('stats/user_table.php'); -} - -/** - * Display the general package statistics table - * - * @return void - */ -function general_stats_table() { - # AUR statistics - $q = "SELECT COUNT(*) FROM PackageBases WHERE PackagerUID IS NOT NULL"; - $pkg_count = db_cache_value($q, 'pkg_count'); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE MaintainerUID IS NULL "; - $q.= "AND PackagerUID IS NOT NULL"; - $orphan_count = db_cache_value($q, 'orphan_count'); - - $q = "SELECT count(*) FROM Users"; - $user_count = db_cache_value($q, 'user_count'); - - $q = "SELECT count(*) FROM Users,AccountTypes WHERE Users.AccountTypeID = AccountTypes.ID AND (AccountTypes.AccountType = 'Trusted User' OR AccountTypes.AccountType = 'Trusted User & Developer')"; - $tu_count = db_cache_value($q, 'tu_count'); - - $targstamp = intval(strtotime("-7 days")); - $yearstamp = intval(strtotime("-1 year")); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE SubmittedTS >= $targstamp "; - $q.= "AND PackagerUID IS NOT NULL"; - $add_count = db_cache_value($q, 'add_count'); - - /* - * A package whose last modification time differs less than an hour - * from the initial submission time is considered new. - */ - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE ModifiedTS >= $targstamp "; - $q.= "AND ModifiedTS - SubmittedTS >= 3600 "; - $q.= "AND PackagerUID IS NOT NULL"; - $update_count = db_cache_value($q, 'update_count'); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE ModifiedTS >= $yearstamp "; - $q.= "AND ModifiedTS - SubmittedTS >= 3600 "; - $q.= "AND PackagerUID IS NOT NULL"; - $update_year_count = db_cache_value($q, 'update_year_count'); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE ModifiedTS - SubmittedTS < 3600 "; - $q.= "AND PackagerUID IS NOT NULL"; - $never_update_count = db_cache_value($q, 'never_update_count'); - - include('stats/general_stats_table.php'); -} diff --git a/web/lib/streams.php b/web/lib/streams.php deleted file mode 100644 index 00cf6cc5..00000000 --- a/web/lib/streams.php +++ /dev/null @@ -1,167 +0,0 @@ -. - - This file is part of PHP-gettext. - - PHP-gettext 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 2 of the License, or - (at your option) any later version. - - PHP-gettext 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 PHP-gettext; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -*/ - - - // Simple class to wrap file streams, string streams, etc. - // seek is essential, and it should be byte stream -class StreamReader { - // should return a string [FIXME: perhaps return array of bytes?] - function read($bytes) { - return false; - } - - // should return new position - function seekto($position) { - return false; - } - - // returns current position - function currentpos() { - return false; - } - - // returns length of entire stream (limit for seekto()s) - function length() { - return false; - } -}; - -class StringReader { - var $_pos; - var $_str; - - function __construct($str='') { - $this->_str = $str; - $this->_pos = 0; - } - - function read($bytes) { - $data = substr($this->_str, $this->_pos, $bytes); - $this->_pos += $bytes; - if (strlen($this->_str)<$this->_pos) - $this->_pos = strlen($this->_str); - - return $data; - } - - function seekto($pos) { - $this->_pos = $pos; - if (strlen($this->_str)<$this->_pos) - $this->_pos = strlen($this->_str); - return $this->_pos; - } - - function currentpos() { - return $this->_pos; - } - - function length() { - return strlen($this->_str); - } - -}; - - -class FileReader { - var $_pos; - var $_fd; - var $_length; - - function __construct($filename) { - if (file_exists($filename)) { - - $this->_length=filesize($filename); - $this->_pos = 0; - $this->_fd = fopen($filename,'rb'); - if (!$this->_fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; - } - } else { - $this->error = 2; // File doesn't exist - return false; - } - } - - function read($bytes) { - if ($bytes) { - fseek($this->_fd, $this->_pos); - - // PHP 5.1.1 does not read more than 8192 bytes in one fread() - // the discussions at PHP Bugs suggest it's the intended behaviour - $data = ''; - while ($bytes > 0) { - $chunk = fread($this->_fd, $bytes); - $data .= $chunk; - $bytes -= strlen($chunk); - } - $this->_pos = ftell($this->_fd); - - return $data; - } else return ''; - } - - function seekto($pos) { - fseek($this->_fd, $pos); - $this->_pos = ftell($this->_fd); - return $this->_pos; - } - - function currentpos() { - return $this->_pos; - } - - function length() { - return $this->_length; - } - - function close() { - fclose($this->_fd); - } - -}; - -// Preloads entire file in memory first, then creates a StringReader -// over it (it assumes knowledge of StringReader internals) -class CachedFileReader extends StringReader { - function __construct($filename) { - if (file_exists($filename)) { - - $length=filesize($filename); - $fd = fopen($filename,'rb'); - - if (!$fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; - } - $this->_str = fread($fd, $length); - fclose($fd); - - } else { - $this->error = 2; // File doesn't exist - return false; - } - } -}; - - -?> diff --git a/web/lib/timezone.inc.php b/web/lib/timezone.inc.php deleted file mode 100644 index 949f846d..00000000 --- a/web/lib/timezone.inc.php +++ /dev/null @@ -1,63 +0,0 @@ - Displayed Description - */ -function generate_timezone_list() { - $php_timezones = DateTimeZone::listIdentifiers(DateTimeZone::ALL); - - $offsets = array(); - foreach ($php_timezones as $timezone) { - $tz = new DateTimeZone($timezone); - $offset = $tz->getOffset(new DateTime()); - $offsets[$timezone] = "(UTC" . ($offset < 0 ? "-" : "+") . gmdate("H:i", abs($offset)) . - ") " . $timezone; - } - - asort($offsets); - return $offsets; -} - -/** - * Set the timezone for the user. - * - * @return null - */ -function set_tz() { - $timezones = generate_timezone_list(); - $update_cookie = false; - - if (isset($_COOKIE["AURTZ"])) { - $timezone = $_COOKIE["AURTZ"]; - } elseif (isset($_COOKIE["AURSID"])) { - $dbh = DB::connect(); - $q = "SELECT Timezone FROM Users, Sessions "; - $q .= "WHERE Users.ID = Sessions.UsersID "; - $q .= "AND Sessions.SessionID = "; - $q .= $dbh->quote($_COOKIE["AURSID"]); - $result = $dbh->query($q); - - if ($result) { - $timezone = $result->fetchColumn(0); - if (!$timezone) { - unset($timezone); - } - } - - $update_cookie = true; - } - - if (!isset($timezone) || !array_key_exists($timezone, $timezones)) { - $timezone = config_get("options", "default_timezone"); - } - date_default_timezone_set($timezone); - - if ($update_cookie) { - $timeout = intval(config_get("options", "persistent_cookie_timeout")); - $cookie_time = time() + $timeout; - setcookie("AURTZ", $timezone, $cookie_time, "/"); - } -} diff --git a/web/lib/translator.inc.php b/web/lib/translator.inc.php deleted file mode 100644 index cbed1274..00000000 --- a/web/lib/translator.inc.php +++ /dev/null @@ -1,139 +0,0 @@ -", ""); - -include_once("confparser.inc.php"); -include_once('DB.class.php'); -include_once('gettext.php'); -include_once('streams.php'); - -global $streamer, $l10n; - -# Languages we have translations for -$SUPPORTED_LANGS = array( - "ar" => "العربية", - "ast" => "Asturianu", - "ca" => "Català", - "cs" => "Český", - "da" => "Dansk", - "de" => "Deutsch", - "en" => "English", - "el" => "Ελληνικά", - "es" => "Español", - "es_419" => "Español (Latinoamérica)", - "fi" => "Suomi", - "fr" => "Français", - "he" => "עברית", - "hr" => "Hrvatski", - "hu" => "Magyar", - "it" => "Italiano", - "ja" => "日本語", - "nb" => "Norsk", - "nl" => "Nederlands", - "pl" => "Polski", - "pt_BR" => "Português (Brasil)", - "pt_PT" => "Português (Portugal)", - "ro" => "Română", - "ru" => "Русский", - "sk" => "Slovenčina", - "sr" => "Srpski", - "tr" => "Türkçe", - "uk" => "Українська", - "zh_CN" => "简体中文", - "zh_TW" => "正體中文" -); - -function __() { - global $LANG; - global $l10n; - - # Create the translation. - $args = func_get_args(); - - # First argument is always string to be translated - $tag = array_shift($args); - - # Translate using gettext_reader initialized before. - $translated = $l10n->translate($tag); - $translated = htmlspecialchars($translated, ENT_QUOTES); - - # Subsequent arguments are strings to be formatted - if (count($args) > 0) { - $translated = vsprintf($translated, $args); - } - - return $translated; -} - -function _n($msgid1, $msgid2, $n) { - global $l10n; - - $translated = sprintf($l10n->ngettext($msgid1, $msgid2, $n), $n); - return htmlspecialchars($translated, ENT_QUOTES); -} - -# set up the visitor's language -# -function set_lang() { - global $LANG; - global $SUPPORTED_LANGS; - global $streamer, $l10n; - - $update_cookie = 0; - if (isset($_POST['setlang'])) { - # visitor is requesting a language change - # - $LANG = $_POST['setlang']; - $update_cookie = 1; - - } elseif (isset($_COOKIE['AURLANG'])) { - # If a cookie is set, use that - # - $LANG = $_COOKIE['AURLANG']; - - } elseif (isset($_COOKIE["AURSID"])) { - # No language but a session; use default lang preference - # - $dbh = DB::connect(); - $q = "SELECT LangPreference FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = "; - $q.= $dbh->quote($_COOKIE["AURSID"]); - $result = $dbh->query($q); - - if ($result) { - $LANG = $result->fetchColumn(0); - if (!$LANG) { - unset($LANG); - } - } - $update_cookie = 1; - } - - # Set $LANG to default if nothing is valid. - if (!isset($LANG) || !array_key_exists($LANG, $SUPPORTED_LANGS)) { - $LANG = config_get('options', 'default_lang'); - } - - if ($update_cookie) { - $timeout = intval(config_get('options', 'persistent_cookie_timeout')); - $cookie_time = time() + $timeout; - setcookie("AURLANG", $LANG, $cookie_time, "/"); - } - - $localedir = config_get('options', 'localedir'); - $streamer = new FileReader($localedir . '/' . $LANG . - '/LC_MESSAGES/aurweb.mo'); - $l10n = new gettext_reader($streamer, true); - - return; -} diff --git a/web/lib/version.inc.php b/web/lib/version.inc.php deleted file mode 100644 index 81d960bc..00000000 --- a/web/lib/version.inc.php +++ /dev/null @@ -1,2 +0,0 @@ - - ' . htmlspecialchars($username) . '') ?> -

    -

    - ', '') ?> -

    - -
    -
    - - - -
    -
    -

    - - -

    - -

    - -

    - -

    - " /> -

    -
    -
    diff --git a/web/template/account_details.php b/web/template/account_details.php deleted file mode 100644 index 84f8b9c5..00000000 --- a/web/template/account_details.php +++ /dev/null @@ -1,93 +0,0 @@ - - - - -
    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - " . __("hidden") . ""; - else: - ?> - "> - -
    " rel="nofollow">
    - -
    format('Y-m-d') ?>
    - -
    Links:
      -
    • - -
    • - - -
    • - -
    -
    diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php deleted file mode 100644 index 4ce6b875..00000000 --- a/web/template/account_edit_form.php +++ /dev/null @@ -1,222 +0,0 @@ - -

    - ', '') ?> - ', '') ?> - ', '') ?> -

    - -
    - - - -
    - - - - - -
    -
    -

    - - () -

    -

    - -

    - -

    - - -

    - -

    - - - - - - -

    - - - -

    - - /> -

    - - -

    - - () -

    -

    - -

    - -

    - - /> -

    -

    - -

    - -

    - - -

    -

    - - - - " . __("Hide Email Address") . "") ?> - -

    - -

    - - -

    - -

    - - -

    - -

    - - -

    - -

    - - -

    - -

    - - -

    -

    - - -

    -
    - - -
    - -

    - - -

    - -

    - - -

    -
    - - -
    - -

    - - -

    -
    - -
    - : -

    - - /> -

    -

    - - /> -

    -

    - - /> -

    -
    - -
    - - -

    - - -

    - - -

    - - () - -

    - -
    - -
    -

    - - - " />   - - " />   - - " /> -

    -
    -
    diff --git a/web/template/account_search_results.php b/web/template/account_search_results.php deleted file mode 100644 index 0f7eb7a4..00000000 --- a/web/template/account_search_results.php +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - $row): ?> - - - - - - - - - - - -
    "> - - - - "> - -   - -
    - - - - - - -
    -
    -
    - - - $ind): - ?> - - - " /> -
    -
    -
    -
    -
    - - - $ind): - ?> - - - -->" /> -
    -
    -
    - -

    - -

    - - diff --git a/web/template/cgit/footer.html b/web/template/cgit/footer.html deleted file mode 100644 index 14c358f1..00000000 --- a/web/template/cgit/footer.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/web/template/cgit/header.html b/web/template/cgit/header.html deleted file mode 100644 index 2d418702..00000000 --- a/web/template/cgit/header.html +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/web/template/comaintainers_form.php b/web/template/comaintainers_form.php deleted file mode 100644 index f61d494c..00000000 --- a/web/template/comaintainers_form.php +++ /dev/null @@ -1,19 +0,0 @@ -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
    -
    - -

    - - -

    -

    - " /> -

    -
    -
    -
    diff --git a/web/template/flag_comment.php b/web/template/flag_comment.php deleted file mode 100644 index dc285a97..00000000 --- a/web/template/flag_comment.php +++ /dev/null @@ -1,26 +0,0 @@ -
    -

    -

    - - ', html_format_username($message['Username']), '', - '', htmlspecialchars($pkgbase_name), '', - '', date('Y-m-d', $message['OutOfDateTS']), ''); ?> - - ', htmlspecialchars($pkgbase_name), ''); ?> - -

    - -

    -

    -

    -
    -

    - -

    -

    - " /> -
    -

    -
    diff --git a/web/template/footer.php b/web/template/footer.php deleted file mode 100644 index 7f97aae0..00000000 --- a/web/template/footer.php +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/web/template/header.php b/web/template/header.php deleted file mode 100644 index 9631be91..00000000 --- a/web/template/header.php +++ /dev/null @@ -1,82 +0,0 @@ -'; ?> - - - - AUR (<?= htmlspecialchars($LANG); ?>)<?php if ($title != "") { print " - " . htmlspecialchars($title); } ?> - - - - ' /> - ' /> - - - - - - - - -
    -
    -
    "> -
    -
    - - -
    -
    -
    -
    -
    -
      - -
    • -
    • - -
    • - - -
    • - -
    • -
    • -
    • - -
    • AUR
    • -
    • -
    • - -
    • - -
    • - - -
    -
    - diff --git a/web/template/pkg_comment_box.php b/web/template/pkg_comment_box.php deleted file mode 100644 index 22f90d4a..00000000 --- a/web/template/pkg_comment_box.php +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    - -
    diff --git a/web/template/pkg_comment_form.php b/web/template/pkg_comment_form.php deleted file mode 100644 index e8a516e3..00000000 --- a/web/template/pkg_comment_form.php +++ /dev/null @@ -1,28 +0,0 @@ -
    -
    -
    - " /> - - - - - -
    -

    - - ', "") ?> -

    -

    - -

    -

    - " /> - - - - - - -

    -
    -
    diff --git a/web/template/pkg_comments.php b/web/template/pkg_comments.php deleted file mode 100644 index ffa9e137..00000000 --- a/web/template/pkg_comments.php +++ /dev/null @@ -1,239 +0,0 @@ - - - - -
    - -
    - -
    -

    - - - - - - - - - - - - -

    - - 1): ?> -

    - $pagestart): ?> - - - - - - - - - - - - -

    - -
    - - $row): ?> - ' . $date_fmtd . ''; - if ($comment_section == "package") { - if ($row['UserName']) { - $user_fmtd = html_format_username($row['UserName']); - $heading = __('%s commented on %s', $user_fmtd, $date_link); - } else { - $heading = __('Anonymous comment on %s', $date_link); - } - } elseif ($comment_section == "account") { - $pkg_uri = '' . htmlspecialchars($row['PackageBaseName']) . ''; - $heading = __('Commented on package %s on %s', $pkg_uri, $date_link); - } - - $is_deleted = $row['DelTS']; - $is_edited = $row['EditedTS']; - $is_pinned = $row['PinnedTS']; - - if ($uid && $is_deleted) { - $date_fmtd = date('Y-m-d H:i', $row['DelTS']); - $heading .= ' ('; - if ($row['DelUserName']) { - $user_fmtd = html_format_username($row['DelUserName']); - $heading .= __('deleted on %s by %s', $date_fmtd, $user_fmtd); - } else { - $heading .= __('deleted on %s', $date_fmtd); - } - $heading .= ')'; - } elseif ($uid && $is_edited) { - $date_fmtd = date('Y-m-d H:i', $row['EditedTS']); - $heading .= ' ('; - if ($row['EditUserName']) { - $user_fmtd = html_format_username($row['EditUserName']); - $heading .= __('edited on %s by %s', $date_fmtd, $user_fmtd); - } else { - $heading .= __('edited on %s', $date_fmtd); - } - $heading .= ')'; - } - - $comment_classes = "comment-header"; - if ($is_deleted) { - $comment_classes .= " comment-deleted"; - } - ?> -

    - - -
    -
    - - - - " /> - -
    -
    - - - -
    -
    - - - - " /> - -
    -
    - - - - <?= __('Edit comment') ?> - - - = 5)): ?> -
    -
    - - - " /> - - " /> - -
    -
    - - - -
    -
    - - - - " /> - -
    -
    - -

    -
    -
    - - - -

    - -

    - -
    -
    - -
    - - diff --git a/web/template/pkg_details.php b/web/template/pkg_details.php deleted file mode 100644 index 25d85b78..00000000 --- a/web/template/pkg_details.php +++ /dev/null @@ -1,317 +0,0 @@ - -
    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0): -?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - (, ) - -
    () - -
    - -
    -
    - - - - - "/> - -
    -
    -' . htmlspecialchars($kw) . "\n"; - } -endif; -?> -
    - - - - , - - - - - -
    - - - - , - - - - - -
    - - - - , - - - - - -
    - - - - , - - - - - -
    - - - - , - - - - - -
    = .2 ? 2 : 6) ?>
    - -
    -
    -

    - 0): ?> -
      - $darr): ?> -
    • - -
    - -
    -
    -

    - 0): ?> -
      - $darr): ?> -
    • - -
    - -
    -
    -

    -
    - 0): ?> -
    -
      - $src): ?> -
    • - -
    -
    - -
    -
    - - diff --git a/web/template/pkg_search_form.php b/web/template/pkg_search_form.php deleted file mode 100644 index 3d0cde6c..00000000 --- a/web/template/pkg_search_form.php +++ /dev/null @@ -1,120 +0,0 @@ - __('Name, Description'), - 'n' => __('Name Only'), - 'b' => __('Package Base'), - 'N' => __('Exact Name'), - 'B' => __('Exact Package Base'), - 'k' => __('Keywords'), - 'm' => __('Maintainer'), - 'c' => __('Co-maintainer'), - 'M' => __('Maintainer, Co-maintainer'), - 's' => __('Submitter') -); - -$outdated_flags = array( - '' => __('All'), - 'on' => __('Flagged'), - 'off' => __('Not Flagged') -); - -$sortby = array( - 'n' => __('Name'), - 'v' => __('Votes'), - 'p' => __('Popularity'), - 'w' => __('Voted'), - 'o' => __('Notify'), - 'm' => __('Maintainer'), - 'l' => __('Last modified') -); - -$orderby = array( - 'a' => __('Ascending'), - 'd' => __('Descending') -); - -$per_page = array(50, 100, 250); -?> - - diff --git a/web/template/pkg_search_results.php b/web/template/pkg_search_results.php deleted file mode 100644 index 61335560..00000000 --- a/web/template/pkg_search_results.php +++ /dev/null @@ -1,153 +0,0 @@ -'; - if ($sb) { - echo '' . $title . ''; - } else { - echo $title; - } - if ($hint) { - echo '?'; - } - echo ''; - }; -} else { - $fmtth = function($title, $sb=false, $so=false, $hint=false) { - echo '' . $title . ''; - }; -} - -if (!$result): ?> -

    - -

    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - -
    - - - - - - - - - - - - - - - - - - - - - $row): ?> - - - - - - class="flagged"> - - - - - - - - - - - - -
     
    ]" value="1" />"> - - - - - - - - - - - - - - - - - -
    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - -

    - - - - - - - - " /> -

    - - -
    - diff --git a/web/template/pkgbase_actions.php b/web/template/pkgbase_actions.php deleted file mode 100644 index 3d208328..00000000 --- a/web/template/pkgbase_actions.php +++ /dev/null @@ -1,49 +0,0 @@ - diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php deleted file mode 100644 index bde29c1c..00000000 --- a/web/template/pkgbase_details.php +++ /dev/null @@ -1,146 +0,0 @@ - -
    -

    - - - - - - - - - 0): -?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - (, ) - -
    () - -
    - -
    -
    - - - - - "/> - -
    -
    -' . htmlspecialchars($kw) . "\n"; - } -endif; -?> -
    = .2 ? 2 : 6) ?>
    - -
    -
    -

    - 0): ?> -
      - $pkg): -?> -
    • - -
    - -
    -
    -
    - - diff --git a/web/template/pkgreq_close_form.php b/web/template/pkgreq_close_form.php deleted file mode 100644 index 6228f6ab..00000000 --- a/web/template/pkgreq_close_form.php +++ /dev/null @@ -1,31 +0,0 @@ -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -

    - : - -

    -
    -
    - - -

    - - -

    -

    - - -

    -

    - " /> -

    -
    -
    -
    diff --git a/web/template/pkgreq_form.php b/web/template/pkgreq_form.php deleted file mode 100644 index 9d74093e..00000000 --- a/web/template/pkgreq_form.php +++ /dev/null @@ -1,81 +0,0 @@ -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    -
    -
    - - - -

    - - -

    - - -

    - - -

    -

    - - -

    -

    - -

    -

    - -

    -

    - -

    -

    - " /> -

    -
    -
    -
    diff --git a/web/template/pkgreq_results.php b/web/template/pkgreq_results.php deleted file mode 100644 index 1a565c3e..00000000 --- a/web/template/pkgreq_results.php +++ /dev/null @@ -1,129 +0,0 @@ - -

    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - - - - - - - - - - - - - - - $row): ?> - $idle_time); - if (!$due) { - $time_left = $idle_time - (time() - intval($row['RequestTS'])); - if ($time_left > 48 * 3600) { - $time_left_fmt = _n("~%d day left", "~%d days left", round($time_left / (24 * 3600))); - } elseif ($time_left > 3600) { - $time_left_fmt = _n("~%d hour left", "~%d hours left", round($time_left / 3600)); - } else { - $time_left_fmt = __("<1 hour left"); - } - } - ?> - - - - - - - - - - - - - - class="flagged"> - - - - - - - - - - - - - - - - - -
    "> - - - () - - - - - - - - - - - - - () - -
    - - -
    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - diff --git a/web/template/search_accounts_form.php b/web/template/search_accounts_form.php deleted file mode 100644 index f7824a94..00000000 --- a/web/template/search_accounts_form.php +++ /dev/null @@ -1,52 +0,0 @@ -
    -
    -
    - -
    -
    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - " />   - " /> -

    -
    -
    diff --git a/web/template/stats/general_stats_table.php b/web/template/stats/general_stats_table.php deleted file mode 100644 index 9dcc3aaf..00000000 --- a/web/template/stats/general_stats_table.php +++ /dev/null @@ -1,36 +0,0 @@ -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/web/template/stats/updates_table.php b/web/template/stats/updates_table.php deleted file mode 100644 index 23a86288..00000000 --- a/web/template/stats/updates_table.php +++ /dev/null @@ -1,19 +0,0 @@ -

    ()

    - -RSS Feed -RSS Feed - - - - getIterator() as $row): ?> - - - - - - -
    - " title=""> - - -
    diff --git a/web/template/stats/user_table.php b/web/template/stats/user_table.php deleted file mode 100644 index e7b00834..00000000 --- a/web/template/stats/user_table.php +++ /dev/null @@ -1,21 +0,0 @@ - - -

    - - - - - - - - - - -
    - - -
    - -
    diff --git a/web/template/template.phps b/web/template/template.phps deleted file mode 100644 index f1a0bb0d..00000000 --- a/web/template/template.phps +++ /dev/null @@ -1,19 +0,0 @@ -\n"; - - -html_footer(AURWEB_VERSION); diff --git a/web/template/tu_details.php b/web/template/tu_details.php deleted file mode 100644 index d739060d..00000000 --- a/web/template/tu_details.php +++ /dev/null @@ -1,123 +0,0 @@ - 0) { - $participation = $total / $active_tus; -} else { - $participation = 0; -} - -if ($yes > $active_tus / 2) { - $vote_accepted = true; -} elseif ($participation > $quorum && $yes > $no) { - $vote_accepted = true; -} else { - $vote_accepted = false; -} -?> -
    -

    - - -

    - -

    - - -

    - : - - - - - N/A - - -
    - -
    - : - - -
    - : - - - - - - - - -

    - -

    - \n", htmlspecialchars($row['Agenda'])) ?> -

    - - - - - - - - - - - - - - - - - - - - - 0): ?> - - - - - -
    - - - - - - %
    -
    - - -
    -

    -
      - -
    • - -
    -
    - - -
    - - -
    -
    - " /> - " /> - " /> - - -
    -
    - - -
    diff --git a/web/template/tu_last_votes_list.php b/web/template/tu_last_votes_list.php deleted file mode 100644 index 6e852581..00000000 --- a/web/template/tu_last_votes_list.php +++ /dev/null @@ -1,36 +0,0 @@ -
    -

    - - - - - - - - - - - - $row): - if ($indx % 2): - $c = "even"; - else: - $c = "odd"; - endif; - $username = username_from_id($row["UserID"]); - ?> - - - - - - -
    - - - -
    -
    diff --git a/web/template/tu_list.php b/web/template/tu_list.php deleted file mode 100644 index 204c89ea..00000000 --- a/web/template/tu_list.php +++ /dev/null @@ -1,82 +0,0 @@ -
    -

    - - -
      -
    • -
    - - - -

    - - - - - - - - - - - - - - - - - - $row): ?> - - - - - - - - - - - - - - -
    - - - - - - - - - - - -
    - -
    -

    - - 0 && $off != 0): - $back = (($off - $limit) <= 0) ? 0 : $off - $limit; ?> - ?off=&by='>‹ - - - - - -

    - -
    From ad61c443f47686f41315735341cf53b4464cf0e0 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sat, 29 Apr 2023 09:55:54 +0200 Subject: [PATCH 1295/1451] fix: restore & move cgit html files restore files accidentally deleted with PHP cleanup. https://gitlab.archlinux.org/archlinux/aurweb/-/tree/1325c71712a12c529d7a3defa9cbabfad296922e/web/template/cgit Signed-off-by: moson-mo --- conf/cgitrc.proto | 4 ++-- docker/cgit-entrypoint.sh | 4 ++-- static/html/cgit/footer.html | 6 ++++++ static/html/cgit/header.html | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 static/html/cgit/footer.html create mode 100644 static/html/cgit/header.html diff --git a/conf/cgitrc.proto b/conf/cgitrc.proto index 1b3eacbd..ed53c51c 100644 --- a/conf/cgitrc.proto +++ b/conf/cgitrc.proto @@ -20,8 +20,8 @@ cache-static-ttl=60 root-title=AUR Package Repositories root-desc=Web interface to the AUR Package Repositories -header=/srv/http/aurweb/web/template/cgit/header.html -footer=/srv/http/aurweb/web/template/cgit/footer.html +header=/srv/http/aurweb/static/html/cgit/header.html +footer=/srv/http/aurweb/static/html/cgit/footer.html max-repodesc-length=50 max-blob-size=2048 max-stats=year diff --git a/docker/cgit-entrypoint.sh b/docker/cgit-entrypoint.sh index a44675e2..282430e2 100755 --- a/docker/cgit-entrypoint.sh +++ b/docker/cgit-entrypoint.sh @@ -5,8 +5,8 @@ mkdir -p /var/data/cgit cp -vf conf/cgitrc.proto /etc/cgitrc sed -ri "s|clone-prefix=.*|clone-prefix=${CGIT_CLONE_PREFIX}|" /etc/cgitrc -sed -ri 's|header=.*|header=/aurweb/web/template/cgit/header.html|' /etc/cgitrc -sed -ri 's|footer=.*|footer=/aurweb/web/template/cgit/footer.html|' /etc/cgitrc +sed -ri 's|header=.*|header=/aurweb/static/html/cgit/header.html|' /etc/cgitrc +sed -ri 's|footer=.*|footer=/aurweb/static/html/cgit/footer.html|' /etc/cgitrc sed -ri 's|repo\.path=.*|repo.path=/aurweb/aur.git|' /etc/cgitrc sed -ri "s|^(css)=.*$|\1=${CGIT_CSS}|" /etc/cgitrc diff --git a/static/html/cgit/footer.html b/static/html/cgit/footer.html new file mode 100644 index 00000000..b3e79568 --- /dev/null +++ b/static/html/cgit/footer.html @@ -0,0 +1,6 @@ + diff --git a/static/html/cgit/header.html b/static/html/cgit/header.html new file mode 100644 index 00000000..2d418702 --- /dev/null +++ b/static/html/cgit/header.html @@ -0,0 +1,15 @@ + From bab17a9d262e1f184deece9003e67df8baae01c4 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sat, 29 Apr 2023 09:59:34 +0200 Subject: [PATCH 1296/1451] doc: amend INSTALL instructions change path for metadata archive files Signed-off-by: moson-mo --- INSTALL | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/INSTALL b/INSTALL index 107fab4b..c6c71ca7 100644 --- a/INSTALL +++ b/INSTALL @@ -30,9 +30,6 @@ read the instructions below. ssl_certificate /etc/ssl/certs/aur.cert.pem; ssl_certificate_key /etc/ssl/private/aur.key.pem; - # Asset root. This is used to match against gzip archives. - root /srv/http/aurweb/web/html; - # TU Bylaws redirect. location = /trusted-user/TUbylaws.html { return 301 https://tu-bylaws.aur.archlinux.org; @@ -62,6 +59,9 @@ read the instructions below. # Static archive assets. location ~ \.gz$ { + # Asset root. This is used to match against gzip archives. + root /srv/http/aurweb/archives; + types { application/gzip text/plain } default_type text/plain; add_header Content-Encoding gzip; From e896edaccc3ef5666298d3c1a42816d35c8d2950 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sun, 30 Apr 2023 10:12:09 +0100 Subject: [PATCH 1297/1451] chore: support for python 3.11 and poetry.lock update Signed-off-by: Leonidas Spyropoulos --- poetry.lock | 259 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 128 insertions(+), 133 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1b98a5b8..5e933e70 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,14 +14,14 @@ files = [ [[package]] name = "alembic" -version = "1.10.3" +version = "1.10.4" description = "A database migration tool for SQLAlchemy." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, - {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, + {file = "alembic-1.10.4-py3-none-any.whl", hash = "sha256:43942c3d4bf2620c466b91c0f4fca136fe51ae972394a0cc8b90810d664e4f5c"}, + {file = "alembic-1.10.4.tar.gz", hash = "sha256:295b54bbb92c4008ab6a7dcd1e227e668416d6f84b98b3c4446a2bc6214a556b"}, ] [package.dependencies] @@ -352,63 +352,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.3" +version = "7.2.4" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"}, - {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"}, - {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"}, - {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"}, - {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"}, - {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"}, - {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"}, - {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"}, - {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"}, - {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"}, - {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"}, - {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"}, - {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"}, - {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"}, - {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"}, - {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"}, - {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"}, - {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"}, - {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"}, - {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"}, - {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"}, - {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"}, - {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"}, - {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"}, - {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"}, - {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"}, - {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"}, - {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"}, - {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"}, - {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"}, - {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"}, - {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"}, - {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"}, - {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"}, - {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"}, - {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"}, - {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"}, - {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"}, - {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"}, - {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"}, - {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"}, - {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"}, - {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"}, - {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"}, - {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"}, - {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"}, - {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"}, - {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"}, - {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"}, - {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"}, - {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"}, + {file = "coverage-7.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e5eedde6e6e241ec3816f05767cc77e7456bf5ec6b373fb29917f0990e2078f"}, + {file = "coverage-7.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c6c6e3b8fb6411a2035da78d86516bfcfd450571d167304911814407697fb7a"}, + {file = "coverage-7.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7668a621afc52db29f6867e0e9c72a1eec9f02c94a7c36599119d557cf6e471"}, + {file = "coverage-7.2.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfb53bef4b2739ff747ebbd76d6ac5384371fd3c7a8af08899074eba034d483"}, + {file = "coverage-7.2.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c4f2e44a2ae15fa6883898e756552db5105ca4bd918634cbd5b7c00e19e8a1"}, + {file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:700bc9fb1074e0c67c09fe96a803de66663830420781df8dc9fb90d7421d4ccb"}, + {file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ac4861241e693e21b280f07844ae0e0707665e1dfcbf9466b793584984ae45c4"}, + {file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3d6f3c5b6738a494f17c73b4aa3aa899865cc33a74aa85e3b5695943b79ad3ce"}, + {file = "coverage-7.2.4-cp310-cp310-win32.whl", hash = "sha256:437da7d2fcc35bf45e04b7e9cfecb7c459ec6f6dc17a8558ed52e8d666c2d9ab"}, + {file = "coverage-7.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:1d3893f285fd76f56651f04d1efd3bdce251c32992a64c51e5d6ec3ba9e3f9c9"}, + {file = "coverage-7.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a17bf32e9e3333d78606ac1073dd20655dc0752d5b923fa76afd3bc91674ab4"}, + {file = "coverage-7.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f7ffdb3af2a01ce91577f84fc0faa056029fe457f3183007cffe7b11ea78b23c"}, + {file = "coverage-7.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89e63b38c7b888e00fd42ce458f838dccb66de06baea2da71801b0fc9070bfa0"}, + {file = "coverage-7.2.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4522dd9aeb9cc2c4c54ce23933beb37a4e106ec2ba94f69138c159024c8a906a"}, + {file = "coverage-7.2.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c7d88468f01a75231797173b52dc66d20a8d91b8bb75c88fc5861268578f52"}, + {file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bc47015fc0455753e8aba1f38b81b731aaf7f004a0c390b404e0fcf1d6c1d72f"}, + {file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c122d120c11a236558c339a59b4b60947b38ac9e3ad30a0e0e02540b37bf536"}, + {file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:50fda3d33b705b9c01e3b772cfa7d14de8aec2ec2870e4320992c26d057fde12"}, + {file = "coverage-7.2.4-cp311-cp311-win32.whl", hash = "sha256:ab08af91cf4d847a6e15d7d5eeae5fead1487caf16ff3a2056dbe64d058fd246"}, + {file = "coverage-7.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:876e4ef3eff00b50787867c5bae84857a9af4c369a9d5b266cd9b19f61e48ef7"}, + {file = "coverage-7.2.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3fc9cde48de956bfbacea026936fbd4974ff1dc2f83397c6f1968f0142c9d50b"}, + {file = "coverage-7.2.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12bc9127c8aca2f7c25c9acca53da3db6799b2999b40f28c2546237b7ea28459"}, + {file = "coverage-7.2.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2857894c22833d3da6e113623a9b7440159b2295280b4e0d954cadbfa724b85a"}, + {file = "coverage-7.2.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4db4e6c115d869cd5397d3d21fd99e4c7053205c33a4ae725c90d19dcd178af"}, + {file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f37ae1804596f13d811e0247ffc8219f5261b3565bdf45fcbb4fc091b8e9ff35"}, + {file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cdee9a77fd0ce000781680b6a1f4b721c567f66f2f73a49be1843ff439d634f3"}, + {file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b65a6a5484b7f2970393d6250553c05b2ede069e0e18abe907fdc7f3528252e"}, + {file = "coverage-7.2.4-cp37-cp37m-win32.whl", hash = "sha256:1a3e8697cb40f28e5bcfb6f4bda7852d96dbb6f6fd7cc306aba4ae690c9905ab"}, + {file = "coverage-7.2.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4078939c4b7053e14e87c65aa68dbed7867e326e450f94038bfe1a1b22078ff9"}, + {file = "coverage-7.2.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:603a2b172126e3b08c11ca34200143089a088cd0297d4cfc4922d2c1c3a892f9"}, + {file = "coverage-7.2.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72751d117ceaad3b1ea3bcb9e85f5409bbe9fb8a40086e17333b994dbccc0718"}, + {file = "coverage-7.2.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f19ba9301e6fb0b94ba71fda9a1b02d11f0aab7f8e2455122a4e2921b6703c2f"}, + {file = "coverage-7.2.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d784177a7fb9d0f58d24d3e60638c8b729c3693963bf67fa919120f750db237"}, + {file = "coverage-7.2.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d2a9180beff1922b09bd7389e23454928e108449e646c26da5c62e29b0bf4e3"}, + {file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:39747afc854a7ee14e5e132da7db179d6281faf97dc51e6d7806651811c47538"}, + {file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60feb703abc8d78e9427d873bcf924c9e30cf540a21971ef5a17154da763b60f"}, + {file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2becddfcbf3d994a8f4f9dd2b6015cae3a3eff50dedc6e4a17c3cccbe8f93d4"}, + {file = "coverage-7.2.4-cp38-cp38-win32.whl", hash = "sha256:56a674ad18d6b04008283ca03c012be913bf89d91c0803c54c24600b300d9e51"}, + {file = "coverage-7.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:ab08e03add2cf5793e66ac1bbbb24acfa90c125476f5724f5d44c56eeec1d635"}, + {file = "coverage-7.2.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92b565c51732ea2e7e541709ccce76391b39f4254260e5922e08e00971e88e33"}, + {file = "coverage-7.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8769a67e8816c7e94d5bf446fc0501641fde78fdff362feb28c2c64d45d0e9b1"}, + {file = "coverage-7.2.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d74d6fbd5a98a5629e8467b719b0abea9ca01a6b13555d125c84f8bf4ea23d"}, + {file = "coverage-7.2.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9f770c6052d9b5c9b0e824fd8c003fe33276473b65b4f10ece9565ceb62438e"}, + {file = "coverage-7.2.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3023ce23e41a6f006c09f7e6d62b6c069c36bdc9f7de16a5ef823acc02e6c63"}, + {file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fabd1f4d12dfd6b4f309208c2f31b116dc5900e0b42dbafe4ee1bc7c998ffbb0"}, + {file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e41a7f44e73b37c6f0132ecfdc1c8b67722f42a3d9b979e6ebc150c8e80cf13a"}, + {file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:864e36947289be05abd83267c4bade35e772526d3e9653444a9dc891faf0d698"}, + {file = "coverage-7.2.4-cp39-cp39-win32.whl", hash = "sha256:ea534200efbf600e60130c48552f99f351cae2906898a9cd924c1c7f2fb02853"}, + {file = "coverage-7.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:00f8fd8a5fe1ffc3aef78ea2dbf553e5c0f4664324e878995e38d41f037eb2b3"}, + {file = "coverage-7.2.4-pp37.pp38.pp39-none-any.whl", hash = "sha256:856bcb837e96adede31018a0854ce7711a5d6174db1a84e629134970676c54fa"}, + {file = "coverage-7.2.4.tar.gz", hash = "sha256:7283f78d07a201ac7d9dc2ac2e4faaea99c4d302f243ee5b4e359f3e170dc008"}, ] [package.dependencies] @@ -528,14 +528,14 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "2.11.0" +version = "2.11.2" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "fakeredis-2.11.0-py3-none-any.whl", hash = "sha256:156ef67713dd53000c28dd341be61a365c20230bc17c8fb8320b0c123e667aff"}, - {file = "fakeredis-2.11.0.tar.gz", hash = "sha256:d25883dc52c31546e586b6ec3c49c5999b3025bfc4812532d71dedcfed56fee1"}, + {file = "fakeredis-2.11.2-py3-none-any.whl", hash = "sha256:69a504328a89e5e5f2d05a4236b570fb45244c96997c5002c8c6a0503b95f289"}, + {file = "fakeredis-2.11.2.tar.gz", hash = "sha256:e0fef512b8ec49679d373456aa4698a4103005ecd7ca0b13170a2c1d3af949c5"}, ] [package.dependencies] @@ -1101,68 +1101,63 @@ files = [ [[package]] name = "orjson" -version = "3.8.10" +version = "3.8.11" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false python-versions = ">= 3.7" files = [ - {file = "orjson-3.8.10-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:4dfe0651e26492d5d929bbf4322de9afbd1c51ac2e3947a7f78492b20359711d"}, - {file = "orjson-3.8.10-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:bc30de5c7b3a402eb59cc0656b8ee53ca36322fc52ab67739c92635174f88336"}, - {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c08b426fae7b9577b528f99af0f7e0ff3ce46858dd9a7d1bf86d30f18df89a4c"}, - {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bce970f293825e008dbf739268dfa41dfe583aa2a1b5ef4efe53a0e92e9671ea"}, - {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b23fb0264bbdd7218aa685cb6fc71f0dcecf34182f0a8596a3a0dff010c06f9"}, - {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0826ad2dc1cea1547edff14ce580374f0061d853cbac088c71162dbfe2e52205"}, - {file = "orjson-3.8.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7bce6e61cea6426309259b04c6ee2295b3f823ea51a033749459fe2dd0423b2"}, - {file = "orjson-3.8.10-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0b470d31244a6f647e5402aac7d2abaf7bb4f52379acf67722a09d35a45c9417"}, - {file = "orjson-3.8.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:48824649019a25d3e52f6454435cf19fe1eb3d05ee697e65d257f58ae3aa94d9"}, - {file = "orjson-3.8.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:faee89e885796a9cc493c930013fa5cfcec9bfaee431ddf00f0fbfb57166a8b3"}, - {file = "orjson-3.8.10-cp310-none-win_amd64.whl", hash = "sha256:3cfe32b1227fe029a5ad989fbec0b453a34e5e6d9a977723f7c3046d062d3537"}, - {file = "orjson-3.8.10-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:2073b62822738d6740bd2492f6035af5c2fd34aa198322b803dc0e70559a17b7"}, - {file = "orjson-3.8.10-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b2c4faf20b6bb5a2d7ac0c16f58eb1a3800abcef188c011296d1dc2bb2224d48"}, - {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c1825997232a324911d11c75d91e1e0338c7b723c149cf53a5fc24496c048a4"}, - {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7e85d4682f3ed7321d36846cad0503e944ea9579ef435d4c162e1b73ead8ac9"}, - {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8cdaacecb92997916603ab232bb096d0fa9e56b418ca956b9754187d65ca06"}, - {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ddabc5e44702d13137949adee3c60b7091e73a664f6e07c7b428eebb2dea7bbf"}, - {file = "orjson-3.8.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27bb26e171e9cfdbec39c7ca4739b6bef8bd06c293d56d92d5e3a3fc017df17d"}, - {file = "orjson-3.8.10-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1810e5446fe68d61732e9743592da0ec807e63972eef076d09e02878c2f5958e"}, - {file = "orjson-3.8.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61e2e51cefe7ef90c4fbbc9fd38ecc091575a3ea7751d56fad95cbebeae2a054"}, - {file = "orjson-3.8.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f3e9ac9483c2b4cd794e760316966b7bd1e6afb52b0218f068a4e80c9b2db4f6"}, - {file = "orjson-3.8.10-cp311-none-win_amd64.whl", hash = "sha256:26aee557cf8c93b2a971b5a4a8e3cca19780573531493ce6573aa1002f5c4378"}, - {file = "orjson-3.8.10-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:11ae68f995a50724032af297c92f20bcde31005e0bf3653b12bff9356394615b"}, - {file = "orjson-3.8.10-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:35d879b46b8029e1e01e9f6067928b470a4efa1ca749b6d053232b873c2dcf66"}, - {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:345e41abd1d9e3ecfb554e1e75ff818cf42e268bd06ad25a96c34e00f73a327e"}, - {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:45a5afc9cda6b8aac066dd50d8194432fbc33e71f7164f95402999b725232d78"}, - {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad632dc330a7b39da42530c8d146f76f727d476c01b719dc6743c2b5701aaf6b"}, - {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf2556ba99292c4dc550560384dd22e88b5cdbe6d98fb4e202e902b5775cf9f"}, - {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b88afd662190f19c3bb5036a903589f88b1d2c2608fbb97281ce000db6b08897"}, - {file = "orjson-3.8.10-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:abce8d319aae800fd2d774db1106f926dee0e8a5ca85998fd76391fcb58ef94f"}, - {file = "orjson-3.8.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e999abca892accada083f7079612307d94dd14cc105a699588a324f843216509"}, - {file = "orjson-3.8.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3fdee68c4bb3c5d6f89ed4560f1384b5d6260e48fbf868bae1a245a3c693d4d"}, - {file = "orjson-3.8.10-cp37-none-win_amd64.whl", hash = "sha256:e5d7f82506212e047b184c06e4bcd48c1483e101969013623cebcf51cf12cad9"}, - {file = "orjson-3.8.10-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:d953e6c2087dcd990e794f8405011369ee11cf13e9aaae3172ee762ee63947f2"}, - {file = "orjson-3.8.10-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:81aa3f321d201bff0bd0f4014ea44e51d58a9a02d8f2b0eeab2cee22611be8e1"}, - {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d27b6182f75896dd8c10ea0f78b9265a3454be72d00632b97f84d7031900dd4"}, - {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1486600bc1dd1db26c588dd482689edba3d72d301accbe4301db4b2b28bd7aa4"}, - {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344ea91c556a2ce6423dc13401b83ab0392aa697a97fa4142c2c63a6fd0bbfef"}, - {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:979f231e3bad1c835627eef1a30db12a8af58bfb475a6758868ea7e81897211f"}, - {file = "orjson-3.8.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fa3a26dcf0f5f2912a8ce8e87273e68b2a9526854d19fd09ea671b154418e88"}, - {file = "orjson-3.8.10-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b6e79d8864794635974b18821b49a7f27859d17b93413d4603efadf2e92da7a5"}, - {file = "orjson-3.8.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce49999bcbbc14791c61844bc8a69af44f5205d219be540e074660038adae6bf"}, - {file = "orjson-3.8.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2ef690335b24f9272dbf6639353c1ffc3f196623a92b851063e28e9515cf7dd"}, - {file = "orjson-3.8.10-cp38-none-win_amd64.whl", hash = "sha256:5a0b1f4e4fa75e26f814161196e365fc0e1a16e3c07428154505b680a17df02f"}, - {file = "orjson-3.8.10-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:af7601a78b99f0515af2f8ab12c955c0072ffcc1e437fb2556f4465783a4d813"}, - {file = "orjson-3.8.10-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6bbd7b3a3e2030b03c68c4d4b19a2ef5b89081cbb43c05fe2010767ef5e408db"}, - {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4355c9aedfefe60904e8bd7901315ebbc8bb828f665e4c9bc94b1432e67cb6f7"}, - {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b0ba074375e25c1594e770e2215941e2017c3cd121889150737fa1123e8bfe"}, - {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34b6901c110c06ab9e8d7d0496db4bc9a0c162ca8d77f67539d22cb39e0a1ef4"}, - {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb62ec16a1c26ad9487727b529103cb6a94a1d4969d5b32dd0eab5c3f4f5a6f2"}, - {file = "orjson-3.8.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595e1e7d04aaaa3d41113e4eb9f765ab642173c4001182684ae9ddc621bb11c8"}, - {file = "orjson-3.8.10-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:64ffd92328473a2f9af059410bd10c703206a4bbc7b70abb1bedcd8761e39eb8"}, - {file = "orjson-3.8.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b1f648ec89c6a426098868460c0ef8c86b457ce1378d7569ff4acb6c0c454048"}, - {file = "orjson-3.8.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6a286ad379972e4f46579e772f0477e6b505f1823aabcd64ef097dbb4549e1a4"}, - {file = "orjson-3.8.10-cp39-none-win_amd64.whl", hash = "sha256:d2874cee6856d7c386b596e50bc517d1973d73dc40b2bd6abec057b5e7c76b2f"}, - {file = "orjson-3.8.10.tar.gz", hash = "sha256:dcf6adb4471b69875034afab51a14b64f1026bc968175a2bb02c5f6b358bd413"}, + {file = "orjson-3.8.11-cp310-cp310-macosx_11_0_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:9fa900bdd84b4576c8dd6f3e2a00b35797f29283af328c6e3d70addfa4c2d599"}, + {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1103e597c16f82c241e1b02beadc9c91cecd93e60433ca73cb6464dcc235f37c"}, + {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d70b6db9d4e1e6057829cd7fe119c217cebaf989f88d14b2445fa69fc568d03e"}, + {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3afccf7f8684dca7f017837a315de0a1ab5c095de22a4eed206d079f9325ed72"}, + {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fedcc428416e23a6c9de62a000c22ae33bbe0108302ad5d5935e29ea739bf37"}, + {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf48ed8d4b6ab9f23b7ee642462369d7133412d72824bad89f9bf4311c06c6a1"}, + {file = "orjson-3.8.11-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c55065bc2075a5ea6ffb30462d84fd3aa5bbb7ae600855c325ee5753feec715"}, + {file = "orjson-3.8.11-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:08729e339ff3146e6de56c1166f014c3d2ec3e79ffb76d6c55d52cc892e5e477"}, + {file = "orjson-3.8.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:358e515b8b19a275b259f5ee1e0efa2859b1d976b5ed5d016ac59f9e6c8788a3"}, + {file = "orjson-3.8.11-cp310-none-win_amd64.whl", hash = "sha256:62eb8bdcf6f4cdbe12743e88ad98696277a75f91a35e8fb93a7ea2b9f4a7000c"}, + {file = "orjson-3.8.11-cp311-cp311-macosx_11_0_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:982ab319b7a5ece4199caf2a2b3a28e62a8e289cb6418548ef98bced7e2a6cfe"}, + {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14903bfeb591a9117b7d40d81e3ebca9700b4e77bd829d6f22ea57941bb0ebf"}, + {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58c068f93d701f9466f667bf3b5cb4e4946aee940df2b07ca5101f1cf1b60ce4"}, + {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9486963d2e65482c565dacb366adb36d22aa22acf7274b61490244c3d87fa631"}, + {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c3b5405edc3a5f9e34516ee1a729f6c46aecf6de960ae07a7b3e95ebdd0e1d9"}, + {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b65424ceee82b94e3613233b67ef110dc58f9d83b0076ec47a506289552a861"}, + {file = "orjson-3.8.11-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:173b8f8c750590f432757292cfb197582e5c14347b913b4017561d47af0e759b"}, + {file = "orjson-3.8.11-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f38c8194ce086e6a9816b4b8dde5e7f383feeed92feec0385d99baf64f9b6e"}, + {file = "orjson-3.8.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:553fdaf9f4b5060a0dcc517ae0c511c289c184a83d6719d03c5602ed0eef0390"}, + {file = "orjson-3.8.11-cp311-none-win_amd64.whl", hash = "sha256:12f647d4da0aab1997e25bed4fa2b76782b5b9d2d1bf3066b5f0a57d34d833c4"}, + {file = "orjson-3.8.11-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:71a656f1c62e84c69060093e20cedff6a92e472d53ff5b8b9026b1b298542a68"}, + {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176d742f53434541e50a5e659694073aa51dcbd8f29a1708a4fa1a320193c615"}, + {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b369019e597b59c4b97e9f925a3b725321fa1481c129d76c74c6ea3823f5d1e8"}, + {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a53b3c02a38aadc5302661c2ca18645093971488992df77ce14fef16f598b2e"}, + {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d7b050135669d2335e40120215ad4120e29958c139f8bab68ce06a1cb1a1b2c"}, + {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66f0c9e4e8f6641497a7dc50591af3704b11468e9fc90cfb5874f28b0a61edb5"}, + {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:235926b38ed9b76ab2bca99ff26ece79c1c46bc10079b06e660b087aecffbe69"}, + {file = "orjson-3.8.11-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c2d3e6b65458ed71b6797f321d6e8bfeeadee9d3d31cac47806a608ea745edd7"}, + {file = "orjson-3.8.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4118dcd2b5a27a22af5ad92414073f25d93bca1868f1f580056003c84841062f"}, + {file = "orjson-3.8.11-cp37-none-win_amd64.whl", hash = "sha256:b68a07794834b7bd53ae2a8b4fe4bf010734cae3f0917d434c83b97acf8e5bce"}, + {file = "orjson-3.8.11-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:98befa717efaab7ddb847ebe47d473f6bd6f0cb53e98e6c3d487c7c58ba2e174"}, + {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f9415b86ef154bf247fa78a6918aac50089c296e26fb6cf15bc9d7e6402a1f8"}, + {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7aeefac55848aeb29f20b91fa55f9e488f446201bb1bb31dc17480d113d8955"}, + {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d47f97b99beb9bcac6e288a76b559543a61e0187443d8089204b757726b1d000"}, + {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7d5aecccfaf2052cd07ed5bec8efba9ddfea055682fcd346047b1a3e9da3034"}, + {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b60dfc1251742e79bb075d7a7c4e37078b932a02e6f005c45761bd90c69189"}, + {file = "orjson-3.8.11-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:ef52f1d5a2f89ef9049781c90ea35d5edf74374ed6ed515c286a706d1b290267"}, + {file = "orjson-3.8.11-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7b4fae3b8fc69c8e76f1c0694f3decfe8a57f87e7ac7779ebb59cd71135438"}, + {file = "orjson-3.8.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f4e4a1001933166fd1c257b920b241b35322bef99ed7329338bf266ac053abe7"}, + {file = "orjson-3.8.11-cp38-none-win_amd64.whl", hash = "sha256:5ff10789cbc08a9fd94507c907ba55b9315e99f20345ff8ef34fac432dacd948"}, + {file = "orjson-3.8.11-cp39-cp39-macosx_11_0_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:c67ac094a4dde914297543af19f22532d7124f3a35245580d8b756c4ff2f5884"}, + {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdf201e77d3fac9d8d6f68d872ef45dccfe46f30b268bb88b6c5af5065b433aa"}, + {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3485c458670c0edb79ca149fe201f199dd9ccfe7ca3acbdef617e3c683e7b97f"}, + {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e97fdbb779a3b8f5d9fc7dfddef5325f81ee45897eb7cb4638d5d9734d42514"}, + {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fc050f8e7f2e4061c8c9968ad0be745b11b03913b77ffa8ceca65914696886c"}, + {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2ef933da50b31c112b252be03d1ef59e0d0552c1a08e48295bd529ce42aaab8"}, + {file = "orjson-3.8.11-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:714c3e2be6ed7e4ff6e887926d6e171bfd94fdee76d7d3bfa74ee19237a2d49d"}, + {file = "orjson-3.8.11-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7e4ded77ac7432a155d1d27a83bcadf722750aea3b9e6c4d47f2a92054ab71cb"}, + {file = "orjson-3.8.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:382f15861a4bf447ab9d07106010e61b217ef6d4245c6cf64af0c12c4c5e2346"}, + {file = "orjson-3.8.11-cp39-none-win_amd64.whl", hash = "sha256:0bc3d1b93a73b46a698c054697eb2d27bdedbc5ea0d11ec5f1a6bfbec36346b5"}, + {file = "orjson-3.8.11.tar.gz", hash = "sha256:882c77126c42dd93bb35288632d69b1e393863a2b752de3e5fe0112833609496"}, ] [[package]] @@ -1566,14 +1561,14 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "requests" -version = "2.28.2" +version = "2.29.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.7" files = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, + {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, + {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, ] [package.dependencies] @@ -1606,14 +1601,14 @@ idna2008 = ["idna"] [[package]] name = "setuptools" -version = "67.7.1" +version = "67.7.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.7.1-py3-none-any.whl", hash = "sha256:6f0839fbdb7e3cfef1fc38d7954f5c1c26bf4eebb155a55c9bf8faf997b9fb67"}, - {file = "setuptools-67.7.1.tar.gz", hash = "sha256:bb16732e8eb928922eabaa022f881ae2b7cdcfaf9993ef1f5e841a96d32b8e0c"}, + {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"}, + {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"}, ] [package.extras] @@ -1807,14 +1802,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.7" +version = "0.11.8" description = "Style preserving TOML library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, - {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, + {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, + {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, ] [[package]] @@ -1910,21 +1905,21 @@ files = [ [[package]] name = "werkzeug" -version = "2.2.3" +version = "2.3.2" description = "The comprehensive WSGI web application library." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"}, - {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"}, + {file = "Werkzeug-2.3.2-py3-none-any.whl", hash = "sha256:b7b8bc1609f35ae8e45d48a9b58d7a4eb1e41eec148d37e977e5df6ebf3398b2"}, + {file = "Werkzeug-2.3.2.tar.gz", hash = "sha256:2f3278e9ef61511cdf82cc28fc5da0f5b501dd8f01ecf5ef6a5d810048f68702"}, ] [package.dependencies] MarkupSafe = ">=2.1.1" [package.extras] -watchdog = ["watchdog"] +watchdog = ["watchdog (>=2.3)"] [[package]] name = "wsproto" @@ -1959,5 +1954,5 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" -python-versions = ">=3.9,<3.11" -content-hash = "b82180015aa365bf55c890e8a3be5e549fa52f4e62cf4619fb71870939dda170" +python-versions = ">=3.9,<3.12" +content-hash = "7eba1b2d0fd17dacea153cb3f6ebdcb11d7888902cbb3a88a65f3c6c38f65699" diff --git a/pyproject.toml b/pyproject.toml index eaaa7221..d75a1d4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ build-backend = "poetry.masonry.api" "Request Mailing List" = "https://lists.archlinux.org/listinfo/aur-requests" [tool.poetry.dependencies] -python = ">=3.9,<3.11" +python = ">=3.9,<3.12" # poetry-dynamic-versioning is used to produce tool.poetry.version # based on git tags. From b3fcfb7679029bb8d72b2ac02c6d294f47224c38 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 30 Apr 2023 20:24:24 +0200 Subject: [PATCH 1298/1451] doc: improve instructions for setting up a dev/test env Provide more detailed information how to get started with a dev/test env. Signed-off-by: moson-mo --- CONTRIBUTING.md | 2 + TESTING | 207 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 155 insertions(+), 54 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a91e3eec..1957ae22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,6 +97,8 @@ Accessible services (on the host): Docker services, by default, are setup to be hot reloaded when source code is changed. +For detailed setup instructions have a look at [TESTING](TESTING) + #### Using INSTALL The [INSTALL](INSTALL) file describes steps to install the application on diff --git a/TESTING b/TESTING index 078d330b..e9cbf33b 100644 --- a/TESTING +++ b/TESTING @@ -1,59 +1,130 @@ Setup Testing Environment ========================= +The quickest way to get you hacking on aurweb is to utilize docker. +In case you prefer to run it bare-metal see instructions further below. + +Containerized environment +------------------------- + +1) Clone the aurweb project: + + $ git clone https://gitlab.archlinux.org/archlinux/aurweb.git + $ cd aurweb + +2) Install the necessary packages: + + # pacman -S --needed docker docker-compose + +3) Build the aurweb:latest image: + + # systemctl start docker + # docker compose build + +4) Run local Docker development instance: + + # docker compose up -d + +5) Browse to local aurweb development server. + + https://localhost:8444/ + +6) [Optionally] populate the database with dummy data: + + # docker compose exec mariadb /bin/bash + # pacman -S --noconfirm words fortune-mod + # poetry run schema/gendummydata.py dummy_data.sql + # mariadb -uaur -paur aurweb < dummy_data.sql + # exit + + Inspect `dummy_data.sql` for test credentials. + Passwords match usernames. + +We now have fully set up environment which we can start and stop with: + + # docker compose start + # docker compose stop + +Proceed with topic "Setup for running tests" + + +Bare Metal installation +----------------------- + Note that this setup is only to test the web interface. If you need to have a full aurweb instance with cgit, ssh interface, etc, follow the directions in INSTALL. -docker-compose --------------- - -1) Clone the aurweb project: - - $ git clone https://gitlab.archlinux.org/archlinux/aurweb.git - -2) Install the necessary packages: - - # pacman -S docker-compose - -2) Build the aurweb:latest image: - - $ cd /path/to/aurweb/ - $ docker-compose build - -3) Run local Docker development instance: - - $ cd /path/to/aurweb/ - $ docker-compose up -d nginx - -4) Browse to local aurweb development server. - - Python: https://localhost:8444/ - -5) [Optionally] populate the database with dummy data: - - $ docker-compose up mariadb - $ docker-compose exec mariadb /bin/sh - # pacman -S --noconfirm words fortune-mod - # poetry run schema/gendummydata.py dummy_data.sql - # mysql -uaur -paur aurweb < dummy_data.sql - -Inspect `dummy_data.sql` for test credentials. Passwords match usernames. - -Bare Metal ----------- - 1) Clone the aurweb project: $ git clone git://git.archlinux.org/aurweb.git + $ cd aurweb 2) Install the necessary packages: - # pacman -S python-poetry + # pacman -S --needed python-poetry mariadb words fortune-mod nginx -4) Install the package/dependencies via `poetry`: +3) Install the package/dependencies via `poetry`: + + $ poetry install + +4) Copy conf/config.dev to conf/config and replace YOUR_AUR_ROOT by the absolute + path to the root of your aurweb clone. sed can do both tasks for you: + + $ sed -e "s;YOUR_AUR_ROOT;$PWD;g" conf/config.dev > conf/config + + Note that when the upstream config.dev is updated, you should compare it to + your conf/config, or regenerate your configuration with the command above. + +5) Set up mariadb: + + # mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql + # systemctl start mariadb + # mariadb -u root + > CREATE USER 'aur'@'localhost' IDENTIFIED BY 'aur'; + > GRANT ALL ON *.* TO 'aur'@'localhost' WITH GRANT OPTION; + > CREATE DATABASE aurweb; + > exit + +6) Prepare a database and insert dummy data: + + $ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb + $ poetry run schema/gendummydata.py dummy_data.sql + $ mariadb -uaur -paur aurweb < dummy_data.sql + +7) Run the test server: + + ## set AUR_CONFIG to our locally created config + $ export AUR_CONFIG=conf/config + + ## with aurweb.spawn + $ poetry run python -m aurweb.spawn + + ## with systemd service + $ sudo install -m644 examples/aurweb.service /etc/systemd/system/ + # systemctl enable --now aurweb.service + + +Setup for running tests +----------------------- + +If you've set up a docker environment, you can run the full test-suite with: + # docker compose run test + +You can collect code-coverage data with: + $ ./util/fix-coverage data/.coverage + +See information further below on how to visualize the data. + +For running individual tests, we need to perform a couple of additional steps. +In case you did the bare-metal install, steps 2, 3, 4 and 5 should be skipped. + +1) Install the necessary packages: + + # pacman -S --needed python-poetry mariadb-libs asciidoc openssh + +2) Install the package/dependencies via `poetry`: - $ cd /path/to/aurweb/ $ poetry install 3) Copy conf/config.dev to conf/config and replace YOUR_AUR_ROOT by the absolute @@ -64,23 +135,51 @@ Bare Metal Note that when the upstream config.dev is updated, you should compare it to your conf/config, or regenerate your configuration with the command above. -4) Prepare a database: +4) Edit the config file conf/config and change the mysql/mariadb portion - $ cd /path/to/aurweb/ + We can make use of our mariadb docker container instead of having to install + mariadb. Change the config as follows: - $ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb + --------------------------------------------------------------------- + ; MySQL database information. User defaults to root for containerized + ; testing with mysqldb. This should be set to a non-root user. + user = root + password = aur + host = 127.0.0.1 + port = 13306 + ;socket = /var/run/mysqld/mysqld.sock + --------------------------------------------------------------------- - $ poetry run schema/gendummydata.py dummy_data.sql - $ mysql -uaur -paur aurweb < dummy_data.sql +5) Start our mariadb docker container -5) Run the test server: + # docker compose start mariadb - ## set AUR_CONFIG to our locally created config - $ export AUR_CONFIG=conf/config +6) Set environment variables - ## with aurweb.spawn - $ poetry run python -m aurweb.spawn + $ export AUR_CONFIG=conf/config + $ export LOG_CONFIG=logging.test.conf - ## with systemd service - $ sudo install -m644 examples/aurweb.service /etc/systemd/system/ - $ systemctl enable --now aurweb.service +7) Compile translation & doc files + + $ make -C po install + $ make -C doc + +Now we can run our python test-suite or individual tests with: + + $ poetry run pytest test/ + $ poetry run pytest test/test_whatever.py + +To run Sharness tests: + + $ poetry run make -C test sh + +The e-Mails that have been generated can be found at test-emails/ + +After test runs, code-coverage reports can be created with: + ## CLI report + $ coverage report + + ## HTML version stored at htmlcov/ + $ coverage html + +More information about tests can be found at test/README.md From 8c5b85db5c7888a1fca661f098bd12a3ec76756f Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 30 Apr 2023 21:10:42 +0200 Subject: [PATCH 1299/1451] housekeep: remove fix for poetry installer The problems with the "modern installer" got fixed. We don't need this workaround anymore. https://github.com/python-poetry/poetry/issues/7572 Signed-off-by: moson-mo --- poetry.toml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 poetry.toml diff --git a/poetry.toml b/poetry.toml deleted file mode 100644 index 8d4747a6..00000000 --- a/poetry.toml +++ /dev/null @@ -1,5 +0,0 @@ -# disable the modern installer until python-installer is fixed -# https://github.com/python-poetry/poetry/issues/7572 - -[installer] -modern-installation = false From a8d14e019457cef9e62cab2da0b4ed67a51ac46d Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Wed, 26 Apr 2023 23:39:10 +0100 Subject: [PATCH 1300/1451] housekeep: remove unused templates and rework existing ones Signed-off-by: Leonidas Spyropoulos --- .gitlab/issue_templates/Account Request.md | 14 ------ .gitlab/issue_templates/Bug.md | 26 +++++++--- .gitlab/issue_templates/Feature.md | 24 ++++++++- .gitlab/issue_templates/Feedback.md | 58 ---------------------- 4 files changed, 41 insertions(+), 81 deletions(-) delete mode 100644 .gitlab/issue_templates/Account Request.md delete mode 100644 .gitlab/issue_templates/Feedback.md diff --git a/.gitlab/issue_templates/Account Request.md b/.gitlab/issue_templates/Account Request.md deleted file mode 100644 index 6831d3ad..00000000 --- a/.gitlab/issue_templates/Account Request.md +++ /dev/null @@ -1,14 +0,0 @@ -## Checklist - -- [ ] I have set a Username in the Details section -- [ ] I have set an Email in the Details section -- [ ] I have set a valid Account Type in the Details section - -## Details - -- Instance: aur-dev.archlinux.org -- Username: the_username_you_want -- Email: valid@email.org -- Account Type: (User|Trusted User) - -/label account-request diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 3e7a04bf..9e20aadb 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -1,12 +1,24 @@ + +/label ~bug ~unconfirmed +/title [BUG] + + ### Checklist -This bug template is meant to provide bug issues for code existing in -the aurweb repository. This bug template is **not meant** to handle -bugs with user-uploaded packages. +**NOTE:** This bug template is meant to provide bug issues for code existing in +the aurweb repository. -To work out a bug you have found in a user-uploaded package, contact -the package's maintainer first. If you receive no response, file the -relevant package request against it so TUs can deal with cleanup. +**This bug template is not meant to handle bugs with user-uploaded packages.** +To report issues you might have found in a user-uploaded package, contact +the package's maintainer in comments. - [ ] I confirm that this is an issue with aurweb's code and not a user-uploaded package. @@ -29,7 +41,7 @@ this bug. ### Logs -If you have any logs relevent to the bug, include them here in +If you have any logs relevant to the bug, include them here in quoted or code blocks. ### Version(s) diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md index c907adcd..630c53c3 100644 --- a/.gitlab/issue_templates/Feature.md +++ b/.gitlab/issue_templates/Feature.md @@ -1,3 +1,25 @@ + +/label ~feature ~unconfirmed +/title [FEATURE] + + +### Checklist + +**NOTE:** This bug template is meant to provide bug issues for code existing in +the aurweb repository. + +**This bug template is not meant to handle bugs with user-uploaded packages.** +To report issues you might have found in a user-uploaded package, contact +the package's maintainer in comments. + - [ ] I have summed up the feature in concise words in the [Summary](#summary) section. - [ ] I have completely described the feature in the [Description](#description) section. - [ ] I have completed the [Blockers](#blockers) section. @@ -28,5 +50,3 @@ Example: - [Feature] Do not allow users to be Tyrants - \<(issue|merge_request)_link\> - -/label feature unconsidered diff --git a/.gitlab/issue_templates/Feedback.md b/.gitlab/issue_templates/Feedback.md deleted file mode 100644 index 950ec0c6..00000000 --- a/.gitlab/issue_templates/Feedback.md +++ /dev/null @@ -1,58 +0,0 @@ -**NOTE:** This issue template is only applicable to FastAPI implementations -in the code-base, which only exists within the `pu` branch. If you wish to -file an issue for the current PHP implementation of aurweb, please file a -standard issue prefixed with `[Bug]` or `[Feature]`. - - -**Checklist** - -- [ ] I have prefixed the issue title with `[Feedback]` along with a message - pointing to the route or feature tested. - - Example: `[Feedback] /packages/{name}` -- [ ] I have completed the [Changes](#changes) section. -- [ ] I have completed the [Bugs](#bugs) section. -- [ ] I have completed the [Improvements](#improvements) section. -- [ ] I have completed the [Summary](#summary) section. - -### Changes - -Please describe changes in user experience when compared to the PHP -implementation. This section can actually hold a lot of info if you -are up for it -- changes in routes, HTML rendering, back-end behavior, -etc. - -If you cannot see any changes from your standpoint, include a short -statement about that fact. - -### Bugs - -Please describe any bugs you've experienced while testing the route -pertaining to this issue. A "perfect" bug report would include your -specific experience, what you expected to occur, and what happened -otherwise. If you can, please include output of `docker-compose logs fastapi` -with your report; especially if any unintended exceptions occurred. - -### Improvements - -If you've experienced improvements in the route when compared to PHP, -please do include those here. We'd like to know if users are noticing -these improvements and how they feel about them. - -There are multiple routes with no improvements. For these, just include -a short sentence about the fact that you've experienced none. - -### Summary - -First: If you've gotten here and completed the [Changes](#changes), -[Bugs](#bugs), and [Improvements](#improvements) sections, we'd like -to thank you very much for your contribution and willingness to test. -We are not a company, and we are not a large team; any bit of assistance -here helps the project astronomically and moves us closer toward a -new release. - -That being said: please include an overall summary of your experience -and how you felt about the current implementation which you're testing -in comparison with PHP (current aur.archlinux.org, or https://localhost:8443 -through docker). - -/label feedback From af4239bcac0fd71ccc4168b70eed77ccf3d567c1 Mon Sep 17 00:00:00 2001 From: Christian Heusel Date: Wed, 22 Mar 2023 08:49:22 +0100 Subject: [PATCH 1301/1451] replace reference to AUR TU Guidelines with AUR Submission Guidelines Signed-off-by: Christian Heusel --- templates/home.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/home.html b/templates/home.html index e8296239..cb103f18 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,10 +1,10 @@

    AUR {% trans %}Home{% endtrans %}

    - {{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information." + {{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s for more information and the %sAUR Submission Guidelines%s if you want to contribute a PKGBUILD." | tr | format('', "", - '', "") + '', "") | safe }} {{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!" From b115aedf97bff1d1455d46298738bd8debd931bd Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sat, 6 May 2023 20:29:05 +0200 Subject: [PATCH 1302/1451] chore(deps): update several dependencies - Removing rfc3986 (1.5.0) - Updating coverage (7.2.4 -> 7.2.5) - Updating fastapi (0.94.1 -> 0.95.1) - Updating httpcore (0.16.3 -> 0.17.0) - Updating sqlalchemy (1.4.47 -> 1.4.48) - Updating httpx (0.23.3 -> 0.24.0) - Updating prometheus-fastapi-instrumentator (5.11.2 -> 6.0.0) - Updating protobuf (4.22.3 -> 4.22.4) - Updating pytest-asyncio (0.20.3 -> 0.21.0) - Updating requests (2.29.0 -> 2.30.0) - Updating uvicorn (0.21.1 -> 0.22.0) - Updating watchfiles (0.18.1 -> 0.19.0) - Updating werkzeug (2.3.2 -> 2.3.3) Signed-off-by: moson-mo --- poetry.lock | 336 ++++++++++++++++++++++++------------------------- pyproject.toml | 41 +++--- 2 files changed, 182 insertions(+), 195 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5e933e70..54a7701d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -352,63 +352,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.4" +version = "7.2.5" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e5eedde6e6e241ec3816f05767cc77e7456bf5ec6b373fb29917f0990e2078f"}, - {file = "coverage-7.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c6c6e3b8fb6411a2035da78d86516bfcfd450571d167304911814407697fb7a"}, - {file = "coverage-7.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7668a621afc52db29f6867e0e9c72a1eec9f02c94a7c36599119d557cf6e471"}, - {file = "coverage-7.2.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfb53bef4b2739ff747ebbd76d6ac5384371fd3c7a8af08899074eba034d483"}, - {file = "coverage-7.2.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c4f2e44a2ae15fa6883898e756552db5105ca4bd918634cbd5b7c00e19e8a1"}, - {file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:700bc9fb1074e0c67c09fe96a803de66663830420781df8dc9fb90d7421d4ccb"}, - {file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ac4861241e693e21b280f07844ae0e0707665e1dfcbf9466b793584984ae45c4"}, - {file = "coverage-7.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3d6f3c5b6738a494f17c73b4aa3aa899865cc33a74aa85e3b5695943b79ad3ce"}, - {file = "coverage-7.2.4-cp310-cp310-win32.whl", hash = "sha256:437da7d2fcc35bf45e04b7e9cfecb7c459ec6f6dc17a8558ed52e8d666c2d9ab"}, - {file = "coverage-7.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:1d3893f285fd76f56651f04d1efd3bdce251c32992a64c51e5d6ec3ba9e3f9c9"}, - {file = "coverage-7.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a17bf32e9e3333d78606ac1073dd20655dc0752d5b923fa76afd3bc91674ab4"}, - {file = "coverage-7.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f7ffdb3af2a01ce91577f84fc0faa056029fe457f3183007cffe7b11ea78b23c"}, - {file = "coverage-7.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89e63b38c7b888e00fd42ce458f838dccb66de06baea2da71801b0fc9070bfa0"}, - {file = "coverage-7.2.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4522dd9aeb9cc2c4c54ce23933beb37a4e106ec2ba94f69138c159024c8a906a"}, - {file = "coverage-7.2.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c7d88468f01a75231797173b52dc66d20a8d91b8bb75c88fc5861268578f52"}, - {file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bc47015fc0455753e8aba1f38b81b731aaf7f004a0c390b404e0fcf1d6c1d72f"}, - {file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c122d120c11a236558c339a59b4b60947b38ac9e3ad30a0e0e02540b37bf536"}, - {file = "coverage-7.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:50fda3d33b705b9c01e3b772cfa7d14de8aec2ec2870e4320992c26d057fde12"}, - {file = "coverage-7.2.4-cp311-cp311-win32.whl", hash = "sha256:ab08af91cf4d847a6e15d7d5eeae5fead1487caf16ff3a2056dbe64d058fd246"}, - {file = "coverage-7.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:876e4ef3eff00b50787867c5bae84857a9af4c369a9d5b266cd9b19f61e48ef7"}, - {file = "coverage-7.2.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3fc9cde48de956bfbacea026936fbd4974ff1dc2f83397c6f1968f0142c9d50b"}, - {file = "coverage-7.2.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12bc9127c8aca2f7c25c9acca53da3db6799b2999b40f28c2546237b7ea28459"}, - {file = "coverage-7.2.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2857894c22833d3da6e113623a9b7440159b2295280b4e0d954cadbfa724b85a"}, - {file = "coverage-7.2.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4db4e6c115d869cd5397d3d21fd99e4c7053205c33a4ae725c90d19dcd178af"}, - {file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f37ae1804596f13d811e0247ffc8219f5261b3565bdf45fcbb4fc091b8e9ff35"}, - {file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cdee9a77fd0ce000781680b6a1f4b721c567f66f2f73a49be1843ff439d634f3"}, - {file = "coverage-7.2.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b65a6a5484b7f2970393d6250553c05b2ede069e0e18abe907fdc7f3528252e"}, - {file = "coverage-7.2.4-cp37-cp37m-win32.whl", hash = "sha256:1a3e8697cb40f28e5bcfb6f4bda7852d96dbb6f6fd7cc306aba4ae690c9905ab"}, - {file = "coverage-7.2.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4078939c4b7053e14e87c65aa68dbed7867e326e450f94038bfe1a1b22078ff9"}, - {file = "coverage-7.2.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:603a2b172126e3b08c11ca34200143089a088cd0297d4cfc4922d2c1c3a892f9"}, - {file = "coverage-7.2.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72751d117ceaad3b1ea3bcb9e85f5409bbe9fb8a40086e17333b994dbccc0718"}, - {file = "coverage-7.2.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f19ba9301e6fb0b94ba71fda9a1b02d11f0aab7f8e2455122a4e2921b6703c2f"}, - {file = "coverage-7.2.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d784177a7fb9d0f58d24d3e60638c8b729c3693963bf67fa919120f750db237"}, - {file = "coverage-7.2.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d2a9180beff1922b09bd7389e23454928e108449e646c26da5c62e29b0bf4e3"}, - {file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:39747afc854a7ee14e5e132da7db179d6281faf97dc51e6d7806651811c47538"}, - {file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60feb703abc8d78e9427d873bcf924c9e30cf540a21971ef5a17154da763b60f"}, - {file = "coverage-7.2.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2becddfcbf3d994a8f4f9dd2b6015cae3a3eff50dedc6e4a17c3cccbe8f93d4"}, - {file = "coverage-7.2.4-cp38-cp38-win32.whl", hash = "sha256:56a674ad18d6b04008283ca03c012be913bf89d91c0803c54c24600b300d9e51"}, - {file = "coverage-7.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:ab08e03add2cf5793e66ac1bbbb24acfa90c125476f5724f5d44c56eeec1d635"}, - {file = "coverage-7.2.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92b565c51732ea2e7e541709ccce76391b39f4254260e5922e08e00971e88e33"}, - {file = "coverage-7.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8769a67e8816c7e94d5bf446fc0501641fde78fdff362feb28c2c64d45d0e9b1"}, - {file = "coverage-7.2.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d74d6fbd5a98a5629e8467b719b0abea9ca01a6b13555d125c84f8bf4ea23d"}, - {file = "coverage-7.2.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9f770c6052d9b5c9b0e824fd8c003fe33276473b65b4f10ece9565ceb62438e"}, - {file = "coverage-7.2.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3023ce23e41a6f006c09f7e6d62b6c069c36bdc9f7de16a5ef823acc02e6c63"}, - {file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fabd1f4d12dfd6b4f309208c2f31b116dc5900e0b42dbafe4ee1bc7c998ffbb0"}, - {file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e41a7f44e73b37c6f0132ecfdc1c8b67722f42a3d9b979e6ebc150c8e80cf13a"}, - {file = "coverage-7.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:864e36947289be05abd83267c4bade35e772526d3e9653444a9dc891faf0d698"}, - {file = "coverage-7.2.4-cp39-cp39-win32.whl", hash = "sha256:ea534200efbf600e60130c48552f99f351cae2906898a9cd924c1c7f2fb02853"}, - {file = "coverage-7.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:00f8fd8a5fe1ffc3aef78ea2dbf553e5c0f4664324e878995e38d41f037eb2b3"}, - {file = "coverage-7.2.4-pp37.pp38.pp39-none-any.whl", hash = "sha256:856bcb837e96adede31018a0854ce7711a5d6174db1a84e629134970676c54fa"}, - {file = "coverage-7.2.4.tar.gz", hash = "sha256:7283f78d07a201ac7d9dc2ac2e4faaea99c4d302f243ee5b4e359f3e170dc008"}, + {file = "coverage-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c"}, + {file = "coverage-7.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a"}, + {file = "coverage-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f"}, + {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a"}, + {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a"}, + {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11"}, + {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5"}, + {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c"}, + {file = "coverage-7.2.5-cp310-cp310-win32.whl", hash = "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5"}, + {file = "coverage-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c"}, + {file = "coverage-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce"}, + {file = "coverage-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88"}, + {file = "coverage-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e"}, + {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2"}, + {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139"}, + {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8"}, + {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed"}, + {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6"}, + {file = "coverage-7.2.5-cp311-cp311-win32.whl", hash = "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b"}, + {file = "coverage-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068"}, + {file = "coverage-7.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1"}, + {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33"}, + {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade"}, + {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5"}, + {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47"}, + {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd"}, + {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969"}, + {file = "coverage-7.2.5-cp37-cp37m-win32.whl", hash = "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718"}, + {file = "coverage-7.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0"}, + {file = "coverage-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84"}, + {file = "coverage-7.2.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790"}, + {file = "coverage-7.2.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771"}, + {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045"}, + {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614"}, + {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3"}, + {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd"}, + {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1"}, + {file = "coverage-7.2.5-cp38-cp38-win32.whl", hash = "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813"}, + {file = "coverage-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212"}, + {file = "coverage-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b"}, + {file = "coverage-7.2.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200"}, + {file = "coverage-7.2.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5"}, + {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e"}, + {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303"}, + {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3"}, + {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a"}, + {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1"}, + {file = "coverage-7.2.5-cp39-cp39-win32.whl", hash = "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31"}, + {file = "coverage-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252"}, + {file = "coverage-7.2.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3"}, + {file = "coverage-7.2.5.tar.gz", hash = "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"}, ] [package.dependencies] @@ -548,14 +548,14 @@ lua = ["lupa (>=1.14,<2.0)"] [[package]] name = "fastapi" -version = "0.94.1" +version = "0.95.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.94.1-py3-none-any.whl", hash = "sha256:451387550c2d25a972193f22e408a82e75a8e7867c834a03076704fe20df3256"}, - {file = "fastapi-0.94.1.tar.gz", hash = "sha256:4a75936dbf9eb74be5eb0d41a793adefe9f3fc6ba66dbdabd160120fd3c2d9cd"}, + {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"}, + {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"}, ] [package.dependencies] @@ -736,14 +736,14 @@ files = [ [[package]] name = "httpcore" -version = "0.16.3" +version = "0.17.0" description = "A minimal low-level HTTP client." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-0.17.0-py3-none-any.whl", hash = "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599"}, + {file = "httpcore-0.17.0.tar.gz", hash = "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c"}, ] [package.dependencies] @@ -758,25 +758,25 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" -version = "0.23.3" +version = "0.24.0" description = "The next generation HTTP client." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"}, + {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"}, ] [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +httpcore = ">=0.15.0,<0.18.0" +idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] @@ -1255,14 +1255,14 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "5.11.2" +version = "6.0.0" description = "Instrument your FastAPI with Prometheus metrics." category = "main" optional = false python-versions = ">=3.7.0,<4.0.0" files = [ - {file = "prometheus_fastapi_instrumentator-5.11.2-py3-none-any.whl", hash = "sha256:c84ae3dc98bebb44f29d0af0c17c9f0782c2fb964ef83353664d9858a632cf81"}, - {file = "prometheus_fastapi_instrumentator-5.11.2.tar.gz", hash = "sha256:9d8d0df8de7d6a73ae387121629dbf32fe022cdfc63e8bacd51f4b8ff21059ea"}, + {file = "prometheus_fastapi_instrumentator-6.0.0-py3-none-any.whl", hash = "sha256:6f66a951a4801667f7311d161f3aebfe0cd202391d0f067fbbe169792e2d987b"}, + {file = "prometheus_fastapi_instrumentator-6.0.0.tar.gz", hash = "sha256:f1ddd0b8ead75e71d055bdf4cb7e995ec6a6ca63543245e7bbc5ca9b14c45191"}, ] [package.dependencies] @@ -1271,25 +1271,25 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "4.22.3" +version = "4.22.4" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.22.3-cp310-abi3-win32.whl", hash = "sha256:8b54f56d13ae4a3ec140076c9d937221f887c8f64954673d46f63751209e839a"}, - {file = "protobuf-4.22.3-cp310-abi3-win_amd64.whl", hash = "sha256:7760730063329d42a9d4c4573b804289b738d4931e363ffbe684716b796bde51"}, - {file = "protobuf-4.22.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:d14fc1a41d1a1909998e8aff7e80d2a7ae14772c4a70e4bf7db8a36690b54425"}, - {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:70659847ee57a5262a65954538088a1d72dfc3e9882695cab9f0c54ffe71663b"}, - {file = "protobuf-4.22.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:13233ee2b9d3bd9a5f216c1fa2c321cd564b93d8f2e4f521a85b585447747997"}, - {file = "protobuf-4.22.3-cp37-cp37m-win32.whl", hash = "sha256:ecae944c6c2ce50dda6bf76ef5496196aeb1b85acb95df5843cd812615ec4b61"}, - {file = "protobuf-4.22.3-cp37-cp37m-win_amd64.whl", hash = "sha256:d4b66266965598ff4c291416be429cef7989d8fae88b55b62095a2331511b3fa"}, - {file = "protobuf-4.22.3-cp38-cp38-win32.whl", hash = "sha256:f08aa300b67f1c012100d8eb62d47129e53d1150f4469fd78a29fa3cb68c66f2"}, - {file = "protobuf-4.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:f2f4710543abec186aee332d6852ef5ae7ce2e9e807a3da570f36de5a732d88e"}, - {file = "protobuf-4.22.3-cp39-cp39-win32.whl", hash = "sha256:7cf56e31907c532e460bb62010a513408e6cdf5b03fb2611e4b67ed398ad046d"}, - {file = "protobuf-4.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:e0e630d8e6a79f48c557cd1835865b593d0547dce221c66ed1b827de59c66c97"}, - {file = "protobuf-4.22.3-py3-none-any.whl", hash = "sha256:52f0a78141078077cfe15fe333ac3e3a077420b9a3f5d1bf9b5fe9d286b4d881"}, - {file = "protobuf-4.22.3.tar.gz", hash = "sha256:23452f2fdea754a8251d0fc88c0317735ae47217e0d27bf330a30eec2848811a"}, + {file = "protobuf-4.22.4-cp310-abi3-win32.whl", hash = "sha256:a4e661247896c2ffea4b894bca2d8657e752bedb8f3c66d7befa2557291be1e8"}, + {file = "protobuf-4.22.4-cp310-abi3-win_amd64.whl", hash = "sha256:7b42086d6027be2730151b49f27b2f5be40f3b036adf7b8da5917f4567f268c3"}, + {file = "protobuf-4.22.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4bfb28d48628deacdb66a95aaa7b6640f3dc82b4edd34db444c7a3cdd90b01fb"}, + {file = "protobuf-4.22.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e98e26328d7c668541d1052b02de4205b1094ef6b2ce57167440d3e39876db48"}, + {file = "protobuf-4.22.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd329e5dd7b6c4b878cab4b85bb6cec880e2adaf4e8aa2c75944dcbb05e1ff1"}, + {file = "protobuf-4.22.4-cp37-cp37m-win32.whl", hash = "sha256:b7728b5da9eee15c0aa3baaee79e94fa877ddcf7e3d2f34b1eab586cd26eea89"}, + {file = "protobuf-4.22.4-cp37-cp37m-win_amd64.whl", hash = "sha256:f4a711588c3a79b6f9c44af4d7f4a2ae868e27063654683932ab6462f90e9656"}, + {file = "protobuf-4.22.4-cp38-cp38-win32.whl", hash = "sha256:11b28b4e779d7f275e3ea0efa3938f4d4e8ed3ca818f9fec3b193f8e9ada99fd"}, + {file = "protobuf-4.22.4-cp38-cp38-win_amd64.whl", hash = "sha256:144d5b46df5e44f914f715accaadf88d617242ba5a40cacef4e8de7effa79954"}, + {file = "protobuf-4.22.4-cp39-cp39-win32.whl", hash = "sha256:5128b4d5efcaef92189e076077ae389700606ff81d2126b8361dc01f3e026197"}, + {file = "protobuf-4.22.4-cp39-cp39-win_amd64.whl", hash = "sha256:9537ae27d43318acf8ce27d0359fe28e6ebe4179c3350bc055bb60ff4dc4fcd3"}, + {file = "protobuf-4.22.4-py3-none-any.whl", hash = "sha256:3b21074b7fb748d8e123acaef9fa63a84fdc1436dc71199d2317b139f77dd6f4"}, + {file = "protobuf-4.22.4.tar.gz", hash = "sha256:21fbaef7f012232eb8d6cb8ba334e931fc6ff8570f5aaedc77d5b22a439aa909"}, ] [[package]] @@ -1437,18 +1437,18 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.20.3" +version = "0.21.0" description = "Pytest support for asyncio" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, + {file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"}, + {file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"}, ] [package.dependencies] -pytest = ">=6.1.0" +pytest = ">=7.0.0" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -1561,44 +1561,26 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "requests" -version = "2.29.0" +version = "2.30.0" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, - {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, + {file = "requests-2.30.0-py3-none-any.whl", hash = "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294"}, + {file = "requests-2.30.0.tar.gz", hash = "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - [[package]] name = "setuptools" version = "67.7.2" @@ -1654,53 +1636,53 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.4.47" +version = "1.4.48" description = "Database Abstraction Library" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.4.47-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:dcfb480bfc9e1fab726003ae00a6bfc67a29bad275b63a4e36d17fe7f13a624e"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:28fda5a69d6182589892422c5a9b02a8fd1125787aab1d83f1392aa955bf8d0a"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27m-win32.whl", hash = "sha256:45e799c1a41822eba6bee4e59b0e38764e1a1ee69873ab2889079865e9ea0e23"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27m-win_amd64.whl", hash = "sha256:10edbb92a9ef611f01b086e271a9f6c1c3e5157c3b0c5ff62310fb2187acbd4a"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7a4df53472c9030a8ddb1cce517757ba38a7a25699bbcabd57dcc8a5d53f324e"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:511d4abc823152dec49461209607bbfb2df60033c8c88a3f7c93293b8ecbb13d"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbe57f39f531c5d68d5594ea4613daa60aba33bb51a8cc42f96f17bbd6305e8d"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca8ab6748e3ec66afccd8b23ec2f92787a58d5353ce9624dccd770427ee67c82"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299b5c5c060b9fbe51808d0d40d8475f7b3873317640b9b7617c7f988cf59fda"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-win32.whl", hash = "sha256:684e5c773222781775c7f77231f412633d8af22493bf35b7fa1029fdf8066d10"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-win_amd64.whl", hash = "sha256:2bba39b12b879c7b35cde18b6e14119c5f1a16bd064a48dd2ac62d21366a5e17"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:795b5b9db573d3ed61fae74285d57d396829e3157642794d3a8f72ec2a5c719b"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:989c62b96596b7938cbc032e39431e6c2d81b635034571d6a43a13920852fb65"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b67bda733da1dcdccaf354e71ef01b46db483a4f6236450d3f9a61efdba35a"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-win32.whl", hash = "sha256:9a198f690ac12a3a807e03a5a45df6a30cd215935f237a46f4248faed62e69c8"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-win_amd64.whl", hash = "sha256:03be6f3cb66e69fb3a09b5ea89d77e4bc942f3bf84b207dba84666a26799c166"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:16ee6fea316790980779268da47a9260d5dd665c96f225d28e7750b0bb2e2a04"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:557675e0befafa08d36d7a9284e8761c97490a248474d778373fb96b0d7fd8de"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb2797fee8a7914fb2c3dc7de404d3f96eb77f20fc60e9ee38dc6b0ca720f2c2"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28297aa29e035f29cba6b16aacd3680fbc6a9db682258d5f2e7b49ec215dbe40"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-win32.whl", hash = "sha256:998e782c8d9fd57fa8704d149ccd52acf03db30d7dd76f467fd21c1c21b414fa"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-win_amd64.whl", hash = "sha256:dde4d02213f1deb49eaaf8be8a6425948963a7af84983b3f22772c63826944de"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e98ef1babe34f37f443b7211cd3ee004d9577a19766e2dbacf62fce73c76245a"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14a3879853208a242b5913f3a17c6ac0eae9dc210ff99c8f10b19d4a1ed8ed9b"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7120a2f72599d4fed7c001fa1cbbc5b4d14929436135768050e284f53e9fbe5e"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:048509d7f3ac27b83ad82fd96a1ab90a34c8e906e4e09c8d677fc531d12c23c5"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-win32.whl", hash = "sha256:6572d7c96c2e3e126d0bb27bfb1d7e2a195b68d951fcc64c146b94f088e5421a"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-win_amd64.whl", hash = "sha256:a6c3929df5eeaf3867724003d5c19fed3f0c290f3edc7911616616684f200ecf"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:71d4bf7768169c4502f6c2b0709a02a33703544f611810fb0c75406a9c576ee1"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd45c60cc4f6d68c30d5179e2c2c8098f7112983532897566bb69c47d87127d3"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fdbb8e9d4e9003f332a93d6a37bca48ba8095086c97a89826a136d8eddfc455"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f216a51451a0a0466e082e163591f6dcb2f9ec182adb3f1f4b1fd3688c7582c"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-win32.whl", hash = "sha256:bd988b3362d7e586ef581eb14771bbb48793a4edb6fcf62da75d3f0f3447060b"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-win_amd64.whl", hash = "sha256:32ab09f2863e3de51529aa84ff0e4fe89a2cb1bfbc11e225b6dbc60814e44c94"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:07764b240645627bc3e82596435bd1a1884646bfc0721642d24c26b12f1df194"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e2a42017984099ef6f56438a6b898ce0538f6fadddaa902870c5aa3e1d82583"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6b6d807c76c20b4bc143a49ad47782228a2ac98bdcdcb069da54280e138847fc"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a94632ba26a666e7be0a7d7cc3f7acab622a04259a3aa0ee50ff6d44ba9df0d"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-win32.whl", hash = "sha256:f80915681ea9001f19b65aee715115f2ad310730c8043127cf3e19b3009892dd"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-win_amd64.whl", hash = "sha256:fc700b862e0a859a37faf85367e205e7acaecae5a098794aff52fdd8aea77b12"}, - {file = "SQLAlchemy-1.4.47.tar.gz", hash = "sha256:95fc02f7fc1f3199aaa47a8a757437134cf618e9d994c84effd53f530c38586f"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4bac3aa3c3d8bc7408097e6fe8bf983caa6e9491c5d2e2488cfcfd8106f13b6a"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dbcae0e528d755f4522cad5842f0942e54b578d79f21a692c44d91352ea6d64e"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27m-win32.whl", hash = "sha256:cbbe8b8bffb199b225d2fe3804421b7b43a0d49983f81dc654d0431d2f855543"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27m-win_amd64.whl", hash = "sha256:627e04a5d54bd50628fc8734d5fc6df2a1aa5962f219c44aad50b00a6cdcf965"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9af1db7a287ef86e0f5cd990b38da6bd9328de739d17e8864f1817710da2d217"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ce7915eecc9c14a93b73f4e1c9d779ca43e955b43ddf1e21df154184f39748e5"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5381ddd09a99638f429f4cbe1b71b025bed318f6a7b23e11d65f3eed5e181c33"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:87609f6d4e81a941a17e61a4c19fee57f795e96f834c4f0a30cee725fc3f81d9"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0808ad34167f394fea21bd4587fc62f3bd81bba232a1e7fbdfa17e6cfa7cd7"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-win32.whl", hash = "sha256:d53cd8bc582da5c1c8c86b6acc4ef42e20985c57d0ebc906445989df566c5603"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-win_amd64.whl", hash = "sha256:4355e5915844afdc5cf22ec29fba1010166e35dd94a21305f49020022167556b"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:066c2b0413e8cb980e6d46bf9d35ca83be81c20af688fedaef01450b06e4aa5e"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c99bf13e07140601d111a7c6f1fc1519914dd4e5228315bbda255e08412f61a4"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee26276f12614d47cc07bc85490a70f559cba965fb178b1c45d46ffa8d73fda"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-win32.whl", hash = "sha256:49c312bcff4728bffc6fb5e5318b8020ed5c8b958a06800f91859fe9633ca20e"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-win_amd64.whl", hash = "sha256:cef2e2abc06eab187a533ec3e1067a71d7bbec69e582401afdf6d8cad4ba3515"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3509159e050bd6d24189ec7af373359f07aed690db91909c131e5068176c5a5d"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc2ab4d9f6d9218a5caa4121bdcf1125303482a1cdcfcdbd8567be8518969c0"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1ddbbcef9bcedaa370c03771ebec7e39e3944782bef49e69430383c376a250b"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f82d8efea1ca92b24f51d3aea1a82897ed2409868a0af04247c8c1e4fef5890"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-win32.whl", hash = "sha256:e3e98d4907805b07743b583a99ecc58bf8807ecb6985576d82d5e8ae103b5272"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-win_amd64.whl", hash = "sha256:25887b4f716e085a1c5162f130b852f84e18d2633942c8ca40dfb8519367c14f"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0817c181271b0ce5df1aa20949f0a9e2426830fed5ecdcc8db449618f12c2730"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe1dd2562313dd9fe1778ed56739ad5d9aae10f9f43d9f4cf81d65b0c85168bb"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:68413aead943883b341b2b77acd7a7fe2377c34d82e64d1840860247cec7ff7c"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbde5642104ac6e95f96e8ad6d18d9382aa20672008cf26068fe36f3004491df"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-win32.whl", hash = "sha256:11c6b1de720f816c22d6ad3bbfa2f026f89c7b78a5c4ffafb220e0183956a92a"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-win_amd64.whl", hash = "sha256:eb5464ee8d4bb6549d368b578e9529d3c43265007193597ddca71c1bae6174e6"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:92e6133cf337c42bfee03ca08c62ba0f2d9695618c8abc14a564f47503157be9"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d29a3fc6d9c45962476b470a81983dd8add6ad26fdbfae6d463b509d5adcda"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:005e942b451cad5285015481ae4e557ff4154dde327840ba91b9ac379be3b6ce"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8cfe951ed074ba5e708ed29c45397a95c4143255b0d022c7c8331a75ae61f3"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-win32.whl", hash = "sha256:2b9af65cc58726129d8414fc1a1a650dcdd594ba12e9c97909f1f57d48e393d3"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-win_amd64.whl", hash = "sha256:2b562e9d1e59be7833edf28b0968f156683d57cabd2137d8121806f38a9d58f4"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a1fc046756cf2a37d7277c93278566ddf8be135c6a58397b4c940abf837011f4"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d9b55252d2ca42a09bcd10a697fa041e696def9dfab0b78c0aaea1485551a08"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6dab89874e72a9ab5462997846d4c760cdb957958be27b03b49cf0de5e5c327c"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd8b5ee5a3acc4371f820934b36f8109ce604ee73cc668c724abb054cebcb6e"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-win32.whl", hash = "sha256:eee09350fd538e29cfe3a496ec6f148504d2da40dbf52adefb0d2f8e4d38ccc4"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-win_amd64.whl", hash = "sha256:7ad2b0f6520ed5038e795cc2852eb5c1f20fa6831d73301ced4aafbe3a10e1f6"}, + {file = "SQLAlchemy-1.4.48.tar.gz", hash = "sha256:b47bc287096d989a0838ce96f7d8e966914a24da877ed41a7531d44b55cdb8df"}, ] [package.dependencies] @@ -1843,14 +1825,14 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.21.1" +version = "0.22.0" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, - {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, + {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"}, + {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"}, ] [package.dependencies] @@ -1862,30 +1844,34 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "watchfiles" -version = "0.18.1" +version = "0.19.0" description = "Simple, modern and high performance file watching and code reload in python." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "watchfiles-0.18.1-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:9891d3c94272108bcecf5597a592e61105279def1313521e637f2d5acbe08bc9"}, - {file = "watchfiles-0.18.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7102342d60207fa635e24c02a51c6628bf0472e5fef067f78a612386840407fc"}, - {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:00ea0081eca5e8e695cffbc3a726bb90da77f4e3f78ce29b86f0d95db4e70ef7"}, - {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8e6db99e49cd7125d8a4c9d33c0735eea7b75a942c6ad68b75be3e91c242fb"}, - {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc7c726855f04f22ac79131b51bf0c9f728cb2117419ed830a43828b2c4a5fcb"}, - {file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbaff354d12235002e62d9d3fa8bcf326a8490c1179aa5c17195a300a9e5952f"}, - {file = "watchfiles-0.18.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:888db233e06907c555eccd10da99b9cd5ed45deca47e41766954292dc9f7b198"}, - {file = "watchfiles-0.18.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:dde79930d1b28f15994ad6613aa2865fc7a403d2bb14585a8714a53233b15717"}, - {file = "watchfiles-0.18.1-cp37-abi3-win32.whl", hash = "sha256:e2b2bdd26bf8d6ed90763e6020b475f7634f919dbd1730ea1b6f8cb88e21de5d"}, - {file = "watchfiles-0.18.1-cp37-abi3-win_amd64.whl", hash = "sha256:c541e0f2c3e95e83e4f84561c893284ba984e9d0025352057396d96dceb09f44"}, - {file = "watchfiles-0.18.1-cp37-abi3-win_arm64.whl", hash = "sha256:9a26272ef3e930330fc0c2c148cc29706cc2c40d25760c7ccea8d768a8feef8b"}, - {file = "watchfiles-0.18.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:9fb12a5e2b42e0b53769455ff93546e6bc9ab14007fbd436978d827a95ca5bd1"}, - {file = "watchfiles-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:548d6b42303d40264118178053c78820533b683b20dfbb254a8706ca48467357"}, - {file = "watchfiles-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0d8fdfebc50ac7569358f5c75f2b98bb473befccf9498cf23b3e39993bb45a"}, - {file = "watchfiles-0.18.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0f9a22fff1745e2bb930b1e971c4c5b67ea3b38ae17a6adb9019371f80961219"}, - {file = "watchfiles-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b02e7fa03cd4059dd61ff0600080a5a9e7a893a85cb8e5178943533656eec65e"}, - {file = "watchfiles-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a868ce2c7565137f852bd4c863a164dc81306cae7378dbdbe4e2aca51ddb8857"}, - {file = "watchfiles-0.18.1.tar.gz", hash = "sha256:4ec0134a5e31797eb3c6c624dbe9354f2a8ee9c720e0b46fc5b7bab472b7c6d4"}, + {file = "watchfiles-0.19.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:91633e64712df3051ca454ca7d1b976baf842d7a3640b87622b323c55f3345e7"}, + {file = "watchfiles-0.19.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b6577b8c6c8701ba8642ea9335a129836347894b666dd1ec2226830e263909d3"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:18b28f6ad871b82df9542ff958d0c86bb0d8310bb09eb8e87d97318a3b5273af"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac19dc9cbc34052394dbe81e149411a62e71999c0a19e1e09ce537867f95ae0"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ea3397aecbc81c19ed7f025e051a7387feefdb789cf768ff994c1228182fda"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0376deac92377817e4fb8f347bf559b7d44ff556d9bc6f6208dd3f79f104aaf"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c75eff897786ee262c9f17a48886f4e98e6cfd335e011c591c305e5d083c056"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb5d45c4143c1dd60f98a16187fd123eda7248f84ef22244818c18d531a249d1"}, + {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:79c533ff593db861ae23436541f481ec896ee3da4e5db8962429b441bbaae16e"}, + {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3d7d267d27aceeeaa3de0dd161a0d64f0a282264d592e335fff7958cc0cbae7c"}, + {file = "watchfiles-0.19.0-cp37-abi3-win32.whl", hash = "sha256:176a9a7641ec2c97b24455135d58012a5be5c6217fc4d5fef0b2b9f75dbf5154"}, + {file = "watchfiles-0.19.0-cp37-abi3-win_amd64.whl", hash = "sha256:945be0baa3e2440151eb3718fd8846751e8b51d8de7b884c90b17d271d34cae8"}, + {file = "watchfiles-0.19.0-cp37-abi3-win_arm64.whl", hash = "sha256:0089c6dc24d436b373c3c57657bf4f9a453b13767150d17284fc6162b2791911"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cae3dde0b4b2078f31527acff6f486e23abed307ba4d3932466ba7cdd5ecec79"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f3920b1285a7d3ce898e303d84791b7bf40d57b7695ad549dc04e6a44c9f120"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9afd0d69429172c796164fd7fe8e821ade9be983f51c659a38da3faaaaac44dc"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68dce92b29575dda0f8d30c11742a8e2b9b8ec768ae414b54f7453f27bdf9545"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5569fc7f967429d4bc87e355cdfdcee6aabe4b620801e2cf5805ea245c06097c"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5471582658ea56fca122c0f0d0116a36807c63fefd6fdc92c71ca9a4491b6b48"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b538014a87f94d92f98f34d3e6d2635478e6be6423a9ea53e4dd96210065e193"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b44221764955b1e703f012c74015306fb7e79a00c15370785f309b1ed9aa8d"}, + {file = "watchfiles-0.19.0.tar.gz", hash = "sha256:d9b073073e048081e502b6c6b0b88714c026a1a4c890569238d04aca5f9ca74b"}, ] [package.dependencies] @@ -1905,14 +1891,14 @@ files = [ [[package]] name = "werkzeug" -version = "2.3.2" +version = "2.3.3" description = "The comprehensive WSGI web application library." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.2-py3-none-any.whl", hash = "sha256:b7b8bc1609f35ae8e45d48a9b58d7a4eb1e41eec148d37e977e5df6ebf3398b2"}, - {file = "Werkzeug-2.3.2.tar.gz", hash = "sha256:2f3278e9ef61511cdf82cc28fc5da0f5b501dd8f01ecf5ef6a5d810048f68702"}, + {file = "Werkzeug-2.3.3-py3-none-any.whl", hash = "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a"}, + {file = "Werkzeug-2.3.3.tar.gz", hash = "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"}, ] [package.dependencies] @@ -1955,4 +1941,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "7eba1b2d0fd17dacea153cb3f6ebdcb11d7888902cbb3a88a65f3c6c38f65699" +content-hash = "9974b6c53a244765e15677b5266c0922601f51ea16068cb621184c89ce4b7f22" diff --git a/pyproject.toml b/pyproject.toml index d75a1d4c..ea94f2cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,48 +62,49 @@ asgiref = "^3.6.0" bcrypt = "^4.0.1" bleach = "^6.0.0" email-validator = "^1.3.1" -fakeredis = "^2.10.0" +fakeredis = "^2.11.2" feedgen = "^0.9.0" -httpx = "^0.23.3" +httpx = "^0.24.0" itsdangerous = "^2.1.2" lxml = "^4.9.2" -orjson = "^3.8.7" -protobuf = "^4.22.1" -pygit2 = "^1.11.1" +orjson = "^3.8.11" +protobuf = "^4.22.4" +pygit2 = "^1.12.0" python-multipart = "^0.0.6" -redis = "^4.5.1" -requests = "^2.28.2" +redis = "^4.5.4" +requests = "^2.30.0" paginate = "^0.5.6" +urllib3 = "^1.26.15" # SQL -alembic = "^1.10.2" +alembic = "^1.10.4" mysqlclient = "^2.1.1" Authlib = "^1.2.0" Jinja2 = "^3.1.2" -Markdown = "^3.4.1" -Werkzeug = "^2.2.3" -SQLAlchemy = "^1.4.46" +Markdown = "^3.4.3" +Werkzeug = "^2.3.3" +SQLAlchemy = "^1.4.48" # ASGI -uvicorn = "^0.21.0" +uvicorn = "^0.22.0" gunicorn = "^20.1.0" Hypercorn = "^0.14.3" -prometheus-fastapi-instrumentator = "^5.11.1" +prometheus-fastapi-instrumentator = "^6.0.0" pytest-xdist = "^3.2.1" -filelock = "^3.9.1" +filelock = "^3.12.0" posix-ipc = "^1.1.1" pyalpm = "^0.10.6" -fastapi = "^0.94.1" +fastapi = "^0.95.1" srcinfo = "^0.1.2" -tomlkit = "^0.11.6" +tomlkit = "^0.11.8" [tool.poetry.dev-dependencies] -coverage = "^7.2.1" -pytest = "^7.2.2" -pytest-asyncio = "^0.20.3" +coverage = "^7.2.5" +pytest = "^7.3.1" +pytest-asyncio = "^0.21.0" pytest-cov = "^4.0.0" pytest-tap = "^3.3" -watchfiles = "^0.18.1" +watchfiles = "^0.19.0" [tool.poetry.scripts] aurweb-git-auth = "aurweb.git.auth:main" From 1d627edbe72de5267cbd8b4f239bb1c61cbaba4c Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 6 May 2023 20:34:54 +0100 Subject: [PATCH 1303/1451] chore(release): prepare for 6.2.3 Signed-off-by: Leonidas Spyropoulos --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea94f2cd..83d10b5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.2.2" +version = "v6.2.3" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From d2e8fa02491f1b7824daf37ca3a1646fd06c17e7 Mon Sep 17 00:00:00 2001 From: "Daniel M. Capella" Date: Sat, 6 May 2023 16:46:07 -0400 Subject: [PATCH 1304/1451] chore(deps): "Group all minor and patch updates together" Treat FastAPI separately due to regular breakage. Co-authored-by: moson-mo --- renovate.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 39a2b6e9..b6721721 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,13 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:base", + "group:allNonMajor" + ], + "packageRules": [ + { + "groupName": "fastapi", + "matchPackageNames": ["fastapi"] + } ] } From 3253a6ad29dc3ed83d0fde90d8f93c8b0c1a6730 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 7 May 2023 09:58:17 +0200 Subject: [PATCH 1305/1451] fix(deps): remove urllib3 from dependency list Previously pinned urllib3 to v1.x. This is not needed though. The incompatibility of v2.x is with poetry itself, but not aurweb. Signed-off-by: moson-mo --- poetry.lock | 33 +++++++++++++++++---------------- pyproject.toml | 3 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index 54a7701d..388379d9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -151,14 +151,14 @@ css = ["tinycss2 (>=1.1.0,<1.2)"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2023.5.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, ] [[package]] @@ -482,18 +482,18 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "email-validator" -version = "1.3.1" +version = "2.0.0.post2" description = "A robust email address syntax and deliverability validation library." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" files = [ - {file = "email_validator-1.3.1-py2.py3-none-any.whl", hash = "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda"}, - {file = "email_validator-1.3.1.tar.gz", hash = "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"}, + {file = "email_validator-2.0.0.post2-py3-none-any.whl", hash = "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c"}, + {file = "email_validator-2.0.0.post2.tar.gz", hash = "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900"}, ] [package.dependencies] -dnspython = ">=1.15.0" +dnspython = ">=2.0.0" idna = ">=2.0.0" [[package]] @@ -1808,20 +1808,21 @@ files = [ [[package]] name = "urllib3" -version = "1.26.15" +version = "2.0.2" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7" files = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, + {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, + {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" @@ -1941,4 +1942,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "9974b6c53a244765e15677b5266c0922601f51ea16068cb621184c89ce4b7f22" +content-hash = "6a96903be0358aa6d6ef1926edf6158dd36060f8bec66bd8bf8b0ee04e7795df" diff --git a/pyproject.toml b/pyproject.toml index 83d10b5d..b27d281a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ aiofiles = "^23.1.0" asgiref = "^3.6.0" bcrypt = "^4.0.1" bleach = "^6.0.0" -email-validator = "^1.3.1" +email-validator = "^2.0.0.post2" fakeredis = "^2.11.2" feedgen = "^0.9.0" httpx = "^0.24.0" @@ -74,7 +74,6 @@ python-multipart = "^0.0.6" redis = "^4.5.4" requests = "^2.30.0" paginate = "^0.5.6" -urllib3 = "^1.26.15" # SQL alembic = "^1.10.4" From d0b0e4d88b465796bf3ff5bfbd6f709e4e367560 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Wed, 17 May 2023 18:22:53 +0200 Subject: [PATCH 1306/1451] fix: update repo information with aurblup script Currently, the "Repo" column in "OfficialProviders" is not updated when a package is moved from one repository to another. Note that we only save a package/provides combination once, hence if a package is available in core and testing at the same time, it would only put just one record into the OfficialProviders table. We iterate through the repos one by one and the last value is kept for mapping a (package/provides) combination to a repo. Due to that, the repos listed in the "sync-db" config setting should be ordered such that the "testing" repos are listed first. Signed-off-by: moson-mo --- aurweb/scripts/aurblup.py | 11 +++++++++++ test/test_aurblup.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/aurweb/scripts/aurblup.py b/aurweb/scripts/aurblup.py index 340d1ccd..10da1d98 100755 --- a/aurweb/scripts/aurblup.py +++ b/aurweb/scripts/aurblup.py @@ -49,6 +49,7 @@ def _main(force: bool = False): .all() ) + # delete providers not existing in any of our alpm repos for name, provides in old_providers.difference(providers): db.delete_all( db.query(OfficialProvider).filter( @@ -59,10 +60,20 @@ def _main(force: bool = False): ) ) + # add new providers that do not yet exist in our DB for name, provides in providers.difference(old_providers): repo = repomap.get((name, provides)) db.create(OfficialProvider, Name=name, Repo=repo, Provides=provides) + # update providers where a pkg was moved from one repo to another + all_providers = db.query(OfficialProvider) + + for op in all_providers: + new_repo = repomap.get((op.Name, op.Provides)) + + if op.Repo != new_repo: + op.Repo = new_repo + def main(force: bool = False): db.get_engine() diff --git a/test/test_aurblup.py b/test/test_aurblup.py index 93a832f9..1489677d 100644 --- a/test/test_aurblup.py +++ b/test/test_aurblup.py @@ -87,3 +87,23 @@ def test_aurblup_cleanup(alpm_db: AlpmDatabase): db.query(OfficialProvider).filter(OfficialProvider.Name == "fake package").all() ) assert len(providers) == 0 + + +def test_aurblup_repo_change(alpm_db: AlpmDatabase): + # Add a package and sync up the database. + alpm_db.add("pkg", "1.0", "x86_64", provides=["pkg2", "pkg3"]) + aurblup.main() + + # We should find an entry with repo "test" + op = db.query(OfficialProvider).filter(OfficialProvider.Name == "pkg").first() + assert op.Repo == "test" + + # Modify the repo to something that does not exist. + op.Repo = "nonsense" + + # Run our script. + aurblup.main() + + # Repo should be set back to "test" + db.refresh(op) + assert op.Repo == "test" From 146943b3b6bccfb07cfbd534248810805774d09c Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 18 May 2023 13:06:21 +0200 Subject: [PATCH 1307/1451] housekeep: support new default repos after git migration community is merged into extra testing -> core-testing & extra-testing Announcement: https://archlinux.org/news/git-migration-announcement/ We list "testing" repos first: See d0b0e4d88b465796bf3ff5bfbd6f709e4e367560 Co-authored-by: artafinde Signed-off-by: moson-mo --- conf/config.defaults | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/config.defaults b/conf/config.defaults index 0cd4b9d4..bb390d8a 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -119,7 +119,7 @@ max-blob-size = 256000 [aurblup] db-path = /srv/http/aurweb/aurblup/ -sync-dbs = core extra community multilib testing community-testing +sync-dbs = core-testing extra-testing multilib-testing core extra multilib server = https://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] From f24fae0ce6547367de6e99a1075ab1009d6e0d14 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 8 May 2023 18:22:16 +0200 Subject: [PATCH 1308/1451] feat: Add "Requests" filter option for package name - Add package name textbox for filtering requests (with auto-suggest) - Make "x pending requests" a link for TU/Dev on the package details page Signed-off-by: moson-mo --- aurweb/routers/requests.py | 16 +++++++++- po/aurweb.pot | 4 +++ static/js/typeahead-requests.js | 6 ++++ templates/partials/packages/actions.html | 18 +++++++---- templates/requests.html | 11 ++++++- test/test_packages_routes.py | 24 ++++++++++++++- test/test_requests.py | 38 ++++++++++++++++++++++++ 7 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 static/js/typeahead-requests.js diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index a259dd63..585dc157 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -40,13 +40,21 @@ async def requests( filter_accepted: bool = False, filter_rejected: bool = False, filter_maintainer_requests: bool = False, + filter_pkg_name: str = None, ): context = make_context(request, "Requests") context["q"] = dict(request.query_params) + # Set pending filter by default if no status filter was provided. + # In case we got a package name filter, but no status filter, + # we enable the other ones too. if not dict(request.query_params).keys() & FILTER_PARAMS: filter_pending = True + if filter_pkg_name: + filter_closed = True + filter_accepted = True + filter_rejected = True O, PP = util.sanitize_params(str(O), str(PP)) context["O"] = O @@ -56,6 +64,7 @@ async def requests( context["filter_accepted"] = filter_accepted context["filter_rejected"] = filter_rejected context["filter_maintainer_requests"] = filter_maintainer_requests + context["filter_pkg_name"] = filter_pkg_name Maintainer = orm.aliased(User) # A PackageRequest query @@ -78,7 +87,7 @@ async def requests( rejected_count = 0 + query.filter(PackageRequest.Status == REJECTED_ID).count() context["rejected_requests"] = rejected_count - # Apply filters + # Apply status filters in_filters = [] if filter_pending: in_filters.append(PENDING_ID) @@ -89,6 +98,11 @@ async def requests( if filter_rejected: in_filters.append(REJECTED_ID) filtered = query.filter(PackageRequest.Status.in_(in_filters)) + + # Name filter + if filter_pkg_name: + filtered = filtered.filter(PackageBase.Name == filter_pkg_name) + # Additionally filter for requests made from package maintainer if filter_maintainer_requests: filtered = filtered.filter(PackageRequest.UsersID == PackageBase.MaintainerUID) diff --git a/po/aurweb.pot b/po/aurweb.pot index f4e3c1ba..b975ab91 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -2362,3 +2362,7 @@ msgstr "" #: templates/partials/packages/comment_form.html msgid "Cancel" msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" diff --git a/static/js/typeahead-requests.js b/static/js/typeahead-requests.js new file mode 100644 index 00000000..9f636eab --- /dev/null +++ b/static/js/typeahead-requests.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + const input = document.getElementById('id_filter_pkg_name'); + const form = document.getElementById('todolist_filter'); + const type = 'suggest-pkgbase'; + typeahead.init(type, input, form); +}); diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html index fa8c994f..ae8cf141 100644 --- a/templates/partials/packages/actions.html +++ b/templates/partials/packages/actions.html @@ -97,11 +97,19 @@ {% endif %} {% if requests %} -

  • - - {{ requests | tn("%d pending request", "%d pending requests") | format(requests) }} - -
  • + {% if request.user.has_credential(creds.PKGREQ_LIST) %} +
  • + + {{ requests | tn("%d pending request", "%d pending requests") | format(requests) }} + +
  • + {% else %} +
  • + + {{ requests | tn("%d pending request", "%d pending requests") | format(requests) }} + +
  • + {% endif %} {% endif %}
  • diff --git a/templates/requests.html b/templates/requests.html index 697fbedb..9eb911f5 100644 --- a/templates/requests.html +++ b/templates/requests.html @@ -62,7 +62,13 @@ value="True" {{ "checked" if filter_maintainer_requests == true }}/>
  • - + + +
    +
    +
    +

    @@ -194,4 +200,7 @@ {% include "partials/pager.html" %} {% endif %} + + + {% endblock %} diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 21ccdd7b..93dc404a 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -555,6 +555,16 @@ def test_package_authenticated(client: TestClient, user: User, package: Package) for expected_text in expected: assert expected_text in resp.text + # make sure we don't have these. Only for Maintainer/TUs/Devs + not_expected = [ + "Disown Package", + "View Requests", + "Delete Package", + "Merge Package", + ] + for unexpected_text in not_expected: + assert unexpected_text not in resp.text + # When no requests are up, make sure we don't see the display for them. root = parse_root(resp.text) selector = '//div[@id="actionlist"]/ul/li/span[@class="flagged"]' @@ -586,8 +596,19 @@ def test_package_authenticated_maintainer( for expected_text in expected: assert expected_text in resp.text + # make sure we don't have these. Only for TUs/Devs + not_expected = [ + "1 pending request", + "Delete Package", + "Merge Package", + ] + for unexpected_text in not_expected: + assert unexpected_text not in resp.text -def test_package_authenticated_tu(client: TestClient, tu_user: User, package: Package): + +def test_package_authenticated_tu( + client: TestClient, tu_user: User, package: Package, pkgreq: PackageRequest +): cookies = {"AURSID": tu_user.login(Request(), "testPassword")} with client as request: request.cookies = cookies @@ -603,6 +624,7 @@ def test_package_authenticated_tu(client: TestClient, tu_user: User, package: Pa "Vote for this package", "Enable notifications", "Manage Co-Maintainers", + "1 pending request", "Submit Request", "Delete Package", "Merge Package", diff --git a/test/test_requests.py b/test/test_requests.py index eb05596c..7ddb76a0 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -912,6 +912,44 @@ def test_requests_for_maintainer_requests( assert len(rows) == 2 +def test_requests_with_package_name_filter( + client: TestClient, + tu_user: User, + user2: User, + packages: list[Package], + requests: list[PackageRequest], +): + # test as TU + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={"filter_pkg_name": packages[0].PackageBase.Name}, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We only expect 1 request for our first package + assert len(rows) == 1 + + # test as regular user, not related to our package + cookies = {"AURSID": user2.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={"filter_pkg_name": packages[0].PackageBase.Name}, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We don't expect to get any requests + assert len(rows) == 0 + + def test_requests_by_deleted_users( client: TestClient, user: User, tu_user: User, pkgreq: PackageRequest ): From 7a88aeb673c420da1dea0b0a29d5f0b8e921b3a0 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Mon, 8 May 2023 21:24:17 +0200 Subject: [PATCH 1309/1451] chore: update .gitignore for test-emails emails generated when running tests are stored in test-emails/ dir Signed-off-by: moson-mo --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 414077be..a3314c27 100644 --- a/.gitignore +++ b/.gitignore @@ -47,5 +47,9 @@ doc/rpc.html # Ignore coverage report coverage.xml + # Ignore pytest report report.xml + +# Ignore test emails +test-emails/ From 1b41e8572a087c8d9b1d2cc59fffa459e89ed5ac Mon Sep 17 00:00:00 2001 From: renovate Date: Fri, 26 May 2023 02:24:39 +0000 Subject: [PATCH 1310/1451] fix(deps): update all non-major dependencies --- poetry.lock | 360 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 180 insertions(+), 182 deletions(-) diff --git a/poetry.lock b/poetry.lock index 388379d9..3a273be4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,14 +14,14 @@ files = [ [[package]] name = "alembic" -version = "1.10.4" +version = "1.11.1" description = "A database migration tool for SQLAlchemy." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.10.4-py3-none-any.whl", hash = "sha256:43942c3d4bf2620c466b91c0f4fca136fe51ae972394a0cc8b90810d664e4f5c"}, - {file = "alembic-1.10.4.tar.gz", hash = "sha256:295b54bbb92c4008ab6a7dcd1e227e668416d6f84b98b3c4446a2bc6214a556b"}, + {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"}, + {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"}, ] [package.dependencies] @@ -55,16 +55,19 @@ trio = ["trio (>=0.16,<0.22)"] [[package]] name = "asgiref" -version = "3.6.0" +version = "3.7.1" description = "ASGI specs, helper code, and adapters" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, - {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, + {file = "asgiref-3.7.1-py3-none-any.whl", hash = "sha256:33958cb2e4b3cd8b1b06ef295bd8605cde65b11df51d3beab39e2e149a610ab3"}, + {file = "asgiref-3.7.1.tar.gz", hash = "sha256:8de379fcc383bcfe4507e229fc31209ea23d4831c850f74063b2c11639474dd2"}, ] +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] @@ -352,63 +355,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.5" +version = "7.2.6" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c"}, - {file = "coverage-7.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a"}, - {file = "coverage-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f"}, - {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a"}, - {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a"}, - {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11"}, - {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5"}, - {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c"}, - {file = "coverage-7.2.5-cp310-cp310-win32.whl", hash = "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5"}, - {file = "coverage-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c"}, - {file = "coverage-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce"}, - {file = "coverage-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88"}, - {file = "coverage-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e"}, - {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2"}, - {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139"}, - {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8"}, - {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed"}, - {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6"}, - {file = "coverage-7.2.5-cp311-cp311-win32.whl", hash = "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b"}, - {file = "coverage-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068"}, - {file = "coverage-7.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1"}, - {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33"}, - {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade"}, - {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5"}, - {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47"}, - {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd"}, - {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969"}, - {file = "coverage-7.2.5-cp37-cp37m-win32.whl", hash = "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718"}, - {file = "coverage-7.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0"}, - {file = "coverage-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84"}, - {file = "coverage-7.2.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790"}, - {file = "coverage-7.2.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771"}, - {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045"}, - {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614"}, - {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3"}, - {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd"}, - {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1"}, - {file = "coverage-7.2.5-cp38-cp38-win32.whl", hash = "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813"}, - {file = "coverage-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212"}, - {file = "coverage-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b"}, - {file = "coverage-7.2.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200"}, - {file = "coverage-7.2.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5"}, - {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e"}, - {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303"}, - {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3"}, - {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a"}, - {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1"}, - {file = "coverage-7.2.5-cp39-cp39-win32.whl", hash = "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31"}, - {file = "coverage-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252"}, - {file = "coverage-7.2.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3"}, - {file = "coverage-7.2.5.tar.gz", hash = "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"}, + {file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"}, + {file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"}, + {file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"}, + {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"}, + {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"}, + {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"}, + {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"}, + {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"}, + {file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"}, + {file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"}, + {file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"}, + {file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"}, + {file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"}, + {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"}, + {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"}, + {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"}, + {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"}, + {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"}, + {file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"}, + {file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"}, + {file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"}, + {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"}, + {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"}, + {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"}, + {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"}, + {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"}, + {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"}, + {file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"}, + {file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"}, + {file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"}, + {file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"}, + {file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"}, + {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"}, + {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"}, + {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"}, + {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"}, + {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"}, + {file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"}, + {file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"}, + {file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"}, + {file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"}, + {file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"}, + {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"}, + {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"}, + {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"}, + {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"}, + {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"}, + {file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"}, + {file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"}, + {file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"}, + {file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"}, ] [package.dependencies] @@ -528,14 +531,14 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "2.11.2" +version = "2.13.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "fakeredis-2.11.2-py3-none-any.whl", hash = "sha256:69a504328a89e5e5f2d05a4236b570fb45244c96997c5002c8c6a0503b95f289"}, - {file = "fakeredis-2.11.2.tar.gz", hash = "sha256:e0fef512b8ec49679d373456aa4698a4103005ecd7ca0b13170a2c1d3af949c5"}, + {file = "fakeredis-2.13.0-py3-none-any.whl", hash = "sha256:df7bb44fb9e593970c626325230e1c321f954ce7b204d4c4452eae5233d554ed"}, + {file = "fakeredis-2.13.0.tar.gz", hash = "sha256:53f00f44f771d2b794f1ea036fa07a33476ab7368f1b0e908daab3eff80336f6"}, ] [package.dependencies] @@ -758,14 +761,14 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" -version = "0.24.0" +version = "0.24.1" description = "The next generation HTTP client." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"}, - {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"}, + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, ] [package.dependencies] @@ -1101,63 +1104,58 @@ files = [ [[package]] name = "orjson" -version = "3.8.11" +version = "3.8.14" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false -python-versions = ">= 3.7" +python-versions = ">=3.7" files = [ - {file = "orjson-3.8.11-cp310-cp310-macosx_11_0_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:9fa900bdd84b4576c8dd6f3e2a00b35797f29283af328c6e3d70addfa4c2d599"}, - {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1103e597c16f82c241e1b02beadc9c91cecd93e60433ca73cb6464dcc235f37c"}, - {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d70b6db9d4e1e6057829cd7fe119c217cebaf989f88d14b2445fa69fc568d03e"}, - {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3afccf7f8684dca7f017837a315de0a1ab5c095de22a4eed206d079f9325ed72"}, - {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fedcc428416e23a6c9de62a000c22ae33bbe0108302ad5d5935e29ea739bf37"}, - {file = "orjson-3.8.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf48ed8d4b6ab9f23b7ee642462369d7133412d72824bad89f9bf4311c06c6a1"}, - {file = "orjson-3.8.11-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c55065bc2075a5ea6ffb30462d84fd3aa5bbb7ae600855c325ee5753feec715"}, - {file = "orjson-3.8.11-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:08729e339ff3146e6de56c1166f014c3d2ec3e79ffb76d6c55d52cc892e5e477"}, - {file = "orjson-3.8.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:358e515b8b19a275b259f5ee1e0efa2859b1d976b5ed5d016ac59f9e6c8788a3"}, - {file = "orjson-3.8.11-cp310-none-win_amd64.whl", hash = "sha256:62eb8bdcf6f4cdbe12743e88ad98696277a75f91a35e8fb93a7ea2b9f4a7000c"}, - {file = "orjson-3.8.11-cp311-cp311-macosx_11_0_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:982ab319b7a5ece4199caf2a2b3a28e62a8e289cb6418548ef98bced7e2a6cfe"}, - {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14903bfeb591a9117b7d40d81e3ebca9700b4e77bd829d6f22ea57941bb0ebf"}, - {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58c068f93d701f9466f667bf3b5cb4e4946aee940df2b07ca5101f1cf1b60ce4"}, - {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9486963d2e65482c565dacb366adb36d22aa22acf7274b61490244c3d87fa631"}, - {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c3b5405edc3a5f9e34516ee1a729f6c46aecf6de960ae07a7b3e95ebdd0e1d9"}, - {file = "orjson-3.8.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b65424ceee82b94e3613233b67ef110dc58f9d83b0076ec47a506289552a861"}, - {file = "orjson-3.8.11-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:173b8f8c750590f432757292cfb197582e5c14347b913b4017561d47af0e759b"}, - {file = "orjson-3.8.11-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f38c8194ce086e6a9816b4b8dde5e7f383feeed92feec0385d99baf64f9b6e"}, - {file = "orjson-3.8.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:553fdaf9f4b5060a0dcc517ae0c511c289c184a83d6719d03c5602ed0eef0390"}, - {file = "orjson-3.8.11-cp311-none-win_amd64.whl", hash = "sha256:12f647d4da0aab1997e25bed4fa2b76782b5b9d2d1bf3066b5f0a57d34d833c4"}, - {file = "orjson-3.8.11-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:71a656f1c62e84c69060093e20cedff6a92e472d53ff5b8b9026b1b298542a68"}, - {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176d742f53434541e50a5e659694073aa51dcbd8f29a1708a4fa1a320193c615"}, - {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b369019e597b59c4b97e9f925a3b725321fa1481c129d76c74c6ea3823f5d1e8"}, - {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a53b3c02a38aadc5302661c2ca18645093971488992df77ce14fef16f598b2e"}, - {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d7b050135669d2335e40120215ad4120e29958c139f8bab68ce06a1cb1a1b2c"}, - {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66f0c9e4e8f6641497a7dc50591af3704b11468e9fc90cfb5874f28b0a61edb5"}, - {file = "orjson-3.8.11-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:235926b38ed9b76ab2bca99ff26ece79c1c46bc10079b06e660b087aecffbe69"}, - {file = "orjson-3.8.11-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c2d3e6b65458ed71b6797f321d6e8bfeeadee9d3d31cac47806a608ea745edd7"}, - {file = "orjson-3.8.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4118dcd2b5a27a22af5ad92414073f25d93bca1868f1f580056003c84841062f"}, - {file = "orjson-3.8.11-cp37-none-win_amd64.whl", hash = "sha256:b68a07794834b7bd53ae2a8b4fe4bf010734cae3f0917d434c83b97acf8e5bce"}, - {file = "orjson-3.8.11-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:98befa717efaab7ddb847ebe47d473f6bd6f0cb53e98e6c3d487c7c58ba2e174"}, - {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f9415b86ef154bf247fa78a6918aac50089c296e26fb6cf15bc9d7e6402a1f8"}, - {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7aeefac55848aeb29f20b91fa55f9e488f446201bb1bb31dc17480d113d8955"}, - {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d47f97b99beb9bcac6e288a76b559543a61e0187443d8089204b757726b1d000"}, - {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7d5aecccfaf2052cd07ed5bec8efba9ddfea055682fcd346047b1a3e9da3034"}, - {file = "orjson-3.8.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b60dfc1251742e79bb075d7a7c4e37078b932a02e6f005c45761bd90c69189"}, - {file = "orjson-3.8.11-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:ef52f1d5a2f89ef9049781c90ea35d5edf74374ed6ed515c286a706d1b290267"}, - {file = "orjson-3.8.11-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7b4fae3b8fc69c8e76f1c0694f3decfe8a57f87e7ac7779ebb59cd71135438"}, - {file = "orjson-3.8.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f4e4a1001933166fd1c257b920b241b35322bef99ed7329338bf266ac053abe7"}, - {file = "orjson-3.8.11-cp38-none-win_amd64.whl", hash = "sha256:5ff10789cbc08a9fd94507c907ba55b9315e99f20345ff8ef34fac432dacd948"}, - {file = "orjson-3.8.11-cp39-cp39-macosx_11_0_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:c67ac094a4dde914297543af19f22532d7124f3a35245580d8b756c4ff2f5884"}, - {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdf201e77d3fac9d8d6f68d872ef45dccfe46f30b268bb88b6c5af5065b433aa"}, - {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3485c458670c0edb79ca149fe201f199dd9ccfe7ca3acbdef617e3c683e7b97f"}, - {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e97fdbb779a3b8f5d9fc7dfddef5325f81ee45897eb7cb4638d5d9734d42514"}, - {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fc050f8e7f2e4061c8c9968ad0be745b11b03913b77ffa8ceca65914696886c"}, - {file = "orjson-3.8.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2ef933da50b31c112b252be03d1ef59e0d0552c1a08e48295bd529ce42aaab8"}, - {file = "orjson-3.8.11-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:714c3e2be6ed7e4ff6e887926d6e171bfd94fdee76d7d3bfa74ee19237a2d49d"}, - {file = "orjson-3.8.11-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7e4ded77ac7432a155d1d27a83bcadf722750aea3b9e6c4d47f2a92054ab71cb"}, - {file = "orjson-3.8.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:382f15861a4bf447ab9d07106010e61b217ef6d4245c6cf64af0c12c4c5e2346"}, - {file = "orjson-3.8.11-cp39-none-win_amd64.whl", hash = "sha256:0bc3d1b93a73b46a698c054697eb2d27bdedbc5ea0d11ec5f1a6bfbec36346b5"}, - {file = "orjson-3.8.11.tar.gz", hash = "sha256:882c77126c42dd93bb35288632d69b1e393863a2b752de3e5fe0112833609496"}, + {file = "orjson-3.8.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7a7b0fead2d0115ef927fa46ad005d7a3988a77187500bf895af67b365c10d1f"}, + {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca90db8f551b8960da95b0d4cad6c0489df52ea03585b6979595be7b31a3f946"}, + {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4ac01a3db4e6a98a8ad1bb1a3e8bfc777928939e87c04e93e0d5006df574a4b"}, + {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf6825e160e4eb0ef65ce37d8c221edcab96ff2ffba65e5da2437a60a12b3ad1"}, + {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80e62afe49e6bfc706e041faa351d7520b5f86572b8e31455802251ea989613"}, + {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6112194c11e611596eed72f46efb0e6b4812682eff3c7b48473d1146c3fa0efb"}, + {file = "orjson-3.8.14-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:739f9f633e1544f2a477fa3bef380f488c8dca6e2521c8dc36424b12554ee31e"}, + {file = "orjson-3.8.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d3d8faded5a514b80b56d0429eb38b429d7a810f8749d25dc10a0cc15b8a3c8"}, + {file = "orjson-3.8.14-cp310-none-win_amd64.whl", hash = "sha256:0bf00c42333412a9338297bf888d7428c99e281e20322070bde8c2314775508b"}, + {file = "orjson-3.8.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d66966fd94719beb84e8ed84833bc59c3c005d3d2d0c42f11d7552d3267c6de7"}, + {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087c0dc93379e8ba2d59e9f586fab8de8c137d164fccf8afd5523a2137570917"}, + {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04c70dc8ca79b0072a16d82f94b9d9dd6598a43dd753ab20039e9f7d2b14f017"}, + {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aedba48264fe87e5060c0e9c2b28909f1e60626e46dc2f77e0c8c16939e2e1f7"}, + {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01640ab79111dd97515cba9fab7c66cb3b0967b0892cc74756a801ff681a01b6"}, + {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b206cca6836a4c6683bcaa523ab467627b5f03902e5e1082dc59cd010e6925f"}, + {file = "orjson-3.8.14-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ee0299b2dda9afce351a5e8c148ea7a886de213f955aa0288fb874fb44829c36"}, + {file = "orjson-3.8.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:31a2a29be559e92dcc5c278787b4166da6f0d45675b59a11c4867f5d1455ebf4"}, + {file = "orjson-3.8.14-cp311-none-win_amd64.whl", hash = "sha256:20b7ffc7736000ea205f9143df322b03961f287b4057606291c62c842ff3c5b5"}, + {file = "orjson-3.8.14-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de1ee13d6b6727ee1db38722695250984bae81b8fc9d05f1176c74d14b1322d9"}, + {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ee09bfbf1d54c127d3061f6721a1a11d2ce502b50597c3d0d2e1bd2d235b764"}, + {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97ebb7fab5f1ae212a6501f17cb7750a6838ffc2f1cebbaa5dec1a90038ca3c6"}, + {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38ca39bae7fbc050332a374062d4cdec28095540fa8bb245eada467897a3a0bb"}, + {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92374bc35b6da344a927d5a850f7db80a91c7b837de2f0ea90fc870314b1ff44"}, + {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9393a63cb0424515ec5e434078b3198de6ec9e057f1d33bad268683935f0a5d5"}, + {file = "orjson-3.8.14-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5fb66f0ac23e861b817c858515ac1f74d1cd9e72e3f82a5b2c9bae9f92286adc"}, + {file = "orjson-3.8.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19415aaf30525a5baff0d72a089fcdd68f19a3674998263c885c3908228c1086"}, + {file = "orjson-3.8.14-cp37-none-win_amd64.whl", hash = "sha256:87ba7882e146e24a7d8b4a7971c20212c2af75ead8096fc3d55330babb1015fb"}, + {file = "orjson-3.8.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9f5cf61b6db68f213c805c55bf0aab9b4cb75a4e9c7f5bfbd4deb3a0aef0ec53"}, + {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33bc310da4ad2ffe8f7f1c9e89692146d9ec5aec2d1c9ef6b67f8dc5e2d63241"}, + {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67a7e883b6f782b106683979ccc43d89b98c28a1f4a33fe3a22e253577499bb1"}, + {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9df820e6c8c84c52ec39ea2cc9c79f7999c839c7d1481a056908dce3b90ce9f9"}, + {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebca14ae80814219ea3327e3dfa7ff618621ff335e45781fac26f5cd0b48f2b4"}, + {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27967be4c16bd09f4aeff8896d9be9cbd00fd72f5815d5980e4776f821e2f77c"}, + {file = "orjson-3.8.14-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:062829b5e20cd8648bf4c11c3a5ee7cf196fa138e573407b5312c849b0cf354d"}, + {file = "orjson-3.8.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e53bc5beb612df8ddddb065f079d3fd30b5b4e73053518524423549d61177f3f"}, + {file = "orjson-3.8.14-cp38-none-win_amd64.whl", hash = "sha256:d03f29b0369bb1ab55c8a67103eb3a9675daaf92f04388568034fe16be48fa5d"}, + {file = "orjson-3.8.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:716a3994e039203f0a59056efa28185d4cac51b922cc5bf27ab9182cfa20e12e"}, + {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb35dd3ba062c1d984d57e6477768ed7b62ed9260f31362b2d69106f9c60ebd"}, + {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0bc6b7abf27f1dc192dadad249df9b513912506dd420ce50fd18864a33789b71"}, + {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2f75b7d9285e35c3d4dff9811185535ff2ea637f06b2b242cb84385f8ffe63"}, + {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:017de5ba22e58dfa6f41914f5edb8cd052d23f171000684c26b2d2ab219db31e"}, + {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a3bf3154f40299b8bc95e9fb8da47436a59a2106fc22cae15f76d649e062da"}, + {file = "orjson-3.8.14-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:64b4fca0531030040e611c6037aaf05359e296877ab0a8e744c26ef9c32738b9"}, + {file = "orjson-3.8.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8a896a12b38fe201a72593810abc1f4f1597e65b8c869d5fc83bbcf75d93398f"}, + {file = "orjson-3.8.14-cp39-none-win_amd64.whl", hash = "sha256:9725226478d1dafe46d26f758eadecc6cf98dcbb985445e14a9c74aaed6ccfea"}, + {file = "orjson-3.8.14.tar.gz", hash = "sha256:5ea93fd3ef7be7386f2516d728c877156de1559cda09453fc7dd7b696d0439b3"}, ] [[package]] @@ -1271,25 +1269,25 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "4.22.4" +version = "4.23.1" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.22.4-cp310-abi3-win32.whl", hash = "sha256:a4e661247896c2ffea4b894bca2d8657e752bedb8f3c66d7befa2557291be1e8"}, - {file = "protobuf-4.22.4-cp310-abi3-win_amd64.whl", hash = "sha256:7b42086d6027be2730151b49f27b2f5be40f3b036adf7b8da5917f4567f268c3"}, - {file = "protobuf-4.22.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4bfb28d48628deacdb66a95aaa7b6640f3dc82b4edd34db444c7a3cdd90b01fb"}, - {file = "protobuf-4.22.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e98e26328d7c668541d1052b02de4205b1094ef6b2ce57167440d3e39876db48"}, - {file = "protobuf-4.22.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd329e5dd7b6c4b878cab4b85bb6cec880e2adaf4e8aa2c75944dcbb05e1ff1"}, - {file = "protobuf-4.22.4-cp37-cp37m-win32.whl", hash = "sha256:b7728b5da9eee15c0aa3baaee79e94fa877ddcf7e3d2f34b1eab586cd26eea89"}, - {file = "protobuf-4.22.4-cp37-cp37m-win_amd64.whl", hash = "sha256:f4a711588c3a79b6f9c44af4d7f4a2ae868e27063654683932ab6462f90e9656"}, - {file = "protobuf-4.22.4-cp38-cp38-win32.whl", hash = "sha256:11b28b4e779d7f275e3ea0efa3938f4d4e8ed3ca818f9fec3b193f8e9ada99fd"}, - {file = "protobuf-4.22.4-cp38-cp38-win_amd64.whl", hash = "sha256:144d5b46df5e44f914f715accaadf88d617242ba5a40cacef4e8de7effa79954"}, - {file = "protobuf-4.22.4-cp39-cp39-win32.whl", hash = "sha256:5128b4d5efcaef92189e076077ae389700606ff81d2126b8361dc01f3e026197"}, - {file = "protobuf-4.22.4-cp39-cp39-win_amd64.whl", hash = "sha256:9537ae27d43318acf8ce27d0359fe28e6ebe4179c3350bc055bb60ff4dc4fcd3"}, - {file = "protobuf-4.22.4-py3-none-any.whl", hash = "sha256:3b21074b7fb748d8e123acaef9fa63a84fdc1436dc71199d2317b139f77dd6f4"}, - {file = "protobuf-4.22.4.tar.gz", hash = "sha256:21fbaef7f012232eb8d6cb8ba334e931fc6ff8570f5aaedc77d5b22a439aa909"}, + {file = "protobuf-4.23.1-cp310-abi3-win32.whl", hash = "sha256:410bcc0a5b279f634d3e16082ce221dfef7c3392fac723500e2e64d1806dd2be"}, + {file = "protobuf-4.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:32e78beda26d7a101fecf15d7a4a792278a0d26a31bc327ff05564a9d68ab8ee"}, + {file = "protobuf-4.23.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f9510cac91e764e86acd74e2b7f7bc5e6127a7f3fb646d7c8033cfb84fd1176a"}, + {file = "protobuf-4.23.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:346990f634272caac1f09efbcfbbacb23098b1f606d172534c6fa2d9758bb436"}, + {file = "protobuf-4.23.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3ce113b3f3362493bddc9069c2163a38f240a9ed685ff83e7bcb756b05e1deb0"}, + {file = "protobuf-4.23.1-cp37-cp37m-win32.whl", hash = "sha256:2036a3a1e7fc27f973fa0a7888dce712393af644f4695385f117886abc792e39"}, + {file = "protobuf-4.23.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3b8905eafe4439076e1f58e9d1fa327025fd2777cf90f14083092ae47f77b0aa"}, + {file = "protobuf-4.23.1-cp38-cp38-win32.whl", hash = "sha256:5b9cd6097e6acae48a68cb29b56bc79339be84eca65b486910bb1e7a30e2b7c1"}, + {file = "protobuf-4.23.1-cp38-cp38-win_amd64.whl", hash = "sha256:decf119d54e820f298ee6d89c72d6b289ea240c32c521f00433f9dc420595f38"}, + {file = "protobuf-4.23.1-cp39-cp39-win32.whl", hash = "sha256:91fac0753c3c4951fbb98a93271c43cc7cf3b93cf67747b3e600bb1e5cc14d61"}, + {file = "protobuf-4.23.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac50be82491369a9ec3710565777e4da87c6d2e20404e0abb1f3a8f10ffd20f0"}, + {file = "protobuf-4.23.1-py3-none-any.whl", hash = "sha256:65f0ac96ef67d7dd09b19a46aad81a851b6f85f89725577f16de38f2d68ad477"}, + {file = "protobuf-4.23.1.tar.gz", hash = "sha256:95789b569418a3e32a53f43d7763be3d490a831e9c08042539462b6d972c2d7e"}, ] [[package]] @@ -1370,43 +1368,43 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygit2" -version = "1.12.0" +version = "1.12.1" description = "Python bindings for libgit2." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pygit2-1.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b44a3b38e62dbf8cb559a40d2b39506a638d13542502ddb927f1c05565048f27"}, - {file = "pygit2-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:834cf5b54d9b49c562669ec986be54c7915585638690c11f1dc4e6a55bc5d79d"}, - {file = "pygit2-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ecb096cdbbf142d8787cf879ab927fc9777d36580d2e5758d02c9474a3b015c"}, - {file = "pygit2-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15620696743ffac71cfdaf270944d9363b70442c1fbe96f5e4a69639c2fe7c23"}, - {file = "pygit2-1.12.0-cp310-cp310-win32.whl", hash = "sha256:de21194e18e4d93f793740b2b979dbe9dd6607f342a4fad3ecedeaf26ec743df"}, - {file = "pygit2-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0a9d49f71bec7c4f2ff8273e0c7caba4b2f21bfc56e2071e429028b20ab9d762"}, - {file = "pygit2-1.12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a428970b44827b703cc3267de8d71648f491546d5b9276505ef5f232a921a34e"}, - {file = "pygit2-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2bb7b674124a38b12a5aaacca3b8c1e29674f3b46cb907af0b3ba75d90e5952a"}, - {file = "pygit2-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de46940b46bee12f4c938aadf4f59617798f704c8ac5f08b5a84914459d604be"}, - {file = "pygit2-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbfb3ebe7f57fe7873d86e84b476869f407d6bb204a39a3e7d04e4a7f0e43c1"}, - {file = "pygit2-1.12.0-cp311-cp311-win32.whl", hash = "sha256:db98978d559d6e84187d463fb3aa83cf6120dadf62058e3d05a97457f9f27247"}, - {file = "pygit2-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:8734a44e0dab8a5e6668e4a926f7171b59b87d65981bd3732efba57c327cec6d"}, - {file = "pygit2-1.12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1bb73ffb345400f8c6fe391431e06b93e26bc4d2048b1ac3f7c54dae5f7b6dc2"}, - {file = "pygit2-1.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdeaf1631803616d303b808cd644ee17164fb675241ab1b0bb243d4a3d3de59f"}, - {file = "pygit2-1.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:652b3f0510ad21ec6275b246aa3e7a2e20f2f9c37a9844804887fabc2db49ca3"}, - {file = "pygit2-1.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2419cd1034bf593847466b188a65bd9d512e13b7da0e8c3a74b487d8014a6c1"}, - {file = "pygit2-1.12.0-cp38-cp38-win32.whl", hash = "sha256:6a445a537de152364b334e73047af9225fe8c6f54c7d815d8c751cb23b79cbef"}, - {file = "pygit2-1.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:ad1cca4533beee034277fe01f0d4029da40d2bd1a944a8cd17bffccc0331cc53"}, - {file = "pygit2-1.12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ad7b21e35e759d033dede5dc4971f6c9b3408f9096b26fabc7cedb49e319680"}, - {file = "pygit2-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e303aa9d7de6039cc4450a1fbd5911fab22867dc4e05f148b0cd7c56f7b84b2"}, - {file = "pygit2-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:869e68cfae7e0e00a799efa26bba3f829bdeafa1462225a7db1317dacb4e6a4e"}, - {file = "pygit2-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c779c15bf6ebce986cb753c8113ccfb329c12d4a73b303ee7ac2c8961288b8cd"}, - {file = "pygit2-1.12.0-cp39-cp39-win32.whl", hash = "sha256:c6ac2fd8ed30016235b06aacc28e5f10e1a17d0f02eab35f5f503138bbee763d"}, - {file = "pygit2-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:2483e4aa8bb4290ab157d575b00b830528c669869d710646a1d4af7209d59e81"}, - {file = "pygit2-1.12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8fca4ca59928436fca5df3d54a7d591e7aa12ebaeaeb1801a99e09970fb8f1d3"}, - {file = "pygit2-1.12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0746791741ba1879faafd12be0b7fb8edd06633508bbf8aabfd28415f1c0b13f"}, - {file = "pygit2-1.12.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b9d8b7e1d143415d462d82fc5d9dd5922c527474871c7b3c3a8aec009b74b1c"}, - {file = "pygit2-1.12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:69ee34f8b77fc60dcf93524fd843eacc416be906b7471746d2ee8214d5a591a0"}, - {file = "pygit2-1.12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c290dadcf42e9d857ea20c37781168de1d1ac31b196b450400f962279aa405f"}, - {file = "pygit2-1.12.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d9bdd2837f9f1cacb571889ac4226844a41476509c325732af06b622293782"}, - {file = "pygit2-1.12.0.tar.gz", hash = "sha256:e9440d08665e35278989939590a53f37a938eada4f9446844930aa2ee30d73be"}, + {file = "pygit2-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50a155528aa611e4a217be31a9d2d8da283cfd978dbba07494cd04ea3d7c8768"}, + {file = "pygit2-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:248e22ccb1ea31f569373a3da3fa73d110ba2585c6326ff74b03c9579fb7b913"}, + {file = "pygit2-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e575e672c5a6cb39234b0076423a560e016d6b88cd50947c2df3bf59c5ccdf3d"}, + {file = "pygit2-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad9b46b52997d131b31ff46f699b074e9745c8fea8d0efb6b72ace43ab25828c"}, + {file = "pygit2-1.12.1-cp310-cp310-win32.whl", hash = "sha256:a8f495df877da04c572ecec4d532ae195680b4781dbf229bab4e801fa9ef20e9"}, + {file = "pygit2-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f1e1355c7fe2938a2bca0d6204a00c02950d13008722879e54a335b3e874006"}, + {file = "pygit2-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8a5c56b0b5dc8a317561070ef7557e180d4937d8b115c5a762d85e0109a216f3"}, + {file = "pygit2-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7c9ca8bc8a722863fc873234748fef3422007d5a6ea90ba3ae338d2907d3d6e"}, + {file = "pygit2-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c02a11f10bc4e329ab941f0c70874d39053c8f78544aefeb506f04cedb621a"}, + {file = "pygit2-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b3af334adf325b7c973417efa220fd5a9ce946b936262eceabc8ad8d46e0310"}, + {file = "pygit2-1.12.1-cp311-cp311-win32.whl", hash = "sha256:86c393962d1341893bbfa91829b3b8545e8ac7622f8b53b9a0b835b9cc1b5198"}, + {file = "pygit2-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:86c7e75ddc76f4e5593b47f9c2074fff242322ed9f4126116749f7c86021520a"}, + {file = "pygit2-1.12.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:939d11677f434024ea25a9137d8a525ef9f9ac474fb8b86399bc9526e6a7bff5"}, + {file = "pygit2-1.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:946f9215c0442995042ea512f764f7a6638d3a09f9d0484d3aeedbf8833f89e6"}, + {file = "pygit2-1.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd574620d3cc80df0b23bf2b7b08d8726e75a338d0fa1b67e4d6738d3ee56635"}, + {file = "pygit2-1.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d0adeff5c43229913f3bdae71c36e77ed19f36bd8dd6b5c141820964b1f5b3"}, + {file = "pygit2-1.12.1-cp38-cp38-win32.whl", hash = "sha256:ed8e2ef97171e994bf4d46c6c6534a3c12dd2dbbc47741e5995eaf8c2c92f71c"}, + {file = "pygit2-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:5318817055a3ca3906bf88344b9a6dc70c640f9b6bc236ac9e767d12bad54361"}, + {file = "pygit2-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cb9c803151ffeb0b8de52a93381108a2c6a9a446c55d659a135f52645e1650eb"}, + {file = "pygit2-1.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:47bf1e196dc23fe38018ad49b021d425edc319328169c597df45d73cf46b62ef"}, + {file = "pygit2-1.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:948479df72223bbcd16b2a88904dc2a3886c15a0107a7cf3b5373c8e34f52f31"}, + {file = "pygit2-1.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4bebe8b310edc2662cbffb94ef1a758252fe2e4c92bc83fac0eaf2bedf8b871"}, + {file = "pygit2-1.12.1-cp39-cp39-win32.whl", hash = "sha256:77bc0ab778ab6fe631f5f9eb831b426376a7b71426c5a913aaa9088382ef1dc9"}, + {file = "pygit2-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:e87b2306a266f6abca94ab37dda807033a6f40faad05c4d1e089f9e8354130a8"}, + {file = "pygit2-1.12.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5d5e8a3b67f5d4ba8e3838c492254688997747989b184b5f1a3af4fef7f9f53e"}, + {file = "pygit2-1.12.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2500b749759f2efdfa5096c0aafeb2d92152766708f5700284427bd658e5c407"}, + {file = "pygit2-1.12.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c21759ca9cc755faa2d17180cd49af004486ca84f3166cac089a2083dcb09114"}, + {file = "pygit2-1.12.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d73074ab64b383e3a1ab03e8070f6b195ef89b9d379ca5682c38dd9c289cc6e2"}, + {file = "pygit2-1.12.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:865c0d1925c52426455317f29c1db718187ec69ed5474faaf3e1c68ff2135767"}, + {file = "pygit2-1.12.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebebbe9125b068337b5415565ec94c9e092c708e430851b2d02e51217bdce4a"}, + {file = "pygit2-1.12.1.tar.gz", hash = "sha256:8218922abedc88a65d5092308d533ca4c4ed634aec86a3493d3bdf1a25aeeff3"}, ] [package.dependencies] @@ -1456,14 +1454,14 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy [[package]] name = "pytest-cov" -version = "4.0.0" +version = "4.1.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] @@ -1491,14 +1489,14 @@ pytest = ">=3.0" [[package]] name = "pytest-xdist" -version = "3.2.1" +version = "3.3.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-xdist-3.2.1.tar.gz", hash = "sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727"}, - {file = "pytest_xdist-3.2.1-py3-none-any.whl", hash = "sha256:37290d161638a20b672401deef1cba812d110ac27e35d213f091d15b8beb40c9"}, + {file = "pytest-xdist-3.3.1.tar.gz", hash = "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93"}, + {file = "pytest_xdist-3.3.1-py3-none-any.whl", hash = "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"}, ] [package.dependencies] @@ -1542,18 +1540,18 @@ dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatc [[package]] name = "redis" -version = "4.5.4" +version = "4.5.5" description = "Python client for Redis database and key-value store" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "redis-4.5.4-py3-none-any.whl", hash = "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2"}, - {file = "redis-4.5.4.tar.gz", hash = "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893"}, + {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"}, + {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"}, ] [package.dependencies] -async-timeout = {version = ">=4.0.2", markers = "python_version <= \"3.11.2\""} +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} [package.extras] hiredis = ["hiredis (>=1.0.0)"] @@ -1561,14 +1559,14 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "requests" -version = "2.30.0" +version = "2.31.0" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "requests-2.30.0-py3-none-any.whl", hash = "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294"}, - {file = "requests-2.30.0.tar.gz", hash = "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] @@ -1892,14 +1890,14 @@ files = [ [[package]] name = "werkzeug" -version = "2.3.3" +version = "2.3.4" description = "The comprehensive WSGI web application library." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.3-py3-none-any.whl", hash = "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a"}, - {file = "Werkzeug-2.3.3.tar.gz", hash = "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"}, + {file = "Werkzeug-2.3.4-py3-none-any.whl", hash = "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f"}, + {file = "Werkzeug-2.3.4.tar.gz", hash = "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76"}, ] [package.dependencies] @@ -1942,4 +1940,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "6a96903be0358aa6d6ef1926edf6158dd36060f8bec66bd8bf8b0ee04e7795df" +content-hash = "caf2a21e3bff699216e53a37697a7a544103fdea9f84a5d54ee94ded3e810973" diff --git a/pyproject.toml b/pyproject.toml index b27d281a..f04deb14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ aiofiles = "^23.1.0" asgiref = "^3.6.0" bcrypt = "^4.0.1" bleach = "^6.0.0" -email-validator = "^2.0.0.post2" +email-validator = "^2.0.0-post.0" fakeredis = "^2.11.2" feedgen = "^0.9.0" httpx = "^0.24.0" From 5fe375bdc3c5ee1bb39acc3c92e6791b3606b942 Mon Sep 17 00:00:00 2001 From: "Daniel M. Capella" Date: Thu, 25 May 2023 17:18:30 -0400 Subject: [PATCH 1311/1451] feat: add link to MergeBaseName in requests.html --- templates/requests.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/requests.html b/templates/requests.html index 9eb911f5..352ed820 100644 --- a/templates/requests.html +++ b/templates/requests.html @@ -109,7 +109,9 @@ {{ result.RequestType.name_display() | tr }} {# If the RequestType is a merge and request.MergeBaseName is valid... #} {% if result.RequestType.ID == 3 and result.MergeBaseName %} - ({{ result.MergeBaseName }}) + + ({{ result.MergeBaseName }}) + {% endif %} {# Comments #} From 2eacc84cd02802704a9d686843d3c2224f35dcb5 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 25 May 2023 13:23:37 +0200 Subject: [PATCH 1312/1451] fix: properly evaluate AURREMEMBER cookie Whenever the AURREMEMBER cookie was defined, regardless of its value, "remember_me" is always set to True The get method of a dict returns a string, converting a value of str "False" into a bool -> True We have to check AURREMEMBERs value instead. Signed-off-by: moson-mo --- aurweb/auth/__init__.py | 4 +--- aurweb/cookies.py | 2 +- aurweb/users/update.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index 5a1fc8d0..83dd424c 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -104,9 +104,7 @@ class BasicAuthBackend(AuthenticationBackend): return unauthenticated timeout = aurweb.config.getint("options", "login_timeout") - remembered = "AURREMEMBER" in conn.cookies and bool( - conn.cookies.get("AURREMEMBER") - ) + remembered = conn.cookies.get("AURREMEMBER") == "True" if remembered: timeout = aurweb.config.getint("options", "persistent_cookie_timeout") diff --git a/aurweb/cookies.py b/aurweb/cookies.py index 841e9adc..2bfcf7a7 100644 --- a/aurweb/cookies.py +++ b/aurweb/cookies.py @@ -65,7 +65,7 @@ def update_response_cookies( "AURLANG", aurlang, secure=secure, httponly=secure, samesite=samesite() ) if aursid: - remember_me = bool(request.cookies.get("AURREMEMBER", False)) + remember_me = request.cookies.get("AURREMEMBER") == "True" response.set_cookie( "AURSID", aursid, diff --git a/aurweb/users/update.py b/aurweb/users/update.py index 21349a39..ace9dace 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -131,7 +131,7 @@ def password( user.update_password(P) if user == request.user: - remember_me = request.cookies.get("AURREMEMBER", False) + remember_me = request.cookies.get("AURREMEMBER") == "True" # If the target user is the request user, login with # the updated password to update the Session record. From edc4ac332d1872c8b4b5fab5d9e789d66d36f795 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 25 May 2023 13:41:59 +0200 Subject: [PATCH 1313/1451] chore: remove setting AURLANG and AURTZ on account edit We don't need to set these cookies when an account is edited. These settings are saved to the DB anyways. (and they are picked up from there as well for any web requests, when no cookies are given) Setting these cookies can even be counter-productive: Imagine a TU/Dev editing another users account. They would overwrite their own cookies with the other users TZ/LANG settings. Signed-off-by: moson-mo --- aurweb/cookies.py | 12 ------------ aurweb/routers/accounts.py | 6 ++---- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/aurweb/cookies.py b/aurweb/cookies.py index 2bfcf7a7..cb4396d7 100644 --- a/aurweb/cookies.py +++ b/aurweb/cookies.py @@ -38,8 +38,6 @@ def timeout(extended: bool) -> int: def update_response_cookies( request: Request, response: Response, - aurtz: str = None, - aurlang: str = None, aursid: str = None, ) -> Response: """Update session cookies. This method is particularly useful @@ -50,20 +48,10 @@ def update_response_cookies( :param request: FastAPI request :param response: FastAPI response - :param aurtz: Optional AURTZ cookie value - :param aurlang: Optional AURLANG cookie value :param aursid: Optional AURSID cookie value :returns: Updated response """ secure = config.getboolean("options", "disable_http_login") - if aurtz: - response.set_cookie( - "AURTZ", aurtz, secure=secure, httponly=secure, samesite=samesite() - ) - if aurlang: - response.set_cookie( - "AURLANG", aurlang, secure=secure, httponly=secure, samesite=samesite() - ) if aursid: remember_me = request.cookies.get("AURREMEMBER") == "True" response.set_cookie( diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 77988d7f..010aae58 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -8,7 +8,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, or_ import aurweb.config -from aurweb import aur_logging, cookies, db, l10n, models, util +from aurweb import aur_logging, db, l10n, models, util from aurweb.auth import account_type_required, creds, requires_auth, requires_guest from aurweb.captcha import get_captcha_salts from aurweb.exceptions import ValidationError, handle_form_exceptions @@ -473,9 +473,7 @@ async def account_edit_post( if not errors: context["complete"] = True - # Update cookies with requests, in case they were changed. - response = render_template(request, "account/edit.html", context) - return cookies.update_response_cookies(request, response, aurtz=TZ, aurlang=L) + return render_template(request, "account/edit.html", context) @router.get("/account/{username}") From 638ca7b1d081f14c21db6d5c40e23678b865d258 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 25 May 2023 13:47:50 +0200 Subject: [PATCH 1314/1451] chore: remove setting AURLANG and AURTZ on login We don't need to set these cookies when logging in. These settings are saved to the DB anyways. (and they are picked up from there as well for any web requests, when no cookies are given) Signed-off-by: moson-mo --- aurweb/routers/auth.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 0e675559..71547429 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -85,20 +85,6 @@ async def login_post( httponly=secure, samesite=cookies.samesite(), ) - response.set_cookie( - "AURTZ", - user.Timezone, - secure=secure, - httponly=secure, - samesite=cookies.samesite(), - ) - response.set_cookie( - "AURLANG", - user.LangPreference, - secure=secure, - httponly=secure, - samesite=cookies.samesite(), - ) response.set_cookie( "AURREMEMBER", remember_me, @@ -125,5 +111,5 @@ async def logout(request: Request, next: str = Form(default="/")): # to redirect to a get request. response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER) response.delete_cookie("AURSID") - response.delete_cookie("AURTZ") + response.delete_cookie("AURREMEMBER") return response From 57c154a72cc6dbc997c07b159e76a1ddd5cc02ee Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 25 May 2023 14:07:27 +0200 Subject: [PATCH 1315/1451] fix: increase expiry for AURLANG cookie; only set when needed We add a new config option for cookies with a 400 day lifetime. AURLANG should survive longer for unauthenticated users. Today they have to set this again after each browser restart. (for users whose browsers wipe session cookies on close) authenticated users don't need this cookie since the setting is saved to the DB Signed-off-by: moson-mo --- aurweb/routers/html.py | 29 +++++++++++++++++++---------- conf/config.defaults | 4 ++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 33aeb904..38303837 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -56,19 +56,28 @@ async def language( query_string = "?" + q if q else str() - # If the user is authenticated, update the user's LangPreference. - if request.user.is_authenticated(): - with db.begin(): - request.user.LangPreference = set_lang - - # In any case, set the response's AURLANG cookie that never expires. response = RedirectResponse( url=f"{next}{query_string}", status_code=HTTPStatus.SEE_OTHER ) - secure = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie( - "AURLANG", set_lang, secure=secure, httponly=secure, samesite=cookies.samesite() - ) + + # If the user is authenticated, update the user's LangPreference. + # Otherwise set an AURLANG cookie + if request.user.is_authenticated(): + with db.begin(): + request.user.LangPreference = set_lang + else: + secure = aurweb.config.getboolean("options", "disable_http_login") + perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout") + + response.set_cookie( + "AURLANG", + set_lang, + secure=secure, + httponly=secure, + max_age=perma_timeout, + samesite=cookies.samesite(), + ) + return response diff --git a/conf/config.defaults b/conf/config.defaults index bb390d8a..17e81b7b 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -14,8 +14,12 @@ passwd_min_len = 8 default_lang = en default_timezone = UTC sql_debug = 0 +; 2 hours - default login_timeout login_timeout = 7200 +; 30 days - default persistent_cookie_timeout persistent_cookie_timeout = 2592000 +; 400 days - default permanent_cookie_timeout +permanent_cookie_timeout = 34560000 max_filesize_uncompressed = 8388608 disable_http_login = 1 aur_location = https://aur.archlinux.org From d3663772313037d3734b7795f0f8828e625a5e2e Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 25 May 2023 14:20:38 +0200 Subject: [PATCH 1316/1451] fix: make AURREMEMBER cookie a permanent one If it's a session cookie it poses issues for users whose browsers wipe session cookies after close. They'd be logged out early even if they chose the "remember me" option when they log in. Signed-off-by: moson-mo --- aurweb/routers/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 71547429..98a655e3 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -70,6 +70,7 @@ async def login_post( return await login_template(request, next, errors=["Account Suspended"]) cookie_timeout = cookies.timeout(remember_me) + perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout") sid = _retry_login(request, user, passwd, cookie_timeout) if not sid: return await login_template(request, next, errors=["Bad username or password."]) @@ -88,6 +89,7 @@ async def login_post( response.set_cookie( "AURREMEMBER", remember_me, + max_age=perma_timeout, secure=secure, httponly=secure, samesite=cookies.samesite(), From 0807ae6b7caada569c8fab45c4f3403ff950b6d1 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 25 May 2023 14:22:51 +0200 Subject: [PATCH 1317/1451] test: add tests for cookie handling add a bunch of test cases to ensure our cookies work properly Signed-off-by: moson-mo --- test/test_cookies.py | 145 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 test/test_cookies.py diff --git a/test/test_cookies.py b/test/test_cookies.py new file mode 100644 index 00000000..a0ace68f --- /dev/null +++ b/test/test_cookies.py @@ -0,0 +1,145 @@ +from datetime import datetime + +import pytest +from fastapi.testclient import TestClient + +from aurweb import config, db +from aurweb.asgi import app +from aurweb.models.account_type import USER_ID +from aurweb.models.user import User +from aurweb.testing.requests import Request + +# Some test global constants. +TEST_USERNAME = "test" +TEST_EMAIL = "test@example.org" +TEST_REFERER = { + "referer": config.get("options", "aur_location") + "/login", +} + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def client() -> TestClient: + client = TestClient(app=app) + + # Necessary for forged login CSRF protection on the login route. Set here + # instead of only on the necessary requests for convenience. + client.headers.update(TEST_REFERER) + + # disable redirects for our tests + client.follow_redirects = False + yield client + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create( + User, + Username=TEST_USERNAME, + Email=TEST_EMAIL, + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield user + + +def test_cookies_login(client: TestClient, user: User): + # Log in with "Remember me" disabled + data = {"user": user.Username, "passwd": "testPassword", "next": "/"} + with client as request: + resp = request.post("/login", data=data) + + local_time = int(datetime.now().timestamp()) + expected_timeout = local_time + config.getint("options", "login_timeout") + expected_permanent = local_time + config.getint( + "options", "permanent_cookie_timeout" + ) + + # Check if we got permanent cookies with expected expiry date. + # Allow 1 second difference to account for timing issues + # between the request and current time. + assert "AURSID", "AURREMEMBER" in resp.cookies + for cookie in resp.cookies.jar: + if cookie.name == "AURSID": + assert abs(cookie.expires - expected_timeout) < 2 + + if cookie.name == "AURREMEMBER": + assert abs(cookie.expires - expected_permanent) < 2 + + # Make some random http call. + # We should get an (updated) AURSID cookie with each request. + sid = resp.cookies.get("AURSID") + with client as request: + request.cookies = {"AURSID": sid} + resp = request.get("/") + + assert "AURSID" in resp.cookies + + # Log out + with client as request: + request.cookies = resp.cookies + resp = request.post("/logout", data=data) + + # Make sure AURSID cookie is removed after logout + assert "AURSID" not in resp.cookies + + # Log in with "Remember me" enabled + data = { + "user": user.Username, + "passwd": "testPassword", + "next": "/", + "remember_me": "True", + } + with client as request: + resp = request.post("/login", data=data) + + # Check if we got a permanent cookie with expected expiry date. + # Allow 1 second difference to account for timing issues + # between the request and current time. + expected_persistent = local_time + config.getint( + "options", "persistent_cookie_timeout" + ) + assert "AURSID" in resp.cookies + for cookie in resp.cookies.jar: + if cookie.name in "AURSID": + assert abs(cookie.expires - expected_persistent) < 2 + + +def test_cookie_language(client: TestClient, user: User): + # Unauthenticated reqeuests should set AURLANG cookie + data = {"set_lang": "en", "next": "/"} + with client as request: + resp = request.post("/language", data=data) + + local_time = int(datetime.now().timestamp()) + expected_permanent = local_time + config.getint( + "options", "permanent_cookie_timeout" + ) + + # Make sure we got an AURLANG cookie + assert "AURLANG" in resp.cookies + assert resp.cookies.get("AURLANG") == "en" + + # Check if we got a permanent cookie with expected expiry date. + # Allow 1 second difference to account for timing issues + # between the request and current time. + for cookie in resp.cookies.jar: + if cookie.name in "AURLANG": + assert abs(cookie.expires - expected_permanent) < 2 + + # Login and change the language + # We should not get a cookie since we store + # our language setting in the DB anyways + sid = user.login(Request(), "testPassword") + data = {"set_lang": "en", "next": "/"} + with client as request: + request.cookies = {"AURSID": sid} + resp = request.post("/language", data=data) + + assert "AURLANG" not in resp.cookies From 22fe4a988a31c78f01ddc62bd1d9cb9c5074046b Mon Sep 17 00:00:00 2001 From: moson-mo Date: Fri, 26 May 2023 11:21:16 +0200 Subject: [PATCH 1318/1451] fix: make AURSID a session cookie if "remember me" is not checked This should match more closely the expectation of a user. A session cookie should vanish on browser close and you thus they need to authenticate again. There is no need to bump the expiration of AURSID either, so we can remove that part. Signed-off-by: moson-mo --- aurweb/cookies.py | 33 --------------------------------- aurweb/routers/auth.py | 7 ++++++- aurweb/templates.py | 13 ++----------- doc/web-auth.md | 18 ++++++------------ test/test_cookies.py | 30 ++++++++++++++++++------------ 5 files changed, 32 insertions(+), 69 deletions(-) diff --git a/aurweb/cookies.py b/aurweb/cookies.py index cb4396d7..022cff1e 100644 --- a/aurweb/cookies.py +++ b/aurweb/cookies.py @@ -1,6 +1,3 @@ -from fastapi import Request -from fastapi.responses import Response - from aurweb import config @@ -33,33 +30,3 @@ def timeout(extended: bool) -> int: if bool(extended): timeout = config.getint("options", "persistent_cookie_timeout") return timeout - - -def update_response_cookies( - request: Request, - response: Response, - aursid: str = None, -) -> Response: - """Update session cookies. This method is particularly useful - when updating a cookie which was already set. - - The AURSID cookie's expiration is based on the AURREMEMBER cookie, - which is retrieved from `request`. - - :param request: FastAPI request - :param response: FastAPI response - :param aursid: Optional AURSID cookie value - :returns: Updated response - """ - secure = config.getboolean("options", "disable_http_login") - if aursid: - remember_me = request.cookies.get("AURREMEMBER") == "True" - response.set_cookie( - "AURSID", - aursid, - secure=secure, - httponly=secure, - max_age=timeout(remember_me), - samesite=samesite(), - ) - return response diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 98a655e3..46dee3a4 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -69,7 +69,12 @@ async def login_post( if user.Suspended: return await login_template(request, next, errors=["Account Suspended"]) - cookie_timeout = cookies.timeout(remember_me) + # If "remember me" was not ticked, we set a session cookie for AURSID, + # otherwise we make it a persistent cookie + cookie_timeout = None + if remember_me: + cookie_timeout = aurweb.config.getint("options", "persistent_cookie_timeout") + perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout") sid = _retry_login(request, user, passwd, cookie_timeout) if not sid: diff --git a/aurweb/templates.py b/aurweb/templates.py index 89316d6d..d20cbe85 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -10,7 +10,7 @@ from fastapi import Request from fastapi.responses import HTMLResponse import aurweb.config -from aurweb import cookies, l10n, time +from aurweb import l10n, time # Prepare jinja2 objects. _loader = jinja2.FileSystemLoader( @@ -145,13 +145,4 @@ def render_template( ): """Render a template as an HTMLResponse.""" rendered = render_raw_template(request, path, context) - response = HTMLResponse(rendered, status_code=int(status_code)) - - sid = None - if request.user.is_authenticated(): - sid = request.cookies.get("AURSID") - - # Re-emit SID via update_response_cookies with an updated expiration. - # This extends the life of a user session based on the AURREMEMBER - # cookie, which is always set to the "Remember Me" state on login. - return cookies.update_response_cookies(request, response, aursid=sid) + return HTMLResponse(rendered, status_code=int(status_code)) diff --git a/doc/web-auth.md b/doc/web-auth.md index dbb4403d..c8604fed 100644 --- a/doc/web-auth.md +++ b/doc/web-auth.md @@ -22,17 +22,11 @@ in the following ways: ### Max-Age The value used for the `AURSID` Max-Age attribute is decided based -off of the "Remember Me" checkbox on the login page. Both paths -use their own independent configuration for the number of seconds -that each type of session should stay alive. - -- "Remember Me" unchecked while logging in - - `options.login_timeout` is used -- "Remember Me" checked while logging in - - `options.persistent_cookie_timeout` is used - -Both `options.login_timeout` and `options.persistent_cookie_timeout` -indicate the number of seconds the session should live. +off of the "Remember Me" checkbox on the login page. If it was not +checked, we don't set Max-Age and it becomes a session cookie. +Otherwise we make it a persistent cookie and for the expiry date +we use `options.persistent_cookie_timeout`. +It indicates the number of seconds the session should live. ### Notes @@ -89,7 +83,7 @@ The following list of steps describes exactly how this verification works: 1. Was the `AURSID` cookie delivered? 1. No, the algorithm ends, you are considered unauthenticated 2. Yes, move on to 2 -2. Was the `AURREMEMBER` cookie delivered with a value of 1? +2. Was the `AURREMEMBER` cookie delivered with a value of `True`? 1. No, set the expected session timeout **T** to `options.login_timeout` 2. Yes, set the expected session timeout **T** to `options.persistent_cookie_timeout` diff --git a/test/test_cookies.py b/test/test_cookies.py index a0ace68f..dd4143cb 100644 --- a/test/test_cookies.py +++ b/test/test_cookies.py @@ -1,4 +1,5 @@ from datetime import datetime +from http import HTTPStatus import pytest from fastapi.testclient import TestClient @@ -56,7 +57,6 @@ def test_cookies_login(client: TestClient, user: User): resp = request.post("/login", data=data) local_time = int(datetime.now().timestamp()) - expected_timeout = local_time + config.getint("options", "login_timeout") expected_permanent = local_time + config.getint( "options", "permanent_cookie_timeout" ) @@ -64,22 +64,15 @@ def test_cookies_login(client: TestClient, user: User): # Check if we got permanent cookies with expected expiry date. # Allow 1 second difference to account for timing issues # between the request and current time. + # AURSID should be a session cookie (no expiry date) assert "AURSID", "AURREMEMBER" in resp.cookies for cookie in resp.cookies.jar: if cookie.name == "AURSID": - assert abs(cookie.expires - expected_timeout) < 2 + assert cookie.expires is None if cookie.name == "AURREMEMBER": assert abs(cookie.expires - expected_permanent) < 2 - - # Make some random http call. - # We should get an (updated) AURSID cookie with each request. - sid = resp.cookies.get("AURSID") - with client as request: - request.cookies = {"AURSID": sid} - resp = request.get("/") - - assert "AURSID" in resp.cookies + assert cookie.value == "False" # Log out with client as request: @@ -102,14 +95,27 @@ def test_cookies_login(client: TestClient, user: User): # Check if we got a permanent cookie with expected expiry date. # Allow 1 second difference to account for timing issues # between the request and current time. + # AURSID should be a persistent cookie expected_persistent = local_time + config.getint( "options", "persistent_cookie_timeout" ) - assert "AURSID" in resp.cookies + assert "AURSID", "AURREMEMBER" in resp.cookies for cookie in resp.cookies.jar: if cookie.name in "AURSID": assert abs(cookie.expires - expected_persistent) < 2 + if cookie.name == "AURREMEMBER": + assert abs(cookie.expires - expected_permanent) < 2 + assert cookie.value == "True" + + # log in again even though we already have a session + with client as request: + resp = request.post("/login", data=data) + + # we are logged in already and should have been redirected + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/" + def test_cookie_language(client: TestClient, user: User): # Unauthenticated reqeuests should set AURLANG cookie From a7882c75339189dbb32145fcf88f02c08a4ff7b9 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Fri, 26 May 2023 23:02:38 +0200 Subject: [PATCH 1319/1451] refactor: remove session_time from user.login The parameter is not used, we can remove it and adapt the callers. Signed-off-by: moson-mo --- aurweb/cookies.py | 24 ------------------------ aurweb/models/user.py | 2 +- aurweb/routers/auth.py | 6 +++--- aurweb/users/update.py | 6 ++---- 4 files changed, 6 insertions(+), 32 deletions(-) diff --git a/aurweb/cookies.py b/aurweb/cookies.py index 022cff1e..84c43f9b 100644 --- a/aurweb/cookies.py +++ b/aurweb/cookies.py @@ -1,6 +1,3 @@ -from aurweb import config - - def samesite() -> str: """Produce cookie SameSite value. @@ -9,24 +6,3 @@ def samesite() -> str: :returns "lax" """ return "lax" - - -def timeout(extended: bool) -> int: - """Produce a session timeout based on `remember_me`. - - This method returns one of AUR_CONFIG's options.persistent_cookie_timeout - and options.login_timeout based on the `extended` argument. - - The `extended` argument is typically the value of the AURREMEMBER - cookie, defaulted to False. - - If `extended` is False, options.login_timeout is returned. Otherwise, - if `extended` is True, options.persistent_cookie_timeout is returned. - - :param extended: Flag which generates an extended timeout when True - :returns: Cookie timeout based on configuration options - """ - timeout = config.getint("options", "login_timeout") - if bool(extended): - timeout = config.getint("options", "persistent_cookie_timeout") - return timeout diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 9846d996..8612c259 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -95,7 +95,7 @@ class User(Base): def _login_approved(self, request: Request): return not is_banned(request) and not self.Suspended - def login(self, request: Request, password: str, session_time: int = 0) -> str: + def login(self, request: Request, password: str) -> str: """Login and authenticate a request.""" from aurweb import db diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 46dee3a4..88eaa0e6 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -29,8 +29,8 @@ async def login_get(request: Request, next: str = "/"): @db.retry_deadlock -def _retry_login(request: Request, user: User, passwd: str, cookie_timeout: int) -> str: - return user.login(request, passwd, cookie_timeout) +def _retry_login(request: Request, user: User, passwd: str) -> str: + return user.login(request, passwd) @router.post("/login", response_class=HTMLResponse) @@ -76,7 +76,7 @@ async def login_post( cookie_timeout = aurweb.config.getint("options", "persistent_cookie_timeout") perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout") - sid = _retry_login(request, user, passwd, cookie_timeout) + sid = _retry_login(request, user, passwd) if not sid: return await login_template(request, next, errors=["Bad username or password."]) diff --git a/aurweb/users/update.py b/aurweb/users/update.py index ace9dace..759088cd 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -2,7 +2,7 @@ from typing import Any from fastapi import Request -from aurweb import cookies, db, models, time, util +from aurweb import db, models, time, util from aurweb.models import SSHPubKey from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.util import strtobool @@ -131,11 +131,9 @@ def password( user.update_password(P) if user == request.user: - remember_me = request.cookies.get("AURREMEMBER") == "True" - # If the target user is the request user, login with # the updated password to update the Session record. - user.login(request, P, cookies.timeout(remember_me)) + user.login(request, P) @db.retry_deadlock From 49e98d64f4f1d6770f067db087f741eb8420548e Mon Sep 17 00:00:00 2001 From: moson-mo Date: Fri, 26 May 2023 23:03:38 +0200 Subject: [PATCH 1320/1451] chore: increase default session/cookie timeout change from 2 to 4 hours. Signed-off-by: moson-mo --- conf/config.defaults | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/config.defaults b/conf/config.defaults index 17e81b7b..c059444d 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -14,8 +14,8 @@ passwd_min_len = 8 default_lang = en default_timezone = UTC sql_debug = 0 -; 2 hours - default login_timeout -login_timeout = 7200 +; 4 hours - default login_timeout +login_timeout = 14400 ; 30 days - default persistent_cookie_timeout persistent_cookie_timeout = 2592000 ; 400 days - default permanent_cookie_timeout From d1a3fee9fe98ebc3bcdb4f472f8812fa738d3b12 Mon Sep 17 00:00:00 2001 From: renovate Date: Fri, 26 May 2023 18:24:33 +0000 Subject: [PATCH 1321/1451] fix(deps): update all non-major dependencies --- poetry.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3a273be4..06855cac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1269,25 +1269,25 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "4.23.1" +version = "4.23.2" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.23.1-cp310-abi3-win32.whl", hash = "sha256:410bcc0a5b279f634d3e16082ce221dfef7c3392fac723500e2e64d1806dd2be"}, - {file = "protobuf-4.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:32e78beda26d7a101fecf15d7a4a792278a0d26a31bc327ff05564a9d68ab8ee"}, - {file = "protobuf-4.23.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f9510cac91e764e86acd74e2b7f7bc5e6127a7f3fb646d7c8033cfb84fd1176a"}, - {file = "protobuf-4.23.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:346990f634272caac1f09efbcfbbacb23098b1f606d172534c6fa2d9758bb436"}, - {file = "protobuf-4.23.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3ce113b3f3362493bddc9069c2163a38f240a9ed685ff83e7bcb756b05e1deb0"}, - {file = "protobuf-4.23.1-cp37-cp37m-win32.whl", hash = "sha256:2036a3a1e7fc27f973fa0a7888dce712393af644f4695385f117886abc792e39"}, - {file = "protobuf-4.23.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3b8905eafe4439076e1f58e9d1fa327025fd2777cf90f14083092ae47f77b0aa"}, - {file = "protobuf-4.23.1-cp38-cp38-win32.whl", hash = "sha256:5b9cd6097e6acae48a68cb29b56bc79339be84eca65b486910bb1e7a30e2b7c1"}, - {file = "protobuf-4.23.1-cp38-cp38-win_amd64.whl", hash = "sha256:decf119d54e820f298ee6d89c72d6b289ea240c32c521f00433f9dc420595f38"}, - {file = "protobuf-4.23.1-cp39-cp39-win32.whl", hash = "sha256:91fac0753c3c4951fbb98a93271c43cc7cf3b93cf67747b3e600bb1e5cc14d61"}, - {file = "protobuf-4.23.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac50be82491369a9ec3710565777e4da87c6d2e20404e0abb1f3a8f10ffd20f0"}, - {file = "protobuf-4.23.1-py3-none-any.whl", hash = "sha256:65f0ac96ef67d7dd09b19a46aad81a851b6f85f89725577f16de38f2d68ad477"}, - {file = "protobuf-4.23.1.tar.gz", hash = "sha256:95789b569418a3e32a53f43d7763be3d490a831e9c08042539462b6d972c2d7e"}, + {file = "protobuf-4.23.2-cp310-abi3-win32.whl", hash = "sha256:384dd44cb4c43f2ccddd3645389a23ae61aeb8cfa15ca3a0f60e7c3ea09b28b3"}, + {file = "protobuf-4.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:09310bce43353b46d73ba7e3bca78273b9bc50349509b9698e64d288c6372c2a"}, + {file = "protobuf-4.23.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b2cfab63a230b39ae603834718db74ac11e52bccaaf19bf20f5cce1a84cf76df"}, + {file = "protobuf-4.23.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:c52cfcbfba8eb791255edd675c1fe6056f723bf832fa67f0442218f8817c076e"}, + {file = "protobuf-4.23.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:86df87016d290143c7ce3be3ad52d055714ebaebb57cc659c387e76cfacd81aa"}, + {file = "protobuf-4.23.2-cp37-cp37m-win32.whl", hash = "sha256:281342ea5eb631c86697e1e048cb7e73b8a4e85f3299a128c116f05f5c668f8f"}, + {file = "protobuf-4.23.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ce744938406de1e64b91410f473736e815f28c3b71201302612a68bf01517fea"}, + {file = "protobuf-4.23.2-cp38-cp38-win32.whl", hash = "sha256:6c081863c379bb1741be8f8193e893511312b1d7329b4a75445d1ea9955be69e"}, + {file = "protobuf-4.23.2-cp38-cp38-win_amd64.whl", hash = "sha256:25e3370eda26469b58b602e29dff069cfaae8eaa0ef4550039cc5ef8dc004511"}, + {file = "protobuf-4.23.2-cp39-cp39-win32.whl", hash = "sha256:efabbbbac1ab519a514579ba9ec52f006c28ae19d97915951f69fa70da2c9e91"}, + {file = "protobuf-4.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:54a533b971288af3b9926e53850c7eb186886c0c84e61daa8444385a4720297f"}, + {file = "protobuf-4.23.2-py3-none-any.whl", hash = "sha256:8da6070310d634c99c0db7df48f10da495cc283fd9e9234877f0cd182d43ab7f"}, + {file = "protobuf-4.23.2.tar.gz", hash = "sha256:20874e7ca4436f683b64ebdbee2129a5a2c301579a67d1a7dda2cdf62fb7f5f7"}, ] [[package]] From 2709585a70a9437d10736e18141e44af053b3b55 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 27 May 2023 11:24:46 +0000 Subject: [PATCH 1322/1451] fix(deps): update dependency fastapi to v0.95.2 --- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 06855cac..16b0f15a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -551,19 +551,19 @@ lua = ["lupa (>=1.14,<2.0)"] [[package]] name = "fastapi" -version = "0.95.1" +version = "0.95.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"}, - {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"}, + {file = "fastapi-0.95.2-py3-none-any.whl", hash = "sha256:d374dbc4ef2ad9b803899bd3360d34c534adc574546e25314ab72c0c4411749f"}, + {file = "fastapi-0.95.2.tar.gz", hash = "sha256:4d9d3e8c71c73f11874bcf5e33626258d143252e329a01002f767306c64fb982"}, ] [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = ">=0.26.1,<0.27.0" +starlette = ">=0.27.0,<0.28.0" [package.extras] all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] @@ -1724,14 +1724,14 @@ parse = ">=1.19.0,<2.0.0" [[package]] name = "starlette" -version = "0.26.1" +version = "0.27.0" description = "The little ASGI library that shines." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, - {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, + {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, + {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, ] [package.dependencies] From ed2f85ad047f4659b03f7b3730ff117522feaaa6 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 27 May 2023 14:28:48 +0100 Subject: [PATCH 1323/1451] chore(release): prepare for 6.2.4 Signed-off-by: Leonidas Spyropoulos --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f04deb14..e25fe90a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.2.3" +version = "v6.2.4" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From e9cc2fb4373cae8c24bab5043f24c019829ceb99 Mon Sep 17 00:00:00 2001 From: Christian Heusel Date: Fri, 2 Jun 2023 16:19:44 +0200 Subject: [PATCH 1324/1451] change: only require .SRCINFO in the latest revision This is done in order to relax the constraints so that dropping packages from the official repos can be done with preserving their history. Its sufficient to also have this present in the latest commit of a push. Signed-off-by: Christian Heusel --- aurweb/git/update.py | 163 +++++++++++++++++++--------------------- test/t1300-git-update.t | 4 +- 2 files changed, 80 insertions(+), 87 deletions(-) diff --git a/aurweb/git/update.py b/aurweb/git/update.py index b1256fdb..467b540f 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -258,6 +258,63 @@ def die_commit(msg, commit): exit(1) +def validate_metadata(metadata, commit): # noqa: C901 + try: + metadata_pkgbase = metadata["pkgbase"] + except KeyError: + die_commit( + "invalid .SRCINFO, does not contain a pkgbase (is the file empty?)", + str(commit.id), + ) + if not re.match(repo_regex, metadata_pkgbase): + die_commit("invalid pkgbase: {:s}".format(metadata_pkgbase), str(commit.id)) + + if not metadata["packages"]: + die_commit("missing pkgname entry", str(commit.id)) + + for pkgname in set(metadata["packages"].keys()): + pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata) + + for field in ("pkgver", "pkgrel", "pkgname"): + if field not in pkginfo: + die_commit( + "missing mandatory field: {:s}".format(field), str(commit.id) + ) + + if "epoch" in pkginfo and not pkginfo["epoch"].isdigit(): + die_commit("invalid epoch: {:s}".format(pkginfo["epoch"]), str(commit.id)) + + if not re.match(r"[a-z0-9][a-z0-9\.+_-]*$", pkginfo["pkgname"]): + die_commit( + "invalid package name: {:s}".format(pkginfo["pkgname"]), + str(commit.id), + ) + + max_len = {"pkgname": 255, "pkgdesc": 255, "url": 8000} + for field in max_len.keys(): + if field in pkginfo and len(pkginfo[field]) > max_len[field]: + die_commit( + "{:s} field too long: {:s}".format(field, pkginfo[field]), + str(commit.id), + ) + + for field in ("install", "changelog"): + if field in pkginfo and not pkginfo[field] in commit.tree: + die_commit( + "missing {:s} file: {:s}".format(field, pkginfo[field]), + str(commit.id), + ) + + for field in extract_arch_fields(pkginfo, "source"): + fname = field["value"] + if len(fname) > 8000: + die_commit("source entry too long: {:s}".format(fname), str(commit.id)) + if "://" in fname or "lp:" in fname: + continue + if fname not in commit.tree: + die_commit("missing source file: {:s}".format(fname), str(commit.id)) + + def main(): # noqa: C901 repo = pygit2.Repository(repo_path) @@ -295,12 +352,30 @@ def main(): # noqa: C901 if sha1_old != "0" * 40: walker.hide(sha1_old) + head_commit = repo[sha1_new] + if ".SRCINFO" not in head_commit.tree: + die_commit("missing .SRCINFO", str(head_commit.id)) + + # Read .SRCINFO from the HEAD commit. + metadata_raw = repo[head_commit.tree[".SRCINFO"].id].data.decode() + (metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw) + if errors: + sys.stderr.write( + "error: The following errors occurred " "when parsing .SRCINFO in commit\n" + ) + sys.stderr.write("error: {:s}:\n".format(str(head_commit.id))) + for error in errors: + for err in error["error"]: + sys.stderr.write("error: line {:d}: {:s}\n".format(error["line"], err)) + exit(1) + + # check if there is a correct .SRCINFO file in the latest revision + validate_metadata(metadata, head_commit) + # Validate all new commits. for commit in walker: - for fname in (".SRCINFO", "PKGBUILD"): - if fname not in commit.tree: - die_commit("missing {:s}".format(fname), str(commit.id)) - + if "PKGBUILD" not in commit.tree: + die_commit("missing PKGBUILD", str(commit.id)) for treeobj in commit.tree: blob = repo[treeobj.id] @@ -320,82 +395,6 @@ def main(): # noqa: C901 str(commit.id), ) - metadata_raw = repo[commit.tree[".SRCINFO"].id].data.decode() - (metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw) - if errors: - sys.stderr.write( - "error: The following errors occurred " - "when parsing .SRCINFO in commit\n" - ) - sys.stderr.write("error: {:s}:\n".format(str(commit.id))) - for error in errors: - for err in error["error"]: - sys.stderr.write( - "error: line {:d}: {:s}\n".format(error["line"], err) - ) - exit(1) - - try: - metadata_pkgbase = metadata["pkgbase"] - except KeyError: - die_commit( - "invalid .SRCINFO, does not contain a pkgbase (is the file empty?)", - str(commit.id), - ) - if not re.match(repo_regex, metadata_pkgbase): - die_commit("invalid pkgbase: {:s}".format(metadata_pkgbase), str(commit.id)) - - if not metadata["packages"]: - die_commit("missing pkgname entry", str(commit.id)) - - for pkgname in set(metadata["packages"].keys()): - pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata) - - for field in ("pkgver", "pkgrel", "pkgname"): - if field not in pkginfo: - die_commit( - "missing mandatory field: {:s}".format(field), str(commit.id) - ) - - if "epoch" in pkginfo and not pkginfo["epoch"].isdigit(): - die_commit( - "invalid epoch: {:s}".format(pkginfo["epoch"]), str(commit.id) - ) - - if not re.match(r"[a-z0-9][a-z0-9\.+_-]*$", pkginfo["pkgname"]): - die_commit( - "invalid package name: {:s}".format(pkginfo["pkgname"]), - str(commit.id), - ) - - max_len = {"pkgname": 255, "pkgdesc": 255, "url": 8000} - for field in max_len.keys(): - if field in pkginfo and len(pkginfo[field]) > max_len[field]: - die_commit( - "{:s} field too long: {:s}".format(field, pkginfo[field]), - str(commit.id), - ) - - for field in ("install", "changelog"): - if field in pkginfo and not pkginfo[field] in commit.tree: - die_commit( - "missing {:s} file: {:s}".format(field, pkginfo[field]), - str(commit.id), - ) - - for field in extract_arch_fields(pkginfo, "source"): - fname = field["value"] - if len(fname) > 8000: - die_commit( - "source entry too long: {:s}".format(fname), str(commit.id) - ) - if "://" in fname or "lp:" in fname: - continue - if fname not in commit.tree: - die_commit( - "missing source file: {:s}".format(fname), str(commit.id) - ) - # Display a warning if .SRCINFO is unchanged. if sha1_old not in ("0000000000000000000000000000000000000000", sha1_new): srcinfo_id_old = repo[sha1_old].tree[".SRCINFO"].id @@ -403,10 +402,6 @@ def main(): # noqa: C901 if srcinfo_id_old == srcinfo_id_new: warn(".SRCINFO unchanged. " "The package database will not be updated!") - # Read .SRCINFO from the HEAD commit. - metadata_raw = repo[repo[sha1_new].tree[".SRCINFO"].id].data.decode() - (metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw) - # Ensure that the package base name matches the repository name. metadata_pkgbase = metadata["pkgbase"] if metadata_pkgbase != pkgbase: diff --git a/test/t1300-git-update.t b/test/t1300-git-update.t index e9d943c0..a8ea5cab 100755 --- a/test/t1300-git-update.t +++ b/test/t1300-git-update.t @@ -175,10 +175,8 @@ test_expect_success 'Removing .SRCINFO with a follow-up fix.' ' git -C aur.git commit -q -m "Remove .SRCINFO" && git -C aur.git revert --no-edit HEAD && new=$(git -C aur.git rev-parse HEAD) && - test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && - grep -q "^error: missing .SRCINFO$" actual + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 ' test_expect_success 'Removing PKGBUILD.' ' From 26b2566b3fa5fe7165deaedd6e0be6b7da6a3b0f Mon Sep 17 00:00:00 2001 From: Christian Heusel Date: Thu, 8 Jun 2023 12:42:31 +0200 Subject: [PATCH 1325/1451] change: print the user name if connecting via ssh this is similar to the message that gitlab produces: $ ssh -T aur.archlinux.org Welcome to AUR, gromit! Interactive shell is disabled. Try `ssh ssh://aur@aur.archlinux.org help` for a list of commands. $ ssh -T gitlab.archlinux.org Welcome to GitLab, @gromit! Signed-off-by: Christian Heusel --- aurweb/git/serve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aurweb/git/serve.py b/aurweb/git/serve.py index 8dbbf3f7..2ac1f10e 100755 --- a/aurweb/git/serve.py +++ b/aurweb/git/serve.py @@ -648,7 +648,7 @@ def main(): ssh_client = os.environ.get("SSH_CLIENT") if not ssh_cmd: - die_with_help("Interactive shell is disabled.") + die_with_help(f"Welcome to AUR, {user}! Interactive shell is disabled.") cmdargv = shlex.split(ssh_cmd) action = cmdargv[0] remote_addr = ssh_client.split(" ")[0] if ssh_client else None From 1c11c901a2d389bf497e886c990b634a70a4df7a Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sat, 10 Jun 2023 09:40:35 +0200 Subject: [PATCH 1326/1451] feat: switch requests filter for pkgname to "contains" Use "contains" filtering instead of an exact match when a package name filter is given. This makes it easier to find requests for a "group" of packages. Signed-off-by: moson-mo --- aurweb/routers/requests.py | 4 ++-- test/test_requests.py | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index 585dc157..4cfda269 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -99,9 +99,9 @@ async def requests( in_filters.append(REJECTED_ID) filtered = query.filter(PackageRequest.Status.in_(in_filters)) - # Name filter + # Name filter (contains) if filter_pkg_name: - filtered = filtered.filter(PackageBase.Name == filter_pkg_name) + filtered = filtered.filter(PackageBase.Name.like(f"%{filter_pkg_name}%")) # Additionally filter for requests made from package maintainer if filter_maintainer_requests: diff --git a/test/test_requests.py b/test/test_requests.py index 7ddb76a0..eb88cd94 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -925,14 +925,28 @@ def test_requests_with_package_name_filter( request.cookies = cookies resp = request.get( "/requests", - params={"filter_pkg_name": packages[0].PackageBase.Name}, + params={"filter_pkg_name": "kg_1"}, ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - # We only expect 1 request for our first package - assert len(rows) == 1 + # We expect 11 requests for all packages containing "kg_1" + assert len(rows) == 11 + + # test as TU, no results + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={"filter_pkg_name": "x"}, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We expect 0 requests since we don't have anything containing "x" + assert len(rows) == 0 # test as regular user, not related to our package cookies = {"AURSID": user2.login(Request(), "testPassword")} From ed17486da6ada6c6bb1ca6fb1fddfbd1ccee4708 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 11 Jun 2023 12:20:02 +0200 Subject: [PATCH 1327/1451] change(git): allow keys/pgp subdir with .asc files This allows migration of git history for packages dropped from a repo to AUR in case they contain PGP key material Signed-off-by: moson-mo --- aurweb/git/update.py | 53 +++++++++++++++------ test/t1300-git-update.t | 103 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 15 deletions(-) diff --git a/aurweb/git/update.py b/aurweb/git/update.py index 467b540f..cd7813e0 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -315,6 +315,14 @@ def validate_metadata(metadata, commit): # noqa: C901 die_commit("missing source file: {:s}".format(fname), str(commit.id)) +def validate_blob_size(blob: pygit2.Object, commit: pygit2.Commit): + if isinstance(blob, pygit2.Blob) and blob.size > max_blob_size: + die_commit( + "maximum blob size ({:s}) exceeded".format(size_humanize(max_blob_size)), + str(commit.id), + ) + + def main(): # noqa: C901 repo = pygit2.Repository(repo_path) @@ -376,25 +384,42 @@ def main(): # noqa: C901 for commit in walker: if "PKGBUILD" not in commit.tree: die_commit("missing PKGBUILD", str(commit.id)) + + # Iterate over files in root dir for treeobj in commit.tree: - blob = repo[treeobj.id] - - if isinstance(blob, pygit2.Tree): + # Don't allow any subdirs besides "keys/" + if isinstance(treeobj, pygit2.Tree) and treeobj.name != "keys": die_commit( - "the repository must not contain subdirectories", str(commit.id) - ) - - if not isinstance(blob, pygit2.Blob): - die_commit("not a blob object: {:s}".format(treeobj), str(commit.id)) - - if blob.size > max_blob_size: - die_commit( - "maximum blob size ({:s}) exceeded".format( - size_humanize(max_blob_size) - ), + "the repository must not contain subdirectories", str(commit.id), ) + # Check size of files in root dir + validate_blob_size(treeobj, commit) + + # If we got a subdir keys/, + # make sure it only contains a pgp/ subdir with key files + if "keys" in commit.tree: + # Check for forbidden files/dirs in keys/ + for keyobj in commit.tree["keys"]: + if not isinstance(keyobj, pygit2.Tree) or keyobj.name != "pgp": + die_commit( + "the keys/ subdir may only contain a pgp/ directory", + str(commit.id), + ) + # Check for forbidden files in keys/pgp/ + if "keys/pgp" in commit.tree: + for pgpobj in commit.tree["keys/pgp"]: + if not isinstance(pgpobj, pygit2.Blob) or not pgpobj.name.endswith( + ".asc" + ): + die_commit( + "the subdir may only contain .asc (PGP pub key) files", + str(commit.id), + ) + # Check file size for pgp key files + validate_blob_size(pgpobj, commit) + # Display a warning if .SRCINFO is unchanged. if sha1_old not in ("0000000000000000000000000000000000000000", sha1_new): srcinfo_id_old = repo[sha1_old].tree[".SRCINFO"].id diff --git a/test/t1300-git-update.t b/test/t1300-git-update.t index a8ea5cab..4fdb487b 100755 --- a/test/t1300-git-update.t +++ b/test/t1300-git-update.t @@ -191,7 +191,7 @@ test_expect_success 'Removing PKGBUILD.' ' grep -q "^error: missing PKGBUILD$" actual ' -test_expect_success 'Pushing a tree with a subdirectory.' ' +test_expect_success 'Pushing a tree with a forbidden subdirectory.' ' old=$(git -C aur.git rev-parse HEAD) && test_when_finished "git -C aur.git reset --hard $old" && mkdir aur.git/subdir && @@ -205,6 +205,107 @@ test_expect_success 'Pushing a tree with a subdirectory.' ' grep -q "^error: the repository must not contain subdirectories$" actual ' +test_expect_success 'Pushing a tree with an allowed subdirectory for pgp keys; wrong files.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + touch aur.git/keys/pgp/nonsense && + git -C aur.git add keys/pgp/nonsense && + git -C aur.git commit -q -m "Add some nonsense" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the subdir may only contain .asc (PGP pub key) files$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory for pgp keys; another subdir.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/bla/ && + touch aur.git/keys/pgp/bla/x.asc && + git -C aur.git add keys/pgp/bla/x.asc && + git -C aur.git commit -q -m "Add some nonsense" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the subdir may only contain .asc (PGP pub key) files$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory for pgp keys; wrong subdir.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/xyz/ && + touch aur.git/keys/xyz/x.asc && + git -C aur.git add keys/xyz/x.asc && + git -C aur.git commit -q -m "Add some nonsense" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the keys/ subdir may only contain a pgp/ directory$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys; additional files' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + touch aur.git/keys/pgp/x.asc && + touch aur.git/keys/nonsense && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git add keys/nonsense && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the keys/ subdir may only contain a pgp/ directory$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys; additional subdir' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + mkdir -p aur.git/somedir/ && + touch aur.git/keys/pgp/x.asc && + touch aur.git/somedir/nonsense && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git add somedir/nonsense && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the repository must not contain subdirectories$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys; keys to large' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + printf "%256001s" x > aur.git/keys/pgp/x.asc && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: maximum blob size (250.00KiB) exceeded$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + touch aur.git/keys/pgp/x.asc && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 +' + test_expect_success 'Pushing a tree with a large blob.' ' old=$(git -C aur.git rev-parse HEAD) && test_when_finished "git -C aur.git reset --hard $old" && From 58158505b06c1856420c22b1827f42eec450b477 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 11 Jun 2023 21:04:35 +0200 Subject: [PATCH 1328/1451] fix: browser hints for password fields Co-authored-by: eNV25 Signed-off-by: moson-mo --- templates/partials/account_form.html | 6 +++--- templates/passreset.html | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 4d135a56..28dc0cd5 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -246,7 +246,7 @@ -

    @@ -255,7 +255,7 @@ {% trans %}Re-type password{% endtrans %}: -

    @@ -333,7 +333,7 @@ -

    {% else %} diff --git a/templates/passreset.html b/templates/passreset.html index 6a31109f..08493fe9 100644 --- a/templates/passreset.html +++ b/templates/passreset.html @@ -26,14 +26,14 @@ {% trans %}Enter your new password:{% endtrans %} - {% trans %}Confirm your new password:{% endtrans %} - From 32461f28eaf786b34d9ee3a8a27d97ee1356228a Mon Sep 17 00:00:00 2001 From: moson-mo Date: Thu, 15 Jun 2023 14:16:38 +0200 Subject: [PATCH 1329/1451] fix(docker): Suppress error PEP-668 When using docker (compose), we don't create a venv and just install python packages system-wide. With python 3.11 (PEP 668) we need to explicitly tell pip to allow this. Signed-off-by: moson-mo --- docker/scripts/install-python-deps.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/scripts/install-python-deps.sh b/docker/scripts/install-python-deps.sh index 01a6eaa7..f1942498 100755 --- a/docker/scripts/install-python-deps.sh +++ b/docker/scripts/install-python-deps.sh @@ -1,10 +1,8 @@ #!/bin/bash set -eou pipefail -# Upgrade PIP; Arch Linux's version of pip is outdated for Poetry. -pip install --upgrade pip - if [ ! -z "${COMPOSE+x}" ]; then + export PIP_BREAK_SYSTEM_PACKAGES=1 poetry config virtualenvs.create false fi poetry install --no-interaction --no-ansi From c6c81f0789e72e2a99dd4474941344350dd246c9 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Fri, 16 Jun 2023 13:33:39 +0200 Subject: [PATCH 1330/1451] housekeep: Amend .gitignore and .dockerignore Prevent some files/dirs to end up in the repo / docker image: * directories typically used for python virtualenvs * files that are being generated by running tests Signed-off-by: moson-mo --- .dockerignore | 19 ++++++++++++++++++- .gitignore | 8 ++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 6ec5547d..56ac1964 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,23 @@ -*/*.mo +# Config files conf/config conf/config.sqlite conf/config.sqlite.defaults conf/docker conf/docker.defaults + +# Compiled translation files +**/*.mo + +# Typical virtualenv directories +env/ +venv/ +.venv/ + +# Test output +htmlcov/ +test-emails/ +test/__pycache__ +test/test-results +test/trash_directory* +.coverage +.pytest_cache diff --git a/.gitignore b/.gitignore index a3314c27..68de7cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ conf/docker conf/docker.defaults data.sql dummy-data.sql* -env/ fastapi_aw/ htmlcov/ po/*.mo @@ -32,7 +31,7 @@ po/*.po~ po/POTFILES schema/aur-schema-sqlite.sql test/test-results/ -test/trash directory* +test/trash_directory* web/locale/*/ web/html/*.gz @@ -53,3 +52,8 @@ report.xml # Ignore test emails test-emails/ + +# Ignore typical virtualenv directories +env/ +venv/ +.venv/ From 143575c9dec9d1126e087dc451417b1910352ed2 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 11 Jun 2023 20:31:51 +0200 Subject: [PATCH 1331/1451] fix: restore command, remove premature creation of pkgbase We're currently creating a "PackageBases" when the "restore" command is executed. This is problematic for pkgbases that never existed before. In those cases it will create the record but fail in the update.py script. Thus it leaves an orphan "PackageBases" record in the DB (which does not have any related "Packages" record(s)) Navigating to such a packages /pkgbase/... URL will result in a crash since it is not foreseen to have "orphan" pkgbase records. We can safely remove the early creation of that record because it'll be taken care of in the update.py script that is being called We'll also fix some tests. Before it was executing a dummy script instead of "update.py" which might be a bit misleading since it did not check the real outcome of our "restore" action. Signed-off-by: moson-mo --- aurweb/git/serve.py | 24 +++++------------------- test/setup.sh | 9 +-------- test/t1200-git-serve.t | 23 +++++++++++++++-------- 3 files changed, 21 insertions(+), 35 deletions(-) diff --git a/aurweb/git/serve.py b/aurweb/git/serve.py index 2ac1f10e..333d0394 100755 --- a/aurweb/git/serve.py +++ b/aurweb/git/serve.py @@ -52,7 +52,7 @@ def list_repos(user): conn.close() -def create_pkgbase(pkgbase, user): +def validate_pkgbase(pkgbase, user): if not re.match(repo_regex, pkgbase): raise aurweb.exceptions.InvalidRepositoryNameException(pkgbase) if pkgbase_exists(pkgbase): @@ -62,26 +62,12 @@ def create_pkgbase(pkgbase, user): cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user]) userid = cur.fetchone()[0] + + conn.close() + if userid == 0: raise aurweb.exceptions.InvalidUserException(user) - now = int(time.time()) - cur = conn.execute( - "INSERT INTO PackageBases (Name, SubmittedTS, " - + "ModifiedTS, SubmitterUID, MaintainerUID, " - + "FlaggerComment) VALUES (?, ?, ?, ?, ?, '')", - [pkgbase, now, now, userid, userid], - ) - pkgbase_id = cur.lastrowid - - cur = conn.execute( - "INSERT INTO PackageNotifications " + "(PackageBaseID, UserID) VALUES (?, ?)", - [pkgbase_id, userid], - ) - - conn.commit() - conn.close() - def pkgbase_adopt(pkgbase, user, privileged): pkgbase_id = pkgbase_from_name(pkgbase) @@ -577,7 +563,7 @@ def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901 checkarg(cmdargv, "repository name") pkgbase = cmdargv[1] - create_pkgbase(pkgbase, user) + validate_pkgbase(pkgbase, user) os.environ["AUR_USER"] = user os.environ["AUR_PKGBASE"] = pkgbase diff --git a/test/setup.sh b/test/setup.sh index 2db897bf..ccf24086 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -56,7 +56,7 @@ ssh-options = restrict repo-path = ./aur.git/ repo-regex = [a-z0-9][a-z0-9.+_-]*$ git-shell-cmd = ./git-shell.sh -git-update-cmd = ./update.sh +git-update-cmd = $GIT_UPDATE ssh-cmdline = ssh aur@aur.archlinux.org [update] @@ -90,13 +90,6 @@ echo $GIT_NAMESPACE EOF chmod +x git-shell.sh -cat >update.sh <<-\EOF -#!/bin/sh -echo $AUR_USER -echo $AUR_PKGBASE -EOF -chmod +x update.sh - AUR_CONFIG=config export AUR_CONFIG diff --git a/test/t1200-git-serve.t b/test/t1200-git-serve.t index dbb465bc..bb3a004f 100755 --- a/test/t1200-git-serve.t +++ b/test/t1200-git-serve.t @@ -137,14 +137,21 @@ test_expect_success "Try to push to someone else's repository as Trusted User." ' test_expect_success "Test restore." ' + # Delete from DB echo "DELETE FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ sqlite3 aur.db && - cat >expected <<-EOF && - user - foobar - EOF + # "Create branch" as if it had been there + new=$(git -C aur.git rev-parse HEAD^) && + echo $new > aur.git/.git/refs/heads/foobar && + # Restore deleted package SSH_ORIGINAL_COMMAND="restore foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - cover "$GIT_SERVE" 2>&1 >actual + cover "$GIT_SERVE" 2>&1 && + # We should find foobar with a new ID (3) in the DB after restore + echo "SELECT ID FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ + sqlite3 aur.db >actual && + cat >expected <<-EOF && + 3 + EOF test_cmp expected actual ' @@ -174,7 +181,7 @@ test_expect_success "Adopt a package base as a regular user." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && - *foobar + foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 >actual && @@ -252,7 +259,7 @@ test_expect_success "Try to steal another user's package as a Trusted User." ' cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && cat >expected <<-EOF && - *foobar + foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && @@ -340,7 +347,7 @@ test_expect_success "Disown a package base and check (co-)maintainer list." ' SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && - *foobar + foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user2 AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 >actual && From e2c113caee0f42584d1a25644423a5d9455ffde0 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Thu, 22 Jun 2023 19:22:56 +0100 Subject: [PATCH 1332/1451] chore(release): prepare for 6.2.5 Signed-off-by: Leonidas Spyropoulos --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e25fe90a..69f04fab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.2.4" +version = "v6.2.5" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From c41f2e854a1aeb4aab963a3756cf0768374a742b Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 2 Jul 2023 13:21:11 +0200 Subject: [PATCH 1333/1451] perf: tweak some search queries We currently sorting on two columns in different tables which is quite expensive in terms of performance: MariaDB is first merging the data into some temporary table to apply the sorting and record limiting. We can tweak a couple of these queries by changing the "order by" clause such that they refer to columns within the same table (PackageBases). So instead performing the second sorting on "Packages.Name", we do this on "PackageBases.Name" instead. This should still be "good enough" to produce properly sorted results. Signed-off-by: moson-mo --- aurweb/packages/search.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 62de1ea8..78b27a9a 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -195,13 +195,13 @@ class PackageSearch: def _sort_by_votes(self, order: str): column = getattr(models.PackageBase.NumVotes, order) - name = getattr(models.Package.Name, order) + name = getattr(models.PackageBase.Name, order) self.query = self.query.order_by(column(), name()) return self def _sort_by_popularity(self, order: str): column = getattr(models.PackageBase.Popularity, order) - name = getattr(models.Package.Name, order) + name = getattr(models.PackageBase.Name, order) self.query = self.query.order_by(column(), name()) return self @@ -236,7 +236,7 @@ class PackageSearch: def _sort_by_last_modified(self, order: str): column = getattr(models.PackageBase.ModifiedTS, order) - name = getattr(models.Package.Name, order) + name = getattr(models.PackageBase.Name, order) self.query = self.query.order_by(column(), name()) return self From 7c8b9ba6bcacfe45e416ec37cf16fa1824659825 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 2 Jul 2023 13:55:21 +0200 Subject: [PATCH 1334/1451] perf: add index to tweak our default search query Adds an index on PackageBases.Popularity and PackageBases.Name to improve performance of our default search query sorted by "Popularity" Signed-off-by: moson-mo --- ...0_add_index_on_packagebases_popularity_.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py diff --git a/migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py b/migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py new file mode 100644 index 00000000..12f97028 --- /dev/null +++ b/migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py @@ -0,0 +1,24 @@ +"""Add index on PackageBases.Popularity and .Name + +Revision ID: c5a6a9b661a0 +Revises: e4e49ffce091 +Create Date: 2023-07-02 13:46:52.522146 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c5a6a9b661a0" +down_revision = "e4e49ffce091" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_index( + "BasesPopularityName", "PackageBases", ["Popularity", "Name"], unique=False + ) + + +def downgrade(): + op.drop_index("BasesPopularityName", table_name="PackageBases") From 3acfb08a0f839ce3582d9ce92c01e321e99e69f3 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sun, 2 Jul 2023 01:06:34 +0200 Subject: [PATCH 1335/1451] feat: cache package search results with Redis The queries being done on the package search page are quite costly. (Especially the default one ordered by "Popularity" when navigating to /packages) Let's add the search results to the Redis cache: Every result of a search query is being pushed to Redis until we hit our maximum of 50k. An entry expires after 3 minutes before it's evicted from the cache. Lifetime an Max values are configurable. Signed-off-by: moson-mo --- aurweb/cache.py | 38 ++++++++--- aurweb/routers/html.py | 20 +++--- aurweb/routers/packages.py | 15 ++++- aurweb/util.py | 8 +++ conf/config.defaults | 6 ++ test/test_cache.py | 121 ++++++++++++++++++++--------------- test/test_packages_routes.py | 13 +++- test/test_util.py | 26 +++++++- 8 files changed, 173 insertions(+), 74 deletions(-) diff --git a/aurweb/cache.py b/aurweb/cache.py index 1572e2fc..56bb45b7 100644 --- a/aurweb/cache.py +++ b/aurweb/cache.py @@ -1,21 +1,43 @@ -from redis import Redis +import pickle + from sqlalchemy import orm +from aurweb import config +from aurweb.aur_redis import redis_connection -async def db_count_cache( - redis: Redis, key: str, query: orm.Query, expire: int = None -) -> int: +_redis = redis_connection() + + +async def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int: """Store and retrieve a query.count() via redis cache. - :param redis: Redis handle :param key: Redis key :param query: SQLAlchemy ORM query :param expire: Optional expiration in seconds :return: query.count() """ - result = redis.get(key) + result = _redis.get(key) if result is None: - redis.set(key, (result := int(query.count()))) + _redis.set(key, (result := int(query.count()))) if expire: - redis.expire(key, expire) + _redis.expire(key, expire) return int(result) + + +async def db_query_cache(key: str, query: orm.Query, expire: int = None): + """Store and retrieve query results via redis cache. + + :param key: Redis key + :param query: SQLAlchemy ORM query + :param expire: Optional expiration in seconds + :return: query.all() + """ + result = _redis.get(key) + if result is None: + if _redis.dbsize() > config.getint("cache", "max_search_entries", 50000): + return query.all() + _redis.set(key, (result := pickle.dumps(query.all())), ex=expire) + if expire: + _redis.expire(key, expire) + + return pickle.loads(result) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 38303837..fc9f3519 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -89,22 +89,20 @@ async def index(request: Request): bases = db.query(models.PackageBase) - redis = aurweb.aur_redis.redis_connection() - cache_expire = 300 # Five minutes. - + cache_expire = aurweb.config.getint("cache", "expiry_time") # Package statistics. context["package_count"] = await db_count_cache( - redis, "package_count", bases, expire=cache_expire + "package_count", bases, expire=cache_expire ) query = bases.filter(models.PackageBase.MaintainerUID.is_(None)) context["orphan_count"] = await db_count_cache( - redis, "orphan_count", query, expire=cache_expire + "orphan_count", query, expire=cache_expire ) query = db.query(models.User) context["user_count"] = await db_count_cache( - redis, "user_count", query, expire=cache_expire + "user_count", query, expire=cache_expire ) query = query.filter( @@ -114,7 +112,7 @@ async def index(request: Request): ) ) context["trusted_user_count"] = await db_count_cache( - redis, "trusted_user_count", query, expire=cache_expire + "trusted_user_count", query, expire=cache_expire ) # Current timestamp. @@ -130,26 +128,26 @@ async def index(request: Request): query = bases.filter(models.PackageBase.SubmittedTS >= seven_days_ago) context["seven_days_old_added"] = await db_count_cache( - redis, "seven_days_old_added", query, expire=cache_expire + "seven_days_old_added", query, expire=cache_expire ) query = updated.filter(models.PackageBase.ModifiedTS >= seven_days_ago) context["seven_days_old_updated"] = await db_count_cache( - redis, "seven_days_old_updated", query, expire=cache_expire + "seven_days_old_updated", query, expire=cache_expire ) year = seven_days * 52 # Fifty two weeks worth: one year. year_ago = now - year query = updated.filter(models.PackageBase.ModifiedTS >= year_ago) context["year_old_updated"] = await db_count_cache( - redis, "year_old_updated", query, expire=cache_expire + "year_old_updated", query, expire=cache_expire ) query = bases.filter( models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS < 3600 ) context["never_updated"] = await db_count_cache( - redis, "never_updated", query, expire=cache_expire + "never_updated", query, expire=cache_expire ) # Get the 15 most recently updated packages. diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 83bfe6e2..779efb4b 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -7,6 +7,7 @@ from fastapi import APIRouter, Form, Query, Request, Response import aurweb.filters # noqa: F401 from aurweb import aur_logging, config, db, defaults, models, util from aurweb.auth import creds, requires_auth +from aurweb.cache import db_count_cache, db_query_cache from aurweb.exceptions import InvariantError, handle_form_exceptions from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.packages import util as pkgutil @@ -14,6 +15,7 @@ from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base from aurweb.pkgbase import actions as pkgbase_actions, util as pkgbaseutil from aurweb.templates import make_context, make_variable_context, render_template +from aurweb.util import hash_query logger = aur_logging.get_logger(__name__) router = APIRouter() @@ -87,7 +89,11 @@ async def packages_get( # Collect search result count here; we've applied our keywords. # Including more query operations below, like ordering, will # increase the amount of time required to collect a count. - num_packages = search.count() + # we use redis for caching the results of the query + cache_expire = config.getint("cache", "expiry_time") + num_packages = await db_count_cache( + hash_query(search.query), search.query, cache_expire + ) # Apply user-specified sort column and ordering. search.sort_by(sort_by, sort_order) @@ -108,7 +114,12 @@ async def packages_get( models.PackageNotification.PackageBaseID.label("Notify"), ) - packages = results.limit(per_page).offset(offset) + # paging + results = results.limit(per_page).offset(offset) + + # we use redis for caching the results of the query + packages = await db_query_cache(hash_query(results), results, cache_expire) + context["packages"] = packages context["packages_count"] = num_packages diff --git a/aurweb/util.py b/aurweb/util.py index d80b0311..7050b482 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -4,6 +4,7 @@ import secrets import shlex import string from datetime import datetime +from hashlib import sha1 from http import HTTPStatus from subprocess import PIPE, Popen from typing import Callable, Iterable, Tuple, Union @@ -13,6 +14,7 @@ import fastapi import pygit2 from email_validator import EmailSyntaxError, validate_email from fastapi.responses import JSONResponse +from sqlalchemy.orm import Query import aurweb.config from aurweb import aur_logging, defaults @@ -200,3 +202,9 @@ def shell_exec(cmdline: str, cwd: str) -> Tuple[int, str, str]: proc = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE) out, err = proc.communicate() return proc.returncode, out.decode().strip(), err.decode().strip() + + +def hash_query(query: Query): + return sha1( + str(query.statement.compile(compile_kwargs={"literal_binds": True})).encode() + ).hexdigest() diff --git a/conf/config.defaults b/conf/config.defaults index c059444d..4e2415ed 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -165,3 +165,9 @@ commit_url = https://gitlab.archlinux.org/archlinux/aurweb/-/commits/%s ; voted on based on `now + range_start <= End <= now + range_end`. range_start = 500 range_end = 172800 + +[cache] +; maximum number of keys/entries (for search results) in our redis cache, default is 50000 +max_search_entries = 50000 +; number of seconds after a cache entry expires, default is 3 minutes +expiry_time = 180 diff --git a/test/test_cache.py b/test/test_cache.py index 83a9755a..e19fa6a2 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -1,6 +1,8 @@ +from unittest import mock + import pytest -from aurweb import cache, db +from aurweb import cache, config, db from aurweb.models.account_type import USER_ID from aurweb.models.user import User @@ -10,68 +12,85 @@ def setup(db_test): return -class StubRedis: - """A class which acts as a RedisConnection without using Redis.""" - - cache = dict() - expires = dict() - - def get(self, key, *args): - if "key" not in self.cache: - self.cache[key] = None - return self.cache[key] - - def set(self, key, *args): - self.cache[key] = list(args)[0] - - def expire(self, key, *args): - self.expires[key] = list(args)[0] - - async def execute(self, command, key, *args): - f = getattr(self, command) - return f(key, *args) - - @pytest.fixture -def redis(): - yield StubRedis() +def user() -> User: + with db.begin(): + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield user + + +@pytest.fixture(autouse=True) +def clear_fakeredis_cache(): + cache._redis.flushall() @pytest.mark.asyncio -async def test_db_count_cache(redis): - db.create( - User, - Username="user1", - Email="user1@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID, - ) - +async def test_db_count_cache(user): query = db.query(User) - # Now, perform several checks that db_count_cache matches query.count(). - # We have no cached value yet. - assert await cache.db_count_cache(redis, "key1", query) == query.count() + assert cache._redis.get("key1") is None + + # Add to cache + assert await cache.db_count_cache("key1", query) == query.count() # It's cached now. - assert await cache.db_count_cache(redis, "key1", query) == query.count() + assert cache._redis.get("key1") is not None + + # It does not expire + assert cache._redis.ttl("key1") == -1 + + # Cache a query with an expire. + value = await cache.db_count_cache("key2", query, 100) + assert value == query.count() + + assert cache._redis.ttl("key2") == 100 @pytest.mark.asyncio -async def test_db_count_cache_expires(redis): - db.create( - User, - Username="user1", - Email="user1@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID, - ) - +async def test_db_query_cache(user): query = db.query(User) - # Cache a query with an expire. - value = await cache.db_count_cache(redis, "key1", query, 100) - assert value == query.count() + # We have no cached value yet. + assert cache._redis.get("key1") is None - assert redis.expires["key1"] == 100 + # Add to cache + await cache.db_query_cache("key1", query) + + # It's cached now. + assert cache._redis.get("key1") is not None + + # Modify our user and make sure we got a cached value + user.Username = "changed" + cached = await cache.db_query_cache("key1", query) + assert cached[0].Username != query.all()[0].Username + + # It does not expire + assert cache._redis.ttl("key1") == -1 + + # Cache a query with an expire. + value = await cache.db_query_cache("key2", query, 100) + assert len(value) == query.count() + assert value[0].Username == query.all()[0].Username + + assert cache._redis.ttl("key2") == 100 + + # Test "max_search_entries" options + def mock_max_search_entries(section: str, key: str, fallback: int) -> str: + if section == "cache" and key == "max_search_entries": + return 1 + return config.getint(section, key) + + with mock.patch("aurweb.config.getint", side_effect=mock_max_search_entries): + # Try to add another entry (we already have 2) + await cache.db_query_cache("key3", query) + + # Make sure it was not added because it exceeds our max. + assert cache._redis.get("key3") is None diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index 93dc404a..fb12e65e 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -5,7 +5,7 @@ from unittest import mock import pytest from fastapi.testclient import TestClient -from aurweb import asgi, config, db, time +from aurweb import asgi, cache, config, db, time from aurweb.filters import datetime_display from aurweb.models import License, PackageLicense from aurweb.models.account_type import USER_ID, AccountType @@ -63,6 +63,11 @@ def setup(db_test): return +@pytest.fixture(autouse=True) +def clear_fakeredis_cache(): + cache._redis.flushall() + + @pytest.fixture def client() -> TestClient: """Yield a FastAPI TestClient.""" @@ -815,6 +820,8 @@ def test_packages_search_by_keywords(client: TestClient, packages: list[Package] # And request packages with that keyword, we should get 1 result. with client as request: + # clear fakeredis cache + cache._redis.flushall() response = request.get("/packages", params={"SeB": "k", "K": "testKeyword"}) assert response.status_code == int(HTTPStatus.OK) @@ -870,6 +877,8 @@ def test_packages_search_by_maintainer( # This time, we should get `package` returned, since it's now an orphan. with client as request: + # clear fakeredis cache + cache._redis.flushall() response = request.get("/packages", params={"SeB": "m"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -902,6 +911,8 @@ def test_packages_search_by_comaintainer( # Then test that it's returned by our search. with client as request: + # clear fakeredis cache + cache._redis.flushall() response = request.get( "/packages", params={"SeB": "c", "K": maintainer.Username} ) diff --git a/test/test_util.py b/test/test_util.py index a138d912..042b9ad9 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -5,7 +5,8 @@ import fastapi import pytest from fastapi.responses import JSONResponse -from aurweb import filters, util +from aurweb import db, filters, util +from aurweb.models.user import User from aurweb.testing.requests import Request @@ -146,3 +147,26 @@ def assert_multiple_keys(pks): assert key1 == k1[1] assert pfx2 == k2[0] assert key2 == k2[1] + + +def test_hash_query(): + # No conditions + query = db.query(User) + assert util.hash_query(query) == "75e76026b7d576536e745ec22892cf8f5d7b5d62" + + # With where clause + query = db.query(User).filter(User.Username == "bla") + assert util.hash_query(query) == "4dca710f33b1344c27ec6a3c266970f4fa6a8a00" + + # With where clause and sorting + query = db.query(User).filter(User.Username == "bla").order_by(User.Username) + assert util.hash_query(query) == "ee2c7846fede430776e140f8dfe1d83cd21d2eed" + + # With where clause, sorting and specific columns + query = ( + db.query(User) + .filter(User.Username == "bla") + .order_by(User.Username) + .with_entities(User.Username) + ) + assert util.hash_query(query) == "c1db751be61443d266cf643005eee7a884dac103" From 814ccf6b04e97659c30ecc18dd63607a3ba485e6 Mon Sep 17 00:00:00 2001 From: moson-mo Date: Tue, 4 Jul 2023 09:40:39 +0200 Subject: [PATCH 1336/1451] feat: add Prometheus metrics for Redis cache Adding a Prometheus counter to be able to monitor cache hits/misses for search queries Signed-off-by: moson-mo --- aurweb/cache.py | 13 +++++++++++-- test/test_metrics.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 test/test_metrics.py diff --git a/aurweb/cache.py b/aurweb/cache.py index 56bb45b7..fe1e5f1d 100644 --- a/aurweb/cache.py +++ b/aurweb/cache.py @@ -1,5 +1,6 @@ import pickle +from prometheus_client import Counter from sqlalchemy import orm from aurweb import config @@ -7,6 +8,11 @@ from aurweb.aur_redis import redis_connection _redis = redis_connection() +# Prometheus metrics +SEARCH_REQUESTS = Counter( + "search_requests", "Number of search requests by cache hit/miss", ["cache"] +) + async def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int: """Store and retrieve a query.count() via redis cache. @@ -24,7 +30,7 @@ async def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int: return int(result) -async def db_query_cache(key: str, query: orm.Query, expire: int = None): +async def db_query_cache(key: str, query: orm.Query, expire: int = None) -> list: """Store and retrieve query results via redis cache. :param key: Redis key @@ -34,10 +40,13 @@ async def db_query_cache(key: str, query: orm.Query, expire: int = None): """ result = _redis.get(key) if result is None: + SEARCH_REQUESTS.labels(cache="miss").inc() if _redis.dbsize() > config.getint("cache", "max_search_entries", 50000): return query.all() - _redis.set(key, (result := pickle.dumps(query.all())), ex=expire) + _redis.set(key, (result := pickle.dumps(query.all()))) if expire: _redis.expire(key, expire) + else: + SEARCH_REQUESTS.labels(cache="hit").inc() return pickle.loads(result) diff --git a/test/test_metrics.py b/test/test_metrics.py new file mode 100644 index 00000000..1859d8cb --- /dev/null +++ b/test/test_metrics.py @@ -0,0 +1,40 @@ +import pytest +from prometheus_client import REGISTRY, generate_latest + +from aurweb import db +from aurweb.cache import db_query_cache +from aurweb.models.account_type import USER_ID +from aurweb.models.user import User + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield user + + +@pytest.mark.asyncio +async def test_search_cache_metrics(user: User): + # Fire off 3 identical queries for caching + for _ in range(3): + await db_query_cache("key", db.query(User)) + + # Get metrics + metrics = str(generate_latest(REGISTRY)) + + # We should have 1 miss and 2 hits + assert 'search_requests_total{cache="miss"} 1.0' in metrics + assert 'search_requests_total{cache="hit"} 2.0' in metrics From 9fe8d524ffabcbd171cbadbbe9b42edc1f5fa91d Mon Sep 17 00:00:00 2001 From: moson Date: Sat, 8 Jul 2023 10:32:26 +0200 Subject: [PATCH 1337/1451] fix(test): MariaDB 11 upgrade, query result order Fix order of recipients for "FlagNotification" test. Apply sorting to the recipients query. (only relevant for tests, but who knows when they change things again) MariaDB 11 includes some changes related to the query optimizer. Turns out that this might have effects on how records are ordered for certain queries. (in case no ORDER BY clause was specified) https://mariadb.com/kb/en/mariadb-11-0-0-release-notes/ Signed-off-by: moson --- aurweb/scripts/notify.py | 1 + test/test_notify.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index ac9022c3..f55254d7 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -334,6 +334,7 @@ class FlagNotification(Notification): .filter(and_(PackageBase.ID == pkgbase_id, User.Suspended == 0)) .with_entities(User.Email, User.LangPreference) .distinct() + .order_by(User.Email) ) self._recipients = [(u.Email, u.LangPreference) for u in query] diff --git a/test/test_notify.py b/test/test_notify.py index 9e61d9ee..1fd7cd83 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -127,20 +127,20 @@ def test_out_of_date(user: User, user1: User, user2: User, pkgbases: list[Packag # Should've gotten three emails: maintainer + the two comaintainers. assert Email.count() == 3 - # Comaintainer 1. + # Maintainer. first = Email(1).parse() - assert first.headers.get("To") == user1.Email + assert first.headers.get("To") == user.Email expected = f"AUR Out-of-date Notification for {pkgbase.Name}" assert first.headers.get("Subject") == expected - # Comaintainer 2. + # Comaintainer 1. second = Email(2).parse() - assert second.headers.get("To") == user2.Email + assert second.headers.get("To") == user1.Email - # Maintainer. + # Comaintainer 2. third = Email(3).parse() - assert third.headers.get("To") == user.Email + assert third.headers.get("To") == user2.Email def test_reset(user: User): From f3f8c0a8710838ba176f4486eb886ce37565b78a Mon Sep 17 00:00:00 2001 From: moson-mo Date: Sat, 1 Jul 2023 12:55:14 +0200 Subject: [PATCH 1338/1451] fix: add recipients to BCC when email is hidden Package requests are sent to the ML as well as users (CC). For those who chose to hide their mail address, we should add them to the BCC list instead. Signed-off-by: moson-mo --- aurweb/scripts/notify.py | 21 +++++++++++---- po/aurweb.pot | 8 ++++++ templates/partials/account_form.html | 7 ++++- test/test_notify.py | 38 ++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index f55254d7..a85339ce 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -45,6 +45,9 @@ class Notification: def get_cc(self): return [] + def get_bcc(self): + return [] + def get_body_fmt(self, lang): body = "" for line in self.get_body(lang).splitlines(): @@ -114,7 +117,7 @@ class Notification: server.login(user, passwd) server.set_debuglevel(0) - deliver_to = [to] + self.get_cc() + deliver_to = [to] + self.get_cc() + self.get_bcc() server.sendmail(sender, deliver_to, msg.as_bytes()) server.quit() @@ -578,10 +581,11 @@ class RequestOpenNotification(Notification): ), ) .filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) - .with_entities(User.Email) + .with_entities(User.Email, User.HideEmail) .distinct() ) - self._cc = [u.Email for u in query] + self._cc = [u.Email for u in query if u.HideEmail == 0] + self._bcc = [u.Email for u in query if u.HideEmail == 1] pkgreq = ( db.query(PackageRequest.Comments).filter(PackageRequest.ID == reqid).first() @@ -598,6 +602,9 @@ class RequestOpenNotification(Notification): def get_cc(self): return self._cc + def get_bcc(self): + return self._bcc + def get_subject(self, lang): return "[PRQ#%d] %s Request for %s" % ( self._reqid, @@ -665,10 +672,11 @@ class RequestCloseNotification(Notification): ), ) .filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) - .with_entities(User.Email) + .with_entities(User.Email, User.HideEmail) .distinct() ) - self._cc = [u.Email for u in query] + self._cc = [u.Email for u in query if u.HideEmail == 0] + self._bcc = [u.Email for u in query if u.HideEmail == 1] pkgreq = ( db.query(PackageRequest) @@ -695,6 +703,9 @@ class RequestCloseNotification(Notification): def get_cc(self): return self._cc + def get_bcc(self): + return self._bcc + def get_subject(self, lang): return "[PRQ#%d] %s Request for %s %s" % ( self._reqid, diff --git a/po/aurweb.pot b/po/aurweb.pot index b975ab91..77bca3b0 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -2366,3 +2366,11 @@ msgstr "" #: templates/requests.html msgid "Package name" msgstr "" + +#: templates/partials/account_form.html +msgid "Note that if you hide your email address, it'll " +"end up on the BCC list for any request notifications. " +"In case someone replies to these notifications, you won't " +"receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 28dc0cd5..7595dcaf 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -115,7 +115,12 @@ {{ "If you do not hide your email address, it is " "visible to all registered AUR users. If you hide your " "email address, it is visible to members of the Arch " - "Linux staff only." | tr }} + "Linux staff only." | tr }}
    + {{ "Note that if you hide your email address, it'll " + "end up on the BCC list for any request notifications. " + "In case someone replies to these notifications, you won't " + "receive an email. However, replies are typically sent to the " + "mailing-list and would then be visible in the archive." | tr }}

    diff --git a/test/test_notify.py b/test/test_notify.py index 1fd7cd83..fbcf350b 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -479,6 +479,44 @@ def test_close_request_comaintainer_cc( assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) +def test_open_close_request_hidden_email( + user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): + pkgbase = pkgbases[0] + + # Enable the "HideEmail" option for our requester + with db.begin(): + user2.HideEmail = 1 + + # Send an open request notification. + notif = notify.RequestOpenNotification( + user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID + ) + + # Make sure our address got added to the bcc list + assert user2.Email in notif.get_bcc() + + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + # Make sure we don't have our address in the Cc header + assert user2.Email not in email.headers.get("Cc") + + # Create a closure notification on the pkgbase we just opened. + notif = notify.RequestCloseNotification(user2.ID, pkgreq.ID, "rejected") + + # Make sure our address got added to the bcc list + assert user2.Email in notif.get_bcc() + + notif.send() + assert Email.count() == 2 + + email = Email(2).parse() + # Make sure we don't have our address in the Cc header + assert user2.Email not in email.headers.get("Cc") + + def test_close_request_closure_comment( user: User, user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] ): From 7cde1ca56041afb9aa00d2d0c46bfd10c2291080 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 8 Jul 2023 09:25:09 +0000 Subject: [PATCH 1339/1451] fix(deps): update all non-major dependencies --- poetry.lock | 622 +++++++++++++++++++++++++++------------------------- 1 file changed, 321 insertions(+), 301 deletions(-) diff --git a/poetry.lock b/poetry.lock index 16b0f15a..dcdcf819 100644 --- a/poetry.lock +++ b/poetry.lock @@ -55,14 +55,14 @@ trio = ["trio (>=0.16,<0.22)"] [[package]] name = "asgiref" -version = "3.7.1" +version = "3.7.2" description = "ASGI specs, helper code, and adapters" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "asgiref-3.7.1-py3-none-any.whl", hash = "sha256:33958cb2e4b3cd8b1b06ef295bd8605cde65b11df51d3beab39e2e149a610ab3"}, - {file = "asgiref-3.7.1.tar.gz", hash = "sha256:8de379fcc383bcfe4507e229fc31209ea23d4831c850f74063b2c11639474dd2"}, + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, ] [package.dependencies] @@ -85,14 +85,14 @@ files = [ [[package]] name = "authlib" -version = "1.2.0" +version = "1.2.1" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." category = "main" optional = false python-versions = "*" files = [ - {file = "Authlib-1.2.0-py2.py3-none-any.whl", hash = "sha256:4ddf4fd6cfa75c9a460b361d4bd9dac71ffda0be879dbe4292a02e92349ad55a"}, - {file = "Authlib-1.2.0.tar.gz", hash = "sha256:4fa3e80883a5915ef9f5bc28630564bc4ed5b5af39812a3ff130ec76bd631e9d"}, + {file = "Authlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:c88984ea00149a90e3537c964327da930779afa4564e354edfd98410bea01911"}, + {file = "Authlib-1.2.1.tar.gz", hash = "sha256:421f7c6b468d907ca2d9afede256f068f87e34d23dd221c07d13d4c234726afb"}, ] [package.dependencies] @@ -355,63 +355,72 @@ files = [ [[package]] name = "coverage" -version = "7.2.6" +version = "7.2.7" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"}, - {file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"}, - {file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"}, - {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"}, - {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"}, - {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"}, - {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"}, - {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"}, - {file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"}, - {file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"}, - {file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"}, - {file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"}, - {file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"}, - {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"}, - {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"}, - {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"}, - {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"}, - {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"}, - {file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"}, - {file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"}, - {file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"}, - {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"}, - {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"}, - {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"}, - {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"}, - {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"}, - {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"}, - {file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"}, - {file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"}, - {file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"}, - {file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"}, - {file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"}, - {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"}, - {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"}, - {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"}, - {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"}, - {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"}, - {file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"}, - {file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"}, - {file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"}, - {file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"}, - {file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"}, - {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"}, - {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"}, - {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"}, - {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"}, - {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"}, - {file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"}, - {file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"}, - {file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"}, - {file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, ] [package.dependencies] @@ -531,19 +540,19 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "2.13.0" -description = "Fake implementation of redis API for testing purposes." +version = "2.16.0" +description = "Python implementation of redis API, can be used for testing purposes." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "fakeredis-2.13.0-py3-none-any.whl", hash = "sha256:df7bb44fb9e593970c626325230e1c321f954ce7b204d4c4452eae5233d554ed"}, - {file = "fakeredis-2.13.0.tar.gz", hash = "sha256:53f00f44f771d2b794f1ea036fa07a33476ab7368f1b0e908daab3eff80336f6"}, + {file = "fakeredis-2.16.0-py3-none-any.whl", hash = "sha256:188514cbd7120ff28c88f2a31e2fddd18fb1b28504478dfa3669c683134c4d82"}, + {file = "fakeredis-2.16.0.tar.gz", hash = "sha256:5abdd734de4ead9d6c7acbd3add1c4aa9b3ab35219339530472d9dd2bdf13057"}, ] [package.dependencies] redis = ">=4" -sortedcontainers = ">=2.4,<3.0" +sortedcontainers = ">=2,<3" [package.extras] json = ["jsonpath-ng (>=1.5,<2.0)"] @@ -588,19 +597,19 @@ python-dateutil = "*" [[package]] name = "filelock" -version = "3.12.0" +version = "3.12.2" description = "A platform independent file lock." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"}, - {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"}, + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, ] [package.extras] -docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] [[package]] name = "greenlet" @@ -896,96 +905,109 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lxml" -version = "4.9.2" +version = "4.9.3" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ - {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, - {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, - {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, - {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"}, - {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"}, - {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"}, - {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"}, - {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"}, - {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"}, - {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"}, - {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"}, - {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"}, - {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"}, - {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"}, - {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"}, - {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"}, - {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"}, - {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"}, - {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"}, - {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"}, - {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"}, - {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"}, - {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, - {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, - {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, - {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"}, - {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"}, - {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, - {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, - {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, - {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"}, - {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"}, - {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"}, - {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"}, - {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"}, - {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"}, - {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"}, - {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"}, - {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"}, - {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"}, - {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"}, - {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"}, - {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"}, - {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"}, - {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"}, - {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"}, - {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"}, - {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, - {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, + {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, + {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, + {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, + {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, + {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, + {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, + {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, + {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, + {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, + {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, + {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, + {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, + {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, + {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, + {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, + {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, + {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, + {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, + {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, + {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, + {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, + {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, + {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.7)"] +source = ["Cython (>=0.29.35)"] [[package]] name = "mako" @@ -1087,75 +1109,75 @@ files = [ [[package]] name = "mysqlclient" -version = "2.1.1" +version = "2.2.0" description = "Python interface to MySQL" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "mysqlclient-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37"}, - {file = "mysqlclient-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b"}, - {file = "mysqlclient-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c"}, - {file = "mysqlclient-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994"}, - {file = "mysqlclient-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855"}, - {file = "mysqlclient-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"}, - {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, + {file = "mysqlclient-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:68837b6bb23170acffb43ae411e47533a560b6360c06dac39aa55700972c93b2"}, + {file = "mysqlclient-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5670679ff1be1cc3fef0fa81bf39f0cd70605ba121141050f02743eb878ac114"}, + {file = "mysqlclient-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:004fe1d30d2c2ff8072f8ea513bcec235fd9b896f70dad369461d0ad7e570e98"}, + {file = "mysqlclient-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c6b142836c7dba4f723bf9c93cc46b6e5081d65b2af807f400dda9eb85a16d0"}, + {file = "mysqlclient-2.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:955dba905a7443ce4788c63fdb9f8d688316260cf60b20ff51ac3b1c77616ede"}, + {file = "mysqlclient-2.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:530ece9995a36cadb6211b9787f0c9e05cdab6702549bdb4236af5e9b535ed6a"}, + {file = "mysqlclient-2.2.0.tar.gz", hash = "sha256:04368445f9c487d8abb7a878e3d23e923e6072c04a6c320f9e0dc8a82efba14e"}, ] [[package]] name = "orjson" -version = "3.8.14" +version = "3.9.2" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "orjson-3.8.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7a7b0fead2d0115ef927fa46ad005d7a3988a77187500bf895af67b365c10d1f"}, - {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca90db8f551b8960da95b0d4cad6c0489df52ea03585b6979595be7b31a3f946"}, - {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4ac01a3db4e6a98a8ad1bb1a3e8bfc777928939e87c04e93e0d5006df574a4b"}, - {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf6825e160e4eb0ef65ce37d8c221edcab96ff2ffba65e5da2437a60a12b3ad1"}, - {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80e62afe49e6bfc706e041faa351d7520b5f86572b8e31455802251ea989613"}, - {file = "orjson-3.8.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6112194c11e611596eed72f46efb0e6b4812682eff3c7b48473d1146c3fa0efb"}, - {file = "orjson-3.8.14-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:739f9f633e1544f2a477fa3bef380f488c8dca6e2521c8dc36424b12554ee31e"}, - {file = "orjson-3.8.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d3d8faded5a514b80b56d0429eb38b429d7a810f8749d25dc10a0cc15b8a3c8"}, - {file = "orjson-3.8.14-cp310-none-win_amd64.whl", hash = "sha256:0bf00c42333412a9338297bf888d7428c99e281e20322070bde8c2314775508b"}, - {file = "orjson-3.8.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d66966fd94719beb84e8ed84833bc59c3c005d3d2d0c42f11d7552d3267c6de7"}, - {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087c0dc93379e8ba2d59e9f586fab8de8c137d164fccf8afd5523a2137570917"}, - {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04c70dc8ca79b0072a16d82f94b9d9dd6598a43dd753ab20039e9f7d2b14f017"}, - {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aedba48264fe87e5060c0e9c2b28909f1e60626e46dc2f77e0c8c16939e2e1f7"}, - {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01640ab79111dd97515cba9fab7c66cb3b0967b0892cc74756a801ff681a01b6"}, - {file = "orjson-3.8.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b206cca6836a4c6683bcaa523ab467627b5f03902e5e1082dc59cd010e6925f"}, - {file = "orjson-3.8.14-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ee0299b2dda9afce351a5e8c148ea7a886de213f955aa0288fb874fb44829c36"}, - {file = "orjson-3.8.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:31a2a29be559e92dcc5c278787b4166da6f0d45675b59a11c4867f5d1455ebf4"}, - {file = "orjson-3.8.14-cp311-none-win_amd64.whl", hash = "sha256:20b7ffc7736000ea205f9143df322b03961f287b4057606291c62c842ff3c5b5"}, - {file = "orjson-3.8.14-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de1ee13d6b6727ee1db38722695250984bae81b8fc9d05f1176c74d14b1322d9"}, - {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ee09bfbf1d54c127d3061f6721a1a11d2ce502b50597c3d0d2e1bd2d235b764"}, - {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97ebb7fab5f1ae212a6501f17cb7750a6838ffc2f1cebbaa5dec1a90038ca3c6"}, - {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38ca39bae7fbc050332a374062d4cdec28095540fa8bb245eada467897a3a0bb"}, - {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92374bc35b6da344a927d5a850f7db80a91c7b837de2f0ea90fc870314b1ff44"}, - {file = "orjson-3.8.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9393a63cb0424515ec5e434078b3198de6ec9e057f1d33bad268683935f0a5d5"}, - {file = "orjson-3.8.14-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5fb66f0ac23e861b817c858515ac1f74d1cd9e72e3f82a5b2c9bae9f92286adc"}, - {file = "orjson-3.8.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19415aaf30525a5baff0d72a089fcdd68f19a3674998263c885c3908228c1086"}, - {file = "orjson-3.8.14-cp37-none-win_amd64.whl", hash = "sha256:87ba7882e146e24a7d8b4a7971c20212c2af75ead8096fc3d55330babb1015fb"}, - {file = "orjson-3.8.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9f5cf61b6db68f213c805c55bf0aab9b4cb75a4e9c7f5bfbd4deb3a0aef0ec53"}, - {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33bc310da4ad2ffe8f7f1c9e89692146d9ec5aec2d1c9ef6b67f8dc5e2d63241"}, - {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67a7e883b6f782b106683979ccc43d89b98c28a1f4a33fe3a22e253577499bb1"}, - {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9df820e6c8c84c52ec39ea2cc9c79f7999c839c7d1481a056908dce3b90ce9f9"}, - {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebca14ae80814219ea3327e3dfa7ff618621ff335e45781fac26f5cd0b48f2b4"}, - {file = "orjson-3.8.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27967be4c16bd09f4aeff8896d9be9cbd00fd72f5815d5980e4776f821e2f77c"}, - {file = "orjson-3.8.14-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:062829b5e20cd8648bf4c11c3a5ee7cf196fa138e573407b5312c849b0cf354d"}, - {file = "orjson-3.8.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e53bc5beb612df8ddddb065f079d3fd30b5b4e73053518524423549d61177f3f"}, - {file = "orjson-3.8.14-cp38-none-win_amd64.whl", hash = "sha256:d03f29b0369bb1ab55c8a67103eb3a9675daaf92f04388568034fe16be48fa5d"}, - {file = "orjson-3.8.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:716a3994e039203f0a59056efa28185d4cac51b922cc5bf27ab9182cfa20e12e"}, - {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb35dd3ba062c1d984d57e6477768ed7b62ed9260f31362b2d69106f9c60ebd"}, - {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0bc6b7abf27f1dc192dadad249df9b513912506dd420ce50fd18864a33789b71"}, - {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2f75b7d9285e35c3d4dff9811185535ff2ea637f06b2b242cb84385f8ffe63"}, - {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:017de5ba22e58dfa6f41914f5edb8cd052d23f171000684c26b2d2ab219db31e"}, - {file = "orjson-3.8.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a3bf3154f40299b8bc95e9fb8da47436a59a2106fc22cae15f76d649e062da"}, - {file = "orjson-3.8.14-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:64b4fca0531030040e611c6037aaf05359e296877ab0a8e744c26ef9c32738b9"}, - {file = "orjson-3.8.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8a896a12b38fe201a72593810abc1f4f1597e65b8c869d5fc83bbcf75d93398f"}, - {file = "orjson-3.8.14-cp39-none-win_amd64.whl", hash = "sha256:9725226478d1dafe46d26f758eadecc6cf98dcbb985445e14a9c74aaed6ccfea"}, - {file = "orjson-3.8.14.tar.gz", hash = "sha256:5ea93fd3ef7be7386f2516d728c877156de1559cda09453fc7dd7b696d0439b3"}, + {file = "orjson-3.9.2-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7323e4ca8322b1ecb87562f1ec2491831c086d9faa9a6c6503f489dadbed37d7"}, + {file = "orjson-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1272688ea1865f711b01ba479dea2d53e037ea00892fd04196b5875f7021d9d3"}, + {file = "orjson-3.9.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b9a26f1d1427a9101a1e8910f2e2df1f44d3d18ad5480ba031b15d5c1cb282e"}, + {file = "orjson-3.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a5ca55b0d8f25f18b471e34abaee4b175924b6cd62f59992945b25963443141"}, + {file = "orjson-3.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:877872db2c0f41fbe21f852ff642ca842a43bc34895b70f71c9d575df31fffb4"}, + {file = "orjson-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a39c2529d75373b7167bf84c814ef9b8f3737a339c225ed6c0df40736df8748"}, + {file = "orjson-3.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:84ebd6fdf138eb0eb4280045442331ee71c0aab5e16397ba6645f32f911bfb37"}, + {file = "orjson-3.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a60a1cfcfe310547a1946506dd4f1ed0a7d5bd5b02c8697d9d5dcd8d2e9245e"}, + {file = "orjson-3.9.2-cp310-none-win_amd64.whl", hash = "sha256:c290c4f81e8fd0c1683638802c11610b2f722b540f8e5e858b6914b495cf90c8"}, + {file = "orjson-3.9.2-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:02ef014f9a605e84b675060785e37ec9c0d2347a04f1307a9d6840ab8ecd6f55"}, + {file = "orjson-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:992af54265ada1c1579500d6594ed73fe333e726de70d64919cf37f93defdd06"}, + {file = "orjson-3.9.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a40958f7af7c6d992ee67b2da4098dca8b770fc3b4b3834d540477788bfa76d3"}, + {file = "orjson-3.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93864dec3e3dd058a2dbe488d11ac0345214a6a12697f53a63e34de7d28d4257"}, + {file = "orjson-3.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16fdf5a82df80c544c3c91516ab3882cd1ac4f1f84eefeafa642e05cef5f6699"}, + {file = "orjson-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275b5a18fd9ed60b2720543d3ddac170051c43d680e47d04ff5203d2c6d8ebf1"}, + {file = "orjson-3.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b9aea6dcb99fcbc9f6d1dd84fca92322fda261da7fb014514bb4689c7c2097a8"}, + {file = "orjson-3.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d74ae0e101d17c22ef67b741ba356ab896fc0fa64b301c2bf2bb0a4d874b190"}, + {file = "orjson-3.9.2-cp311-none-win_amd64.whl", hash = "sha256:6320b28e7bdb58c3a3a5efffe04b9edad3318d82409e84670a9b24e8035a249d"}, + {file = "orjson-3.9.2-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:368e9cc91ecb7ac21f2aa475e1901204110cf3e714e98649c2502227d248f947"}, + {file = "orjson-3.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58e9e70f0dcd6a802c35887f306b555ff7a214840aad7de24901fc8bd9cf5dde"}, + {file = "orjson-3.9.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00c983896c2e01c94c0ef72fd7373b2aa06d0c0eed0342c4884559f812a6835b"}, + {file = "orjson-3.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ee743e8890b16c87a2f89733f983370672272b61ee77429c0a5899b2c98c1a7"}, + {file = "orjson-3.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7b065942d362aad4818ff599d2f104c35a565c2cbcbab8c09ec49edba91da75"}, + {file = "orjson-3.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e46e9c5b404bb9e41d5555762fd410d5466b7eb1ec170ad1b1609cbebe71df21"}, + {file = "orjson-3.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8170157288714678ffd64f5de33039e1164a73fd8b6be40a8a273f80093f5c4f"}, + {file = "orjson-3.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e3e2f087161947dafe8319ea2cfcb9cea4bb9d2172ecc60ac3c9738f72ef2909"}, + {file = "orjson-3.9.2-cp37-none-win_amd64.whl", hash = "sha256:d7de3dbbe74109ae598692113cec327fd30c5a30ebca819b21dfa4052f7b08ef"}, + {file = "orjson-3.9.2-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8cd4385c59bbc1433cad4a80aca65d2d9039646a9c57f8084897549b55913b17"}, + {file = "orjson-3.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a74036aab1a80c361039290cdbc51aa7adc7ea13f56e5ef94e9be536abd227bd"}, + {file = "orjson-3.9.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1aaa46d7d4ae55335f635eadc9be0bd9bcf742e6757209fc6dc697e390010adc"}, + {file = "orjson-3.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e52c67ed6bb368083aa2078ea3ccbd9721920b93d4b06c43eb4e20c4c860046"}, + {file = "orjson-3.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a6cdfcf9c7dd4026b2b01fdff56986251dc0cc1e980c690c79eec3ae07b36e7"}, + {file = "orjson-3.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1882a70bb69595b9ec5aac0040a819e94d2833fe54901e2b32f5e734bc259a8b"}, + {file = "orjson-3.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fc05e060d452145ab3c0b5420769e7356050ea311fc03cb9d79c481982917cca"}, + {file = "orjson-3.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f8bc2c40d9bb26efefb10949d261a47ca196772c308babc538dd9f4b73e8d386"}, + {file = "orjson-3.9.2-cp38-none-win_amd64.whl", hash = "sha256:3164fc20a585ec30a9aff33ad5de3b20ce85702b2b2a456852c413e3f0d7ab09"}, + {file = "orjson-3.9.2-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7a6ccadf788531595ed4728aa746bc271955448d2460ff0ef8e21eb3f2a281ba"}, + {file = "orjson-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3245d230370f571c945f69aab823c279a868dc877352817e22e551de155cb06c"}, + {file = "orjson-3.9.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:205925b179550a4ee39b8418dd4c94ad6b777d165d7d22614771c771d44f57bd"}, + {file = "orjson-3.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0325fe2d69512187761f7368c8cda1959bcb75fc56b8e7a884e9569112320e57"}, + {file = "orjson-3.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:806704cd58708acc66a064a9a58e3be25cf1c3f9f159e8757bd3f515bfabdfa1"}, + {file = "orjson-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03fb36f187a0c19ff38f6289418863df8b9b7880cdbe279e920bef3a09d8dab1"}, + {file = "orjson-3.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20925d07a97c49c6305bff1635318d9fc1804aa4ccacb5fb0deb8a910e57d97a"}, + {file = "orjson-3.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:eebfed53bec5674e981ebe8ed2cf00b3f7bcda62d634733ff779c264307ea505"}, + {file = "orjson-3.9.2-cp39-none-win_amd64.whl", hash = "sha256:869b961df5fcedf6c79f4096119b35679b63272362e9b745e668f0391a892d39"}, + {file = "orjson-3.9.2.tar.gz", hash = "sha256:24257c8f641979bf25ecd3e27251b5cc194cdd3a6e96004aac8446f5e63d9664"}, ] [[package]] @@ -1189,6 +1211,7 @@ category = "main" optional = false python-versions = "*" files = [ + {file = "parse-1.19.0-py2.py3-none-any.whl", hash = "sha256:6ce007645384a91150cb7cd7c8a9db2559e273c2e2542b508cd1e342508c2601"}, {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"}, ] @@ -1269,25 +1292,25 @@ prometheus-client = ">=0.8.0,<1.0.0" [[package]] name = "protobuf" -version = "4.23.2" +version = "4.23.4" description = "" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "protobuf-4.23.2-cp310-abi3-win32.whl", hash = "sha256:384dd44cb4c43f2ccddd3645389a23ae61aeb8cfa15ca3a0f60e7c3ea09b28b3"}, - {file = "protobuf-4.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:09310bce43353b46d73ba7e3bca78273b9bc50349509b9698e64d288c6372c2a"}, - {file = "protobuf-4.23.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b2cfab63a230b39ae603834718db74ac11e52bccaaf19bf20f5cce1a84cf76df"}, - {file = "protobuf-4.23.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:c52cfcbfba8eb791255edd675c1fe6056f723bf832fa67f0442218f8817c076e"}, - {file = "protobuf-4.23.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:86df87016d290143c7ce3be3ad52d055714ebaebb57cc659c387e76cfacd81aa"}, - {file = "protobuf-4.23.2-cp37-cp37m-win32.whl", hash = "sha256:281342ea5eb631c86697e1e048cb7e73b8a4e85f3299a128c116f05f5c668f8f"}, - {file = "protobuf-4.23.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ce744938406de1e64b91410f473736e815f28c3b71201302612a68bf01517fea"}, - {file = "protobuf-4.23.2-cp38-cp38-win32.whl", hash = "sha256:6c081863c379bb1741be8f8193e893511312b1d7329b4a75445d1ea9955be69e"}, - {file = "protobuf-4.23.2-cp38-cp38-win_amd64.whl", hash = "sha256:25e3370eda26469b58b602e29dff069cfaae8eaa0ef4550039cc5ef8dc004511"}, - {file = "protobuf-4.23.2-cp39-cp39-win32.whl", hash = "sha256:efabbbbac1ab519a514579ba9ec52f006c28ae19d97915951f69fa70da2c9e91"}, - {file = "protobuf-4.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:54a533b971288af3b9926e53850c7eb186886c0c84e61daa8444385a4720297f"}, - {file = "protobuf-4.23.2-py3-none-any.whl", hash = "sha256:8da6070310d634c99c0db7df48f10da495cc283fd9e9234877f0cd182d43ab7f"}, - {file = "protobuf-4.23.2.tar.gz", hash = "sha256:20874e7ca4436f683b64ebdbee2129a5a2c301579a67d1a7dda2cdf62fb7f5f7"}, + {file = "protobuf-4.23.4-cp310-abi3-win32.whl", hash = "sha256:5fea3c64d41ea5ecf5697b83e41d09b9589e6f20b677ab3c48e5f242d9b7897b"}, + {file = "protobuf-4.23.4-cp310-abi3-win_amd64.whl", hash = "sha256:7b19b6266d92ca6a2a87effa88ecc4af73ebc5cfde194dc737cf8ef23a9a3b12"}, + {file = "protobuf-4.23.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8547bf44fe8cec3c69e3042f5c4fb3e36eb2a7a013bb0a44c018fc1e427aafbd"}, + {file = "protobuf-4.23.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a"}, + {file = "protobuf-4.23.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:effeac51ab79332d44fba74660d40ae79985901ac21bca408f8dc335a81aa597"}, + {file = "protobuf-4.23.4-cp37-cp37m-win32.whl", hash = "sha256:c3e0939433c40796ca4cfc0fac08af50b00eb66a40bbbc5dee711998fb0bbc1e"}, + {file = "protobuf-4.23.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9053df6df8e5a76c84339ee4a9f5a2661ceee4a0dab019e8663c50ba324208b0"}, + {file = "protobuf-4.23.4-cp38-cp38-win32.whl", hash = "sha256:e1c915778d8ced71e26fcf43c0866d7499891bca14c4368448a82edc61fdbc70"}, + {file = "protobuf-4.23.4-cp38-cp38-win_amd64.whl", hash = "sha256:351cc90f7d10839c480aeb9b870a211e322bf05f6ab3f55fcb2f51331f80a7d2"}, + {file = "protobuf-4.23.4-cp39-cp39-win32.whl", hash = "sha256:6dd9b9940e3f17077e820b75851126615ee38643c2c5332aa7a359988820c720"}, + {file = "protobuf-4.23.4-cp39-cp39-win_amd64.whl", hash = "sha256:0a5759f5696895de8cc913f084e27fd4125e8fb0914bb729a17816a33819f474"}, + {file = "protobuf-4.23.4-py3-none-any.whl", hash = "sha256:e9d0be5bf34b275b9f87ba7407796556abeeba635455d036c7351f7c183ef8ff"}, + {file = "protobuf-4.23.4.tar.gz", hash = "sha256:ccd9430c0719dce806b93f89c91de7977304729e55377f872a92465d548329a9"}, ] [[package]] @@ -1368,43 +1391,43 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygit2" -version = "1.12.1" +version = "1.12.2" description = "Python bindings for libgit2." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pygit2-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50a155528aa611e4a217be31a9d2d8da283cfd978dbba07494cd04ea3d7c8768"}, - {file = "pygit2-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:248e22ccb1ea31f569373a3da3fa73d110ba2585c6326ff74b03c9579fb7b913"}, - {file = "pygit2-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e575e672c5a6cb39234b0076423a560e016d6b88cd50947c2df3bf59c5ccdf3d"}, - {file = "pygit2-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad9b46b52997d131b31ff46f699b074e9745c8fea8d0efb6b72ace43ab25828c"}, - {file = "pygit2-1.12.1-cp310-cp310-win32.whl", hash = "sha256:a8f495df877da04c572ecec4d532ae195680b4781dbf229bab4e801fa9ef20e9"}, - {file = "pygit2-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f1e1355c7fe2938a2bca0d6204a00c02950d13008722879e54a335b3e874006"}, - {file = "pygit2-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8a5c56b0b5dc8a317561070ef7557e180d4937d8b115c5a762d85e0109a216f3"}, - {file = "pygit2-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7c9ca8bc8a722863fc873234748fef3422007d5a6ea90ba3ae338d2907d3d6e"}, - {file = "pygit2-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c02a11f10bc4e329ab941f0c70874d39053c8f78544aefeb506f04cedb621a"}, - {file = "pygit2-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b3af334adf325b7c973417efa220fd5a9ce946b936262eceabc8ad8d46e0310"}, - {file = "pygit2-1.12.1-cp311-cp311-win32.whl", hash = "sha256:86c393962d1341893bbfa91829b3b8545e8ac7622f8b53b9a0b835b9cc1b5198"}, - {file = "pygit2-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:86c7e75ddc76f4e5593b47f9c2074fff242322ed9f4126116749f7c86021520a"}, - {file = "pygit2-1.12.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:939d11677f434024ea25a9137d8a525ef9f9ac474fb8b86399bc9526e6a7bff5"}, - {file = "pygit2-1.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:946f9215c0442995042ea512f764f7a6638d3a09f9d0484d3aeedbf8833f89e6"}, - {file = "pygit2-1.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd574620d3cc80df0b23bf2b7b08d8726e75a338d0fa1b67e4d6738d3ee56635"}, - {file = "pygit2-1.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d0adeff5c43229913f3bdae71c36e77ed19f36bd8dd6b5c141820964b1f5b3"}, - {file = "pygit2-1.12.1-cp38-cp38-win32.whl", hash = "sha256:ed8e2ef97171e994bf4d46c6c6534a3c12dd2dbbc47741e5995eaf8c2c92f71c"}, - {file = "pygit2-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:5318817055a3ca3906bf88344b9a6dc70c640f9b6bc236ac9e767d12bad54361"}, - {file = "pygit2-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cb9c803151ffeb0b8de52a93381108a2c6a9a446c55d659a135f52645e1650eb"}, - {file = "pygit2-1.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:47bf1e196dc23fe38018ad49b021d425edc319328169c597df45d73cf46b62ef"}, - {file = "pygit2-1.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:948479df72223bbcd16b2a88904dc2a3886c15a0107a7cf3b5373c8e34f52f31"}, - {file = "pygit2-1.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4bebe8b310edc2662cbffb94ef1a758252fe2e4c92bc83fac0eaf2bedf8b871"}, - {file = "pygit2-1.12.1-cp39-cp39-win32.whl", hash = "sha256:77bc0ab778ab6fe631f5f9eb831b426376a7b71426c5a913aaa9088382ef1dc9"}, - {file = "pygit2-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:e87b2306a266f6abca94ab37dda807033a6f40faad05c4d1e089f9e8354130a8"}, - {file = "pygit2-1.12.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5d5e8a3b67f5d4ba8e3838c492254688997747989b184b5f1a3af4fef7f9f53e"}, - {file = "pygit2-1.12.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2500b749759f2efdfa5096c0aafeb2d92152766708f5700284427bd658e5c407"}, - {file = "pygit2-1.12.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c21759ca9cc755faa2d17180cd49af004486ca84f3166cac089a2083dcb09114"}, - {file = "pygit2-1.12.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d73074ab64b383e3a1ab03e8070f6b195ef89b9d379ca5682c38dd9c289cc6e2"}, - {file = "pygit2-1.12.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:865c0d1925c52426455317f29c1db718187ec69ed5474faaf3e1c68ff2135767"}, - {file = "pygit2-1.12.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebebbe9125b068337b5415565ec94c9e092c708e430851b2d02e51217bdce4a"}, - {file = "pygit2-1.12.1.tar.gz", hash = "sha256:8218922abedc88a65d5092308d533ca4c4ed634aec86a3493d3bdf1a25aeeff3"}, + {file = "pygit2-1.12.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:79fbd99d3e08ca7478150eeba28ca4d4103f564148eab8d00aba8f1e6fc60654"}, + {file = "pygit2-1.12.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be3bb0139f464947523022a5af343a2e862c4ff250a57ec9f631449e7c0ba7c0"}, + {file = "pygit2-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4df3e5745fdf3111a6ccc905eae99f22f1a180728f714795138ca540cc2a50a"}, + {file = "pygit2-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:214bd214784fcbef7a8494d1d59e0cd3a731c0d24ce0f230dcc843322ee33b08"}, + {file = "pygit2-1.12.2-cp310-cp310-win32.whl", hash = "sha256:336c864ac961e7be8ba06e9ed8c999e4f624a8ccd90121cc4e40956d8b57acac"}, + {file = "pygit2-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:fb9eb57b75ce586928053692a25aae2a50fef3ad36661c57c07d4902899b1df3"}, + {file = "pygit2-1.12.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8f813d35d836c5b0d1962c387754786bcc7f1c3c8e11207b9eeb30238ac4cc7"}, + {file = "pygit2-1.12.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25a6548930328c5247bfb7c67d29104e63b036cb5390f032d9f91f63efb70434"}, + {file = "pygit2-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a365ffca23d910381749fdbcc367db52fe808f9aa4852914dd9ef8b711384a32"}, + {file = "pygit2-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec04c27be5d5af1ceecdcc0464e07081222f91f285f156dc53b23751d146569a"}, + {file = "pygit2-1.12.2-cp311-cp311-win32.whl", hash = "sha256:546091316c9a8c37b9867ddcc6c9f7402ca4d0b9db3f349212a7b5e71988e359"}, + {file = "pygit2-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bf14196cbfffbcd286f459a1d4fc660c5d5dfa8fb422e21216961df575410d6"}, + {file = "pygit2-1.12.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7bb30ab1fdaa4c30821fed33892958b6d92d50dbd03c76f7775b4e5d62f53a2e"}, + {file = "pygit2-1.12.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e7e705aaecad85b883022e81e054fbd27d26023fc031618ee61c51516580517e"}, + {file = "pygit2-1.12.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac2b5f408eb882e79645ebb43039ac37739c3edd25d857cc97d7482a684b613f"}, + {file = "pygit2-1.12.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7f3ad2b7b0c80be991bb47d8a2f2535cc9bf090746eb8679231ee565fde81"}, + {file = "pygit2-1.12.2-cp38-cp38-win32.whl", hash = "sha256:5b3ab4d6302990f7adb2b015bcbda1f0715277008d0c66440497e6f8313bf9cb"}, + {file = "pygit2-1.12.2-cp38-cp38-win_amd64.whl", hash = "sha256:c74e7601cb8b8dc3d02fd32274e200a7761cffd20ee531442bf1fa115c8f99a5"}, + {file = "pygit2-1.12.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a4083ba093c69142e0400114a4ef75e87834637d2bbfd77b964614bf70f624f"}, + {file = "pygit2-1.12.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:926f2e48c4eaa179249d417b8382290b86b0f01dbf41d289f763576209276b9f"}, + {file = "pygit2-1.12.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14ae27491347a0ac4bbe8347b09d752cfe7fea1121c14525415e0cca6db4a836"}, + {file = "pygit2-1.12.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f65483ab5e3563c58f60debe2acc0979fdf6fd633432fcfbddf727a9a265ba4"}, + {file = "pygit2-1.12.2-cp39-cp39-win32.whl", hash = "sha256:8da8517809635ea3da950d9cf99c6d1851352d92b6db309382db88a01c3b0bfd"}, + {file = "pygit2-1.12.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9c2359b99eed8e7fac30c06e6b4ae277a6a0537d6b4b88a190828c3d7eb9ef2"}, + {file = "pygit2-1.12.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:685378852ef8eb081333bc80dbdfc4f1333cf4a8f3baf614c4135e02ad1ee38a"}, + {file = "pygit2-1.12.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdf655e5f801990f5cad721b6ccbe7610962f0a4f1c20373dbf9c0be39374a81"}, + {file = "pygit2-1.12.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:857c5cde635d470f58803d67bfb281dc4f6336065a0253bfbed001f18e2d0767"}, + {file = "pygit2-1.12.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fe35a72af61961dbb7fb4abcdaa36d5f1c85b2cd3daae94137eeb9c07215cdd3"}, + {file = "pygit2-1.12.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f443d3641762b2bb9c76400bb18beb4ba27dd35bc098a8bfae82e6a190c52ab"}, + {file = "pygit2-1.12.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1e26649e1540b6a774f812e2fc9890320ff4d33f16db1bb02626318b5ceae2"}, + {file = "pygit2-1.12.2.tar.gz", hash = "sha256:56e85d0e66de957d599d1efb2409d39afeefd8f01009bfda0796b42a4b678358"}, ] [package.dependencies] @@ -1412,14 +1435,14 @@ cffi = ">=1.9.1" [[package]] name = "pytest" -version = "7.3.1" +version = "7.4.0" description = "pytest: simple powerful testing with Python" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, - {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] @@ -1431,7 +1454,7 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" @@ -1540,14 +1563,14 @@ dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatc [[package]] name = "redis" -version = "4.5.5" +version = "4.6.0" description = "Python client for Redis database and key-value store" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"}, - {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"}, + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, ] [package.dependencies] @@ -1634,53 +1657,50 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.4.48" +version = "1.4.49" description = "Database Abstraction Library" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.4.48-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4bac3aa3c3d8bc7408097e6fe8bf983caa6e9491c5d2e2488cfcfd8106f13b6a"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dbcae0e528d755f4522cad5842f0942e54b578d79f21a692c44d91352ea6d64e"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27m-win32.whl", hash = "sha256:cbbe8b8bffb199b225d2fe3804421b7b43a0d49983f81dc654d0431d2f855543"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27m-win_amd64.whl", hash = "sha256:627e04a5d54bd50628fc8734d5fc6df2a1aa5962f219c44aad50b00a6cdcf965"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9af1db7a287ef86e0f5cd990b38da6bd9328de739d17e8864f1817710da2d217"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ce7915eecc9c14a93b73f4e1c9d779ca43e955b43ddf1e21df154184f39748e5"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5381ddd09a99638f429f4cbe1b71b025bed318f6a7b23e11d65f3eed5e181c33"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:87609f6d4e81a941a17e61a4c19fee57f795e96f834c4f0a30cee725fc3f81d9"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0808ad34167f394fea21bd4587fc62f3bd81bba232a1e7fbdfa17e6cfa7cd7"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-win32.whl", hash = "sha256:d53cd8bc582da5c1c8c86b6acc4ef42e20985c57d0ebc906445989df566c5603"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-win_amd64.whl", hash = "sha256:4355e5915844afdc5cf22ec29fba1010166e35dd94a21305f49020022167556b"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:066c2b0413e8cb980e6d46bf9d35ca83be81c20af688fedaef01450b06e4aa5e"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c99bf13e07140601d111a7c6f1fc1519914dd4e5228315bbda255e08412f61a4"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee26276f12614d47cc07bc85490a70f559cba965fb178b1c45d46ffa8d73fda"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-win32.whl", hash = "sha256:49c312bcff4728bffc6fb5e5318b8020ed5c8b958a06800f91859fe9633ca20e"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-win_amd64.whl", hash = "sha256:cef2e2abc06eab187a533ec3e1067a71d7bbec69e582401afdf6d8cad4ba3515"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3509159e050bd6d24189ec7af373359f07aed690db91909c131e5068176c5a5d"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc2ab4d9f6d9218a5caa4121bdcf1125303482a1cdcfcdbd8567be8518969c0"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1ddbbcef9bcedaa370c03771ebec7e39e3944782bef49e69430383c376a250b"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f82d8efea1ca92b24f51d3aea1a82897ed2409868a0af04247c8c1e4fef5890"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-win32.whl", hash = "sha256:e3e98d4907805b07743b583a99ecc58bf8807ecb6985576d82d5e8ae103b5272"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-win_amd64.whl", hash = "sha256:25887b4f716e085a1c5162f130b852f84e18d2633942c8ca40dfb8519367c14f"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0817c181271b0ce5df1aa20949f0a9e2426830fed5ecdcc8db449618f12c2730"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe1dd2562313dd9fe1778ed56739ad5d9aae10f9f43d9f4cf81d65b0c85168bb"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:68413aead943883b341b2b77acd7a7fe2377c34d82e64d1840860247cec7ff7c"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbde5642104ac6e95f96e8ad6d18d9382aa20672008cf26068fe36f3004491df"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-win32.whl", hash = "sha256:11c6b1de720f816c22d6ad3bbfa2f026f89c7b78a5c4ffafb220e0183956a92a"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-win_amd64.whl", hash = "sha256:eb5464ee8d4bb6549d368b578e9529d3c43265007193597ddca71c1bae6174e6"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:92e6133cf337c42bfee03ca08c62ba0f2d9695618c8abc14a564f47503157be9"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d29a3fc6d9c45962476b470a81983dd8add6ad26fdbfae6d463b509d5adcda"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:005e942b451cad5285015481ae4e557ff4154dde327840ba91b9ac379be3b6ce"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8cfe951ed074ba5e708ed29c45397a95c4143255b0d022c7c8331a75ae61f3"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-win32.whl", hash = "sha256:2b9af65cc58726129d8414fc1a1a650dcdd594ba12e9c97909f1f57d48e393d3"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-win_amd64.whl", hash = "sha256:2b562e9d1e59be7833edf28b0968f156683d57cabd2137d8121806f38a9d58f4"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a1fc046756cf2a37d7277c93278566ddf8be135c6a58397b4c940abf837011f4"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d9b55252d2ca42a09bcd10a697fa041e696def9dfab0b78c0aaea1485551a08"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6dab89874e72a9ab5462997846d4c760cdb957958be27b03b49cf0de5e5c327c"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd8b5ee5a3acc4371f820934b36f8109ce604ee73cc668c724abb054cebcb6e"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-win32.whl", hash = "sha256:eee09350fd538e29cfe3a496ec6f148504d2da40dbf52adefb0d2f8e4d38ccc4"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-win_amd64.whl", hash = "sha256:7ad2b0f6520ed5038e795cc2852eb5c1f20fa6831d73301ced4aafbe3a10e1f6"}, - {file = "SQLAlchemy-1.4.48.tar.gz", hash = "sha256:b47bc287096d989a0838ce96f7d8e966914a24da877ed41a7531d44b55cdb8df"}, + {file = "SQLAlchemy-1.4.49-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6"}, + {file = "SQLAlchemy-1.4.49-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"}, + {file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"}, ] [package.dependencies] @@ -1890,14 +1910,14 @@ files = [ [[package]] name = "werkzeug" -version = "2.3.4" +version = "2.3.6" description = "The comprehensive WSGI web application library." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.4-py3-none-any.whl", hash = "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f"}, - {file = "Werkzeug-2.3.4.tar.gz", hash = "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76"}, + {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, + {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, ] [package.dependencies] From 81d29b4c66b284459a020b92c8db8de0f2c71bfc Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 8 Jul 2023 11:24:29 +0000 Subject: [PATCH 1340/1451] fix(deps): update dependency fastapi to ^0.100.0 --- poetry.lock | 16 +++++++--------- pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index dcdcf819..f3f19d11 100644 --- a/poetry.lock +++ b/poetry.lock @@ -560,25 +560,23 @@ lua = ["lupa (>=1.14,<2.0)"] [[package]] name = "fastapi" -version = "0.95.2" +version = "0.100.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.95.2-py3-none-any.whl", hash = "sha256:d374dbc4ef2ad9b803899bd3360d34c534adc574546e25314ab72c0c4411749f"}, - {file = "fastapi-0.95.2.tar.gz", hash = "sha256:4d9d3e8c71c73f11874bcf5e33626258d143252e329a01002f767306c64fb982"}, + {file = "fastapi-0.100.0-py3-none-any.whl", hash = "sha256:271662daf986da8fa98dc2b7c7f61c4abdfdccfb4786d79ed8b2878f172c6d5f"}, + {file = "fastapi-0.100.0.tar.gz", hash = "sha256:acb5f941ea8215663283c10018323ba7ea737c571b67fc7e88e9469c7eb1d12e"}, ] [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<3.0.0" starlette = ">=0.27.0,<0.28.0" +typing-extensions = ">=4.5.0" [package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "feedgen" @@ -1960,4 +1958,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "caf2a21e3bff699216e53a37697a7a544103fdea9f84a5d54ee94ded3e810973" +content-hash = "bbab458ee508b073ea3693caaa5d8704ff1a800ddecd816bf39a6561729777c0" diff --git a/pyproject.toml b/pyproject.toml index 69f04fab..34c5a135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ pytest-xdist = "^3.2.1" filelock = "^3.12.0" posix-ipc = "^1.1.1" pyalpm = "^0.10.6" -fastapi = "^0.95.1" +fastapi = "^0.100.0" srcinfo = "^0.1.2" tomlkit = "^0.11.8" From 1f40f6c5a0a6c22a071e0729d5bdff60018b303c Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 8 Jul 2023 12:38:19 +0100 Subject: [PATCH 1341/1451] housekeep: set current maintainers Signed-off-by: Leonidas Spyropoulos --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 34c5a135..012658a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ authors = [ "Kevin Morris " ] maintainers = [ - "Eli Schwartz " + "Leonidas Spyropoulos ", + "Mario Oenning " ] packages = [ { include = "aurweb" } From 4821fc131286bcea51ca0ea257d90b9ae20b85b0 Mon Sep 17 00:00:00 2001 From: moson Date: Sat, 8 Jul 2023 13:23:32 +0200 Subject: [PATCH 1342/1451] fix: show placeholder for deleted user in comments show "" in comment headers in case a user deleted their account. Signed-off-by: moson --- templates/partials/packages/comment.html | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index faac0753..3573a914 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -1,5 +1,9 @@ {% set header_cls = "comment-header" %} {% set article_cls = "article-content" %} +{% set comment_by = comment.User.Username %} +{% if not comment.User %} + {% set comment_by = "<deleted-account>" %} +{% endif %} {% if comment.Deleter %} {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} @@ -9,15 +13,15 @@ {% if not (request.user.HideDeletedComments and comment.DelTS) %}

    {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} - {% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %} + {% set view_account_info = 'View account information for %s' | tr | format(comment_by) %} {{ "%s commented on %s" | tr | format( ('%s' | format( - comment.User.Username, + comment_by, view_account_info, - comment.User.Username - )) if request.user.is_authenticated() else - (comment.User.Username), + comment_by + )) if request.user.is_authenticated() and comment.User else + (comment_by), '%s' | format( comment.ID, datetime_display(comment.CommentTS) From 225ce23761938dcfbb02809ac2371697038c037b Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Sat, 8 Jul 2023 12:54:43 +0100 Subject: [PATCH 1343/1451] chore(release): prepare for 6.2.6 Signed-off-by: Leonidas Spyropoulos --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 012658a0..ddd4f638 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.2.5" +version = "v6.2.6" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 5ccfa7c0fdc491df8556550092fac40fb0027284 Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 9 Jul 2023 14:52:15 +0200 Subject: [PATCH 1344/1451] fix: same ssh key entered multiple times Users might accidentally past their ssh key multiple times when they try to register or edit their account. Convert our of list of keys to a set, removing any double keys. Signed-off-by: moson --- aurweb/util.py | 4 ++-- test/test_accounts_routes.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/aurweb/util.py b/aurweb/util.py index 7050b482..3410e4d8 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -192,9 +192,9 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: return prefix, key -def parse_ssh_keys(string: str) -> list[Tuple[str, str]]: +def parse_ssh_keys(string: str) -> set[Tuple[str, str]]: """Parse a list of SSH public keys.""" - return [parse_ssh_key(e) for e in string.strip().splitlines(True) if e.strip()] + return set([parse_ssh_key(e) for e in string.strip().splitlines(True) if e.strip()]) def shell_exec(cmdline: str, cwd: str) -> Tuple[int, str, str]: diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index d3ddb174..c9d77c1f 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -644,6 +644,18 @@ def test_post_register_with_ssh_pubkey(client: TestClient): assert response.status_code == int(HTTPStatus.OK) + # Let's create another user accidentally pasting their key twice + with db.begin(): + db.query(SSHPubKey).delete() + + pk_double = pk + "\n" + pk + with client as request: + response = post_register( + request, U="doubleKey", E="doubleKey@email.org", PK=pk_double + ) + + assert response.status_code == int(HTTPStatus.OK) + def test_get_account_edit_tu_as_tu(client: TestClient, tu_user: User): """Test edit get route of another TU as a TU.""" @@ -1082,6 +1094,19 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): assert response.status_code == int(HTTPStatus.OK) + # Accidentally enter the same key twice + pk = make_ssh_pubkey() + post_data["PK"] = pk + "\n" + pk + + with client as request: + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) + + assert response.status_code == int(HTTPStatus.OK) + def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): request = Request() From c0bbe21d8183ed2d07eadcb5e0fd27526c70b78f Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 9 Jul 2023 16:13:02 +0200 Subject: [PATCH 1345/1451] fix(test): correct test for ssh-key parsing Our set of keys returned by "util.parse_ssh_keys" is unordered so we have to adapt our test to not rely on a specific order for multiple keys. Fixes: 5ccfa7c0fdc4 ("fix: same ssh key entered multiple times") Signed-off-by: moson --- test/test_util.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index 042b9ad9..1c3b51af 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -142,11 +142,8 @@ def assert_multiple_keys(pks): keys = util.parse_ssh_keys(pks) assert len(keys) == 2 pfx1, key1, pfx2, key2 = pks.split() - k1, k2 = keys - assert pfx1 == k1[0] - assert key1 == k1[1] - assert pfx2 == k2[0] - assert key2 == k2[1] + assert (pfx1, key1) in keys + assert (pfx2, key2) in keys def test_hash_query(): From fa1212f2dee216bd02b321e3747c22fc854b31a5 Mon Sep 17 00:00:00 2001 From: moson Date: Mon, 10 Jul 2023 18:02:20 +0200 Subject: [PATCH 1346/1451] fix: translations not containing string formatting In some translations we might be missing replacement placeholders (%). This turns out to be problematic when calling the format function. Wrap the jinja2 format function and just return the string unformatted when % is missing. Fixes: #341 Signed-off-by: moson --- aurweb/filters.py | 15 +++++++++++++++ test/test_filters.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/aurweb/filters.py b/aurweb/filters.py index 893f21af..38322cdf 100644 --- a/aurweb/filters.py +++ b/aurweb/filters.py @@ -8,6 +8,7 @@ from zoneinfo import ZoneInfo import fastapi import paginate from jinja2 import pass_context +from jinja2.filters import do_format import aurweb.models from aurweb import config, l10n @@ -164,3 +165,17 @@ def date_display(context: dict[str, Any], dt: Union[int, datetime]) -> str: @pass_context def datetime_display(context: dict[str, Any], dt: Union[int, datetime]) -> str: return date_strftime(context, dt, "%Y-%m-%d %H:%M (%Z)") + + +@register_filter("format") +def safe_format(value: str, *args: Any, **kwargs: Any) -> str: + """Wrapper for jinja2 format function to perform additional checks.""" + + # If we don't have anything to be formatted, just return the value. + # We have some translations that do not contain placeholders for replacement. + # In these cases the jinja2 function is throwing an error: + # "TypeError: not all arguments converted during string formatting" + if "%" not in value: + return value + + return do_format(value, *args, **kwargs) diff --git a/test/test_filters.py b/test/test_filters.py index e74ddb87..b56b80ab 100644 --- a/test/test_filters.py +++ b/test/test_filters.py @@ -1,6 +1,8 @@ from datetime import datetime from zoneinfo import ZoneInfo +import pytest + from aurweb import filters, time @@ -34,3 +36,18 @@ def test_to_qs(): query = {"a": "b", "c": [1, 2, 3]} qs = filters.to_qs(query) assert qs == "a=b&c=1&c=2&c=3" + + +@pytest.mark.parametrize( + "value, args, expected", + [ + ("", (), ""), + ("a", (), "a"), + ("a", (1,), "a"), + ("%s", ("a",), "a"), + ("%s", ("ab",), "ab"), + ("%s%d", ("a", 1), "a1"), + ], +) +def test_safe_format(value: str, args: tuple, expected: str): + assert filters.safe_format(value, *args) == expected From 27819b4465cd6cde7ef86caca812b1dd6f285880 Mon Sep 17 00:00:00 2001 From: moson Date: Thu, 13 Jul 2023 18:27:02 +0200 Subject: [PATCH 1347/1451] fix: /rss lazy load issue & perf improvements Some fixes for the /rss endpoints * Load all data in one go: Previously data was lazy loaded thus it made several sub-queries per package (> 200 queries for composing the rss data for a single request). Now we are performing a single SQL query. (request time improvement: 550ms -> 130ms) This also fixes aurweb-errors#510 and alike * Remove some "dead code": The fields "source, author, link" were never included in the rss output (wrong or insufficient data passed to the different entry.xyz functions) Nobody seems to be missing them anyways, so let's remove em. * Remove "Last-Modified" header: Obsolete since nginx can/will only handle "If-Modified-Since" requests in it's current configuration. All requests are passed to fastapi anyways. Signed-off-by: moson --- aurweb/routers/rss.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/aurweb/routers/rss.py b/aurweb/routers/rss.py index ee85b738..727d2b6a 100644 --- a/aurweb/routers/rss.py +++ b/aurweb/routers/rss.py @@ -1,5 +1,3 @@ -from datetime import datetime - from fastapi import APIRouter, Request from fastapi.responses import Response from feedgen.feed import FeedGenerator @@ -10,12 +8,11 @@ from aurweb.models import Package, PackageBase router = APIRouter() -def make_rss_feed(request: Request, packages: list, date_attr: str): +def make_rss_feed(request: Request, packages: list): """Create an RSS Feed string for some packages. :param request: A FastAPI request :param packages: A list of packages to add to the RSS feed - :param date_attr: The date attribute (DB column) to use :return: RSS Feed string """ @@ -36,18 +33,11 @@ def make_rss_feed(request: Request, packages: list, date_attr: str): entry = feed.add_entry(order="append") entry.title(pkg.Name) entry.link(href=f"{base}/packages/{pkg.Name}", rel="alternate") - entry.link(href=f"{base}/rss", rel="self", type="application/rss+xml") entry.description(pkg.Description or str()) - - attr = getattr(pkg.PackageBase, date_attr) - dt = filters.timestamp_to_datetime(attr) + dt = filters.timestamp_to_datetime(pkg.Timestamp) dt = filters.as_timezone(dt, request.user.Timezone) entry.pubDate(dt.strftime("%Y-%m-%d %H:%M:%S%z")) - - entry.source(f"{base}") - if pkg.PackageBase.Maintainer: - entry.author(author={"name": pkg.PackageBase.Maintainer.Username}) - entry.guid(f"{pkg.Name} - {attr}") + entry.guid(f"{pkg.Name}-{pkg.Timestamp}") return feed.rss_str() @@ -59,15 +49,15 @@ async def rss(request: Request): .join(PackageBase) .order_by(PackageBase.SubmittedTS.desc()) .limit(100) + .with_entities( + Package.Name, + Package.Description, + PackageBase.SubmittedTS.label("Timestamp"), + ) ) - feed = make_rss_feed(request, packages, "SubmittedTS") + feed = make_rss_feed(request, packages) response = Response(feed, media_type="application/rss+xml") - package = packages.first() - if package: - dt = datetime.utcfromtimestamp(package.PackageBase.SubmittedTS) - modified = dt.strftime("%a, %d %m %Y %H:%M:%S GMT") - response.headers["Last-Modified"] = modified return response @@ -79,14 +69,14 @@ async def rss_modified(request: Request): .join(PackageBase) .order_by(PackageBase.ModifiedTS.desc()) .limit(100) + .with_entities( + Package.Name, + Package.Description, + PackageBase.ModifiedTS.label("Timestamp"), + ) ) - feed = make_rss_feed(request, packages, "ModifiedTS") + feed = make_rss_feed(request, packages) response = Response(feed, media_type="application/rss+xml") - package = packages.first() - if package: - dt = datetime.utcfromtimestamp(package.PackageBase.ModifiedTS) - modified = dt.strftime("%a, %d %m %Y %H:%M:%S GMT") - response.headers["Last-Modified"] = modified return response From 862221f5ce244323208f9034b5c14bb0852d2cf8 Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 15 Jul 2023 20:27:12 +0000 Subject: [PATCH 1348/1451] fix(deps): update all non-major dependencies --- poetry.lock | 52 ++++++++++++++++++++------------------------------ pyproject.toml | 2 +- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/poetry.lock b/poetry.lock index f3f19d11..368371db 100644 --- a/poetry.lock +++ b/poetry.lock @@ -792,27 +792,27 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "hypercorn" -version = "0.14.3" +version = "0.14.4" description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Hypercorn-0.14.3-py3-none-any.whl", hash = "sha256:7c491d5184f28ee960dcdc14ab45d14633ca79d72ddd13cf4fcb4cb854d679ab"}, - {file = "Hypercorn-0.14.3.tar.gz", hash = "sha256:4a87a0b7bbe9dc75fab06dbe4b301b9b90416e9866c23a377df21a969d6ab8dd"}, + {file = "hypercorn-0.14.4-py3-none-any.whl", hash = "sha256:f956200dbf8677684e6e976219ffa6691d6cf795281184b41dbb0b135ab37b8d"}, + {file = "hypercorn-0.14.4.tar.gz", hash = "sha256:3fa504efc46a271640023c9b88c3184fd64993f47a282e8ae1a13ccb285c2f67"}, ] [package.dependencies] h11 = "*" h2 = ">=3.1.0" priority = "*" -toml = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} wsproto = ">=0.14.0" [package.extras] docs = ["pydata_sphinx_theme"] h3 = ["aioquic (>=0.9.0,<1.0)"] -trio = ["trio (>=0.11.0)"] +trio = ["exceptiongroup (>=1.1.0)", "trio (>=0.22.0)"] uvloop = ["uvloop"] [[package]] @@ -912,6 +912,8 @@ files = [ {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, + {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, + {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, @@ -1274,14 +1276,14 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "6.0.0" +version = "6.1.0" description = "Instrument your FastAPI with Prometheus metrics." category = "main" optional = false python-versions = ">=3.7.0,<4.0.0" files = [ - {file = "prometheus_fastapi_instrumentator-6.0.0-py3-none-any.whl", hash = "sha256:6f66a951a4801667f7311d161f3aebfe0cd202391d0f067fbbe169792e2d987b"}, - {file = "prometheus_fastapi_instrumentator-6.0.0.tar.gz", hash = "sha256:f1ddd0b8ead75e71d055bdf4cb7e995ec6a6ca63543245e7bbc5ca9b14c45191"}, + {file = "prometheus_fastapi_instrumentator-6.1.0-py3-none-any.whl", hash = "sha256:2279ac1cf5b9566a4c3a07f78c9c5ee19648ed90976ab87d73d672abc1bfa017"}, + {file = "prometheus_fastapi_instrumentator-6.1.0.tar.gz", hash = "sha256:1820d7a90389ce100f7d1285495ead388818ae0882e761c1f3e6e62a410bdf13"}, ] [package.dependencies] @@ -1456,14 +1458,14 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.21.0" +version = "0.21.1" description = "Pytest support for asyncio" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"}, - {file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"}, + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] [package.dependencies] @@ -1494,14 +1496,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-tap" -version = "3.3" +version = "3.4" description = "Test Anything Protocol (TAP) reporting plugin for pytest" category = "dev" optional = false python-versions = "*" files = [ - {file = "pytest-tap-3.3.tar.gz", hash = "sha256:5f0919a147cf0396b2f10d64d365a0bf8062e06543e93c675c9d37f5605e983c"}, - {file = "pytest_tap-3.3-py3-none-any.whl", hash = "sha256:4fbbc0e090c2e94f6199bee4e4f68ab3c5e176b37a72a589ad84e0f72a2fce55"}, + {file = "pytest-tap-3.4.tar.gz", hash = "sha256:a7c2a4a3e8b4bf18522e46d74208f8579a191dd972c59182104ad9a4967318fb"}, + {file = "pytest_tap-3.4-py3-none-any.whl", hash = "sha256:d97a2115c94415086f6faec395d243b3c18ea846ce1c1653a4b2588082be35d8"}, ] [package.dependencies] @@ -1774,18 +1776,6 @@ files = [ [package.extras] yaml = ["PyYAML (>=5.1)", "more-itertools"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -1842,14 +1832,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.22.0" +version = "0.23.0" description = "The lightning-fast ASGI server." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"}, - {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"}, + {file = "uvicorn-0.23.0-py3-none-any.whl", hash = "sha256:479599b2c0bb1b9b394c6d43901a1eb0c1ec72c7d237b5bafea23c5b2d4cdf10"}, + {file = "uvicorn-0.23.0.tar.gz", hash = "sha256:d38ab90c0e2c6fe3a054cddeb962cfd5d0e0e6608eaaff4a01d5c36a67f3168c"}, ] [package.dependencies] @@ -1958,4 +1948,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "bbab458ee508b073ea3693caaa5d8704ff1a800ddecd816bf39a6561729777c0" +content-hash = "b67f1b1599794a6890b0a31b2b127880d75c84beeeae3df4ecb3ae92296948da" diff --git a/pyproject.toml b/pyproject.toml index ddd4f638..e98e887f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ Werkzeug = "^2.3.3" SQLAlchemy = "^1.4.48" # ASGI -uvicorn = "^0.22.0" +uvicorn = "^0.23.0" gunicorn = "^20.1.0" Hypercorn = "^0.14.3" prometheus-fastapi-instrumentator = "^6.0.0" From 5729d6787f43574e70f6b87543065838d475f880 Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 16 Jul 2023 13:27:02 +0200 Subject: [PATCH 1349/1451] fix: git links in comments for multiple OIDs The chance of finding multiple object IDs when performing lookups with a shortened SHA1 hash (7 digits) seems to be quite high. In those cases pygit2 will throw an error. Let's catch those exceptions and gracefully handle them. Fixes: aurweb-errors#496 (and alike) Signed-off-by: moson --- aurweb/scripts/rendercomment.py | 9 ++++++-- test/test_rendercomment.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index e640c1d4..31f3fdd4 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -72,8 +72,13 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): def handleMatch(self, m, data): oid = m.group(1) - if oid not in self._repo: - # Unknown OID; preserve the orginal text. + # Lookup might raise ValueError in case multiple object ID's were found + try: + if oid not in self._repo: + # Unknown OID; preserve the orginal text. + return None, None, None + except ValueError: + # Multiple OID's found; preserve the orginal text. return None, None, None el = Element("a") diff --git a/test/test_rendercomment.py b/test/test_rendercomment.py index 59eb7191..f9edb45b 100644 --- a/test/test_rendercomment.py +++ b/test/test_rendercomment.py @@ -1,5 +1,7 @@ +import os from unittest import mock +import pygit2 import pytest from aurweb import aur_logging, config, db, time @@ -166,6 +168,43 @@ http://example.com/{commit_hash}\ assert comment.RenderedComment == expected +def test_git_commit_link_multiple_oids( + git: GitRepository, user: User, package: Package +): + # Make sure we get reproducible hashes by hardcoding the dates + date = "Sun, 16 Jul 2023 06:06:06 +0200" + os.environ["GIT_COMMITTER_DATE"] = date + os.environ["GIT_AUTHOR_DATE"] = date + + # Package names that cause two object IDs starting with "09a3468" + pkgnames = [ + "bfa3e330-23c5-11ee-9b6c-9c2dcdf2810d", + "54c6a420-23c6-11ee-9b6c-9c2dcdf2810d", + ] + + # Create our commits + for pkgname in pkgnames: + with db.begin(): + package = db.create( + Package, PackageBase=package.PackageBase, Name=pkgname, Version="1.0" + ) + git.commit(package, pkgname) + + repo_path = config.get("serve", "repo-path") + repo = pygit2.Repository(repo_path) + + # Make sure we get an error when we search the git repo for "09a3468" + with pytest.raises(ValueError) as oid_error: + assert "09a3468" in repo + assert "ambiguous OID prefix" in oid_error + + # Create a comment, referencing "09a3468" + comment = create_comment(user, package.PackageBase, "Commit 09a3468 is nasty!") + + # Make sure our comment does not contain a link. + assert comment.RenderedComment == "

    Commit 09a3468 is nasty!

    " + + def test_flyspray_issue_link(user: User, pkgbase: PackageBase): text = """\ FS#1234567. From bc03d8b8f20ac0a1e6a2b03069632c8a064332f0 Mon Sep 17 00:00:00 2001 From: moson Date: Thu, 20 Jul 2023 18:21:05 +0200 Subject: [PATCH 1350/1451] fix: Fix middleware checking for accepted terms The current query is a bit mixed up. The intention was to return the number of unaccepted records. Now it does also count all records that were accepted by some other user though. Let's check the total number of terms vs. the number of accepted records (by our user) instead. Signed-off-by: moson --- aurweb/asgi.py | 19 ++++++++----------- test/test_accounts_routes.py | 6 ++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/aurweb/asgi.py b/aurweb/asgi.py index eb02413b..1be77ff9 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -14,7 +14,7 @@ from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from jinja2 import TemplateNotFound from prometheus_client import multiprocess -from sqlalchemy import and_, or_ +from sqlalchemy import and_ from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.sessions import SessionMiddleware @@ -277,21 +277,18 @@ async def check_terms_of_service(request: Request, call_next: typing.Callable): """This middleware function redirects authenticated users if they have any outstanding Terms to agree to.""" if request.user.is_authenticated() and request.url.path != "/tos": - unaccepted = ( + accepted = ( query(Term) .join(AcceptedTerm) .filter( - or_( - AcceptedTerm.UsersID != request.user.ID, - and_( - AcceptedTerm.UsersID == request.user.ID, - AcceptedTerm.TermsID == Term.ID, - AcceptedTerm.Revision < Term.Revision, - ), - ) + and_( + AcceptedTerm.UsersID == request.user.ID, + AcceptedTerm.TermsID == Term.ID, + AcceptedTerm.Revision >= Term.Revision, + ), ) ) - if query(Term).count() > unaccepted.count(): + if query(Term).count() - accepted.count() > 0: return RedirectResponse("/tos", status_code=int(http.HTTPStatus.SEE_OTHER)) return await util.error_or_result(call_next, request) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index c9d77c1f..3c481d0a 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -1915,6 +1915,12 @@ def test_get_terms_of_service(client: TestClient, user: User): # We accepted the term, there's nothing left to accept. assert response.status_code == int(HTTPStatus.SEE_OTHER) + # Make sure we don't get redirected to /tos when browsing "Home" + with client as request: + request.cookies = cookies + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + # Bump the term's revision. with db.begin(): term.Revision = 2 From 347c2ce721b5782ff0324eb80fdc5613b4ebe478 Mon Sep 17 00:00:00 2001 From: moson Date: Sat, 22 Jul 2023 10:43:19 +0200 Subject: [PATCH 1351/1451] change: Change order of commit validation routine We currently validate all commits going from latest -> oldest. It would be nicer to go oldest -> latest so that, in case of errors, we would indicate which commit "introduced" the problem. Signed-off-by: moson --- aurweb/git/update.py | 2 +- test/t1300-git-update.t | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aurweb/git/update.py b/aurweb/git/update.py index cd7813e0..4c4fff0f 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -356,7 +356,7 @@ def main(): # noqa: C901 die("denying non-fast-forward (you should pull first)") # Prepare the walker that validates new commits. - walker = repo.walk(sha1_new, pygit2.GIT_SORT_TOPOLOGICAL) + walker = repo.walk(sha1_new, pygit2.GIT_SORT_REVERSE) if sha1_old != "0" * 40: walker.hide(sha1_old) diff --git a/test/t1300-git-update.t b/test/t1300-git-update.t index 4fdb487b..0fb2da17 100755 --- a/test/t1300-git-update.t +++ b/test/t1300-git-update.t @@ -312,11 +312,16 @@ test_expect_success 'Pushing a tree with a large blob.' ' printf "%256001s" x >aur.git/file && git -C aur.git add file && git -C aur.git commit -q -m "Add large blob" && + first_error=$(git -C aur.git rev-parse HEAD) && + touch aur.git/another.file && + git -C aur.git add another.file && + git -C aur.git commit -q -m "Add another commit" && new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && - grep -q "^error: maximum blob size (250.00KiB) exceeded$" actual + grep -q "^error: maximum blob size (250.00KiB) exceeded$" actual && + grep -q "^error: $first_error:$" actual ' test_expect_success 'Pushing .SRCINFO with a non-matching package base.' ' From 44c158b8c2667ace8e44d2cac3b7aed4efcc1464 Mon Sep 17 00:00:00 2001 From: moson Date: Sat, 22 Jul 2023 16:31:50 +0200 Subject: [PATCH 1352/1451] feat: Implement statistics class & additional metrics The new module/class helps us constructing queries and count records to expose various statistics on the homepage. We also utilize for some new prometheus metrics (package and user gauges). Record counts are being cached with Redis. Signed-off-by: moson --- aurweb/cache.py | 11 ++-- aurweb/prometheus.py | 18 ++++++- aurweb/routers/html.py | 72 ++++---------------------- aurweb/routers/packages.py | 6 +-- aurweb/statistics.py | 102 +++++++++++++++++++++++++++++++++++++ test/test_cache.py | 18 +++---- test/test_metrics.py | 5 +- 7 files changed, 143 insertions(+), 89 deletions(-) create mode 100644 aurweb/statistics.py diff --git a/aurweb/cache.py b/aurweb/cache.py index fe1e5f1d..bb374e57 100644 --- a/aurweb/cache.py +++ b/aurweb/cache.py @@ -1,20 +1,15 @@ import pickle -from prometheus_client import Counter from sqlalchemy import orm from aurweb import config from aurweb.aur_redis import redis_connection +from aurweb.prometheus import SEARCH_REQUESTS _redis = redis_connection() -# Prometheus metrics -SEARCH_REQUESTS = Counter( - "search_requests", "Number of search requests by cache hit/miss", ["cache"] -) - -async def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int: +def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int: """Store and retrieve a query.count() via redis cache. :param key: Redis key @@ -30,7 +25,7 @@ async def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int: return int(result) -async def db_query_cache(key: str, query: orm.Query, expire: int = None) -> list: +def db_query_cache(key: str, query: orm.Query, expire: int = None) -> list: """Store and retrieve query results via redis cache. :param key: Redis key diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index b8b7984f..d3455551 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -1,6 +1,6 @@ from typing import Any, Callable, Optional -from prometheus_client import Counter +from prometheus_client import Counter, Gauge from prometheus_fastapi_instrumentator import Instrumentator from prometheus_fastapi_instrumentator.metrics import Info from starlette.routing import Match, Route @@ -11,10 +11,26 @@ logger = aur_logging.get_logger(__name__) _instrumentator = Instrumentator() +# Custom metrics +SEARCH_REQUESTS = Counter( + "aur_search_requests", "Number of search requests by cache hit/miss", ["cache"] +) +USERS = Gauge( + "aur_users", "Number of AUR users by type", ["type"], multiprocess_mode="livemax" +) +PACKAGES = Gauge( + "aur_packages", + "Number of AUR packages by state", + ["state"], + multiprocess_mode="livemax", +) + + def instrumentator(): return _instrumentator +# FastAPI metrics # Taken from https://github.com/stephenhillier/starlette_exporter # Their license is included in LICENSES/starlette_exporter. # The code has been modified to remove child route checks diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index fc9f3519..c3bcee49 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -17,11 +17,10 @@ from sqlalchemy import case, or_ import aurweb.config import aurweb.models.package_request from aurweb import aur_logging, cookies, db, models, time, util -from aurweb.cache import db_count_cache from aurweb.exceptions import handle_form_exceptions -from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID from aurweb.models.package_request import PENDING_ID from aurweb.packages.util import query_notified, query_voted, updated_packages +from aurweb.statistics import Statistics, update_prometheus_metrics from aurweb.templates import make_context, render_template logger = aur_logging.get_logger(__name__) @@ -87,68 +86,12 @@ async def index(request: Request): context = make_context(request, "Home") context["ssh_fingerprints"] = util.get_ssh_fingerprints() - bases = db.query(models.PackageBase) - cache_expire = aurweb.config.getint("cache", "expiry_time") + # Package statistics. - context["package_count"] = await db_count_cache( - "package_count", bases, expire=cache_expire - ) - - query = bases.filter(models.PackageBase.MaintainerUID.is_(None)) - context["orphan_count"] = await db_count_cache( - "orphan_count", query, expire=cache_expire - ) - - query = db.query(models.User) - context["user_count"] = await db_count_cache( - "user_count", query, expire=cache_expire - ) - - query = query.filter( - or_( - models.User.AccountTypeID == TRUSTED_USER_ID, - models.User.AccountTypeID == TRUSTED_USER_AND_DEV_ID, - ) - ) - context["trusted_user_count"] = await db_count_cache( - "trusted_user_count", query, expire=cache_expire - ) - - # Current timestamp. - now = time.utcnow() - - seven_days = 86400 * 7 # Seven days worth of seconds. - seven_days_ago = now - seven_days - - one_hour = 3600 - updated = bases.filter( - models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS >= one_hour - ) - - query = bases.filter(models.PackageBase.SubmittedTS >= seven_days_ago) - context["seven_days_old_added"] = await db_count_cache( - "seven_days_old_added", query, expire=cache_expire - ) - - query = updated.filter(models.PackageBase.ModifiedTS >= seven_days_ago) - context["seven_days_old_updated"] = await db_count_cache( - "seven_days_old_updated", query, expire=cache_expire - ) - - year = seven_days * 52 # Fifty two weeks worth: one year. - year_ago = now - year - query = updated.filter(models.PackageBase.ModifiedTS >= year_ago) - context["year_old_updated"] = await db_count_cache( - "year_old_updated", query, expire=cache_expire - ) - - query = bases.filter( - models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS < 3600 - ) - context["never_updated"] = await db_count_cache( - "never_updated", query, expire=cache_expire - ) + stats = Statistics(cache_expire) + for counter in stats.HOMEPAGE_COUNTERS: + context[counter] = stats.get_count(counter) # Get the 15 most recently updated packages. context["package_updates"] = updated_packages(15, cache_expire) @@ -193,7 +136,7 @@ async def index(request: Request): ) archive_time = aurweb.config.getint("options", "request_archive_time") - start = now - archive_time + start = time.utcnow() - archive_time # Package requests created by request.user. context["package_requests"] = ( @@ -269,6 +212,9 @@ async def metrics(request: Request): status_code=HTTPStatus.SERVICE_UNAVAILABLE, ) + # update prometheus gauges for packages and users + update_prometheus_metrics() + registry = CollectorRegistry() multiprocess.MultiProcessCollector(registry) data = generate_latest(registry) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 779efb4b..f1b2a138 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -91,9 +91,7 @@ async def packages_get( # increase the amount of time required to collect a count. # we use redis for caching the results of the query cache_expire = config.getint("cache", "expiry_time") - num_packages = await db_count_cache( - hash_query(search.query), search.query, cache_expire - ) + num_packages = db_count_cache(hash_query(search.query), search.query, cache_expire) # Apply user-specified sort column and ordering. search.sort_by(sort_by, sort_order) @@ -118,7 +116,7 @@ async def packages_get( results = results.limit(per_page).offset(offset) # we use redis for caching the results of the query - packages = await db_query_cache(hash_query(results), results, cache_expire) + packages = db_query_cache(hash_query(results), results, cache_expire) context["packages"] = packages context["packages_count"] = num_packages diff --git a/aurweb/statistics.py b/aurweb/statistics.py new file mode 100644 index 00000000..934caa37 --- /dev/null +++ b/aurweb/statistics.py @@ -0,0 +1,102 @@ +from aurweb import config, db, time +from aurweb.cache import db_count_cache +from aurweb.models import PackageBase, User +from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.prometheus import PACKAGES, USERS + + +class Statistics: + HOMEPAGE_COUNTERS = [ + "package_count", + "orphan_count", + "seven_days_old_added", + "seven_days_old_updated", + "year_old_updated", + "never_updated", + "user_count", + "trusted_user_count", + ] + PROMETHEUS_USER_COUNTERS = [ + ("trusted_user_count", "tu"), + ("regular_user_count", "user"), + ] + PROMETHEUS_PACKAGE_COUNTERS = [ + ("orphan_count", "orphan"), + ("never_updated", "not_updated"), + ("updated_packages", "updated"), + ] + + seven_days = 86400 * 7 + one_hour = 3600 + year = seven_days * 52 + + def __init__(self, cache_expire: int = None) -> "Statistics": + self.expiry_time = cache_expire + self.now = time.utcnow() + self.seven_days_ago = self.now - self.seven_days + self.year_ago = self.now - self.year + self.user_query = db.query(User) + self.bases_query = db.query(PackageBase) + self.updated_query = db.query(PackageBase).filter( + PackageBase.ModifiedTS - PackageBase.SubmittedTS >= self.one_hour + ) + + def get_count(self, counter: str) -> int: + query = None + match counter: + case "package_count": + query = self.bases_query + case "orphan_count": + query = self.bases_query.filter(PackageBase.MaintainerUID.is_(None)) + case "seven_days_old_added": + query = self.bases_query.filter( + PackageBase.SubmittedTS >= self.seven_days_ago + ) + case "seven_days_old_updated": + query = self.updated_query.filter( + PackageBase.ModifiedTS >= self.seven_days_ago + ) + case "year_old_updated": + query = self.updated_query.filter( + PackageBase.ModifiedTS >= self.year_ago + ) + case "never_updated": + query = self.bases_query.filter( + PackageBase.ModifiedTS - PackageBase.SubmittedTS < self.one_hour + ) + case "updated_packages": + query = self.bases_query.filter( + PackageBase.ModifiedTS - PackageBase.SubmittedTS > self.one_hour, + ~PackageBase.MaintainerUID.is_(None), + ) + case "user_count": + query = self.user_query + case "trusted_user_count": + query = self.user_query.filter( + User.AccountTypeID.in_( + ( + TRUSTED_USER_ID, + TRUSTED_USER_AND_DEV_ID, + ) + ) + ) + case "regular_user_count": + query = self.user_query.filter(User.AccountTypeID == USER_ID) + case _: + return -1 + + return db_count_cache(counter, query, expire=self.expiry_time) + + +def update_prometheus_metrics(): + cache_expire = config.getint("cache", "expiry_time") + stats = Statistics(cache_expire) + # Users gauge + for counter, utype in stats.PROMETHEUS_USER_COUNTERS: + count = stats.get_count(counter) + USERS.labels(utype).set(count) + + # Packages gauge + for counter, state in stats.PROMETHEUS_PACKAGE_COUNTERS: + count = stats.get_count(counter) + PACKAGES.labels(state).set(count) diff --git a/test/test_cache.py b/test/test_cache.py index e19fa6a2..a599ab32 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -31,15 +31,14 @@ def clear_fakeredis_cache(): cache._redis.flushall() -@pytest.mark.asyncio -async def test_db_count_cache(user): +def test_db_count_cache(user): query = db.query(User) # We have no cached value yet. assert cache._redis.get("key1") is None # Add to cache - assert await cache.db_count_cache("key1", query) == query.count() + assert cache.db_count_cache("key1", query) == query.count() # It's cached now. assert cache._redis.get("key1") is not None @@ -48,35 +47,34 @@ async def test_db_count_cache(user): assert cache._redis.ttl("key1") == -1 # Cache a query with an expire. - value = await cache.db_count_cache("key2", query, 100) + value = cache.db_count_cache("key2", query, 100) assert value == query.count() assert cache._redis.ttl("key2") == 100 -@pytest.mark.asyncio -async def test_db_query_cache(user): +def test_db_query_cache(user): query = db.query(User) # We have no cached value yet. assert cache._redis.get("key1") is None # Add to cache - await cache.db_query_cache("key1", query) + cache.db_query_cache("key1", query) # It's cached now. assert cache._redis.get("key1") is not None # Modify our user and make sure we got a cached value user.Username = "changed" - cached = await cache.db_query_cache("key1", query) + cached = cache.db_query_cache("key1", query) assert cached[0].Username != query.all()[0].Username # It does not expire assert cache._redis.ttl("key1") == -1 # Cache a query with an expire. - value = await cache.db_query_cache("key2", query, 100) + value = cache.db_query_cache("key2", query, 100) assert len(value) == query.count() assert value[0].Username == query.all()[0].Username @@ -90,7 +88,7 @@ async def test_db_query_cache(user): with mock.patch("aurweb.config.getint", side_effect=mock_max_search_entries): # Try to add another entry (we already have 2) - await cache.db_query_cache("key3", query) + cache.db_query_cache("key3", query) # Make sure it was not added because it exceeds our max. assert cache._redis.get("key3") is None diff --git a/test/test_metrics.py b/test/test_metrics.py index 1859d8cb..6f67d926 100644 --- a/test/test_metrics.py +++ b/test/test_metrics.py @@ -26,11 +26,10 @@ def user() -> User: yield user -@pytest.mark.asyncio -async def test_search_cache_metrics(user: User): +def test_search_cache_metrics(user: User): # Fire off 3 identical queries for caching for _ in range(3): - await db_query_cache("key", db.query(User)) + db_query_cache("key", db.query(User)) # Get metrics metrics = str(generate_latest(REGISTRY)) From 8699457917a05caae41a7cd2b7ecb6d94a7955b7 Mon Sep 17 00:00:00 2001 From: moson Date: Sat, 22 Jul 2023 21:23:16 +0200 Subject: [PATCH 1353/1451] feat: Separate cache expiry for stats and search Allows us to set different cache eviction timespans for search queries and statistics. Stats and especially "last package updates" should probably be refreshed more often, whereas we might want to cache search results for a bit longer. So this gives us a bit more flexibility playing around with different settings and tweak things. Signed-off-by: moson --- aurweb/routers/html.py | 2 +- aurweb/routers/packages.py | 2 +- aurweb/statistics.py | 2 +- conf/config.defaults | 6 ++++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index c3bcee49..2ec497bd 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -86,7 +86,7 @@ async def index(request: Request): context = make_context(request, "Home") context["ssh_fingerprints"] = util.get_ssh_fingerprints() - cache_expire = aurweb.config.getint("cache", "expiry_time") + cache_expire = aurweb.config.getint("cache", "expiry_time_statistics", 300) # Package statistics. stats = Statistics(cache_expire) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index f1b2a138..3f96d71c 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -90,7 +90,7 @@ async def packages_get( # Including more query operations below, like ordering, will # increase the amount of time required to collect a count. # we use redis for caching the results of the query - cache_expire = config.getint("cache", "expiry_time") + cache_expire = config.getint("cache", "expiry_time_search", 600) num_packages = db_count_cache(hash_query(search.query), search.query, cache_expire) # Apply user-specified sort column and ordering. diff --git a/aurweb/statistics.py b/aurweb/statistics.py index 934caa37..6e9dbe1f 100644 --- a/aurweb/statistics.py +++ b/aurweb/statistics.py @@ -89,7 +89,7 @@ class Statistics: def update_prometheus_metrics(): - cache_expire = config.getint("cache", "expiry_time") + cache_expire = config.getint("cache", "expiry_time_statistics", 300) stats = Statistics(cache_expire) # Users gauge for counter, utype in stats.PROMETHEUS_USER_COUNTERS: diff --git a/conf/config.defaults b/conf/config.defaults index 4e2415ed..ab0a9b67 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -169,5 +169,7 @@ range_end = 172800 [cache] ; maximum number of keys/entries (for search results) in our redis cache, default is 50000 max_search_entries = 50000 -; number of seconds after a cache entry expires, default is 3 minutes -expiry_time = 180 +; number of seconds after a cache entry for search queries expires, default is 10 minutes +expiry_time_search = 600 +; number of seconds after a cache entry for statistics queries expires, default is 5 minutes +expiry_time_statistics = 300 From 6cd70a5c9fb57c42af7c2254045eba9fe6aa17e0 Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 23 Jul 2023 11:34:50 +0200 Subject: [PATCH 1354/1451] test: Add tests for user/package statistics Signed-off-by: moson --- test/test_statistics.py | 125 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 test/test_statistics.py diff --git a/test/test_statistics.py b/test/test_statistics.py new file mode 100644 index 00000000..dda7b357 --- /dev/null +++ b/test/test_statistics.py @@ -0,0 +1,125 @@ +import pytest +from prometheus_client import REGISTRY, generate_latest + +from aurweb import cache, db, time +from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.user import User +from aurweb.statistics import Statistics, update_prometheus_metrics + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture(autouse=True) +def clear_fakeredis_cache(): + cache._redis.flushall() + + +@pytest.fixture +def test_data(): + # Create some test data (users and packages) + with db.begin(): + for i in range(10): + user = db.create( + User, + Username=f"test{i}", + Email=f"test{i}@example.org", + RealName=f"Test User {i}", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + + now = time.utcnow() + old = now - 60 * 60 * 24 * 8 # 8 days + older = now - 60 * 60 * 24 * 400 # 400 days + + pkgbase = db.create( + PackageBase, + Name=f"test-package{i}", + Maintainer=user, + SubmittedTS=old, + ModifiedTS=now, + ) + db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + + # Modify some data to get some variances for our counters + if i == 1: + user.AccountTypeID = TRUSTED_USER_ID + pkgbase.Maintainer = None + pkgbase.SubmittedTS = now + + if i == 2: + pkgbase.SubmittedTS = older + + if i == 3: + pkgbase.SubmittedTS = older + pkgbase.ModifiedTS = old + yield + + +@pytest.fixture +def stats() -> Statistics: + yield Statistics() + + +@pytest.mark.parametrize( + "counter, expected", + [ + ("package_count", 10), + ("orphan_count", 1), + ("seven_days_old_added", 1), + ("seven_days_old_updated", 8), + ("year_old_updated", 9), + ("never_updated", 1), + ("user_count", 10), + ("trusted_user_count", 1), + ("regular_user_count", 9), + ("updated_packages", 9), + ("nonsense", -1), + ], +) +def test_get_count(stats: Statistics, test_data, counter: str, expected: int): + assert stats.get_count(counter) == expected + + +def test_get_count_change(stats: Statistics, test_data): + pkgs_before = stats.get_count("package_count") + tus_before = stats.get_count("trusted_user_count") + + assert pkgs_before == 10 + assert tus_before == 1 + + # Let's delete a package and promote a user to TU + with db.begin(): + pkgbase = db.query(PackageBase).first() + db.delete(pkgbase) + + user = db.query(User).filter(User.AccountTypeID == USER_ID).first() + user.AccountTypeID = TRUSTED_USER_ID + + # Values should end up in (fake) redis cache so they should be the same + assert stats.get_count("package_count") == pkgs_before + assert stats.get_count("trusted_user_count") == tus_before + + # Let's clear the cache and check again + cache._redis.flushall() + assert stats.get_count("package_count") != pkgs_before + assert stats.get_count("trusted_user_count") != tus_before + + +def test_update_prometheus_metrics(test_data): + metrics = str(generate_latest(REGISTRY)) + + assert "aur_users{" not in metrics + assert "aur_packages{" not in metrics + + # Let's update our metrics. We should find our gauges now + update_prometheus_metrics() + metrics = str(generate_latest(REGISTRY)) + + assert 'aur_users{type="user"} 9.0' in metrics + assert 'aur_packages{state="updated"} 9.0' in metrics From e45878a058071e59f0f08eb3dca597f560448298 Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 23 Jul 2023 18:53:58 +0200 Subject: [PATCH 1355/1451] fix: Fix issue with requests totals Problem is that we join with PackageBase, thus we are missing requests for packages that were deleted. Fixes: #483 Signed-off-by: moson --- aurweb/routers/html.py | 11 ++--- aurweb/routers/requests.py | 16 ++----- aurweb/statistics.py | 95 ++++++++++++++++++++++++++++---------- test/test_statistics.py | 32 ++++++++++++- 4 files changed, 111 insertions(+), 43 deletions(-) diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index 2ec497bd..63cc3bb8 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -16,11 +16,10 @@ from sqlalchemy import case, or_ import aurweb.config import aurweb.models.package_request -from aurweb import aur_logging, cookies, db, models, time, util +from aurweb import aur_logging, cookies, db, models, statistics, time, util from aurweb.exceptions import handle_form_exceptions from aurweb.models.package_request import PENDING_ID from aurweb.packages.util import query_notified, query_voted, updated_packages -from aurweb.statistics import Statistics, update_prometheus_metrics from aurweb.templates import make_context, render_template logger = aur_logging.get_logger(__name__) @@ -89,9 +88,9 @@ async def index(request: Request): cache_expire = aurweb.config.getint("cache", "expiry_time_statistics", 300) # Package statistics. - stats = Statistics(cache_expire) - for counter in stats.HOMEPAGE_COUNTERS: - context[counter] = stats.get_count(counter) + counts = statistics.get_homepage_counts() + for k in counts: + context[k] = counts[k] # Get the 15 most recently updated packages. context["package_updates"] = updated_packages(15, cache_expire) @@ -213,7 +212,7 @@ async def metrics(request: Request): ) # update prometheus gauges for packages and users - update_prometheus_metrics() + statistics.update_prometheus_metrics() registry = CollectorRegistry() multiprocess.MultiProcessCollector(registry) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index 4cfda269..a67419fe 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -16,6 +16,7 @@ from aurweb.models.package_request import ( ) from aurweb.requests.util import get_pkgreq_by_id from aurweb.scripts import notify +from aurweb.statistics import get_request_counts from aurweb.templates import make_context, render_template FILTER_PARAMS = { @@ -31,7 +32,7 @@ router = APIRouter() @router.get("/requests") @requires_auth -async def requests( +async def requests( # noqa: C901 request: Request, O: int = Query(default=defaults.O), PP: int = Query(default=defaults.PP), @@ -74,18 +75,11 @@ async def requests( .join(User, PackageRequest.UsersID == User.ID, isouter=True) .join(Maintainer, PackageBase.MaintainerUID == Maintainer.ID, isouter=True) ) - # query = db.query(PackageRequest).join(User) # Requests statistics - context["total_requests"] = query.count() - pending_count = 0 + query.filter(PackageRequest.Status == PENDING_ID).count() - context["pending_requests"] = pending_count - closed_count = 0 + query.filter(PackageRequest.Status == CLOSED_ID).count() - context["closed_requests"] = closed_count - accepted_count = 0 + query.filter(PackageRequest.Status == ACCEPTED_ID).count() - context["accepted_requests"] = accepted_count - rejected_count = 0 + query.filter(PackageRequest.Status == REJECTED_ID).count() - context["rejected_requests"] = rejected_count + counts = get_request_counts() + for k in counts: + context[k] = counts[k] # Apply status filters in_filters = [] diff --git a/aurweb/statistics.py b/aurweb/statistics.py index 6e9dbe1f..3c1298b7 100644 --- a/aurweb/statistics.py +++ b/aurweb/statistics.py @@ -1,31 +1,46 @@ from aurweb import config, db, time from aurweb.cache import db_count_cache -from aurweb.models import PackageBase, User +from aurweb.models import PackageBase, PackageRequest, User from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.package_request import ( + ACCEPTED_ID, + CLOSED_ID, + PENDING_ID, + REJECTED_ID, +) from aurweb.prometheus import PACKAGES, USERS +cache_expire = config.getint("cache", "expiry_time_statistics", 300) + +HOMEPAGE_COUNTERS = [ + "package_count", + "orphan_count", + "seven_days_old_added", + "seven_days_old_updated", + "year_old_updated", + "never_updated", + "user_count", + "trusted_user_count", +] +REQUEST_COUNTERS = [ + "total_requests", + "pending_requests", + "closed_requests", + "accepted_requests", + "rejected_requests", +] +PROMETHEUS_USER_COUNTERS = [ + ("trusted_user_count", "tu"), + ("regular_user_count", "user"), +] +PROMETHEUS_PACKAGE_COUNTERS = [ + ("orphan_count", "orphan"), + ("never_updated", "not_updated"), + ("updated_packages", "updated"), +] + class Statistics: - HOMEPAGE_COUNTERS = [ - "package_count", - "orphan_count", - "seven_days_old_added", - "seven_days_old_updated", - "year_old_updated", - "never_updated", - "user_count", - "trusted_user_count", - ] - PROMETHEUS_USER_COUNTERS = [ - ("trusted_user_count", "tu"), - ("regular_user_count", "user"), - ] - PROMETHEUS_PACKAGE_COUNTERS = [ - ("orphan_count", "orphan"), - ("never_updated", "not_updated"), - ("updated_packages", "updated"), - ] - seven_days = 86400 * 7 one_hour = 3600 year = seven_days * 52 @@ -35,15 +50,18 @@ class Statistics: self.now = time.utcnow() self.seven_days_ago = self.now - self.seven_days self.year_ago = self.now - self.year + self.user_query = db.query(User) self.bases_query = db.query(PackageBase) self.updated_query = db.query(PackageBase).filter( PackageBase.ModifiedTS - PackageBase.SubmittedTS >= self.one_hour ) + self.request_query = db.query(PackageRequest) def get_count(self, counter: str) -> int: query = None match counter: + # Packages case "package_count": query = self.bases_query case "orphan_count": @@ -69,6 +87,7 @@ class Statistics: PackageBase.ModifiedTS - PackageBase.SubmittedTS > self.one_hour, ~PackageBase.MaintainerUID.is_(None), ) + # Users case "user_count": query = self.user_query case "trusted_user_count": @@ -82,6 +101,18 @@ class Statistics: ) case "regular_user_count": query = self.user_query.filter(User.AccountTypeID == USER_ID) + + # Requests + case "total_requests": + query = self.request_query + case "pending_requests": + query = self.request_query.filter(PackageRequest.Status == PENDING_ID) + case "closed_requests": + query = self.request_query.filter(PackageRequest.Status == CLOSED_ID) + case "accepted_requests": + query = self.request_query.filter(PackageRequest.Status == ACCEPTED_ID) + case "rejected_requests": + query = self.request_query.filter(PackageRequest.Status == REJECTED_ID) case _: return -1 @@ -89,14 +120,30 @@ class Statistics: def update_prometheus_metrics(): - cache_expire = config.getint("cache", "expiry_time_statistics", 300) stats = Statistics(cache_expire) # Users gauge - for counter, utype in stats.PROMETHEUS_USER_COUNTERS: + for counter, utype in PROMETHEUS_USER_COUNTERS: count = stats.get_count(counter) USERS.labels(utype).set(count) # Packages gauge - for counter, state in stats.PROMETHEUS_PACKAGE_COUNTERS: + for counter, state in PROMETHEUS_PACKAGE_COUNTERS: count = stats.get_count(counter) PACKAGES.labels(state).set(count) + + +def _get_counts(counters: list[str]) -> dict[str, int]: + stats = Statistics(cache_expire) + result = dict() + for counter in counters: + result[counter] = stats.get_count(counter) + + return result + + +def get_homepage_counts() -> dict[str, int]: + return _get_counts(HOMEPAGE_COUNTERS) + + +def get_request_counts() -> dict[str, int]: + return _get_counts(REQUEST_COUNTERS) diff --git a/test/test_statistics.py b/test/test_statistics.py index dda7b357..a6a814c5 100644 --- a/test/test_statistics.py +++ b/test/test_statistics.py @@ -2,9 +2,15 @@ import pytest from prometheus_client import REGISTRY, generate_latest from aurweb import cache, db, time +from aurweb.models import Package, PackageBase, PackageRequest from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID -from aurweb.models.package import Package -from aurweb.models.package_base import PackageBase +from aurweb.models.package_request import ( + ACCEPTED_ID, + CLOSED_ID, + PENDING_ID, + REJECTED_ID, +) +from aurweb.models.request_type import DELETION_ID, ORPHAN_ID from aurweb.models.user import User from aurweb.statistics import Statistics, update_prometheus_metrics @@ -45,19 +51,36 @@ def test_data(): ModifiedTS=now, ) db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + pkgreq = db.create( + PackageRequest, + ReqTypeID=ORPHAN_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + RequestTS=now, + Comments=str(), + ClosureComment=str(), + ) # Modify some data to get some variances for our counters if i == 1: user.AccountTypeID = TRUSTED_USER_ID pkgbase.Maintainer = None pkgbase.SubmittedTS = now + pkgreq.Status = PENDING_ID + pkgreq.ReqTypeID = DELETION_ID if i == 2: pkgbase.SubmittedTS = older + pkgreq.Status = ACCEPTED_ID if i == 3: pkgbase.SubmittedTS = older pkgbase.ModifiedTS = old + pkgreq.Status = CLOSED_ID + + if i == 4: + pkgreq.Status = REJECTED_ID yield @@ -79,6 +102,11 @@ def stats() -> Statistics: ("trusted_user_count", 1), ("regular_user_count", 9), ("updated_packages", 9), + ("total_requests", 10), + ("pending_requests", 7), + ("closed_requests", 1), + ("accepted_requests", 1), + ("rejected_requests", 1), ("nonsense", -1), ], ) From 375895f08011c2c91b52b79a2d41fe1504524acf Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 23 Jul 2023 22:46:44 +0200 Subject: [PATCH 1356/1451] feat: Add Prometheus metrics for requests Adds gauge for requests by type and status Signed-off-by: moson --- aurweb/prometheus.py | 6 ++++++ aurweb/statistics.py | 22 +++++++++++++++++++--- test/test_statistics.py | 6 ++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index d3455551..40b99a90 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -24,6 +24,12 @@ PACKAGES = Gauge( ["state"], multiprocess_mode="livemax", ) +REQUESTS = Gauge( + "aur_requests", + "Number of AUR requests by type and status", + ["type", "status"], + multiprocess_mode="livemax", +) def instrumentator(): diff --git a/aurweb/statistics.py b/aurweb/statistics.py index 3c1298b7..f301b59c 100644 --- a/aurweb/statistics.py +++ b/aurweb/statistics.py @@ -1,6 +1,8 @@ +from sqlalchemy import func + from aurweb import config, db, time -from aurweb.cache import db_count_cache -from aurweb.models import PackageBase, PackageRequest, User +from aurweb.cache import db_count_cache, db_query_cache +from aurweb.models import PackageBase, PackageRequest, RequestType, User from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID from aurweb.models.package_request import ( ACCEPTED_ID, @@ -8,7 +10,7 @@ from aurweb.models.package_request import ( PENDING_ID, REJECTED_ID, ) -from aurweb.prometheus import PACKAGES, USERS +from aurweb.prometheus import PACKAGES, REQUESTS, USERS cache_expire = config.getint("cache", "expiry_time_statistics", 300) @@ -131,6 +133,20 @@ def update_prometheus_metrics(): count = stats.get_count(counter) PACKAGES.labels(state).set(count) + # Requests gauge + query = ( + db.get_session() + .query(PackageRequest, func.count(PackageRequest.ID), RequestType.Name) + .join(RequestType) + .group_by(RequestType.Name, PackageRequest.Status) + ) + results = db_query_cache("request_metrics", query, cache_expire) + for record in results: + status = record[0].status_display() + count = record[1] + rtype = record[2] + REQUESTS.labels(type=rtype, status=status).set(count) + def _get_counts(counters: list[str]) -> dict[str, int]: stats = Statistics(cache_expire) diff --git a/test/test_statistics.py b/test/test_statistics.py index a6a814c5..db262fa3 100644 --- a/test/test_statistics.py +++ b/test/test_statistics.py @@ -144,6 +144,7 @@ def test_update_prometheus_metrics(test_data): assert "aur_users{" not in metrics assert "aur_packages{" not in metrics + assert "aur_requests{" not in metrics # Let's update our metrics. We should find our gauges now update_prometheus_metrics() @@ -151,3 +152,8 @@ def test_update_prometheus_metrics(test_data): assert 'aur_users{type="user"} 9.0' in metrics assert 'aur_packages{state="updated"} 9.0' in metrics + assert 'aur_requests{status="Pending",type="orphan"} 6.0' in metrics + assert 'aur_requests{status="Closed",type="orphan"} 1.0' in metrics + assert 'aur_requests{status="Accepted",type="orphan"} 1.0' in metrics + assert 'aur_requests{status="Rejected",type="orphan"} 1.0' in metrics + assert 'aur_requests{status="Pending",type="deletion"} 1.0' in metrics From f74f94b50170d82c1d3f899037dc0debd2222725 Mon Sep 17 00:00:00 2001 From: renovate Date: Mon, 24 Jul 2023 11:24:26 +0000 Subject: [PATCH 1357/1451] fix(deps): update dependency gunicorn to v21 --- poetry.lock | 27 +++++---------------------- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/poetry.lock b/poetry.lock index 368371db..42010de5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -685,18 +685,18 @@ test = ["objgraph", "psutil"] [[package]] name = "gunicorn" -version = "20.1.0" +version = "21.2.0" description = "WSGI HTTP Server for UNIX" category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, - {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, + {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, + {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, ] [package.dependencies] -setuptools = ">=3.0" +packaging = "*" [package.extras] eventlet = ["eventlet (>=0.24.1)"] @@ -1602,23 +1602,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "setuptools" -version = "67.7.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"}, - {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -1948,4 +1931,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "b67f1b1599794a6890b0a31b2b127880d75c84beeeae3df4ecb3ae92296948da" +content-hash = "48d66bc7145b8cdac8da9977d6d2b0d554b382193a28f275743697d0a17d2f58" diff --git a/pyproject.toml b/pyproject.toml index e98e887f..e743e675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ SQLAlchemy = "^1.4.48" # ASGI uvicorn = "^0.23.0" -gunicorn = "^20.1.0" +gunicorn = "^21.0.0" Hypercorn = "^0.14.3" prometheus-fastapi-instrumentator = "^6.0.0" pytest-xdist = "^3.2.1" From 969b84afe4f2d74a005bd35594d9a76de5351e95 Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 25 Jul 2023 11:24:30 +0000 Subject: [PATCH 1358/1451] fix(deps): update all non-major dependencies --- poetry.lock | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 42010de5..9897fafb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -540,14 +540,14 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "2.16.0" +version = "2.17.0" description = "Python implementation of redis API, can be used for testing purposes." category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "fakeredis-2.16.0-py3-none-any.whl", hash = "sha256:188514cbd7120ff28c88f2a31e2fddd18fb1b28504478dfa3669c683134c4d82"}, - {file = "fakeredis-2.16.0.tar.gz", hash = "sha256:5abdd734de4ead9d6c7acbd3add1c4aa9b3ab35219339530472d9dd2bdf13057"}, + {file = "fakeredis-2.17.0-py3-none-any.whl", hash = "sha256:a99ef6e5642c31e91d36be78809fec3743e2bf7aaa682685b0d65a849fecd148"}, + {file = "fakeredis-2.17.0.tar.gz", hash = "sha256:e304bc7addb2f862c3550cb7db58548418a0fadd4cd78a4de66464c84fbc2195"}, ] [package.dependencies] @@ -1815,19 +1815,20 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.23.0" +version = "0.23.1" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.0-py3-none-any.whl", hash = "sha256:479599b2c0bb1b9b394c6d43901a1eb0c1ec72c7d237b5bafea23c5b2d4cdf10"}, - {file = "uvicorn-0.23.0.tar.gz", hash = "sha256:d38ab90c0e2c6fe3a054cddeb962cfd5d0e0e6608eaaff4a01d5c36a67f3168c"}, + {file = "uvicorn-0.23.1-py3-none-any.whl", hash = "sha256:1d55d46b83ee4ce82b4e82f621f2050adb3eb7b5481c13f9af1744951cae2f1f"}, + {file = "uvicorn-0.23.1.tar.gz", hash = "sha256:da9b0c8443b2d7ee9db00a345f1eee6db7317432c9d4400f5049cc8d358383be"}, ] [package.dependencies] click = ">=7.0" h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] From 7a44f379687cb10817b8b2b3f6a636e289832b36 Mon Sep 17 00:00:00 2001 From: renovate Date: Thu, 27 Jul 2023 19:24:28 +0000 Subject: [PATCH 1359/1451] fix(deps): update dependency fastapi to v0.100.1 --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9897fafb..c4d3adb6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -560,14 +560,14 @@ lua = ["lupa (>=1.14,<2.0)"] [[package]] name = "fastapi" -version = "0.100.0" +version = "0.100.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.100.0-py3-none-any.whl", hash = "sha256:271662daf986da8fa98dc2b7c7f61c4abdfdccfb4786d79ed8b2878f172c6d5f"}, - {file = "fastapi-0.100.0.tar.gz", hash = "sha256:acb5f941ea8215663283c10018323ba7ea737c571b67fc7e88e9469c7eb1d12e"}, + {file = "fastapi-0.100.1-py3-none-any.whl", hash = "sha256:ec6dd52bfc4eff3063cfcd0713b43c87640fefb2687bbbe3d8a08d94049cdf32"}, + {file = "fastapi-0.100.1.tar.gz", hash = "sha256:522700d7a469e4a973d92321ab93312448fbe20fca9c8da97effc7e7bc56df23"}, ] [package.dependencies] From 94b62d2949c0b627bbeac4107c681ab7eccfff7d Mon Sep 17 00:00:00 2001 From: moson Date: Fri, 4 Aug 2023 14:12:50 +0200 Subject: [PATCH 1360/1451] fix: Check if user exists when editing account We should check if a user (target) exists before validating permissions. Otherwise things crash when a TU is trying to edit an account that does not exist. Fixes: aurweb-errors#529 Signed-off-by: moson --- aurweb/routers/accounts.py | 3 +++ test/test_accounts_routes.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 010aae58..1c81ec1d 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -374,6 +374,9 @@ def cannot_edit( :param user: Target user to be edited :return: RedirectResponse if approval != granted else None """ + # raise 404 if user does not exist + if not user: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) approved = request.user.can_edit_user(user) if not approved and (to := "/"): if user: diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 3c481d0a..3ff6291a 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -764,6 +764,17 @@ def test_get_account_edit_unauthorized(client: TestClient, user: User): assert response.headers.get("location") == expected +def test_get_account_edit_not_exists(client: TestClient, tu_user: User): + """Test that users do not have an Account Type field.""" + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = "/account/doesnotexist/edit" + + with client as request: + request.cookies = cookies + response = request.get(endpoint) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + def test_post_account_edit(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -872,6 +883,19 @@ def test_post_account_edit_dev(client: TestClient, tu_user: User): assert expected in response.content.decode() +def test_post_account_edit_not_exists(client: TestClient, tu_user: User): + request = Request() + sid = tu_user.login(request, "testPassword") + + post_data = {"U": "test", "E": "test666@example.org", "passwd": "testPassword"} + + endpoint = "/account/doesnotexist/edit" + with client as request: + request.cookies = {"AURSID": sid} + response = request.post(endpoint, data=post_data) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + def test_post_account_edit_language(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") From 8ad03522de34a40992165649fc0605390db93a98 Mon Sep 17 00:00:00 2001 From: renovate Date: Fri, 4 Aug 2023 14:25:22 +0000 Subject: [PATCH 1361/1451] fix(deps): update all non-major dependencies --- poetry.lock | 27 ++++++++++++++------------- pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index c4d3adb6..623884a0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,14 +14,14 @@ files = [ [[package]] name = "alembic" -version = "1.11.1" +version = "1.11.2" description = "A database migration tool for SQLAlchemy." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"}, - {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"}, + {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"}, + {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"}, ] [package.dependencies] @@ -1031,20 +1031,21 @@ testing = ["pytest"] [[package]] name = "markdown" -version = "3.4.3" +version = "3.4.4" description = "Python implementation of John Gruber's Markdown." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Markdown-3.4.3-py3-none-any.whl", hash = "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2"}, - {file = "Markdown-3.4.3.tar.gz", hash = "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520"}, + {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, + {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, ] [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] testing = ["coverage", "pyyaml"] [[package]] @@ -1773,14 +1774,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.8" +version = "0.12.1" description = "Style preserving TOML library" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, - {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, + {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, + {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, ] [[package]] @@ -1815,14 +1816,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.23.1" +version = "0.23.2" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.1-py3-none-any.whl", hash = "sha256:1d55d46b83ee4ce82b4e82f621f2050adb3eb7b5481c13f9af1744951cae2f1f"}, - {file = "uvicorn-0.23.1.tar.gz", hash = "sha256:da9b0c8443b2d7ee9db00a345f1eee6db7317432c9d4400f5049cc8d358383be"}, + {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, + {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, ] [package.dependencies] @@ -1932,4 +1933,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "48d66bc7145b8cdac8da9977d6d2b0d554b382193a28f275743697d0a17d2f58" +content-hash = "ac9dbb5b28292c4a3dd2318a2f5c9120dfaa117ee834fac05995c5d0cbdc460d" diff --git a/pyproject.toml b/pyproject.toml index e743e675..f4682bad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ posix-ipc = "^1.1.1" pyalpm = "^0.10.6" fastapi = "^0.100.0" srcinfo = "^0.1.2" -tomlkit = "^0.11.8" +tomlkit = "^0.12.0" [tool.poetry.dev-dependencies] coverage = "^7.2.5" From f05f1dbac798c5fd41e0f19c9b0419fa302c9bf4 Mon Sep 17 00:00:00 2001 From: Leonidas Spyropoulos Date: Fri, 4 Aug 2023 19:18:38 +0300 Subject: [PATCH 1362/1451] chore(release): prepare for 6.2.7 Signed-off-by: Leonidas Spyropoulos --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f4682bad..359923a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ combine_as_imports = true # [tool.poetry] name = "aurweb" -version = "v6.2.6" +version = "v6.2.7" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" From 3005e82f607e7b20ac5e32d50f0ffb124a8737e0 Mon Sep 17 00:00:00 2001 From: moson Date: Fri, 18 Aug 2023 22:04:55 +0200 Subject: [PATCH 1363/1451] fix: Cleanup prometheus metrics for dead workers The current "cleanup" function that is removing orphan prometheus files is actually never invoked. We move this to a default gunicorn config file to register our hook(s). https://docs.gunicorn.org/en/stable/configure.html https://docs.gunicorn.org/en/stable/settings.html#child-exit Signed-off-by: moson --- aurweb/asgi.py | 7 ------- gunicorn.conf.py | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 gunicorn.conf.py diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 1be77ff9..9b6ffcb3 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -13,7 +13,6 @@ from fastapi import FastAPI, HTTPException, Request, Response from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from jinja2 import TemplateNotFound -from prometheus_client import multiprocess from sqlalchemy import and_ from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.authentication import AuthenticationMiddleware @@ -91,12 +90,6 @@ async def app_startup(): get_engine() -def child_exit(server, worker): # pragma: no cover - """This function is required for gunicorn customization - of prometheus multiprocessing.""" - multiprocess.mark_process_dead(worker.pid) - - async def internal_server_error(request: Request, exc: Exception) -> Response: """ Catch all uncaught Exceptions thrown in a route. diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 00000000..4f1c3a8c --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,7 @@ +from prometheus_client import multiprocess + + +def child_exit(server, worker): # pragma: no cover + """This function is required for gunicorn customization + of prometheus multiprocessing.""" + multiprocess.mark_process_dead(worker.pid) From 6c610b26a39a56db562e4b1ed3c95420a73ee766 Mon Sep 17 00:00:00 2001 From: Kristian Klausen Date: Fri, 28 Jul 2023 22:42:44 +0200 Subject: [PATCH 1364/1451] feat: Add terraform config for review-app[1] Also removed the logic for deploying to the long gone aur-dev box. Ansible will be added in a upcoming commit for configurating and deploying aurweb on the VM. [1] https://docs.gitlab.com/ee/ci/review_apps/ --- .gitignore | 4 +++ .gitlab-ci.yml | 71 +++++++++++++++++++++++---------------- ci/tf/.terraform.lock.hcl | 61 +++++++++++++++++++++++++++++++++ ci/tf/main.tf | 67 ++++++++++++++++++++++++++++++++++++ ci/tf/terraform.tfvars | 4 +++ ci/tf/variables.tf | 36 ++++++++++++++++++++ ci/tf/versions.tf | 13 +++++++ 7 files changed, 227 insertions(+), 29 deletions(-) create mode 100644 ci/tf/.terraform.lock.hcl create mode 100644 ci/tf/main.tf create mode 100644 ci/tf/terraform.tfvars create mode 100644 ci/tf/variables.tf create mode 100644 ci/tf/versions.tf diff --git a/.gitignore b/.gitignore index 68de7cd5..97157118 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ test-emails/ env/ venv/ .venv/ + +# Ignore some terraform files +/ci/tf/.terraform +/ci/tf/terraform.tfstate* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 10dd1787..4bd71920 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,34 +61,47 @@ test: coverage_format: cobertura path: coverage.xml -deploy: - stage: deploy - tags: - - secure - rules: - - if: $CI_COMMIT_BRANCH == "pu" - when: manual - variables: - FASTAPI_BACKEND: gunicorn - FASTAPI_WORKERS: 5 - AURWEB_FASTAPI_PREFIX: https://aur-dev.archlinux.org - AURWEB_SSHD_PREFIX: ssh://aur@aur-dev.archlinux.org:2222 - COMMIT_HASH: $CI_COMMIT_SHA - GIT_DATA_DIR: git_data - script: - - pacman -Syu --noconfirm docker docker-compose socat openssh - - chmod 600 ${SSH_KEY} - - socat "UNIX-LISTEN:/tmp/docker.sock,reuseaddr,fork" EXEC:"ssh -o UserKnownHostsFile=${SSH_KNOWN_HOSTS} -Ti ${SSH_KEY} ${SSH_USER}@${SSH_HOST}" & - - export DOCKER_HOST="unix:///tmp/docker.sock" - # Set secure login config for aurweb. - - sed -ri "s/^(disable_http_login).*$/\1 = 1/" conf/config.dev - - docker-compose build - - docker-compose -f docker-compose.yml -f docker-compose.aur-dev.yml down --remove-orphans - - docker-compose -f docker-compose.yml -f docker-compose.aur-dev.yml up -d - - docker image prune -f - - docker container prune -f - - docker volume prune -f +.init_tf: &init_tf + - pacman -Syu --needed --noconfirm --cachedir .pkg-cache terraform + - export TF_VAR_name="aurweb-${CI_COMMIT_REF_SLUG}" + - TF_ADDRESS="${CI_API_V4_URL}/projects/${TF_STATE_PROJECT}/terraform/state/${CI_COMMIT_REF_SLUG}" + - cd ci/tf + - > + terraform init \ + -backend-config="address=${TF_ADDRESS}" \ + -backend-config="lock_address=${TF_ADDRESS}/lock" \ + -backend-config="unlock_address=${TF_ADDRESS}/lock" \ + -backend-config="username=x-access-token" \ + -backend-config="password=${TF_STATE_GITLAB_ACCESS_TOKEN}" \ + -backend-config="lock_method=POST" \ + -backend-config="unlock_method=DELETE" \ + -backend-config="retry_wait_min=5" +deploy_review: + stage: deploy + script: + - *init_tf + - terraform apply -auto-approve environment: - name: development - url: https://aur-dev.archlinux.org + name: review/$CI_COMMIT_REF_NAME + url: https://aurweb-$CI_ENVIRONMENT_SLUG.sandbox.archlinux.page + on_stop: stop_review + auto_stop_in: 1 week + rules: + - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" + when: manual + +stop_review: + stage: deploy + needs: + - deploy_review + script: + - *init_tf + - terraform destroy -auto-approve + - 'curl --silent --show-error --fail --header "Private-Token: ${TF_STATE_GITLAB_ACCESS_TOKEN}" --request DELETE "${CI_API_V4_URL}/projects/${TF_STATE_PROJECT}/terraform/state/${CI_COMMIT_REF_SLUG}"' + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + rules: + - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" + when: manual diff --git a/ci/tf/.terraform.lock.hcl b/ci/tf/.terraform.lock.hcl new file mode 100644 index 00000000..aa5501c4 --- /dev/null +++ b/ci/tf/.terraform.lock.hcl @@ -0,0 +1,61 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/dns" { + version = "3.3.2" + hashes = [ + "h1:HjskPLRqmCw8Q/kiSuzti3iJBSpcAvcBFdlwFFQuoDE=", + "zh:05d2d50e301318362a4a82e6b7a9734ace07bc01abaaa649c566baf98814755f", + "zh:1e9fd1c3bfdda777e83e42831dd45b7b9e794250a0f351e5fd39762e8a0fe15b", + "zh:40e715fc7a2ede21f919567249b613844692c2f8a64f93ee64e5b68bae7ac2a2", + "zh:454d7aa83000a6e2ba7a7bfde4bcf5d7ed36298b22d760995ca5738ab02ee468", + "zh:46124ded51b4153ad90f12b0305fdbe0c23261b9669aa58a94a31c9cca2f4b19", + "zh:55a4f13d20f73534515a6b05701abdbfc54f4e375ba25b2dffa12afdad20e49d", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7903b1ceb8211e2b8c79290e2e70906a4b88f4fba71c900eb3a425ce12f1716a", + "zh:b79fc4f444ef7a2fd7111a80428c070ad824f43a681699e99ab7f83074dfedbd", + "zh:ca9f45e0c4cb94e7d62536c226024afef3018b1de84f1ea4608b51bcd497a2a0", + "zh:ddc8bd894559d7d176e0ceb0bb1ae266519b01b315362ebfee8327bb7e7e5fa8", + "zh:e77334c0794ef8f9354b10e606040f6b0b67b373f5ff1db65bddcdd4569b428b", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.4" + hashes = [ + "h1:pe9vq86dZZKCm+8k1RhzARwENslF3SXb9ErHbQfgjXU=", + "zh:23671ed83e1fcf79745534841e10291bbf34046b27d6e68a5d0aab77206f4a55", + "zh:45292421211ffd9e8e3eb3655677700e3c5047f71d8f7650d2ce30242335f848", + "zh:59fedb519f4433c0fdb1d58b27c210b27415fddd0cd73c5312530b4309c088be", + "zh:5a8eec2409a9ff7cd0758a9d818c74bcba92a240e6c5e54b99df68fff312bbd5", + "zh:5e6a4b39f3171f53292ab88058a59e64825f2b842760a4869e64dc1dc093d1fe", + "zh:810547d0bf9311d21c81cc306126d3547e7bd3f194fc295836acf164b9f8424e", + "zh:824a5f3617624243bed0259d7dd37d76017097dc3193dac669be342b90b2ab48", + "zh:9361ccc7048be5dcbc2fafe2d8216939765b3160bd52734f7a9fd917a39ecbd8", + "zh:aa02ea625aaf672e649296bce7580f62d724268189fe9ad7c1b36bb0fa12fa60", + "zh:c71b4cd40d6ec7815dfeefd57d88bc592c0c42f5e5858dcc88245d371b4b8b1e", + "zh:dabcd52f36b43d250a3d71ad7abfa07b5622c69068d989e60b79b2bb4f220316", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hetznercloud/hcloud" { + version = "1.42.0" + hashes = [ + "h1:cr9lh26H3YbWSHb7OUnCoYw169cYO3Cjpt3yPnRhXS0=", + "zh:153b5f39d780e9a18bc1ea377d872647d328d943813cbd25d3d20863f8a37782", + "zh:35b9e95760c58cca756e34ad5f4138ac6126aa3e8c41b4a0f1d5dc9ee5666c73", + "zh:47a3cdbce982f2b4e17f73d4934bdb3e905a849b36fb59b80f87d852496ed049", + "zh:6a718c244c2ba300fbd43791661a061ad1ab16225ef3e8aeaa3db8c9eff12c85", + "zh:a2cbfc95c5e2c9422ed0a7b6292192c38241220d5b7813c678f937ab3ef962ae", + "zh:b837e118e08fd36aa8be48af7e9d0d3d112d2680c79cfc71cfe2501fb40dbefa", + "zh:bf66db8c680e18b77e16dc1f20ed1cdcc7876bfb7848c320ccb86f0fb80661ed", + "zh:c1ad80bbe48dc8a272a02dcdb4b12f019606f445606651c01e561b9d72d816b1", + "zh:d4e616701128ad14a6b5a427b0e9145ece4cad02aa3b5f9945c6d0b9ada8ab70", + "zh:d9d01f727037d028720100a5bc9fd213cb01e63e4b439a16f2f482c147976530", + "zh:dea047ee4d679370d4376fb746c4b959bf51dd06047c1c2656b32789c2433643", + "zh:e5ad7a3c556894bd40b28a874e7d2f6924876fa75fa443136a7d6ab9a00abbaa", + "zh:edf6e7e129157bd45e3da4a330d1ace17a336d417c3b77c620f302d440c368e8", + "zh:f610bc729866d58da9cffa4deae34dbfdba96655e855a87c6bb2cb7b35a8961c", + ] +} diff --git a/ci/tf/main.tf b/ci/tf/main.tf new file mode 100644 index 00000000..b149a621 --- /dev/null +++ b/ci/tf/main.tf @@ -0,0 +1,67 @@ +terraform { + backend "http" { + } +} + +provider "hcloud" { + token = var.hcloud_token +} + +provider "dns" { + update { + server = var.dns_server + key_name = var.dns_tsig_key + key_algorithm = var.dns_tsig_algorithm + key_secret = var.dns_tsig_secret + } +} + +resource "tls_private_key" "this" { + algorithm = "ED25519" +} + +resource "hcloud_ssh_key" "this" { + name = var.name + public_key = tls_private_key.this.public_key_openssh +} + +data "hcloud_image" "this" { + with_selector = "custom_image=archlinux" + most_recent = true + with_status = ["available"] +} + +resource "hcloud_server" "this" { + name = var.name + image = data.hcloud_image.this.id + server_type = var.server_type + datacenter = var.datacenter + ssh_keys = [hcloud_ssh_key.this.name] + + public_net { + ipv4_enabled = true + ipv6_enabled = true + } +} + +resource "hcloud_rdns" "this" { + for_each = { ipv4 : hcloud_server.this.ipv4_address, ipv6 : hcloud_server.this.ipv6_address } + + server_id = hcloud_server.this.id + ip_address = each.value + dns_ptr = "${var.name}.${var.dns_zone}" +} + +resource "dns_a_record_set" "this" { + zone = "${var.dns_zone}." + name = var.name + addresses = [hcloud_server.this.ipv4_address] + ttl = 300 +} + +resource "dns_aaaa_record_set" "this" { + zone = "${var.dns_zone}." + name = var.name + addresses = [hcloud_server.this.ipv6_address] + ttl = 300 +} diff --git a/ci/tf/terraform.tfvars b/ci/tf/terraform.tfvars new file mode 100644 index 00000000..14818592 --- /dev/null +++ b/ci/tf/terraform.tfvars @@ -0,0 +1,4 @@ +server_type = "cpx11" +datacenter = "fsn1-dc14" +dns_server = "redirect.archlinux.org" +dns_zone = "sandbox.archlinux.page" diff --git a/ci/tf/variables.tf b/ci/tf/variables.tf new file mode 100644 index 00000000..a4e710ee --- /dev/null +++ b/ci/tf/variables.tf @@ -0,0 +1,36 @@ +variable "hcloud_token" { + type = string + sensitive = true +} + +variable "dns_server" { + type = string +} + +variable "dns_tsig_key" { + type = string +} + +variable "dns_tsig_algorithm" { + type = string +} + +variable "dns_tsig_secret" { + type = string +} + +variable "dns_zone" { + type = string +} + +variable "name" { + type = string +} + +variable "server_type" { + type = string +} + +variable "datacenter" { + type = string +} diff --git a/ci/tf/versions.tf b/ci/tf/versions.tf new file mode 100644 index 00000000..2c72215a --- /dev/null +++ b/ci/tf/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + tls = { + source = "hashicorp/tls" + } + hcloud = { + source = "hetznercloud/hcloud" + } + dns = { + source = "hashicorp/dns" + } + } +} From 9eda6a42c69581dfdc14dc1b0d51f744985c7202 Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 27 Aug 2023 13:54:39 +0200 Subject: [PATCH 1365/1451] feat: Add ansible provisioning step for review-app Clone infrastructure repository and run playbook to provision our VM with aurweb. Signed-off-by: moson --- .gitlab-ci.yml | 54 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4bd71920..cf80ab24 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,8 @@ variables: TEST_RECURSION_LIMIT: 10000 CURRENT_DIR: "$(pwd)" LOG_CONFIG: logging.test.conf + DEV_FQDN: aurweb-$CI_COMMIT_REF_SLUG.sandbox.archlinux.page + INFRASTRUCTURE_REPO: https://gitlab.archlinux.org/archlinux/infrastructure.git lint: stage: .pre @@ -84,13 +86,63 @@ deploy_review: - terraform apply -auto-approve environment: name: review/$CI_COMMIT_REF_NAME - url: https://aurweb-$CI_ENVIRONMENT_SLUG.sandbox.archlinux.page + url: https://$DEV_FQDN on_stop: stop_review auto_stop_in: 1 week rules: - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" when: manual +provision_review: + stage: deploy + needs: + - deploy_review + script: + - *init_tf + - pacman -Syu --noconfirm --needed --cachedir .pkg-cache ansible git openssh jq + # Get ssh key from terraform state file + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - terraform show -json | + jq -r '.values.root_module.resources[] | + select(.address == "tls_private_key.this") | + .values.private_key_openssh' > ~/.ssh/id_ed25519 + - chmod 400 ~/.ssh/id_ed25519 + # Clone infra repo + - git clone $INFRASTRUCTURE_REPO + - cd infrastructure + # Remove vault files + - rm $(git grep -l 'ANSIBLE_VAULT;1.1;AES256$') + # Remove vault config + - sed -i '/^vault/d' ansible.cfg + # Add host config + - mkdir -p host_vars/$DEV_FQDN + - 'echo "filesystem: btrfs" > host_vars/$DEV_FQDN/misc' + # Add host + - echo "$DEV_FQDN" > hosts + # Add our pubkey and hostkeys + - ssh-keyscan $DEV_FQDN >> ~/.ssh/known_hosts + - ssh-keygen -f ~/.ssh/id_ed25519 -y > pubkeys/aurweb-dev.pub + # Run our ansible playbook + - > + ansible-playbook playbooks/aur-dev.archlinux.org.yml \ + -e "aurdev_fqdn=$DEV_FQDN" \ + -e "aurweb_repository=$CI_REPOSITORY_URL" \ + -e "aurweb_version=$CI_COMMIT_SHA" \ + -e "{\"vault_mariadb_users\":{\"root\":\"aur\"}}" \ + -e "vault_aurweb_db_password=aur" \ + -e "vault_aurweb_gitlab_instance=https://does.not.exist" \ + -e "vault_aurweb_error_project=aur" \ + -e "vault_aurweb_error_token=aur" \ + -e "vault_aurweb_secret=aur" \ + -e "vault_goaurrpc_metrics_token=aur" \ + -e '{"root_additional_keys": ["moson.pub", "aurweb-dev.pub"]}' + environment: + name: review/$CI_COMMIT_REF_NAME + action: access + rules: + - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" + stop_review: stage: deploy needs: From 5699e9bb41638fc1d6040f3e70a90fab38257458 Mon Sep 17 00:00:00 2001 From: moson Date: Sat, 26 Aug 2023 14:47:21 +0200 Subject: [PATCH 1366/1451] fix(test): Remove file locking and semaphore All tests within a file run in the same worker and out test DB names are unique per file as well. We don't really need a locking mechanism here. Same is valid for the test-emails. The only potential issue is that it might try to create the same directory multiple times and thus run into an error. However, that can be covered by specifying "exist_ok=True" with os.makedirs such that those errors are ignored. Signed-off-by: moson --- test/conftest.py | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 15a982aa..c36f78dd 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -43,7 +43,6 @@ from multiprocessing import Lock import py import pytest -from posix_ipc import O_CREAT, Semaphore from sqlalchemy import create_engine from sqlalchemy.engine import URL from sqlalchemy.engine.base import Engine @@ -54,7 +53,6 @@ import aurweb.config import aurweb.db from aurweb import aur_logging, initdb, testing from aurweb.testing.email import Email -from aurweb.testing.filelock import FileLock from aurweb.testing.git import GitRepository logger = aur_logging.get_logger(__name__) @@ -133,20 +131,16 @@ def _drop_database(engine: Engine, dbname: str) -> None: def setup_email(): - # TODO: Fix this data race! This try/catch is ugly; why is it even - # racing here? Perhaps we need to multiproc + multithread lock - # inside of setup_database to block the check? - with Semaphore("/test-emails", flags=O_CREAT, initial_value=1): - if not os.path.exists(Email.TEST_DIR): - # Create the directory. - os.makedirs(Email.TEST_DIR) + if not os.path.exists(Email.TEST_DIR): + # Create the directory. + os.makedirs(Email.TEST_DIR, exist_ok=True) - # Cleanup all email files for this test suite. - prefix = Email.email_prefix(suite=True) - files = os.listdir(Email.TEST_DIR) - for file in files: - if file.startswith(prefix): - os.remove(os.path.join(Email.TEST_DIR, file)) + # Cleanup all email files for this test suite. + prefix = Email.email_prefix(suite=True) + files = os.listdir(Email.TEST_DIR) + for file in files: + if file.startswith(prefix): + os.remove(os.path.join(Email.TEST_DIR, file)) @pytest.fixture(scope="module") @@ -155,20 +149,8 @@ def setup_database(tmp_path_factory: pathlib.Path, worker_id: str) -> None: engine = test_engine() dbname = aurweb.db.name() - if worker_id == "master": # pragma: no cover - # If we're not running tests through multiproc pytest-xdist. - setup_email() - yield _create_database(engine, dbname) - _drop_database(engine, dbname) - return - - def setup(path): - setup_email() - _create_database(engine, dbname) - - tmpdir = tmp_path_factory.getbasetemp().parent - file_lock = FileLock(tmpdir, dbname) - file_lock.lock(on_create=setup) + setup_email() + _create_database(engine, dbname) yield # Run the test function depending on this fixture. _drop_database(engine, dbname) # Cleanup the database. From 1433553c05993b097e812e43496bf140df49144c Mon Sep 17 00:00:00 2001 From: moson Date: Sat, 26 Aug 2023 17:08:36 +0200 Subject: [PATCH 1367/1451] fix(test): Clear previous prometheus data for test It could happen that test data is already generated by a previous test. (running in the same worker) Make sure we clear everything before performing our checks. Signed-off-by: moson --- test/test_statistics.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_statistics.py b/test/test_statistics.py index db262fa3..80223cbd 100644 --- a/test/test_statistics.py +++ b/test/test_statistics.py @@ -1,7 +1,7 @@ import pytest from prometheus_client import REGISTRY, generate_latest -from aurweb import cache, db, time +from aurweb import cache, db, prometheus, time from aurweb.models import Package, PackageBase, PackageRequest from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID from aurweb.models.package_request import ( @@ -140,6 +140,11 @@ def test_get_count_change(stats: Statistics, test_data): def test_update_prometheus_metrics(test_data): + # Make sure any previous data is cleared + prometheus.USERS.clear() + prometheus.PACKAGES.clear() + prometheus.REQUESTS.clear() + metrics = str(generate_latest(REGISTRY)) assert "aur_users{" not in metrics From 0a7b02956feeaaeea4813650b12ead15cfc822af Mon Sep 17 00:00:00 2001 From: moson Date: Sun, 3 Sep 2023 14:17:11 +0200 Subject: [PATCH 1368/1451] feat: Indicate dependency source Dependencies might reside in the AUR or official repositories. Add "AUR" as superscript letters to indicate if a package/provider is present in the AUR. Signed-off-by: moson --- aurweb/models/package_dependency.py | 7 +++- aurweb/packages/util.py | 8 ++-- .../partials/packages/package_metadata.html | 7 +++- test/test_package_dependency.py | 17 ++++++++ test/test_packages_util.py | 40 +++++++++++++++++++ 5 files changed, 72 insertions(+), 7 deletions(-) diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index 587ba68d..9cf1eda0 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -57,14 +57,17 @@ class PackageDependency(Base): params=("NULL"), ) - def is_package(self) -> bool: + def is_aur_package(self) -> bool: pkg = db.query(_Package).filter(_Package.Name == self.DepName).exists() + return db.query(pkg).scalar() + + def is_package(self) -> bool: official = ( db.query(_OfficialProvider) .filter(_OfficialProvider.Name == self.DepName) .exists() ) - return db.query(pkg).scalar() or db.query(official).scalar() + return self.is_aur_package() or db.query(official).scalar() def provides(self) -> list[PackageRelation]: from aurweb.models.relation_type import PROVIDES_ID diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index 78d79508..cfd1e9e9 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -83,9 +83,11 @@ def package_link(package: Union[Package, OfficialProvider]) -> str: @register_filter("provides_markup") def provides_markup(provides: Providers) -> str: - return ", ".join( - [f'{pkg.Name}' for pkg in provides] - ) + links = [] + for pkg in provides: + aur = "ᴬᵁᴿ" if not pkg.is_official else "" + links.append(f'{pkg.Name}{aur}') + return ", ".join(links) def get_pkg_or_base( diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html index 50d38b48..c8d583a1 100644 --- a/templates/partials/packages/package_metadata.html +++ b/templates/partials/packages/package_metadata.html @@ -14,12 +14,15 @@ {% endif %} {{ dep.DepName }} - {% if broken %} + {%- if broken %} {% if not provides %} {% endif %} - {% else %} + {% else -%} + {%- if dep.is_aur_package() -%} + ᴬᵁᴿ + {% endif %} {% endif %} {% if provides %} diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py index 9366bb55..1cd2d305 100644 --- a/test/test_package_dependency.py +++ b/test/test_package_dependency.py @@ -4,6 +4,7 @@ from sqlalchemy.exc import IntegrityError from aurweb import db from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID +from aurweb.models.official_provider import OfficialProvider from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_dependency import PackageDependency @@ -58,6 +59,22 @@ def test_package_dependencies(user: User, package: Package): db.create(Package, PackageBase=base, Name=pkgdep.DepName) assert pkgdep.is_package() + assert pkgdep.is_aur_package() + + # Test with OfficialProvider + with db.begin(): + pkgdep = db.create( + PackageDependency, + Package=package, + DepTypeID=DEPENDS_ID, + DepName="test-repo-pkg", + ) + db.create( + OfficialProvider, Name=pkgdep.DepName, Repo="extra", Provides=pkgdep.DepName + ) + + assert pkgdep.is_package() + assert not pkgdep.is_aur_package() def test_package_dependencies_null_package_raises(): diff --git a/test/test_packages_util.py b/test/test_packages_util.py index bae84614..b429181b 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -10,8 +10,10 @@ from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_relation import PackageRelation from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote +from aurweb.models.relation_type import PROVIDES_ID from aurweb.models.user import User from aurweb.packages import util @@ -155,3 +157,41 @@ def test_pkg_required(package: Package): # We should have 1 record assert qry.count() == 1 + + +def test_provides_markup(package: Package): + # Create dependency and provider for AUR pkg + with db.begin(): + dep = db.create( + PackageDependency, + Package=package, + DepName="test", + DepTypeID=DEPENDS_ID, + ) + rel_pkg = db.create(Package, PackageBase=package.PackageBase, Name=dep.DepName) + db.create( + PackageRelation, + Package=rel_pkg, + RelName=dep.DepName, + RelTypeID=PROVIDES_ID, + ) + + # AUR provider links should end with ᴬᵁᴿ + link = util.provides_markup(dep.provides()) + assert link.endswith("ᴬᵁᴿ") + assert OFFICIAL_BASE not in link + + # Remove AUR provider and add official one + with db.begin(): + db.delete(rel_pkg) + db.create( + OfficialProvider, + Name="official-pkg", + Repo="extra", + Provides=dep.DepName, + ) + + # Repo provider links should not have any suffix + link = util.provides_markup(dep.provides()) + assert link.endswith("") + assert OFFICIAL_BASE in link From 7466e964498cd9d19b93e1f38394ae358a5e6a5f Mon Sep 17 00:00:00 2001 From: moson Date: Tue, 26 Sep 2023 13:47:03 +0200 Subject: [PATCH 1369/1451] fix(ci): Exclude review-app jobs for renovate MR's Signed-off-by: moson --- .gitlab-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cf80ab24..fb40d414 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -90,6 +90,8 @@ deploy_review: on_stop: stop_review auto_stop_in: 1 week rules: + - if: $CI_COMMIT_REF_NAME =~ /^renovate\// + when: never - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" when: manual @@ -141,6 +143,8 @@ provision_review: name: review/$CI_COMMIT_REF_NAME action: access rules: + - if: $CI_COMMIT_REF_NAME =~ /^renovate\// + when: never - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" stop_review: @@ -155,5 +159,7 @@ stop_review: name: review/$CI_COMMIT_REF_NAME action: stop rules: + - if: $CI_COMMIT_REF_NAME =~ /^renovate\// + when: never - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" when: manual From 1702075875514de8170b4393d5326cb61e7c5e6e Mon Sep 17 00:00:00 2001 From: moson Date: Fri, 1 Sep 2023 13:25:21 +0200 Subject: [PATCH 1370/1451] housekeep: TU rename - code changes Renaming of symbols. Functions, variables, values, DB values, etc. Basically everything that is not user-facing. This only covers "Trusted User" things: tests, comments, etc. will covered in a following commit. --- aurweb/auth/__init__.py | 4 +- aurweb/auth/creds.py | 76 ++++++++++--------- aurweb/initdb.py | 4 +- aurweb/models/account_type.py | 12 +-- aurweb/models/user.py | 12 +-- aurweb/pkgbase/actions.py | 2 +- aurweb/routers/__init__.py | 4 +- aurweb/routers/accounts.py | 16 ++-- ...{trusted_user.py => package_maintainer.py} | 59 +++++++------- aurweb/statistics.py | 16 ++-- aurweb/users/validate.py | 2 +- ...d126029_rename_tu_to_package_maintainer.py | 37 +++++++++ schema/gendummydata.py | 28 +++---- templates/addvote.html | 12 +-- templates/partials/archdev-navbar.html | 2 +- .../partials/packages/search_actions.html | 2 +- templates/partials/packages/statistics.html | 2 +- templates/partials/tu/proposals.html | 2 +- templates/tu/index.html | 6 +- test/test_accounts_routes.py | 48 ++++++------ test/test_adduser.py | 4 +- test/test_auth.py | 4 +- test/test_homepage.py | 2 +- test/test_html.py | 10 +-- test/test_notify.py | 4 +- test/test_packages_routes.py | 4 +- test/test_pkgbase_routes.py | 4 +- test/test_requests.py | 4 +- test/test_statistics.py | 14 ++-- test/test_trusted_user_routes.py | 14 ++-- test/test_tu_vote.py | 4 +- test/test_tu_voteinfo.py | 4 +- test/test_tuvotereminder.py | 8 +- test/test_user.py | 42 +++++----- 34 files changed, 265 insertions(+), 203 deletions(-) rename aurweb/routers/{trusted_user.py => package_maintainer.py} (88%) create mode 100644 migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index 83dd424c..e895dcdb 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -71,7 +71,7 @@ class AnonymousUser: return False @staticmethod - def is_trusted_user(): + def is_package_maintainer(): return False @staticmethod @@ -205,7 +205,7 @@ def account_type_required(one_of: set): @router.get('/some_route') @auth_required(True) - @account_type_required({"Trusted User", "Trusted User & Developer"}) + @account_type_required({"Package Maintainer", "Package Maintainer & Developer"}) async def some_route(request: fastapi.Request): return Response() diff --git a/aurweb/auth/creds.py b/aurweb/auth/creds.py index 17d02a5b..594188ca 100644 --- a/aurweb/auth/creds.py +++ b/aurweb/auth/creds.py @@ -1,7 +1,7 @@ from aurweb.models.account_type import ( DEVELOPER_ID, - TRUSTED_USER_AND_DEV_ID, - TRUSTED_USER_ID, + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, USER_ID, ) from aurweb.models.user import User @@ -30,47 +30,49 @@ PKGBASE_VOTE = 16 PKGREQ_FILE = 23 PKGREQ_CLOSE = 17 PKGREQ_LIST = 18 -TU_ADD_VOTE = 19 -TU_LIST_VOTES = 20 -TU_VOTE = 21 +PM_ADD_VOTE = 19 +PM_LIST_VOTES = 20 +PM_VOTE = 21 PKGBASE_MERGE = 29 -user_developer_or_trusted_user = set( - [USER_ID, TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID] +user_developer_or_package_maintainer = set( + [USER_ID, PACKAGE_MAINTAINER_ID, DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID] ) -trusted_user_or_dev = set([TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) -developer = set([DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) -trusted_user = set([TRUSTED_USER_ID, TRUSTED_USER_AND_DEV_ID]) +package_maintainer_or_dev = set( + [PACKAGE_MAINTAINER_ID, DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID] +) +developer = set([DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID]) +package_maintainer = set([PACKAGE_MAINTAINER_ID, PACKAGE_MAINTAINER_AND_DEV_ID]) cred_filters = { - PKGBASE_FLAG: user_developer_or_trusted_user, - PKGBASE_NOTIFY: user_developer_or_trusted_user, - PKGBASE_VOTE: user_developer_or_trusted_user, - PKGREQ_FILE: user_developer_or_trusted_user, - ACCOUNT_CHANGE_TYPE: trusted_user_or_dev, - ACCOUNT_EDIT: trusted_user_or_dev, - ACCOUNT_LAST_LOGIN: trusted_user_or_dev, - ACCOUNT_LIST_COMMENTS: trusted_user_or_dev, - ACCOUNT_SEARCH: trusted_user_or_dev, - COMMENT_DELETE: trusted_user_or_dev, - COMMENT_UNDELETE: trusted_user_or_dev, - COMMENT_VIEW_DELETED: trusted_user_or_dev, - COMMENT_EDIT: trusted_user_or_dev, - COMMENT_PIN: trusted_user_or_dev, - PKGBASE_ADOPT: trusted_user_or_dev, - PKGBASE_SET_KEYWORDS: trusted_user_or_dev, - PKGBASE_DELETE: trusted_user_or_dev, - PKGBASE_EDIT_COMAINTAINERS: trusted_user_or_dev, - PKGBASE_DISOWN: trusted_user_or_dev, - PKGBASE_LIST_VOTERS: trusted_user_or_dev, - PKGBASE_UNFLAG: trusted_user_or_dev, - PKGREQ_CLOSE: trusted_user_or_dev, - PKGREQ_LIST: trusted_user_or_dev, - TU_ADD_VOTE: trusted_user, - TU_LIST_VOTES: trusted_user_or_dev, - TU_VOTE: trusted_user, + PKGBASE_FLAG: user_developer_or_package_maintainer, + PKGBASE_NOTIFY: user_developer_or_package_maintainer, + PKGBASE_VOTE: user_developer_or_package_maintainer, + PKGREQ_FILE: user_developer_or_package_maintainer, + ACCOUNT_CHANGE_TYPE: package_maintainer_or_dev, + ACCOUNT_EDIT: package_maintainer_or_dev, + ACCOUNT_LAST_LOGIN: package_maintainer_or_dev, + ACCOUNT_LIST_COMMENTS: package_maintainer_or_dev, + ACCOUNT_SEARCH: package_maintainer_or_dev, + COMMENT_DELETE: package_maintainer_or_dev, + COMMENT_UNDELETE: package_maintainer_or_dev, + COMMENT_VIEW_DELETED: package_maintainer_or_dev, + COMMENT_EDIT: package_maintainer_or_dev, + COMMENT_PIN: package_maintainer_or_dev, + PKGBASE_ADOPT: package_maintainer_or_dev, + PKGBASE_SET_KEYWORDS: package_maintainer_or_dev, + PKGBASE_DELETE: package_maintainer_or_dev, + PKGBASE_EDIT_COMAINTAINERS: package_maintainer_or_dev, + PKGBASE_DISOWN: package_maintainer_or_dev, + PKGBASE_LIST_VOTERS: package_maintainer_or_dev, + PKGBASE_UNFLAG: package_maintainer_or_dev, + PKGREQ_CLOSE: package_maintainer_or_dev, + PKGREQ_LIST: package_maintainer_or_dev, + PM_ADD_VOTE: package_maintainer, + PM_LIST_VOTES: package_maintainer_or_dev, + PM_VOTE: package_maintainer, ACCOUNT_EDIT_DEV: developer, - PKGBASE_MERGE: trusted_user_or_dev, + PKGBASE_MERGE: package_maintainer_or_dev, } diff --git a/aurweb/initdb.py b/aurweb/initdb.py index ee59212c..7181ea3e 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -13,9 +13,9 @@ def feed_initial_data(conn): aurweb.schema.AccountTypes.insert(), [ {"ID": 1, "AccountType": "User"}, - {"ID": 2, "AccountType": "Trusted User"}, + {"ID": 2, "AccountType": "Package Maintainer"}, {"ID": 3, "AccountType": "Developer"}, - {"ID": 4, "AccountType": "Trusted User & Developer"}, + {"ID": 4, "AccountType": "Package Maintainer & Developer"}, ], ) conn.execute( diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py index 315800a7..70bfc2c5 100644 --- a/aurweb/models/account_type.py +++ b/aurweb/models/account_type.py @@ -2,21 +2,21 @@ from aurweb import schema from aurweb.models.declarative import Base USER = "User" -TRUSTED_USER = "Trusted User" +PACKAGE_MAINTAINER = "Package Maintainer" DEVELOPER = "Developer" -TRUSTED_USER_AND_DEV = "Trusted User & Developer" +PACKAGE_MAINTAINER_AND_DEV = "Package Maintainer & Developer" USER_ID = 1 -TRUSTED_USER_ID = 2 +PACKAGE_MAINTAINER_ID = 2 DEVELOPER_ID = 3 -TRUSTED_USER_AND_DEV_ID = 4 +PACKAGE_MAINTAINER_AND_DEV_ID = 4 # Map string constants to integer constants. ACCOUNT_TYPE_ID = { USER: USER_ID, - TRUSTED_USER: TRUSTED_USER_ID, + PACKAGE_MAINTAINER: PACKAGE_MAINTAINER_ID, DEVELOPER: DEVELOPER_ID, - TRUSTED_USER_AND_DEV: TRUSTED_USER_AND_DEV_ID, + PACKAGE_MAINTAINER_AND_DEV: PACKAGE_MAINTAINER_AND_DEV_ID, } # Reversed ACCOUNT_TYPE_ID mapping. diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 8612c259..f90d19eb 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -157,25 +157,25 @@ class User(Base): with db.begin(): db.delete(self.session) - def is_trusted_user(self): + def is_package_maintainer(self): return self.AccountType.ID in { - aurweb.models.account_type.TRUSTED_USER_ID, - aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID, } def is_developer(self): return self.AccountType.ID in { aurweb.models.account_type.DEVELOPER_ID, - aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID, } def is_elevated(self): """A User is 'elevated' when they have either a Trusted User or Developer AccountType.""" return self.AccountType.ID in { - aurweb.models.account_type.TRUSTED_USER_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_ID, aurweb.models.account_type.DEVELOPER_ID, - aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID, } def can_edit_user(self, target: "User") -> bool: diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 00efc1ff..f3688f54 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -187,7 +187,7 @@ def pkgbase_merge_instance( # Log this out for accountability purposes. logger.info( - f"Trusted User '{request.user.Username}' merged " + f"Package Maintainer '{request.user.Username}' merged " f"'{pkgbasename}' into '{target.Name}'." ) diff --git a/aurweb/routers/__init__.py b/aurweb/routers/__init__.py index f77bce4f..ccd70662 100644 --- a/aurweb/routers/__init__.py +++ b/aurweb/routers/__init__.py @@ -7,13 +7,13 @@ from . import ( accounts, auth, html, + package_maintainer, packages, pkgbase, requests, rpc, rss, sso, - trusted_user, ) """ @@ -28,7 +28,7 @@ APP_ROUTES = [ packages, pkgbase, requests, - trusted_user, + package_maintainer, rss, rpc, sso, diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 1c81ec1d..a2d167bc 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -184,9 +184,9 @@ def make_account_form_context( lambda e: request.user.AccountTypeID >= e[0], [ (at.USER_ID, f"Normal {at.USER}"), - (at.TRUSTED_USER_ID, at.TRUSTED_USER), + (at.PACKAGE_MAINTAINER_ID, at.PACKAGE_MAINTAINER), (at.DEVELOPER_ID, at.DEVELOPER), - (at.TRUSTED_USER_AND_DEV_ID, at.TRUSTED_USER_AND_DEV), + (at.PACKAGE_MAINTAINER_AND_DEV_ID, at.PACKAGE_MAINTAINER_AND_DEV), ], ) ) @@ -520,7 +520,9 @@ async def account_comments(request: Request, username: str): @router.get("/accounts") @requires_auth -@account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) +@account_type_required( + {at.PACKAGE_MAINTAINER, at.DEVELOPER, at.PACKAGE_MAINTAINER_AND_DEV} +) async def accounts(request: Request): context = make_context(request, "Accounts") return render_template(request, "account/search.html", context) @@ -529,7 +531,9 @@ async def accounts(request: Request): @router.post("/accounts") @handle_form_exceptions @requires_auth -@account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) +@account_type_required( + {at.PACKAGE_MAINTAINER, at.DEVELOPER, at.PACKAGE_MAINTAINER_AND_DEV} +) async def accounts_post( request: Request, O: int = Form(default=0), # Offset @@ -564,9 +568,9 @@ async def accounts_post( # Convert parameter T to an AccountType ID. account_types = { "u": at.USER_ID, - "t": at.TRUSTED_USER_ID, + "t": at.PACKAGE_MAINTAINER_ID, "d": at.DEVELOPER_ID, - "td": at.TRUSTED_USER_AND_DEV_ID, + "td": at.PACKAGE_MAINTAINER_AND_DEV_ID, } account_type_id = account_types.get(T, None) diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/package_maintainer.py similarity index 88% rename from aurweb/routers/trusted_user.py rename to aurweb/routers/package_maintainer.py index 4248347d..c5e70dcf 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/package_maintainer.py @@ -11,7 +11,10 @@ from aurweb import aur_logging, db, l10n, models, time from aurweb.auth import creds, requires_auth from aurweb.exceptions import handle_form_exceptions from aurweb.models import User -from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID +from aurweb.models.account_type import ( + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, +) from aurweb.templates import make_context, make_variable_context, render_template router = APIRouter() @@ -26,32 +29,32 @@ ADDVOTE_SPECIFICS = { # When a proposal is added, duration is added to the current # timestamp. # "addvote_type": (duration, quorum) - "add_tu": (7 * 24 * 60 * 60, 0.66), - "remove_tu": (7 * 24 * 60 * 60, 0.75), - "remove_inactive_tu": (5 * 24 * 60 * 60, 0.66), + "add_pm": (7 * 24 * 60 * 60, 0.66), + "remove_pm": (7 * 24 * 60 * 60, 0.75), + "remove_inactive_pm": (5 * 24 * 60 * 60, 0.66), "bylaws": (7 * 24 * 60 * 60, 0.75), } -def populate_trusted_user_counts(context: dict[str, Any]) -> None: - tu_query = db.query(User).filter( +def populate_package_maintainer_counts(context: dict[str, Any]) -> None: + pm_query = db.query(User).filter( or_( - User.AccountTypeID == TRUSTED_USER_ID, - User.AccountTypeID == TRUSTED_USER_AND_DEV_ID, + User.AccountTypeID == PACKAGE_MAINTAINER_ID, + User.AccountTypeID == PACKAGE_MAINTAINER_AND_DEV_ID, ) ) - context["trusted_user_count"] = tu_query.count() + context["package_maintainer_count"] = pm_query.count() # In case any records have a None InactivityTS. - active_tu_query = tu_query.filter( + active_pm_query = pm_query.filter( or_(User.InactivityTS.is_(None), User.InactivityTS == 0) ) - context["active_trusted_user_count"] = active_tu_query.count() + context["active_package_maintainer_count"] = active_pm_query.count() @router.get("/tu") @requires_auth -async def trusted_user( +async def package_maintainer( request: Request, coff: int = 0, # current offset cby: str = "desc", # current by @@ -63,7 +66,7 @@ async def trusted_user( if not request.user.has_credential(creds.TU_LIST_VOTES): return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) - context = make_context(request, "Trusted User") + context = make_context(request, "Package Maintainer") current_by, past_by = cby, pby current_off, past_off = coff, poff @@ -108,7 +111,7 @@ async def trusted_user( context["past_off"] = past_off last_vote = func.max(models.TUVote.VoteID).label("LastVote") - last_votes_by_tu = ( + last_votes_by_pm = ( db.query(models.TUVote) .join(models.User) .join(models.TUVoteInfo, models.TUVoteInfo.ID == models.TUVote.VoteID) @@ -124,12 +127,12 @@ async def trusted_user( .group_by(models.TUVote.UserID) .order_by(last_vote.desc(), models.User.Username.asc()) ) - context["last_votes_by_tu"] = last_votes_by_tu.all() + context["last_votes_by_pm"] = last_votes_by_pm.all() context["current_by_next"] = "asc" if current_by == "desc" else "desc" context["past_by_next"] = "asc" if past_by == "desc" else "desc" - populate_trusted_user_counts(context) + populate_package_maintainer_counts(context) context["q"] = { "coff": current_off, @@ -178,11 +181,11 @@ def render_proposal( @router.get("/tu/{proposal}") @requires_auth -async def trusted_user_proposal(request: Request, proposal: int): +async def package_maintainer_proposal(request: Request, proposal: int): if not request.user.has_credential(creds.TU_LIST_VOTES): return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) - context = await make_variable_context(request, "Trusted User") + context = await make_variable_context(request, "Package Maintainer") proposal = int(proposal) voteinfo = ( @@ -221,13 +224,13 @@ async def trusted_user_proposal(request: Request, proposal: int): @router.post("/tu/{proposal}") @handle_form_exceptions @requires_auth -async def trusted_user_proposal_post( +async def package_maintainer_proposal_post( request: Request, proposal: int, decision: str = Form(...) ): if not request.user.has_credential(creds.TU_LIST_VOTES): return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) - context = await make_variable_context(request, "Trusted User") + context = await make_variable_context(request, "Package Maintainer") proposal = int(proposal) # Make sure it's an int. voteinfo = ( @@ -285,8 +288,8 @@ async def trusted_user_proposal_post( @router.get("/addvote") @requires_auth -async def trusted_user_addvote( - request: Request, user: str = str(), type: str = "add_tu", agenda: str = str() +async def package_maintainer_addvote( + request: Request, user: str = str(), type: str = "add_pm", agenda: str = str() ): if not request.user.has_credential(creds.TU_ADD_VOTE): return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) @@ -295,7 +298,7 @@ async def trusted_user_addvote( if type not in ADDVOTE_SPECIFICS: context["error"] = "Invalid type." - type = "add_tu" # Default it. + type = "add_pm" # Default it. context["user"] = user context["type"] = type @@ -308,7 +311,7 @@ async def trusted_user_addvote( @router.post("/addvote") @handle_form_exceptions @requires_auth -async def trusted_user_addvote_post( +async def package_maintainer_addvote_post( request: Request, user: str = Form(default=str()), type: str = Form(default=str()), @@ -352,7 +355,7 @@ async def trusted_user_addvote_post( if type not in ADDVOTE_SPECIFICS: context["error"] = "Invalid type." - context["type"] = type = "add_tu" # Default for rendering. + context["type"] = type = "add_pm" # Default for rendering. return render_addvote(context, HTTPStatus.BAD_REQUEST) if not agenda: @@ -364,11 +367,11 @@ async def trusted_user_addvote_post( timestamp = time.utcnow() # Active TU types we filter for. - types = {TRUSTED_USER_ID, TRUSTED_USER_AND_DEV_ID} + types = {PACKAGE_MAINTAINER_ID, PACKAGE_MAINTAINER_AND_DEV_ID} # Create a new TUVoteInfo (proposal)! with db.begin(): - active_tus = ( + active_pms = ( db.query(User) .filter( and_( @@ -386,7 +389,7 @@ async def trusted_user_addvote_post( Submitted=timestamp, End=(timestamp + duration), Quorum=quorum, - ActiveTUs=active_tus, + ActiveTUs=active_pms, Submitter=request.user, ) diff --git a/aurweb/statistics.py b/aurweb/statistics.py index f301b59c..00a5c151 100644 --- a/aurweb/statistics.py +++ b/aurweb/statistics.py @@ -3,7 +3,11 @@ from sqlalchemy import func from aurweb import config, db, time from aurweb.cache import db_count_cache, db_query_cache from aurweb.models import PackageBase, PackageRequest, RequestType, User -from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.account_type import ( + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, + USER_ID, +) from aurweb.models.package_request import ( ACCEPTED_ID, CLOSED_ID, @@ -22,7 +26,7 @@ HOMEPAGE_COUNTERS = [ "year_old_updated", "never_updated", "user_count", - "trusted_user_count", + "package_maintainer_count", ] REQUEST_COUNTERS = [ "total_requests", @@ -32,7 +36,7 @@ REQUEST_COUNTERS = [ "rejected_requests", ] PROMETHEUS_USER_COUNTERS = [ - ("trusted_user_count", "tu"), + ("package_maintainer_count", "package_maintainer"), ("regular_user_count", "user"), ] PROMETHEUS_PACKAGE_COUNTERS = [ @@ -92,12 +96,12 @@ class Statistics: # Users case "user_count": query = self.user_query - case "trusted_user_count": + case "package_maintainer_count": query = self.user_query.filter( User.AccountTypeID.in_( ( - TRUSTED_USER_ID, - TRUSTED_USER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, + PACKAGE_MAINTAINER_AND_DEV_ID, ) ) ) diff --git a/aurweb/users/validate.py b/aurweb/users/validate.py index 8fc68864..5f1fcd43 100644 --- a/aurweb/users/validate.py +++ b/aurweb/users/validate.py @@ -220,7 +220,7 @@ def invalid_account_type( raise ValidationError([error]) logger.debug( - f"Trusted User '{request.user.Username}' has " + f"Package Maintainer '{request.user.Username}' has " f"modified '{user.Username}' account's type to" f" {name}." ) diff --git a/migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py b/migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py new file mode 100644 index 00000000..005549b0 --- /dev/null +++ b/migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py @@ -0,0 +1,37 @@ +"""Rename TU to Package Maintainer + +Revision ID: 6a64dd126029 +Revises: c5a6a9b661a0 +Create Date: 2023-09-01 13:48:15.315244 + +""" +from aurweb import db +from aurweb.models import AccountType + +# revision identifiers, used by Alembic. +revision = "6a64dd126029" +down_revision = "c5a6a9b661a0" +branch_labels = None +depends_on = None + +# AccountTypes +# ID 2 -> Trusted User / Package Maintainer +# ID 4 -> Trusted User & Developer / Package Maintainer & Developer + + +def upgrade(): + with db.begin(): + tu = db.query(AccountType).filter(AccountType.ID == 2).first() + tudev = db.query(AccountType).filter(AccountType.ID == 4).first() + + tu.AccountType = "Package Maintainer" + tudev.AccountType = "Package Maintainer & Developer" + + +def downgrade(): + with db.begin(): + pm = db.query(AccountType).filter(AccountType.ID == 2).first() + pmdev = db.query(AccountType).filter(AccountType.ID == 4).first() + + pm.AccountType = "Trusted User" + pmdev.AccountType = "Trusted User & Developer" diff --git a/schema/gendummydata.py b/schema/gendummydata.py index dfc8eee5..25f85c74 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -156,9 +156,9 @@ contents = None # developer/tu IDs # developers = [] -trustedusers = [] +packagemaintainers = [] has_devs = 0 -has_tus = 0 +has_pms = 0 # Just let python throw the errors if any happen # @@ -170,7 +170,7 @@ out.write("BEGIN;\n") log.debug("Creating SQL statements for users.") for u in user_keys: account_type = 1 # default to normal user - if not has_devs or not has_tus: + if not has_devs or not has_pms: account_type = random.randrange(1, 4) if account_type == 3 and not has_devs: # this will be a dev account @@ -178,12 +178,12 @@ for u in user_keys: developers.append(seen_users[u]) if len(developers) >= MAX_DEVS * MAX_USERS: has_devs = 1 - elif account_type == 2 and not has_tus: + elif account_type == 2 and not has_pms: # this will be a trusted user account # - trustedusers.append(seen_users[u]) - if len(trustedusers) >= MAX_TUS * MAX_USERS: - has_tus = 1 + packagemaintainers.append(seen_users[u]) + if len(packagemaintainers) >= MAX_TUS * MAX_USERS: + has_pms = 1 else: # a normal user account # @@ -205,8 +205,10 @@ for u in user_keys: out.write(s) log.debug("Number of developers: %d" % len(developers)) -log.debug("Number of trusted users: %d" % len(trustedusers)) -log.debug("Number of users: %d" % (MAX_USERS - len(developers) - len(trustedusers))) +log.debug("Number of package maintainers: %d" % len(packagemaintainers)) +log.debug( + "Number of users: %d" % (MAX_USERS - len(developers) - len(packagemaintainers)) +) log.debug("Number of packages: %d" % MAX_PKGS) log.debug("Gathering text from fortune file...") @@ -224,8 +226,8 @@ for p in list(seen_pkgs.keys()): muid = developers[random.randrange(0, len(developers))] puid = developers[random.randrange(0, len(developers))] else: - muid = trustedusers[random.randrange(0, len(trustedusers))] - puid = trustedusers[random.randrange(0, len(trustedusers))] + muid = packagemaintainers[random.randrange(0, len(packagemaintainers))] + puid = packagemaintainers[random.randrange(0, len(packagemaintainers))] if count % 20 == 0: # every so often, there are orphans... muid = "NULL" @@ -339,7 +341,7 @@ for p in seen_pkgs_keys: # Create trusted user proposals # -log.debug("Creating SQL statements for trusted user proposals.") +log.debug("Creating SQL statements for package maintainer proposals.") count = 0 for t in range(0, OPEN_PROPOSALS + CLOSE_PROPOSALS): now = int(time.time()) @@ -353,7 +355,7 @@ for t in range(0, OPEN_PROPOSALS + CLOSE_PROPOSALS): user = "" else: user = user_keys[random.randrange(0, len(user_keys))] - suid = trustedusers[random.randrange(0, len(trustedusers))] + suid = packagemaintainers[random.randrange(0, len(packagemaintainers))] s = ( "INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End," " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n" diff --git a/templates/addvote.html b/templates/addvote.html index 8777cbf3..30b65c0e 100644 --- a/templates/addvote.html +++ b/templates/addvote.html @@ -19,22 +19,22 @@

    - + - +

    diff --git a/templates/addvote.html b/templates/addvote.html index 30b65c0e..cc12f42b 100644 --- a/templates/addvote.html +++ b/templates/addvote.html @@ -24,21 +24,21 @@ selected {% endif %} > - {{ "Addition of a TU" | tr }} + {{ "Addition of a Package Maintainer" | tr }}

  • - {% trans %}Trusted User{% endtrans %} + {% trans %}Package Maintainer{% endtrans %}
  • {% endif %} diff --git a/templates/partials/packages/statistics.html b/templates/partials/packages/statistics.html index 7c3c3ef6..7ce5fba1 100644 --- a/templates/partials/packages/statistics.html +++ b/templates/partials/packages/statistics.html @@ -42,7 +42,7 @@ - {{ "Trusted Users" | tr }} + {{ "Package Maintainers" | tr }} {{ package_maintainer_count }} diff --git a/templates/partials/support.html b/templates/partials/support.html index a2890cc5..b175a040 100644 --- a/templates/partials/support.html +++ b/templates/partials/support.html @@ -10,7 +10,7 @@

    • {% trans %}Orphan Request{% endtrans %}: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}
    • -
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}
    • +
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the maintainer and file orphan request if necessary.{% endtrans %}
    • {% trans %}Merge Request{% endtrans %}: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}

    @@ -44,7 +44,7 @@

    {% trans %}Discussion{% endtrans %}

    - {{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." + {{ "General discussion regarding the Arch User Repository (AUR) and Package Maintainer structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." | tr | format('', "", '', "") @@ -55,7 +55,7 @@

    {% trans %}Bug Reporting{% endtrans %}

    - {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page." + {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the maintainer or leave a comment on the appropriate package page." | tr | format('', "", "", "") diff --git a/templates/partials/tu/proposal/details.html b/templates/partials/tu/proposal/details.html index 4cbee9ad..c74a5c5e 100644 --- a/templates/partials/tu/proposal/details.html +++ b/templates/partials/tu/proposal/details.html @@ -22,7 +22,7 @@

    - {{ "Active" | tr }} {{ "Trusted Users" | tr }} {{ "assigned" | tr }}: + {{ "Active" | tr }} {{ "Package Maintainers" | tr }} {{ "assigned" | tr }}: {{ voteinfo.ActiveTUs }}
    diff --git a/templates/pkgbase/request.html b/templates/pkgbase/request.html index 61654a49..3ffa2d2d 100644 --- a/templates/pkgbase/request.html +++ b/templates/pkgbase/request.html @@ -69,8 +69,8 @@

    {{ - "By submitting a deletion request, you ask a Trusted " - "User to delete the package base. This type of " + "By submitting a deletion request, you ask a Package " + "Maintainer to delete the package base. This type of " "request should be used for duplicates, software " "abandoned by upstream, as well as illegal and " "irreparably broken packages." | tr @@ -79,8 +79,8 @@