mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-31 13:53:41 +00:00 
			
		
		
		
	feat: add patch controls to web, review web, enrich info tab (#115)
* add ability to specify one-time patch on package addition * support vars in interface
This commit is contained in:
		| @ -200,6 +200,14 @@ Alternatively you can create full-diff patches, which are calculated by using `` | ||||
|  | ||||
| The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management). | ||||
|  | ||||
| It is also possible to create simple patch during package addition, e.g.: | ||||
|  | ||||
| .. code-block:: shell | ||||
|  | ||||
|    sudo -u ahriman ahriman package-add ahriman --variable PKGEXT=.pkg.tar.xz | ||||
|  | ||||
| The ``--variable`` argument accepts variables in shell like format: quotation and lists are supported as usual, but functions are not. This feature is useful in particular in order to override specific makepkg variables during build. | ||||
|  | ||||
| How to build package from official repository | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
|  | ||||
|  | ||||
| @ -51,7 +51,7 @@ | ||||
|                             </button> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <button id="package-update-button" class="btn dropdown-item" onclick="updatePackages()" hidden> | ||||
|                             <button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden> | ||||
|                                 <i class="bi bi-play"></i> update | ||||
|                             </button> | ||||
|                         </li> | ||||
| @ -61,7 +61,7 @@ | ||||
|                             </button> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <button id="package-remove-button" class="btn dropdown-item" onclick="removePackages()" disabled hidden> | ||||
|                             <button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden> | ||||
|                                 <i class="bi bi-trash"></i> remove | ||||
|                             </button> | ||||
|                         </li> | ||||
| @ -77,7 +77,8 @@ | ||||
|                 </button> | ||||
|             </div> | ||||
|  | ||||
|             <table id="packages" class="table table-striped table-hover" | ||||
|             <table id="packages" | ||||
|                    data-classes="table table-hover" | ||||
|                    data-export-options='{"fileName": "packages"}' | ||||
|                    data-filter-control="true" | ||||
|                    data-filter-control-visible="false" | ||||
| @ -102,13 +103,13 @@ | ||||
|                     <tr> | ||||
|                         <th data-checkbox="true"></th> | ||||
|                         <th data-sortable="true" data-switchable="false" data-field="base" data-filter-control="input" data-filter-control-placeholder="(any base)">package base</th> | ||||
|                         <th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-field="packages" data-filter-control="input" data-filter-control-placeholder="(any package)">packages</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="packager" data-filter-control="select" data-filter-custom-search="filterContains" data-filter-control-placeholder="(any packager)">packager</th> | ||||
|                         <th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th> | ||||
|                         <th data-sortable="true" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th> | ||||
|                         <th data-sortable="true" data-align="center" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|             </table> | ||||
|  | ||||
| @ -8,20 +8,20 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="key-import-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label> | ||||
|                         <div class="col-sm-10"> | ||||
|                         <label for="key-import-fingerprint-input" class="col-2 col-form-label">fingerprint</label> | ||||
|                         <div class="col-10"> | ||||
|                             <input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="key-import-server-input" class="col-sm-2 col-form-label">key server</label> | ||||
|                         <div class="col-sm-10"> | ||||
|                         <label for="key-import-server-input" class="col-2 col-form-label">key server</label> | ||||
|                         <div class="col-10"> | ||||
|                             <input id="key-import-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <div class="col-sm-2"></div> | ||||
|                         <div class="col-sm-10"> | ||||
|                         <div class="col-2"></div> | ||||
|                         <div class="col-10"> | ||||
|                             <pre class="language-less"><samp id="key-import-body-input" class="pre-scrollable language-less"></samp><button id="key-import-copy-button" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
| @ -8,14 +8,14 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="login-username" class="col-sm-4 col-form-label">username</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="login-username" class="col-4 col-form-label">username</label> | ||||
|                         <div class="col-8"> | ||||
|                             <input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="login-password" class="col-sm-4 col-form-label">password</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="login-password" class="col-4 col-form-label">password</label> | ||||
|                         <div class="col-8"> | ||||
|                             <div class="input-group"> | ||||
|                                 <input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required> | ||||
|                                 <div class="input-group-append"> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade"> | ||||
|     <div class="modal-dialog" role="document"> | ||||
|     <div class="modal-dialog modal-lg" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <form id="package-add-form" onsubmit="return false"> | ||||
|                 <div class="modal-header"> | ||||
| @ -8,9 +8,9 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-add-repository-input" class="col-sm-4 col-form-label">repository</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                             <select id="package-add-repository-input" class="form-control" name="repository" required> | ||||
|                         <label for="package-add-repository-input" class="col-3 col-form-label">repository</label> | ||||
|                         <div class="col-9"> | ||||
|                             <select id="package-add-repository-input" class="form-control" required> | ||||
|                                 {% for repository in repositories %} | ||||
|                                     <option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option> | ||||
|                                 {% endfor %} | ||||
| @ -18,12 +18,18 @@ | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-add-input" class="col-sm-4 col-form-label">package</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                             <input id="package-add-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required> | ||||
|                         <label for="package-add-input" class="col-3 col-form-label">package</label> | ||||
|                         <div class="col-9"> | ||||
|                             <input id="package-add-input" type="text" list="package-add-known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" required> | ||||
|                             <datalist id="package-add-known-packages-dlist"></datalist> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <div class="col-12"> | ||||
|                             <button id="package-add-variable-button" type="button" class="form-control btn btn-light rounded" onclick="packageAddVariableInputCreate()"><i class="bi bi-plus"></i> add environment variable </button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div id="package-add-variables-div" class="form-group row"></div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button> | ||||
| @ -39,9 +45,11 @@ | ||||
|     const packageAddForm = $("#package-add-form"); | ||||
|     packageAddModal.on("shown.bs.modal", () => { | ||||
|         $(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true); | ||||
|  | ||||
|     }); | ||||
|     packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); }); | ||||
|     packageAddModal.on("hidden.bs.modal", () => { | ||||
|         packageAddVariablesDiv.empty(); | ||||
|         packageAddForm.trigger("reset"); | ||||
|     }); | ||||
|  | ||||
|     const packageAddInput = $("#package-add-input"); | ||||
|     const packageAddRepositoryInput = $("#package-add-repository-input"); | ||||
| @ -71,25 +79,81 @@ | ||||
|         }, this), 500)); | ||||
|     }); | ||||
|  | ||||
|     function packagesAdd() { | ||||
|         const packages = packageAddInput.val(); | ||||
|     const packageAddVariablesDiv = $("#package-add-variables-div"); | ||||
|  | ||||
|     function packageAddVariableInputCreate() { | ||||
|         const variableInput = document.createElement("div"); | ||||
|         variableInput.classList.add("input-group"); | ||||
|         variableInput.classList.add("package-add-variable"); | ||||
|  | ||||
|         const variableNameInput = document.createElement("input"); | ||||
|         variableNameInput.type = "text"; | ||||
|         variableNameInput.classList.add("form-control"); | ||||
|         variableNameInput.classList.add("package-add-variable-name"); | ||||
|         variableNameInput.placeholder = "name"; | ||||
|         variableNameInput.ariaLabel = "variable name"; | ||||
|  | ||||
|         const variableSeparator = document.createElement("span"); | ||||
|         variableSeparator.classList.add("input-group-text") | ||||
|         variableSeparator.textContent = "="; | ||||
|  | ||||
|         const variableValueInput = document.createElement("input"); | ||||
|         variableValueInput.type = "text"; | ||||
|         variableValueInput.classList.add("form-control"); | ||||
|         variableValueInput.classList.add("package-add-variable-value"); | ||||
|         variableValueInput.placeholder = "value"; | ||||
|         variableValueInput.ariaLabel = "variable value"; | ||||
|  | ||||
|         const variableButtonRemove = document.createElement("button"); | ||||
|         variableButtonRemove.type = "button"; | ||||
|         variableButtonRemove.classList.add("btn"); | ||||
|         variableButtonRemove.classList.add("btn-outline-danger"); | ||||
|         variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>"; | ||||
|         variableButtonRemove.onclick = _ => { return variableInput.remove(); }; | ||||
|  | ||||
|         // bring them together | ||||
|         variableInput.appendChild(variableNameInput); | ||||
|         variableInput.appendChild(variableSeparator); | ||||
|         variableInput.appendChild(variableValueInput); | ||||
|         variableInput.appendChild(variableButtonRemove); | ||||
|  | ||||
|         packageAddVariablesDiv.append(variableInput); | ||||
|     } | ||||
|  | ||||
|     function patchesParse() { | ||||
|         const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => { | ||||
|             const richElement = $(element); | ||||
|             return { | ||||
|                 key: richElement.find(".package-add-variable-name").val(), | ||||
|                 value: richElement.find(".package-add-variable-value").val(), | ||||
|             }; | ||||
|         }).filter((_, patch) => patch.key).get(); | ||||
|         return {patches: patches}; | ||||
|     } | ||||
|  | ||||
|     function packagesAdd(packages, patches) { | ||||
|         packages = packages ?? packageAddInput.val(); | ||||
|         patches = patches ?? patchesParse(); | ||||
|         const repository = getRepositorySelector(packageAddRepositoryInput); | ||||
|  | ||||
|         if (packages) { | ||||
|             packageAddModal.modal("hide"); | ||||
|             const onSuccess = update => `Packages ${update} have been added`; | ||||
|             const onFailure = error => `Package addition failed: ${error}`; | ||||
|             doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure); | ||||
|             doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function packagesRequest() { | ||||
|         const packages = packageAddInput.val(); | ||||
|     function packagesRequest(packages, patches) { | ||||
|         packages = packages ?? packageAddInput.val(); | ||||
|         patches = patches ?? patchesParse(); | ||||
|         const repository = getRepositorySelector(packageAddRepositoryInput); | ||||
|  | ||||
|         if (packages) { | ||||
|             packageAddModal.modal("hide"); | ||||
|             const onSuccess = update => `Packages ${update} have been requested`; | ||||
|             const onFailure = error => `Package request failed: ${error}`; | ||||
|             doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure); | ||||
|             doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches); | ||||
|         } | ||||
|     } | ||||
| </script> | ||||
|  | ||||
| @ -6,10 +6,47 @@ | ||||
|                 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> | ||||
|             </div> | ||||
|             <div class="modal-body"> | ||||
|                 <div class="form-group row"> | ||||
|                     <div class="input-group"> | ||||
|                         <table class="table table-borderless"> | ||||
|                             <tbody> | ||||
|                                 <tr> | ||||
|                                     <td style="width: 10%; text-align: right">version</td> | ||||
|                                     <td id="package-info-version" style="width: 40%"></td> | ||||
|                                     <td style="width: 10%; text-align: right">packager</td> | ||||
|                                     <td id="package-info-packager" style="width: 40%"></td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td style="width: 10%; text-align: right">groups</td> | ||||
|                                     <td id="package-info-groups" style="width: 40%"></td> | ||||
|                                     <td style="width: 10%; text-align: right">licenses</td> | ||||
|                                     <td id="package-info-licenses" style="width: 40%"></td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td style="width: 10%; text-align: right">packages</td> | ||||
|                                     <td id="package-info-packages" style="width: 40%"></td> | ||||
|                                     <td style="width: 10%; text-align: right">depends</td> | ||||
|                                     <td id="package-info-depends" style="width: 40%"></td> | ||||
|                                 </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <hr class="col-12"> | ||||
|  | ||||
|                 <h3>Environment variables</h3> | ||||
|                 <div id="package-info-variables-div" class="form-group row"></div> | ||||
|  | ||||
|                 <hr class="col-12"> | ||||
|  | ||||
|                 <h3>Build logs</h3> | ||||
|                 <pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="package-info-logs-copy-button" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre> | ||||
|             </div> | ||||
|             <div class="modal-footer"> | ||||
|                 <button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button> | ||||
|                 <button type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i> update</button> | ||||
|                 <button type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i> remove</button> | ||||
|                 <button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i> reload</button> | ||||
|                 <button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button> | ||||
|             </div> | ||||
|         </div> | ||||
| @ -20,30 +57,78 @@ | ||||
|     const packageInfoModal = $("#package-info-modal"); | ||||
|     const packageInfoModalHeader = $("#package-info-modal-header"); | ||||
|     const packageInfo = $("#package-info"); | ||||
|     packageInfoModal.on("hidden.bs.modal", () => { | ||||
|         packageInfoDepends.empty(); | ||||
|         packageInfoGroups.empty(); | ||||
|         packageInfoLicenses.empty(); | ||||
|         packageInfoPackager.empty(); | ||||
|         packageInfoPackages.empty(); | ||||
|         packageInfoVersion.empty(); | ||||
|  | ||||
|         packageInfoVariablesDiv.empty(); | ||||
|  | ||||
|         packageInfoModal.trigger("reset"); | ||||
|     }); | ||||
|  | ||||
|     const packageInfoLogsInput = $("#package-info-logs-input"); | ||||
|     const packageInfoLogsCopyButton = $("#package-info-logs-copy-button"); | ||||
|  | ||||
|     const packageInfoDepends = $("#package-info-depends"); | ||||
|     const packageInfoGroups = $("#package-info-groups"); | ||||
|     const packageInfoLicenses = $("#package-info-licenses"); | ||||
|     const packageInfoPackager = $("#package-info-packager"); | ||||
|     const packageInfoPackages = $("#package-info-packages"); | ||||
|     const packageInfoVersion = $("#package-info-version"); | ||||
|  | ||||
|     const packageInfoVariablesDiv = $("#package-info-variables-div"); | ||||
|  | ||||
|     async function copyLogs() { | ||||
|         const logs = packageInfoLogsInput.text(); | ||||
|         await copyToClipboard(logs, packageInfoLogsCopyButton); | ||||
|     } | ||||
|  | ||||
|     function showLogs(packageBase) { | ||||
|         const isPackageBaseSet = packageBase !== undefined; | ||||
|         if (isPackageBaseSet) | ||||
|             packageInfoModal.data("package", packageBase); // set package base as currently used | ||||
|         else | ||||
|             packageBase = packageInfoModal.data("package"); // read package base from the current window attribute | ||||
|     function insertVariable(packageBase, variable) { | ||||
|         const variableInput = document.createElement("div"); | ||||
|         variableInput.classList.add("input-group"); | ||||
|  | ||||
|         const headerClass = status => { | ||||
|             if (status === "pending") return ["bg-warning"]; | ||||
|             if (status === "building") return ["bg-warning"]; | ||||
|             if (status === "failed") return ["bg-danger", "text-white"]; | ||||
|             if (status === "success") return ["bg-success", "text-white"]; | ||||
|             return ["bg-secondary", "text-white"]; | ||||
|         const variableNameInput = document.createElement("input"); | ||||
|         variableNameInput.classList.add("form-control"); | ||||
|         variableNameInput.readOnly = true; | ||||
|         variableNameInput.value = variable.key; | ||||
|  | ||||
|         const variableSeparator = document.createElement("span"); | ||||
|         variableSeparator.classList.add("input-group-text") | ||||
|         variableSeparator.textContent = "="; | ||||
|  | ||||
|         const variableValueInput = document.createElement("input"); | ||||
|         variableValueInput.classList.add("form-control"); | ||||
|         variableValueInput.readOnly = true; | ||||
|         variableValueInput.value = variable.value; | ||||
|  | ||||
|         const variableButtonRemove = document.createElement("button"); | ||||
|         variableButtonRemove.type = "button"; | ||||
|         variableButtonRemove.classList.add("btn"); | ||||
|         variableButtonRemove.classList.add("btn-outline-danger"); | ||||
|         variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>"; | ||||
|         variableButtonRemove.onclick = _ => { | ||||
|             $.ajax({ | ||||
|                 url: `/api/v1/packages/${packageBase}/patches/${variable.key}`, | ||||
|                 type: "DELETE", | ||||
|                 dataType: "json", | ||||
|                 success: _ => variableInput.remove(), | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         // bring them together | ||||
|         variableInput.appendChild(variableNameInput); | ||||
|         variableInput.appendChild(variableSeparator); | ||||
|         variableInput.appendChild(variableValueInput); | ||||
|         variableInput.appendChild(variableButtonRemove); | ||||
|  | ||||
|         packageInfoVariablesDiv.append(variableInput); | ||||
|     } | ||||
|  | ||||
|     function loadLogs(packageBase, onFailure) { | ||||
|         $.ajax({ | ||||
|             url: `/api/v2/packages/${packageBase}/logs`, | ||||
|             data: { | ||||
| @ -53,26 +138,101 @@ | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`); | ||||
|                 const logs = response.logs.map(log_record => { | ||||
|                     const [timestamp, record] = log_record; | ||||
|                     return `[${new Date(1000 * timestamp).toISOString()}] ${record}`; | ||||
|                 const logs = response.map(log_record => { | ||||
|                     return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`; | ||||
|                 }); | ||||
|                 packageInfoLogsInput.text(logs.join("\n")); | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function loadPackage(packageBase, onFailure) { | ||||
|         const headerClass = status => { | ||||
|             if (status === "pending") return ["bg-warning"]; | ||||
|             if (status === "building") return ["bg-warning"]; | ||||
|             if (status === "failed") return ["bg-danger", "text-white"]; | ||||
|             if (status === "success") return ["bg-success", "text-white"]; | ||||
|             return ["bg-secondary", "text-white"]; | ||||
|         }; | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: `/api/v1/packages/${packageBase}`, | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const description = response.find(Boolean); | ||||
|                 const packages = Object.keys(description.package.packages); | ||||
|  | ||||
|                 packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`); | ||||
|  | ||||
|                 packageInfoModalHeader.removeClass(); | ||||
|                 packageInfoModalHeader.addClass("modal-header"); | ||||
|                 headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz)); | ||||
|                 headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz)); | ||||
|  | ||||
|                 if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again | ||||
|             }, | ||||
|             error: (jqXHR, _, errorThrown) => { | ||||
|                 // show failed modal in case if first time loading | ||||
|                 if (isPackageBaseSet) { | ||||
|                     const message = error => `Could not load package ${packageBase} logs: ${error}`; | ||||
|                     showFailure("Load failure", message, jqXHR, errorThrown); | ||||
|                 } | ||||
|                 packageInfoDepends.html(listToTable( | ||||
|                     Object.values(description.package.packages) | ||||
|                         .reduce((accumulator, currentValue) => { | ||||
|                             return accumulator.concat(currentValue.depends.filter(v => packages.indexOf(v) === -1)) | ||||
|                                 .concat(currentValue.make_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (make)`)) | ||||
|                                 .concat(currentValue.opt_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (optional)`)); | ||||
|                         }, []) | ||||
|                 )); | ||||
|                 packageInfoGroups.html(listToTable(extractListProperties(description.package, "groups"))); | ||||
|                 packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses"))); | ||||
|                 packageInfoPackager.text(description.package.packager); | ||||
|                 packageInfoPackages.html(listToTable(packages)); | ||||
|                 packageInfoVersion.text(description.package.version); | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function loadPatches(packageBase, onFailure) { | ||||
|         $.ajax({ | ||||
|             url: `/api/v1/packages/${packageBase}/patches`, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 packageInfoVariablesDiv.empty(); | ||||
|                 response.map(patch => insertVariable(packageBase, patch)); | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function packageInfoRemove() { | ||||
|         const packageBase = packageInfoModal.data("package"); | ||||
|         if (packageBase) return packagesRemove([packageBase]); | ||||
|     } | ||||
|  | ||||
|     function packageInfoUpdate() { | ||||
|         const packageBase = packageInfoModal.data("package"); | ||||
|         if (packageBase) return packagesAdd(packageBase, []); | ||||
|     } | ||||
|  | ||||
|     function showPackageInfo(packageBase) { | ||||
|         const isPackageBaseSet = packageBase !== undefined; | ||||
|         if (isPackageBaseSet) | ||||
|             packageInfoModal.data("package", packageBase); // set package base as currently used | ||||
|         else | ||||
|             packageBase = packageInfoModal.data("package"); // read package base from the current window attribute | ||||
|  | ||||
|         const onFailure = (jqXHR, _, errorThrown) => { | ||||
|             if (isPackageBaseSet) { | ||||
|                 const message = error => `Could not load package ${packageBase} info: ${error}`; | ||||
|                 showFailure("Load failure", message, jqXHR, errorThrown); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         loadPackage(packageBase, onFailure); | ||||
|         loadPatches(packageBase, onFailure); | ||||
|         loadLogs(packageBase, onFailure); | ||||
|  | ||||
|         if (isPackageBaseSet) packageInfoModal.modal("show"); | ||||
|     } | ||||
| </script> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade"> | ||||
|     <div class="modal-dialog" role="document"> | ||||
|     <div class="modal-dialog modal-lg" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <form id="package-rebuild-form" onsubmit="return false"> | ||||
|                 <div class="modal-header"> | ||||
| @ -8,8 +8,8 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-rebuild-repository-input" class="col-sm-4 col-form-label">repository</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="package-rebuild-repository-input" class="col-3 col-form-label">repository</label> | ||||
|                         <div class="col-9"> | ||||
|                             <select id="package-rebuild-repository-input" class="form-control" name="repository" required> | ||||
|                                 {% for repository in repositories %} | ||||
|                                     <option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option> | ||||
| @ -18,8 +18,8 @@ | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-rebuild-dependency-input" class="col-sm-4 col-form-label">dependency</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="package-rebuild-dependency-input" class="col-3 col-form-label">dependency</label> | ||||
|                         <div class="col-9"> | ||||
|                             <input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
| @ -25,7 +25,7 @@ | ||||
|         if (0 === cell || "base" === cell) { | ||||
|             const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript | ||||
|             table.bootstrapTable(method, {field: "id", values: [data.id]}); | ||||
|         } else showLogs(data.id); | ||||
|         } else showPackageInfo(data.id); | ||||
|     }); | ||||
|     table.on("created-controls.bs.table", () => { | ||||
|         const pickerInput = $(".bootstrap-table-filter-control-timestamp"); | ||||
| @ -50,7 +50,7 @@ | ||||
|     const statusBadge = $("#badge-status"); | ||||
|     const versionBadge = $("#badge-version"); | ||||
|  | ||||
|     function doPackageAction(uri, packages, repository, successText, failureText) { | ||||
|     function doPackageAction(uri, packages, repository, successText, failureText, data) { | ||||
|         const queryParams = $.param({ | ||||
|             architecture: repository.architecture, | ||||
|             repository: repository.repository, | ||||
| @ -58,7 +58,7 @@ | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: `${uri}?${queryParams}`, | ||||
|             data: JSON.stringify({packages: packages}), | ||||
|             data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})), | ||||
|             type: "POST", | ||||
|             contentType: "application/json", | ||||
|             success: _ => { | ||||
| @ -71,31 +71,30 @@ | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function filterListGroups() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "groups"); | ||||
|     } | ||||
|  | ||||
|     function filterListLicenses() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "licenses"); | ||||
|     } | ||||
|  | ||||
|     function filterListPackagers() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "packager"); | ||||
|     } | ||||
|  | ||||
|     function getRepositorySelector(selector) { | ||||
|         const selected = selector.find(":selected"); | ||||
|         return { | ||||
|             architecture: selected.data("architecture"), | ||||
|             repository: selected.data("repository"), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     function getSelection() { | ||||
|         return table.bootstrapTable("getSelections").map(row => row.id); | ||||
|     } | ||||
|  | ||||
|     function removePackages() { | ||||
|         const onSuccess = update => `Packages ${update} have been removed`; | ||||
|         const onFailure = error => `Could not remove packages: ${error}`; | ||||
|         doPackageAction("/api/v1/service/remove", getSelection(), repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function selectRepository() { | ||||
|         const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}"; | ||||
|         const element = $(`#${fragment}-lnk`); | ||||
|         element.click(); | ||||
|     } | ||||
|  | ||||
|     function updatePackages() { | ||||
|         const currentSelection = getSelection(); | ||||
|         const [url, onSuccess] = currentSelection.length === 0 | ||||
|             ? ["/api/v1/service/update", _ => `Repository update has been run`] | ||||
|             : ["/api/v1/service/add", update => `Run update for packages ${update}`]; | ||||
|         const onFailure = error => `Packages update failed: ${error}`; | ||||
|         doPackageAction(url, currentSelection, repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function hideControls(hidden) { | ||||
|         keyImportButton.attr("hidden", hidden); | ||||
|         packageAddButton.attr("hidden", hidden); | ||||
| @ -104,6 +103,24 @@ | ||||
|         packageUpdateButton.attr("hidden", hidden); | ||||
|     } | ||||
|  | ||||
|     function packagesRemove(packages) { | ||||
|         packages = packages ?? getSelection(); | ||||
|         const onSuccess = update => `Packages ${update} have been removed`; | ||||
|         const onFailure = error => `Could not remove packages: ${error}`; | ||||
|  | ||||
|         doPackageAction("/api/v1/service/remove", packages, repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function packagesUpdate() { | ||||
|         const currentSelection = getSelection(); | ||||
|         const [url, onSuccess] = currentSelection.length === 0 | ||||
|             ? ["/api/v1/service/update", _ => `Repository update has been run`] | ||||
|             : ["/api/v1/service/add", update => `Run update for packages ${update}`]; | ||||
|         const onFailure = error => `Packages update failed: ${error}`; | ||||
|  | ||||
|         doPackageAction(url, currentSelection, repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function reload() { | ||||
|         table.bootstrapTable("showLoading"); | ||||
|  | ||||
| @ -124,18 +141,6 @@ | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const extractListProperties = (description, property) => { | ||||
|                     return Object.values(description.packages) | ||||
|                         .map(pkg => pkg[property]) | ||||
|                         .reduce((left, right) => left.concat(right), []); | ||||
|                 }; | ||||
|                 const listToTable = data => { | ||||
|                     return Array.from(new Set(data)) | ||||
|                         .sort() | ||||
|                         .map(entry => safe(entry)) | ||||
|                         .join("<br>"); | ||||
|                 }; | ||||
|  | ||||
|                 const payload = response.map(description => { | ||||
|                     const package_base = description.package.base; | ||||
|                     const web_url = description.package.remote.web_url; | ||||
| @ -147,7 +152,7 @@ | ||||
|                         packages: listToTable(Object.keys(description.package.packages)), | ||||
|                         groups: listToTable(extractListProperties(description.package, "groups")), | ||||
|                         licenses: listToTable(extractListProperties(description.package, "licenses")), | ||||
|                         timestamp: new Date(1000 * description.status.timestamp).toISOString(), | ||||
|                         timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(), | ||||
|                         status: description.status.status, | ||||
|                     }; | ||||
|                 }); | ||||
| @ -186,7 +191,7 @@ | ||||
|  | ||||
|                 statusBadge | ||||
|                     .popover("dispose") | ||||
|                     .attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`) | ||||
|                     .attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`) | ||||
|                     .popover(); | ||||
|                 statusBadge.removeClass(); | ||||
|                 statusBadge.addClass("btn"); | ||||
| @ -195,6 +200,12 @@ | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function selectRepository() { | ||||
|         const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}"; | ||||
|         const element = $(`#${fragment}-lnk`); | ||||
|         element.click(); | ||||
|     } | ||||
|  | ||||
|     function statusFormat(value) { | ||||
|         const cellClass = status => { | ||||
|             if (status === "pending") return "table-warning"; | ||||
| @ -206,26 +217,6 @@ | ||||
|         return {classes: cellClass(value)}; | ||||
|     } | ||||
|  | ||||
|     function filterListGroups() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "groups"); | ||||
|     } | ||||
|  | ||||
|     function filterListLicenses() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "licenses"); | ||||
|     } | ||||
|  | ||||
|     function filterListPackagers() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "packager"); | ||||
|     } | ||||
|  | ||||
|     function getRepositorySelector(selector) { | ||||
|         const selected = selector.find(":selected"); | ||||
|         return { | ||||
|             architecture: selected.data("architecture"), | ||||
|             repository: selected.data("repository"), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     $(() => { | ||||
|         table.bootstrapTable({}); | ||||
|         statusBadge.popover(); | ||||
|  | ||||
| @ -30,7 +30,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa | ||||
|         </div> | ||||
|  | ||||
|         <div class="container"> | ||||
|             <table id="packages" class="table table-striped table-hover" | ||||
|             <table id="packages" | ||||
|                    data-classes="table table-hover" | ||||
|                    data-export-options='{"fileName": "packages"}' | ||||
|                    data-filter-control="true" | ||||
|                    data-filter-control-visible="false" | ||||
| @ -53,16 +54,16 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa | ||||
|                 <thead class="table-primary"> | ||||
|                     <tr> | ||||
|                         <th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th> | ||||
|                         <th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="url">upstream url</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th> | ||||
|                         <th data-sortable="true" data-field="archive_size">archive size</th> | ||||
|                         <th data-sortable="true" data-field="installed_size">installed size</th> | ||||
|                         <th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="archive_size">archive size</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="installed_size">installed size</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|  | ||||
|  | ||||
| @ -36,19 +36,17 @@ | ||||
|         }, 2000); | ||||
|     } | ||||
|  | ||||
|     function safe(string) { | ||||
|         return String(string) | ||||
|             .replace(/&/g, "&") | ||||
|             .replace(/</g, "<") | ||||
|             .replace(/>/g, ">") | ||||
|             .replace(/"/g, """); | ||||
|     } | ||||
|  | ||||
|     function extractDataList(data, column) { | ||||
|         const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array | ||||
|         return Array.from(new Set(elements)).sort(); | ||||
|     } | ||||
|  | ||||
|     function extractListProperties(description, property) { | ||||
|         return Object.values(description.packages) | ||||
|             .map(pkg => pkg[property]) | ||||
|             .reduce((left, right) => left.concat(right), []); | ||||
|     } | ||||
|  | ||||
|     function filterContains(text, value) { | ||||
|         return value.includes(text.toLowerCase().trim()); | ||||
|     } | ||||
| @ -67,4 +65,24 @@ | ||||
|         // the library removes all symbols from string, so it is just string | ||||
|         return value.includes(dataList[index].toLowerCase()); | ||||
|     } | ||||
|  | ||||
|     function listToTable(data) { | ||||
|         return Array.from(new Set(data)) | ||||
|             .sort() | ||||
|             .map(entry => safe(entry)) | ||||
|             .join("<br>"); | ||||
|     } | ||||
|  | ||||
|     function safe(string) { | ||||
|         return String(string) | ||||
|             .replace(/&/g, "&") | ||||
|             .replace(/</g, "<") | ||||
|             .replace(/>/g, ">") | ||||
|             .replace(/"/g, """); | ||||
|     } | ||||
|  | ||||
|     Date.prototype.toISOStringShort = function() { | ||||
|         const pad = number => String(number).padStart(2, "0"); | ||||
|         return `${this.getFullYear()}-${pad(this.getMonth())}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`; | ||||
|     } | ||||
| </script> | ||||
|  | ||||
| @ -276,6 +276,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser: | ||||
|     parser.add_argument("-s", "--source", help="explicitly specify the package source for this command", | ||||
|                         type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto) | ||||
|     parser.add_argument("-u", "--username", help="build as user", default=extract_user()) | ||||
|     parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append") | ||||
|     parser.set_defaults(handler=handlers.Add) | ||||
|     return parser | ||||
|  | ||||
|  | ||||
| @ -23,6 +23,7 @@ from ahriman.application.application import Application | ||||
| from ahriman.application.handlers import Handler | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.models.packagers import Packagers | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.repository_id import RepositoryId | ||||
|  | ||||
|  | ||||
| @ -45,7 +46,12 @@ class Add(Handler): | ||||
|         """ | ||||
|         application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh) | ||||
|         application.on_start() | ||||
|  | ||||
|         application.add(args.package, args.source, args.username) | ||||
|         patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else [] | ||||
|         for package in args.package:  # for each requested package insert patch | ||||
|             application.database.patches_insert(package, patches) | ||||
|  | ||||
|         if not args.now: | ||||
|             return | ||||
|  | ||||
|  | ||||
| @ -162,7 +162,7 @@ class Handler: | ||||
|         if args.repository_id is not None: | ||||
|             separator = "/" if "/" in args.repository_id else "-"  # systemd and non-systemd identifiers | ||||
|             # repository parts is optional for backward compatibility | ||||
|             architecture, *repository_parts = args.repository_id.split(separator) | ||||
|             architecture, *repository_parts = args.repository_id.split(separator)  # maxsplit isn't used intentionally | ||||
|             args.architecture = architecture | ||||
|             if repository_parts: | ||||
|                 args.repository = "-".join(repository_parts)  # replace slash with dash | ||||
|  | ||||
| @ -115,7 +115,7 @@ class Patch(Handler): | ||||
|             package_base(str): package base | ||||
|             patch(PkgbuildPatch): patch descriptor | ||||
|         """ | ||||
|         application.database.patches_insert(package_base, patch) | ||||
|         application.database.patches_insert(package_base, [patch]) | ||||
|  | ||||
|     @staticmethod | ||||
|     def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None, | ||||
|  | ||||
| @ -33,8 +33,8 @@ class PackageOperations(Operations): | ||||
|     package operations | ||||
|     """ | ||||
|  | ||||
|     def _package_remove_package_base(self, connection: Connection, package_base: str, | ||||
|                                      repository_id: RepositoryId) -> None: | ||||
|     @staticmethod | ||||
|     def _package_remove_package_base(connection: Connection, package_base: str, repository_id: RepositoryId) -> None: | ||||
|         """ | ||||
|         remove package base information | ||||
|  | ||||
| @ -50,8 +50,9 @@ class PackageOperations(Operations): | ||||
|             """delete from package_bases where package_base = :package_base and repository = :repository""", | ||||
|             {"package_base": package_base, "repository": repository_id.id}) | ||||
|  | ||||
|     def _package_remove_packages(self, connection: Connection, package_base: str, | ||||
|                                  current_packages: Iterable[str], repository_id: RepositoryId) -> None: | ||||
|     @staticmethod | ||||
|     def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str], | ||||
|                                  repository_id: RepositoryId) -> None: | ||||
|         """ | ||||
|         remove packages belong to the package base | ||||
|  | ||||
| @ -74,8 +75,8 @@ class PackageOperations(Operations): | ||||
|             """delete from packages where package = :package and repository = :repository""", | ||||
|             packages) | ||||
|  | ||||
|     def _package_update_insert_base(self, connection: Connection, package: Package, | ||||
|                                     repository_id: RepositoryId) -> None: | ||||
|     @staticmethod | ||||
|     def _package_update_insert_base(connection: Connection, package: Package, repository_id: RepositoryId) -> None: | ||||
|         """ | ||||
|         insert base package into table | ||||
|  | ||||
| @ -107,8 +108,8 @@ class PackageOperations(Operations): | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     def _package_update_insert_packages(self, connection: Connection, package: Package, | ||||
|                                         repository_id: RepositoryId) -> None: | ||||
|     @staticmethod | ||||
|     def _package_update_insert_packages(connection: Connection, package: Package, repository_id: RepositoryId) -> None: | ||||
|         """ | ||||
|         insert packages into table | ||||
|  | ||||
| @ -149,7 +150,8 @@ class PackageOperations(Operations): | ||||
|             """, | ||||
|             package_list) | ||||
|  | ||||
|     def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus, | ||||
|     @staticmethod | ||||
|     def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus, | ||||
|                                       repository_id: RepositoryId) -> None: | ||||
|         """ | ||||
|         insert base package status into table | ||||
| @ -176,8 +178,8 @@ class PackageOperations(Operations): | ||||
|                 "repository": repository_id.id, | ||||
|             }) | ||||
|  | ||||
|     def _packages_get_select_package_bases(self, connection: Connection, | ||||
|                                            repository_id: RepositoryId) -> dict[str, Package]: | ||||
|     @staticmethod | ||||
|     def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]: | ||||
|         """ | ||||
|         select package bases from the table | ||||
|  | ||||
| @ -201,7 +203,8 @@ class PackageOperations(Operations): | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|     def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package], | ||||
|     @staticmethod | ||||
|     def _packages_get_select_packages(connection: Connection, packages: dict[str, Package], | ||||
|                                       repository_id: RepositoryId) -> dict[str, Package]: | ||||
|         """ | ||||
|         select packages from the table | ||||
| @ -223,8 +226,8 @@ class PackageOperations(Operations): | ||||
|             packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row) | ||||
|         return packages | ||||
|  | ||||
|     def _packages_get_select_statuses(self, connection: Connection, | ||||
|                                       repository_id: RepositoryId) -> dict[str, BuildStatus]: | ||||
|     @staticmethod | ||||
|     def _packages_get_select_statuses(connection: Connection, repository_id: RepositoryId) -> dict[str, BuildStatus]: | ||||
|         """ | ||||
|         select package build statuses from the table | ||||
|  | ||||
|  | ||||
| @ -18,7 +18,6 @@ | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from collections import defaultdict | ||||
|  | ||||
| from sqlite3 import Connection | ||||
|  | ||||
| from ahriman.core.database.operations import Operations | ||||
| @ -42,16 +41,16 @@ class PatchOperations(Operations): | ||||
|         """ | ||||
|         return self.patches_list(package_base, None).get(package_base, []) | ||||
|  | ||||
|     def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None: | ||||
|     def patches_insert(self, package_base: str, patches: list[PkgbuildPatch]) -> None: | ||||
|         """ | ||||
|         insert or update patch in database | ||||
|  | ||||
|         Args: | ||||
|             package_base(str): package base to insert | ||||
|             patch(PkgbuildPatch): patch content | ||||
|             patches(list[PkgbuildPatch]): patch content | ||||
|         """ | ||||
|         def run(connection: Connection) -> None: | ||||
|             connection.execute( | ||||
|             connection.executemany( | ||||
|                 """ | ||||
|                 insert into patches | ||||
|                 (package_base, variable, patch) | ||||
| @ -60,7 +59,14 @@ class PatchOperations(Operations): | ||||
|                 on conflict (package_base, coalesce(variable, '')) do update set | ||||
|                 patch = :patch | ||||
|                 """, | ||||
|                 {"package_base": package_base, "variable": patch.key, "patch": patch.value}) | ||||
|                 [ | ||||
|                     { | ||||
|                         "package_base": package_base, | ||||
|                         "variable": patch.key, | ||||
|                         "patch": patch.value, | ||||
|                     } for patch in patches | ||||
|                 ] | ||||
|             ) | ||||
|  | ||||
|         return self.with_connection(run, commit=True) | ||||
|  | ||||
| @ -89,7 +95,7 @@ class PatchOperations(Operations): | ||||
|             if variables is not None and patch.key not in variables: | ||||
|                 continue | ||||
|             patches[package].append(patch) | ||||
|         return dict(patches) | ||||
|         return patches | ||||
|  | ||||
|     def patches_remove(self, package_base: str, variables: list[str] | None) -> None: | ||||
|         """ | ||||
| @ -102,12 +108,21 @@ class PatchOperations(Operations): | ||||
|         def run_many(connection: Connection) -> None: | ||||
|             patches = variables or []  # suppress mypy warning | ||||
|             connection.executemany( | ||||
|                 """delete from patches where package_base = :package_base and variable = :variable""", | ||||
|                 [{"package_base": package_base, "variable": variable} for variable in patches]) | ||||
|                 """ | ||||
|                 delete from patches where package_base = :package_base and variable = :variable | ||||
|                 """, | ||||
|                 [ | ||||
|                     { | ||||
|                         "package_base": package_base, | ||||
|                         "variable": variable, | ||||
|                     } for variable in patches | ||||
|                 ]) | ||||
|  | ||||
|         def run(connection: Connection) -> None: | ||||
|             connection.execute( | ||||
|                 """delete from patches where package_base = :package_base""", | ||||
|                 """ | ||||
|                 delete from patches where package_base = :package_base | ||||
|                 """, | ||||
|                 {"package_base": package_base}) | ||||
|  | ||||
|         if variables is not None: | ||||
|  | ||||
| @ -28,6 +28,7 @@ from multiprocessing import Process, Queue | ||||
| from threading import Lock, Thread | ||||
|  | ||||
| from ahriman.core.log import LazyLogging | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.process_status import ProcessStatus | ||||
| from ahriman.models.repository_id import RepositoryId | ||||
|  | ||||
| @ -96,7 +97,8 @@ class Spawn(Thread, LazyLogging): | ||||
|  | ||||
|         queue.put(ProcessStatus(process_id, result, consumed_time)) | ||||
|  | ||||
|     def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str, **kwargs: str | None) -> str: | ||||
|     def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str, | ||||
|                        **kwargs: str | list[str] | None) -> str: | ||||
|         """ | ||||
|         spawn external ahriman process with supplied arguments | ||||
|  | ||||
| @ -104,7 +106,7 @@ class Spawn(Thread, LazyLogging): | ||||
|             repository_id(RepositoryId): repository unique identifier | ||||
|             command(str): subcommand to run | ||||
|             *args(str): positional command arguments | ||||
|             **kwargs(str): named command arguments | ||||
|             **kwargs(str | list[str] | None): named command arguments | ||||
|  | ||||
|         Returns: | ||||
|             str: spawned process identifier | ||||
| @ -118,9 +120,13 @@ class Spawn(Thread, LazyLogging): | ||||
|         for argument, value in kwargs.items(): | ||||
|             if value is None: | ||||
|                 continue  # skip null values | ||||
|             arguments.append(f"--{argument}") | ||||
|             if value: | ||||
|                 arguments.append(value) | ||||
|             flag = f"--{argument}" | ||||
|             if isinstance(value, list): | ||||
|                 arguments.extend(list(sum(((flag, v) for v in value), ()))) | ||||
|             elif value: | ||||
|                 arguments.extend([flag, value]) | ||||
|             else: | ||||
|                 arguments.append(flag)  # boolean argument | ||||
|  | ||||
|         process_id = str(uuid.uuid4()) | ||||
|         self.logger.info("full command line arguments of %s are %s using repository %s", | ||||
| @ -167,7 +173,7 @@ class Spawn(Thread, LazyLogging): | ||||
|         return self._spawn_process(repository_id, "service-key-import", key, **kwargs) | ||||
|  | ||||
|     def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *, | ||||
|                      now: bool) -> str: | ||||
|                      patches: list[PkgbuildPatch], now: bool) -> str: | ||||
|         """ | ||||
|         add packages | ||||
|  | ||||
| @ -175,14 +181,18 @@ class Spawn(Thread, LazyLogging): | ||||
|             repository_id(RepositoryId): repository unique identifier | ||||
|             packages(Iterable[str]): packages list to add | ||||
|             username(str | None): optional override of username for build process | ||||
|             patches(list[PkgbuildPatch]): list of patches to be passed | ||||
|             now(bool): build packages now | ||||
|  | ||||
|         Returns: | ||||
|             str: spawned process identifier | ||||
|         """ | ||||
|         kwargs = {"username": username} | ||||
|         kwargs: dict[str, str | list[str] | None] = {"username": username} | ||||
|         if now: | ||||
|             kwargs["now"] = "" | ||||
|         if patches: | ||||
|             kwargs["variable"] = [patch.serialize() for patch in patches] | ||||
|  | ||||
|         return self._spawn_process(repository_id, "package-add", *packages, **kwargs) | ||||
|  | ||||
|     def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str: | ||||
|  | ||||
| @ -23,6 +23,7 @@ from ahriman.core.log import LazyLogging | ||||
| from ahriman.models.build_status import BuildStatus, BuildStatusEnum | ||||
| from ahriman.models.log_record_id import LogRecordId | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.repository_id import RepositoryId | ||||
|  | ||||
|  | ||||
| @ -162,6 +163,40 @@ class Watcher(LazyLogging): | ||||
|         self.known[package_base] = (package, full_status) | ||||
|         self.database.package_update(package, full_status, self.repository_id) | ||||
|  | ||||
|     def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]: | ||||
|         """ | ||||
|         get patches for the package | ||||
|  | ||||
|         Args: | ||||
|             package_base(str): package base | ||||
|             variable(str | None): patch variable name if any | ||||
|  | ||||
|         Returns: | ||||
|             list[PkgbuildPatch]: list of patches which are stored for the package | ||||
|         """ | ||||
|         variables = [variable] if variable is not None else None | ||||
|         return self.database.patches_list(package_base, variables).get(package_base, []) | ||||
|  | ||||
|     def patches_remove(self, package_base: str, variable: str) -> None: | ||||
|         """ | ||||
|         remove package patch | ||||
|  | ||||
|         Args: | ||||
|             package_base(str): package base | ||||
|             variable(str): patch variable name | ||||
|         """ | ||||
|         self.database.patches_remove(package_base, [variable]) | ||||
|  | ||||
|     def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None: | ||||
|         """ | ||||
|         update package patch | ||||
|  | ||||
|         Args: | ||||
|             package_base(str): package base | ||||
|             patch(PkgbuildPatch): package patch | ||||
|         """ | ||||
|         self.database.patches_insert(package_base, [patch]) | ||||
|  | ||||
|     def status_update(self, status: BuildStatusEnum) -> None: | ||||
|         """ | ||||
|         update service status | ||||
|  | ||||
| @ -56,6 +56,7 @@ __all__ = [ | ||||
|     "srcinfo_property", | ||||
|     "srcinfo_property_list", | ||||
|     "trim_package", | ||||
|     "unquote", | ||||
|     "utcnow", | ||||
|     "walk", | ||||
| ] | ||||
| @ -465,6 +466,38 @@ def trim_package(package_name: str) -> str: | ||||
|     return package_name | ||||
|  | ||||
|  | ||||
| def unquote(source: str) -> str: | ||||
|     """ | ||||
|     like ``shlex.quote``, but opposite | ||||
|  | ||||
|     Args: | ||||
|         source(str): source string to remove quotes | ||||
|  | ||||
|     Returns: | ||||
|         str: string with quotes removed | ||||
|  | ||||
|     Raises: | ||||
|         ValueError: if no closing quotation | ||||
|     """ | ||||
|     def generator() -> Generator[str, None, None]: | ||||
|         token = None | ||||
|         for char in source: | ||||
|             if token is not None: | ||||
|                 if char == token: | ||||
|                     token = None  # closed quote | ||||
|                 else: | ||||
|                     yield char  # character inside quotes | ||||
|             elif char in ("'", "\""): | ||||
|                 token = char  # first quote found | ||||
|             else: | ||||
|                 yield char  # normal character | ||||
|  | ||||
|         if token is not None: | ||||
|             raise ValueError("No closing quotation") | ||||
|  | ||||
|     return "".join(generator()) | ||||
|  | ||||
|  | ||||
| def utcnow() -> datetime.datetime: | ||||
|     """ | ||||
|     get current time | ||||
|  | ||||
| @ -19,8 +19,11 @@ | ||||
| # | ||||
| import shlex | ||||
|  | ||||
| from dataclasses import dataclass, field | ||||
| from dataclasses import dataclass | ||||
| from pathlib import Path | ||||
| from typing import Any, Self | ||||
|  | ||||
| from ahriman.core.util import dataclass_view, unquote | ||||
|  | ||||
|  | ||||
| @dataclass(frozen=True) | ||||
| @ -33,12 +36,12 @@ class PkgbuildPatch: | ||||
|             considered as full PKGBUILD diffs | ||||
|         value(str | list[str]): value of the stored PKGBUILD property. It must be either string or list of string | ||||
|             values | ||||
|         unsafe(bool): if set, value will be not quoted, might break PKGBUILD | ||||
|     """ | ||||
|  | ||||
|     key: str | None | ||||
|     value: str | list[str] | ||||
|     unsafe: bool = field(default=False, kw_only=True) | ||||
|  | ||||
|     quote = shlex.quote | ||||
|  | ||||
|     def __post_init__(self) -> None: | ||||
|         """ | ||||
| @ -66,17 +69,26 @@ class PkgbuildPatch: | ||||
|         """ | ||||
|         return self.key is None | ||||
|  | ||||
|     def quote(self, value: str) -> str: | ||||
|     @classmethod | ||||
|     def from_env(cls, variable: str) -> Self: | ||||
|         """ | ||||
|         quote value according to the unsafe flag | ||||
|         construct patch from environment variable. Functions are not supported | ||||
|  | ||||
|         Args: | ||||
|             value(str): value to be quoted | ||||
|             variable(str): variable in bash form, i.e. KEY=VALUE | ||||
|  | ||||
|         Returns: | ||||
|             str: quoted string in case if unsafe is False and as is otherwise | ||||
|             Self: package properties | ||||
|         """ | ||||
|         return value if self.unsafe else shlex.quote(value) | ||||
|         key, *value_parts = variable.split("=", maxsplit=1) | ||||
|  | ||||
|         raw_value = next(iter(value_parts), "")  # extract raw value | ||||
|         if raw_value.startswith("(") and raw_value.endswith(")"): | ||||
|             value: str | list[str] = shlex.split(raw_value[1:-1])  # arrays for poor | ||||
|         else: | ||||
|             value = unquote(raw_value) | ||||
|  | ||||
|         return cls(key, value) | ||||
|  | ||||
|     def serialize(self) -> str: | ||||
|         """ | ||||
| @ -88,14 +100,23 @@ class PkgbuildPatch: | ||||
|             str: serialized key-value pair, print-friendly | ||||
|         """ | ||||
|         if isinstance(self.value, list):  # list like | ||||
|             value = " ".join(map(self.quote, self.value)) | ||||
|             value = " ".join(map(PkgbuildPatch.quote, self.value)) | ||||
|             return f"""{self.key}=({value})""" | ||||
|         if self.is_plain_diff:  # no additional logic for plain diffs | ||||
|             return self.value | ||||
|         # we suppose that function values are only supported in string-like values | ||||
|         if self.is_function: | ||||
|             return f"{self.key} {self.value}"  # no quoting enabled here | ||||
|         return f"""{self.key}={self.quote(self.value)}""" | ||||
|         return f"""{self.key}={PkgbuildPatch.quote(self.value)}""" | ||||
|  | ||||
|     def view(self) -> dict[str, Any]: | ||||
|         """ | ||||
|         generate json patch view | ||||
|  | ||||
|         Returns: | ||||
|             dict[str, Any]: json-friendly dictionary | ||||
|         """ | ||||
|         return dataclass_view(self) | ||||
|  | ||||
|     def write(self, pkgbuild_path: Path) -> None: | ||||
|         """ | ||||
|  | ||||
| @ -52,6 +52,8 @@ class RepositoryId: | ||||
|         Returns: | ||||
|             str: unique id for this repository | ||||
|         """ | ||||
|         if self.is_empty: | ||||
|             return "" | ||||
|         return f"{self.architecture}-{self.name}"  # basically the same as used for command line | ||||
|  | ||||
|     def query(self) -> list[tuple[str, str]]: | ||||
|  | ||||
| @ -25,14 +25,17 @@ from ahriman.web.schemas.file_schema import FileSchema | ||||
| from ahriman.web.schemas.internal_status_schema import InternalStatusSchema | ||||
| from ahriman.web.schemas.log_schema import LogSchema | ||||
| from ahriman.web.schemas.login_schema import LoginSchema | ||||
| from ahriman.web.schemas.logs_schema import LogsSchema, LogsSchemaV2 | ||||
| from ahriman.web.schemas.logs_schema import LogsSchema | ||||
| from ahriman.web.schemas.oauth2_schema import OAuth2Schema | ||||
| from ahriman.web.schemas.package_name_schema import PackageNameSchema | ||||
| from ahriman.web.schemas.package_names_schema import PackageNamesSchema | ||||
| from ahriman.web.schemas.package_patch_schema import PackagePatchSchema | ||||
| from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema | ||||
| from ahriman.web.schemas.package_schema import PackageSchema | ||||
| from ahriman.web.schemas.package_status_schema import PackageStatusSimplifiedSchema, PackageStatusSchema | ||||
| from ahriman.web.schemas.pagination_schema import PaginationSchema | ||||
| from ahriman.web.schemas.patch_name_schema import PatchNameSchema | ||||
| from ahriman.web.schemas.patch_schema import PatchSchema | ||||
| from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema | ||||
| from ahriman.web.schemas.pgp_key_schema import PGPKeySchema | ||||
| from ahriman.web.schemas.process_id_schema import ProcessIdSchema | ||||
| @ -41,4 +44,5 @@ from ahriman.web.schemas.remote_schema import RemoteSchema | ||||
| from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema | ||||
| from ahriman.web.schemas.search_schema import SearchSchema | ||||
| from ahriman.web.schemas.status_schema import StatusSchema | ||||
| from ahriman.web.schemas.versioned_log_schema import VersionedLogSchema | ||||
| from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema | ||||
|  | ||||
| @ -17,13 +17,10 @@ | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from marshmallow import fields | ||||
|  | ||||
| from ahriman import __version__ | ||||
| from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema | ||||
| from marshmallow import Schema, fields | ||||
|  | ||||
|  | ||||
| class LogSchema(RepositoryIdSchema): | ||||
| class LogSchema(Schema): | ||||
|     """ | ||||
|     request package log schema | ||||
|     """ | ||||
| @ -32,10 +29,6 @@ class LogSchema(RepositoryIdSchema): | ||||
|         "description": "Log record timestamp", | ||||
|         "example": 1680537091.233495, | ||||
|     }) | ||||
|     version = fields.Integer(required=True, metadata={ | ||||
|         "description": "Package version to tag", | ||||
|         "example": __version__, | ||||
|     }) | ||||
|     message = fields.String(required=True, metadata={ | ||||
|         "description": "Log message", | ||||
|     }) | ||||
|  | ||||
| @ -27,23 +27,9 @@ class LogsSchema(Schema): | ||||
|     response package logs schema | ||||
|     """ | ||||
|  | ||||
|     package_base = fields.String(required=True, metadata={ | ||||
|         "description": "Package base name", | ||||
|         "example": "ahriman", | ||||
|     }) | ||||
|     status = fields.Nested(StatusSchema(), required=True, metadata={ | ||||
|         "description": "Last package status", | ||||
|     }) | ||||
|     logs = fields.String(required=True, metadata={ | ||||
|         "description": "Full package log from the last build", | ||||
|     }) | ||||
|  | ||||
|  | ||||
| class LogsSchemaV2(Schema): | ||||
|     """ | ||||
|     response package logs api v2 schema | ||||
|     """ | ||||
|  | ||||
|     package_base = fields.String(required=True, metadata={ | ||||
|         "description": "Package base name", | ||||
|         "example": "ahriman", | ||||
| @ -51,7 +37,3 @@ class LogsSchemaV2(Schema): | ||||
|     status = fields.Nested(StatusSchema(), required=True, metadata={ | ||||
|         "description": "Last package status", | ||||
|     }) | ||||
|     logs = fields.List(fields.Tuple([fields.Float(), fields.String()]), required=True, metadata={  # type: ignore[no-untyped-call] | ||||
|         "description": "Package log records timestamp and message", | ||||
|         "example": [(1680537091.233495, "log record")] | ||||
|     }) | ||||
|  | ||||
							
								
								
									
										33
									
								
								src/ahriman/web/schemas/package_patch_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/ahriman/web/schemas/package_patch_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| # | ||||
| # Copyright (c) 2021-2023 ahriman team. | ||||
| # | ||||
| # This file is part of ahriman | ||||
| # (see https://github.com/arcan1s/ahriman). | ||||
| # | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from marshmallow import fields | ||||
|  | ||||
| from ahriman.web.schemas.package_names_schema import PackageNamesSchema | ||||
| from ahriman.web.schemas.patch_schema import PatchSchema | ||||
|  | ||||
|  | ||||
| class PackagePatchSchema(PackageNamesSchema): | ||||
|     """ | ||||
|     response schema with packages and patches | ||||
|     """ | ||||
|  | ||||
|     patches = fields.Nested(PatchSchema(many=True), metadata={ | ||||
|         "description": "optional environment variables to be applied as patches" | ||||
|     }) | ||||
							
								
								
									
										33
									
								
								src/ahriman/web/schemas/patch_name_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/ahriman/web/schemas/patch_name_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| # | ||||
| # Copyright (c) 2021-2023 ahriman team. | ||||
| # | ||||
| # This file is part of ahriman | ||||
| # (see https://github.com/arcan1s/ahriman). | ||||
| # | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from marshmallow import fields | ||||
|  | ||||
| from ahriman.web.schemas.package_name_schema import PackageNameSchema | ||||
|  | ||||
|  | ||||
| class PatchNameSchema(PackageNameSchema): | ||||
|     """ | ||||
|     request package patch schema | ||||
|     """ | ||||
|  | ||||
|     patch = fields.String(required=True, metadata={ | ||||
|         "description": "Variable name", | ||||
|         "example": "PKGEXT", | ||||
|     }) | ||||
							
								
								
									
										33
									
								
								src/ahriman/web/schemas/patch_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/ahriman/web/schemas/patch_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| # | ||||
| # Copyright (c) 2021-2023 ahriman team. | ||||
| # | ||||
| # This file is part of ahriman | ||||
| # (see https://github.com/arcan1s/ahriman). | ||||
| # | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from marshmallow import Schema, fields | ||||
|  | ||||
|  | ||||
| class PatchSchema(Schema): | ||||
|     """ | ||||
|     request and response patch schema | ||||
|     """ | ||||
|  | ||||
|     key = fields.String(required=True, metadata={ | ||||
|         "description": "environment variable name", | ||||
|     }) | ||||
|     value = fields.String(metadata={ | ||||
|         "description": "environment variable value", | ||||
|     }) | ||||
							
								
								
									
										35
									
								
								src/ahriman/web/schemas/versioned_log_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/ahriman/web/schemas/versioned_log_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| # | ||||
| # Copyright (c) 2021-2023 ahriman team. | ||||
| # | ||||
| # This file is part of ahriman | ||||
| # (see https://github.com/arcan1s/ahriman). | ||||
| # | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from marshmallow import fields | ||||
|  | ||||
| from ahriman import __version__ | ||||
| from ahriman.web.schemas.log_schema import LogSchema | ||||
| from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema | ||||
|  | ||||
|  | ||||
| class VersionedLogSchema(LogSchema, RepositoryIdSchema): | ||||
|     """ | ||||
|     request package log schema | ||||
|     """ | ||||
|  | ||||
|     version = fields.Integer(required=True, metadata={ | ||||
|         "description": "Package version to tag", | ||||
|         "example": __version__, | ||||
|     }) | ||||
| @ -21,8 +21,9 @@ import aiohttp_apispec  # type: ignore[import-untyped] | ||||
|  | ||||
| from aiohttp.web import HTTPBadRequest, Response, json_response | ||||
|  | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema | ||||
| from ahriman.web.views.base import BaseView | ||||
|  | ||||
|  | ||||
| @ -53,7 +54,7 @@ class AddView(BaseView): | ||||
|     ) | ||||
|     @aiohttp_apispec.cookies_schema(AuthSchema) | ||||
|     @aiohttp_apispec.querystring_schema(RepositoryIdSchema) | ||||
|     @aiohttp_apispec.json_schema(PackageNamesSchema) | ||||
|     @aiohttp_apispec.json_schema(PackagePatchSchema) | ||||
|     async def post(self) -> Response: | ||||
|         """ | ||||
|         add new package | ||||
| @ -65,13 +66,14 @@ class AddView(BaseView): | ||||
|             HTTPBadRequest: if bad data is supplied | ||||
|         """ | ||||
|         try: | ||||
|             data = await self.extract_data(["packages"]) | ||||
|             data = await self.extract_data(["packages", "patches"]) | ||||
|             packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages") | ||||
|             patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])] | ||||
|         except Exception as ex: | ||||
|             raise HTTPBadRequest(reason=str(ex)) | ||||
|  | ||||
|         repository_id = self.repository_id() | ||||
|         username = await self.username() | ||||
|         process_id = self.spawner.packages_add(repository_id, packages, username, now=True) | ||||
|         process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=True) | ||||
|  | ||||
|         return json_response({"process_id": process_id}) | ||||
|  | ||||
| @ -21,8 +21,9 @@ import aiohttp_apispec  # type: ignore[import-untyped] | ||||
|  | ||||
| from aiohttp.web import HTTPBadRequest, Response, json_response | ||||
|  | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema | ||||
| from ahriman.web.views.base import BaseView | ||||
|  | ||||
|  | ||||
| @ -53,7 +54,7 @@ class RequestView(BaseView): | ||||
|     ) | ||||
|     @aiohttp_apispec.cookies_schema(AuthSchema) | ||||
|     @aiohttp_apispec.querystring_schema(RepositoryIdSchema) | ||||
|     @aiohttp_apispec.json_schema(PackageNamesSchema) | ||||
|     @aiohttp_apispec.json_schema(PackagePatchSchema) | ||||
|     async def post(self) -> Response: | ||||
|         """ | ||||
|         request to add new package | ||||
| @ -65,13 +66,14 @@ class RequestView(BaseView): | ||||
|             HTTPBadRequest: if bad data is supplied | ||||
|         """ | ||||
|         try: | ||||
|             data = await self.extract_data(["packages"]) | ||||
|             data = await self.extract_data(["packages", "patches"]) | ||||
|             packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages") | ||||
|             patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])] | ||||
|         except Exception as ex: | ||||
|             raise HTTPBadRequest(reason=str(ex)) | ||||
|  | ||||
|         username = await self.username() | ||||
|         repository_id = self.repository_id() | ||||
|         process_id = self.spawner.packages_add(repository_id, packages, username, now=False) | ||||
|         process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=False) | ||||
|  | ||||
|         return json_response({"process_id": process_id}) | ||||
|  | ||||
| @ -25,7 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError | ||||
| from ahriman.core.util import pretty_datetime | ||||
| from ahriman.models.log_record_id import LogRecordId | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \ | ||||
|     VersionedLogSchema | ||||
| from ahriman.web.views.base import BaseView | ||||
|  | ||||
|  | ||||
| @ -128,7 +129,7 @@ class LogsView(BaseView): | ||||
|     ) | ||||
|     @aiohttp_apispec.cookies_schema(AuthSchema) | ||||
|     @aiohttp_apispec.match_info_schema(PackageNameSchema) | ||||
|     @aiohttp_apispec.json_schema(LogSchema) | ||||
|     @aiohttp_apispec.json_schema(VersionedLogSchema) | ||||
|     async def post(self) -> None: | ||||
|         """ | ||||
|         create new package log record | ||||
|  | ||||
							
								
								
									
										103
									
								
								src/ahriman/web/views/v1/status/patch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/ahriman/web/views/v1/status/patch.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,103 @@ | ||||
| # | ||||
| # Copyright (c) 2021-2023 ahriman team. | ||||
| # | ||||
| # This file is part of ahriman | ||||
| # (see https://github.com/arcan1s/ahriman). | ||||
| # | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| import aiohttp_apispec  # type: ignore[import-untyped] | ||||
|  | ||||
| from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response | ||||
|  | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, PatchNameSchema, PatchSchema | ||||
| from ahriman.web.views.base import BaseView | ||||
|  | ||||
|  | ||||
| class PatchView(BaseView): | ||||
|     """ | ||||
|     package patch web view | ||||
|  | ||||
|     Attributes: | ||||
|         DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self | ||||
|         GET_PERMISSION(UserAccess): (class attribute) get permissions of self | ||||
|     """ | ||||
|  | ||||
|     DELETE_PERMISSION = UserAccess.Full | ||||
|     GET_PERMISSION = UserAccess.Reporter | ||||
|     ROUTES = ["/api/v1/packages/{package}/patches/{patch}"] | ||||
|  | ||||
|     @aiohttp_apispec.docs( | ||||
|         tags=["Packages"], | ||||
|         summary="Delete package patch", | ||||
|         description="Delete package patch by variable", | ||||
|         responses={ | ||||
|             204: {"description": "Success response"}, | ||||
|             401: {"description": "Authorization required", "schema": ErrorSchema}, | ||||
|             403: {"description": "Access is forbidden", "schema": ErrorSchema}, | ||||
|             500: {"description": "Internal server error", "schema": ErrorSchema}, | ||||
|         }, | ||||
|         security=[{"token": [DELETE_PERMISSION]}], | ||||
|     ) | ||||
|     @aiohttp_apispec.cookies_schema(AuthSchema) | ||||
|     @aiohttp_apispec.match_info_schema(PatchNameSchema) | ||||
|     async def delete(self) -> None: | ||||
|         """ | ||||
|         delete package patch | ||||
|  | ||||
|         Raises: | ||||
|             HTTPNoContent: on success response | ||||
|         """ | ||||
|         package_base = self.request.match_info["package"] | ||||
|         variable = self.request.match_info["patch"] | ||||
|         self.service().patches_remove(package_base, variable) | ||||
|  | ||||
|         raise HTTPNoContent | ||||
|  | ||||
|     @aiohttp_apispec.docs( | ||||
|         tags=["Packages"], | ||||
|         summary="Get package patch", | ||||
|         description="Retrieve package patch by variable", | ||||
|         responses={ | ||||
|             200: {"description": "Success response", "schema": PatchSchema}, | ||||
|             401: {"description": "Authorization required", "schema": ErrorSchema}, | ||||
|             403: {"description": "Access is forbidden", "schema": ErrorSchema}, | ||||
|             404: {"description": "Patch name is unknown", "schema": ErrorSchema}, | ||||
|             500: {"description": "Internal server error", "schema": ErrorSchema}, | ||||
|         }, | ||||
|         security=[{"token": [GET_PERMISSION]}], | ||||
|     ) | ||||
|     @aiohttp_apispec.cookies_schema(AuthSchema) | ||||
|     @aiohttp_apispec.match_info_schema(PatchNameSchema) | ||||
|     async def get(self) -> Response: | ||||
|         """ | ||||
|         get package patch | ||||
|  | ||||
|         Returns: | ||||
|             Response: 200 with package patch on success | ||||
|  | ||||
|         Raises: | ||||
|             HTTPNotFound: if package base is unknown | ||||
|         """ | ||||
|         package_base = self.request.match_info["package"] | ||||
|         variable = self.request.match_info["patch"] | ||||
|  | ||||
|         patches = self.service().patches_get(package_base, variable) | ||||
|         selected = next((patch for patch in patches if patch.key == variable), None) | ||||
|  | ||||
|         if selected is None: | ||||
|             raise HTTPNotFound | ||||
|  | ||||
|         return json_response(selected.view()) | ||||
							
								
								
									
										108
									
								
								src/ahriman/web/views/v1/status/patches.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/ahriman/web/views/v1/status/patches.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| # | ||||
| # Copyright (c) 2021-2023 ahriman team. | ||||
| # | ||||
| # This file is part of ahriman | ||||
| # (see https://github.com/arcan1s/ahriman). | ||||
| # | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| import aiohttp_apispec  # type: ignore[import-untyped] | ||||
|  | ||||
| from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response | ||||
|  | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PatchSchema | ||||
| from ahriman.web.views.base import BaseView | ||||
|  | ||||
|  | ||||
| class PatchesView(BaseView): | ||||
|     """ | ||||
|     package patches web view | ||||
|  | ||||
|     Attributes: | ||||
|         GET_PERMISSION(UserAccess): (class attribute) get permissions of self | ||||
|         POST_PERMISSION(UserAccess): (class attribute) post permissions of self | ||||
|     """ | ||||
|  | ||||
|     GET_PERMISSION = UserAccess.Reporter | ||||
|     POST_PERMISSION = UserAccess.Full | ||||
|     ROUTES = ["/api/v1/packages/{package}/patches"] | ||||
|  | ||||
|     @aiohttp_apispec.docs( | ||||
|         tags=["Packages"], | ||||
|         summary="Get package patches", | ||||
|         description="Retrieve all package patches", | ||||
|         responses={ | ||||
|             200: {"description": "Success response", "schema": PatchSchema(many=True)}, | ||||
|             401: {"description": "Authorization required", "schema": ErrorSchema}, | ||||
|             403: {"description": "Access is forbidden", "schema": ErrorSchema}, | ||||
|             500: {"description": "Internal server error", "schema": ErrorSchema}, | ||||
|         }, | ||||
|         security=[{"token": [GET_PERMISSION]}], | ||||
|     ) | ||||
|     @aiohttp_apispec.cookies_schema(AuthSchema) | ||||
|     @aiohttp_apispec.match_info_schema(PackageNameSchema) | ||||
|     async def get(self) -> Response: | ||||
|         """ | ||||
|         get package patches | ||||
|  | ||||
|         Returns: | ||||
|             Response: 200 with package patches on success | ||||
|  | ||||
|         Raises: | ||||
|             HTTPNotFound: if package base is unknown | ||||
|         """ | ||||
|         package_base = self.request.match_info["package"] | ||||
|         patches = self.service().patches_get(package_base, None) | ||||
|  | ||||
|         response = [patch.view() for patch in patches] | ||||
|         return json_response(response) | ||||
|  | ||||
|     @aiohttp_apispec.docs( | ||||
|         tags=["Packages"], | ||||
|         summary="Update package patch", | ||||
|         description="Update or create package patch", | ||||
|         responses={ | ||||
|             204: {"description": "Success response"}, | ||||
|             400: {"description": "Bad data is supplied", "schema": ErrorSchema}, | ||||
|             401: {"description": "Authorization required", "schema": ErrorSchema}, | ||||
|             403: {"description": "Access is forbidden", "schema": ErrorSchema}, | ||||
|             500: {"description": "Internal server error", "schema": ErrorSchema}, | ||||
|         }, | ||||
|         security=[{"token": [GET_PERMISSION]}], | ||||
|     ) | ||||
|     @aiohttp_apispec.cookies_schema(AuthSchema) | ||||
|     @aiohttp_apispec.match_info_schema(PackageNameSchema) | ||||
|     @aiohttp_apispec.json_schema(PatchSchema) | ||||
|     async def post(self) -> None: | ||||
|         """ | ||||
|         update or create package patch | ||||
|  | ||||
|         Raises: | ||||
|             HTTPBadRequest: if bad data is supplied | ||||
|             HTTPNoContent: on success response | ||||
|         """ | ||||
|         package_base = self.request.match_info["package"] | ||||
|         data = await self.extract_data() | ||||
|  | ||||
|         try: | ||||
|             key = data["key"] | ||||
|             value = data["value"] | ||||
|         except Exception as ex: | ||||
|             raise HTTPBadRequest(reason=str(ex)) | ||||
|  | ||||
|         self.service().patches_update(package_base, PkgbuildPatch(key, value)) | ||||
|  | ||||
|         raise HTTPNoContent | ||||
| @ -19,11 +19,10 @@ | ||||
| # | ||||
| import aiohttp_apispec  # type: ignore[import-untyped] | ||||
|  | ||||
| from aiohttp.web import HTTPNotFound, Response, json_response | ||||
| from aiohttp.web import Response, json_response | ||||
|  | ||||
| from ahriman.core.exceptions import UnknownPackageError | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchemaV2, PackageNameSchema, PaginationSchema | ||||
| from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, PackageNameSchema, PaginationSchema | ||||
| from ahriman.web.views.base import BaseView | ||||
|  | ||||
|  | ||||
| @ -43,7 +42,7 @@ class LogsView(BaseView): | ||||
|         summary="Get paginated package logs", | ||||
|         description="Retrieve package logs and the last package status", | ||||
|         responses={ | ||||
|             200: {"description": "Success response", "schema": LogsSchemaV2}, | ||||
|             200: {"description": "Success response", "schema": LogSchema(many=True)}, | ||||
|             400: {"description": "Bad data is supplied", "schema": ErrorSchema}, | ||||
|             401: {"description": "Authorization required", "schema": ErrorSchema}, | ||||
|             403: {"description": "Access is forbidden", "schema": ErrorSchema}, | ||||
| @ -67,16 +66,12 @@ class LogsView(BaseView): | ||||
|         """ | ||||
|         package_base = self.request.match_info["package"] | ||||
|         limit, offset = self.page() | ||||
|  | ||||
|         try: | ||||
|             _, status = self.service().package_get(package_base) | ||||
|         except UnknownPackageError: | ||||
|             raise HTTPNotFound(reason=f"Package {package_base} is unknown") | ||||
|         logs = self.service().logs_get(package_base, limit, offset) | ||||
|  | ||||
|         response = { | ||||
|             "package_base": package_base, | ||||
|             "status": status.view(), | ||||
|             "logs": logs, | ||||
|         } | ||||
|         response = [ | ||||
|             { | ||||
|                 "created": created, | ||||
|                 "message": message, | ||||
|             } for created, message in logs | ||||
|         ] | ||||
|         return json_response(response) | ||||
|  | ||||
| @ -9,6 +9,7 @@ from ahriman.core.repository import Repository | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.package_source import PackageSource | ||||
| from ahriman.models.packagers import Packagers | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.result import Result | ||||
|  | ||||
|  | ||||
| @ -22,7 +23,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: | ||||
|     Returns: | ||||
|         argparse.Namespace: generated arguments for these test cases | ||||
|     """ | ||||
|     args.package = [] | ||||
|     args.package = ["ahriman"] | ||||
|     args.exit_code = False | ||||
|     args.increment = True | ||||
|     args.now = False | ||||
| @ -30,6 +31,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: | ||||
|     args.source = PackageSource.Auto | ||||
|     args.dependencies = True | ||||
|     args.username = "username" | ||||
|     args.variable = None | ||||
|     return args | ||||
|  | ||||
|  | ||||
| @ -51,6 +53,22 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: | ||||
|     on_start_mock.assert_called_once_with() | ||||
|  | ||||
|  | ||||
| def test_run_with_patches(args: argparse.Namespace, configuration: Configuration, repository: Repository, | ||||
|                           mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must run command and insert temporary patches | ||||
|     """ | ||||
|     args = _default_args(args) | ||||
|     args.variable = ["KEY=VALUE"] | ||||
|     mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) | ||||
|     mocker.patch("ahriman.application.application.Application.add") | ||||
|     application_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert") | ||||
|  | ||||
|     _, repository_id = configuration.check_loaded() | ||||
|     Add.run(args, repository_id, configuration, report=False) | ||||
|     application_mock.assert_called_once_with(args.package[0], [PkgbuildPatch("KEY", "VALUE")]) | ||||
|  | ||||
|  | ||||
| def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, repository: Repository, | ||||
|                           package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|  | ||||
| @ -180,7 +180,7 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo | ||||
|     """ | ||||
|     create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert") | ||||
|     Patch.patch_set_create(application, package_ahriman.base, PkgbuildPatch("version", package_ahriman.version)) | ||||
|     create_mock.assert_called_once_with(package_ahriman.base, PkgbuildPatch("version", package_ahriman.version)) | ||||
|     create_mock.assert_called_once_with(package_ahriman.base, [PkgbuildPatch("version", package_ahriman.version)]) | ||||
|  | ||||
|  | ||||
| def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|  | ||||
| @ -254,6 +254,22 @@ def test_subparsers_package_add_option_refresh(parser: argparse.ArgumentParser) | ||||
|     assert args.refresh == 2 | ||||
|  | ||||
|  | ||||
| def test_subparsers_package_add_option_variable_empty(parser: argparse.ArgumentParser) -> None: | ||||
|     """ | ||||
|     package-add command must accept empty variable list as None | ||||
|     """ | ||||
|     args = parser.parse_args(["package-add", "ahriman"]) | ||||
|     assert args.variable is None | ||||
|  | ||||
|  | ||||
| def test_subparsers_package_add_option_variable_multiple(parser: argparse.ArgumentParser) -> None: | ||||
|     """ | ||||
|     repo-rebuild command must accept multiple depends-on | ||||
|     """ | ||||
|     args = parser.parse_args(["package-add", "ahriman", "-v", "var1", "-v", "var2"]) | ||||
|     assert args.variable == ["var1", "var2"] | ||||
|  | ||||
|  | ||||
| def test_subparsers_package_remove_option_architecture(parser: argparse.ArgumentParser) -> None: | ||||
|     """ | ||||
|     package-remove command must correctly parse architecture list | ||||
|  | ||||
| @ -20,6 +20,7 @@ def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> No | ||||
|     """ | ||||
|     mocker.patch("ahriman.models.package.Package.from_build", return_value=task_ahriman.package) | ||||
|     load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") | ||||
|  | ||||
|     task_ahriman.init(Path("ahriman"), database, None) | ||||
|     load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, [], task_ahriman.paths) | ||||
|  | ||||
|  | ||||
| @ -8,7 +8,7 @@ from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.database.migrations.m011_repository_name import migrate_data, migrate_package_repository, steps | ||||
|  | ||||
|  | ||||
| def test_migration_check_depends() -> None: | ||||
| def test_migration_repository_name() -> None: | ||||
|     """ | ||||
|     migration must not be empty | ||||
|     """ | ||||
|  | ||||
| @ -7,9 +7,9 @@ def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_ | ||||
|     """ | ||||
|     must insert patch to database | ||||
|     """ | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch_1")) | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch_3")) | ||||
|     database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch_2")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch_1")]) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch_3")]) | ||||
|     database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch_2")]) | ||||
|     assert database.patches_get(package_ahriman.base) == [ | ||||
|         PkgbuildPatch(None, "patch_1"), PkgbuildPatch("key", "patch_3") | ||||
|     ] | ||||
| @ -19,9 +19,9 @@ def test_patches_list(database: SQLite, package_ahriman: Package, package_python | ||||
|     """ | ||||
|     must list all patches | ||||
|     """ | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3")) | ||||
|     database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")]) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch3")]) | ||||
|     database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")]) | ||||
|     assert database.patches_list(None, None) == { | ||||
|         package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")], | ||||
|         package_python_schedule.base: [PkgbuildPatch(None, "patch2")], | ||||
| @ -32,8 +32,8 @@ def test_patches_list_filter(database: SQLite, package_ahriman: Package, package | ||||
|     """ | ||||
|     must list all patches filtered by package name (same as get) | ||||
|     """ | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) | ||||
|     database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")]) | ||||
|     database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")]) | ||||
|  | ||||
|     assert database.patches_list(package_ahriman.base, None) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]} | ||||
|     assert database.patches_list(package_python_schedule.base, None) == { | ||||
| @ -46,9 +46,9 @@ def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Pack | ||||
|     """ | ||||
|     must list all patches filtered by package name (same as get) | ||||
|     """ | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch2")) | ||||
|     database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch3")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")]) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch2")]) | ||||
|     database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch3")]) | ||||
|  | ||||
|     assert database.patches_list(None, None) == { | ||||
|         package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")], | ||||
| @ -63,8 +63,8 @@ def test_patches_insert_remove(database: SQLite, package_ahriman: Package, packa | ||||
|     """ | ||||
|     must remove patch from database | ||||
|     """ | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) | ||||
|     database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")]) | ||||
|     database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")]) | ||||
|     database.patches_remove(package_ahriman.base, None) | ||||
|  | ||||
|     assert database.patches_get(package_ahriman.base) == [] | ||||
| @ -76,9 +76,9 @@ def test_patches_insert_remove_by_variable(database: SQLite, package_ahriman: Pa | ||||
|     """ | ||||
|     must remove patch from database by variable | ||||
|     """ | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3")) | ||||
|     database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")]) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch3")]) | ||||
|     database.patches_insert(package_python_schedule.base, [PkgbuildPatch(None, "patch2")]) | ||||
|     database.patches_remove(package_ahriman.base, ["key"]) | ||||
|  | ||||
|     assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")] | ||||
| @ -89,8 +89,14 @@ def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> No | ||||
|     """ | ||||
|     must update patch in database | ||||
|     """ | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")]) | ||||
|     assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")] | ||||
|  | ||||
|     database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch2")) | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch2")]) | ||||
|     assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")] | ||||
|  | ||||
|     database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch3"), PkgbuildPatch("key", "patch4")]) | ||||
|     assert database.patches_get(package_ahriman.base) == [ | ||||
|         PkgbuildPatch(None, "patch3"), | ||||
|         PkgbuildPatch("key", "patch4"), | ||||
|     ] | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| import pytest | ||||
|  | ||||
| from pytest_mock import MockerFixture | ||||
| from unittest.mock import call as MockCall | ||||
|  | ||||
| from ahriman.core.exceptions import UnknownPackageError | ||||
| from ahriman.core.status.watcher import Watcher | ||||
| from ahriman.models.build_status import BuildStatus, BuildStatusEnum | ||||
| from ahriman.models.log_record_id import LogRecordId | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
|  | ||||
|  | ||||
| def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
| @ -163,6 +165,42 @@ def test_package_update_unknown(watcher: Watcher, package_ahriman: Package) -> N | ||||
|         watcher.package_update(package_ahriman.base, BuildStatusEnum.Unknown, None) | ||||
|  | ||||
|  | ||||
| def test_patches_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must return patches for the package | ||||
|     """ | ||||
|     patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_list") | ||||
|  | ||||
|     watcher.patches_get(package_ahriman.base, None) | ||||
|     watcher.patches_get(package_ahriman.base, "var") | ||||
|     patches_mock.assert_has_calls([ | ||||
|         MockCall(package_ahriman.base, None), | ||||
|         MockCall().get(package_ahriman.base, []), | ||||
|         MockCall(package_ahriman.base, ["var"]), | ||||
|         MockCall().get(package_ahriman.base, []), | ||||
|     ]) | ||||
|  | ||||
|  | ||||
| def test_patches_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must remove patches for the package | ||||
|     """ | ||||
|     patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove") | ||||
|     watcher.patches_remove(package_ahriman.base, "var") | ||||
|     patches_mock.assert_called_once_with(package_ahriman.base, ["var"]) | ||||
|  | ||||
|  | ||||
| def test_patches_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must update patches for the package | ||||
|     """ | ||||
|     patch = PkgbuildPatch("key", "value") | ||||
|     patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert") | ||||
|  | ||||
|     watcher.patches_update(package_ahriman.base, patch) | ||||
|     patches_mock.assert_called_once_with(package_ahriman.base, [patch]) | ||||
|  | ||||
|  | ||||
| def test_status_update(watcher: Watcher) -> None: | ||||
|     """ | ||||
|     must update service status | ||||
|  | ||||
| @ -4,6 +4,7 @@ from pytest_mock import MockerFixture | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| from ahriman.core.spawn import Spawn | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.process_status import ProcessStatus | ||||
| from ahriman.models.repository_id import RepositoryId | ||||
|  | ||||
| @ -56,11 +57,12 @@ def test_spawn_process(spawner: Spawn, repository_id: RepositoryId, mocker: Mock | ||||
|     """ | ||||
|     start_mock = mocker.patch("multiprocessing.Process.start") | ||||
|  | ||||
|     assert spawner._spawn_process(repository_id, "add", "ahriman", now="", maybe="?", none=None) | ||||
|     assert spawner._spawn_process(repository_id, "command", "argument", | ||||
|                                   empty="", string="v", list=["a", "b"], empty_list=[], none=None) | ||||
|     start_mock.assert_called_once_with() | ||||
|     spawner.args_parser.parse_args.assert_called_once_with( | ||||
|         spawner.command_arguments + [ | ||||
|             "add", "ahriman", "--now", "--maybe", "?" | ||||
|             "command", "argument", "--empty", "--string", "v", "--list", "a", "--list", "b", | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
| @ -99,7 +101,7 @@ def test_packages_add(spawner: Spawn, repository_id: RepositoryId, mocker: Mocke | ||||
|     must call package addition | ||||
|     """ | ||||
|     spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") | ||||
|     assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, now=False) | ||||
|     assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=[], now=False) | ||||
|     spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None) | ||||
|  | ||||
|  | ||||
| @ -108,7 +110,7 @@ def test_packages_add_with_build(spawner: Spawn, repository_id: RepositoryId, mo | ||||
|     must call package addition with update | ||||
|     """ | ||||
|     spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") | ||||
|     assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, now=True) | ||||
|     assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=[], now=True) | ||||
|     spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None, now="") | ||||
|  | ||||
|  | ||||
| @ -117,10 +119,21 @@ def test_packages_add_with_username(spawner: Spawn, repository_id: RepositoryId, | ||||
|     must call package addition with username | ||||
|     """ | ||||
|     spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") | ||||
|     assert spawner.packages_add(repository_id, ["ahriman", "linux"], "username", now=False) | ||||
|     assert spawner.packages_add(repository_id, ["ahriman", "linux"], "username", patches=[], now=False) | ||||
|     spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username="username") | ||||
|  | ||||
|  | ||||
| def test_packages_add_with_patches(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must call package addition with patches | ||||
|     """ | ||||
|     patches = [PkgbuildPatch("key", "value"), PkgbuildPatch("key", "value")] | ||||
|     spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") | ||||
|     assert spawner.packages_add(repository_id, ["ahriman", "linux"], None, patches=patches, now=False) | ||||
|     spawn_mock.assert_called_once_with(repository_id, "package-add", "ahriman", "linux", username=None, | ||||
|                                        variable=[patch.serialize() for patch in patches]) | ||||
|  | ||||
|  | ||||
| def test_packages_rebuild(spawner: Spawn, repository_id: RepositoryId, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must call package rebuild | ||||
|  | ||||
| @ -2,6 +2,7 @@ import datetime | ||||
| import logging | ||||
| import os | ||||
| import pytest | ||||
| import shlex | ||||
|  | ||||
| from pathlib import Path | ||||
| from pytest_mock import MockerFixture | ||||
| @ -11,7 +12,7 @@ from unittest.mock import call as MockCall | ||||
| from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError | ||||
| from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \ | ||||
|     full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \ | ||||
|     srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk | ||||
|     srcinfo_property, srcinfo_property_list, trim_package, unquote, utcnow, walk | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.package_source import PackageSource | ||||
| from ahriman.models.repository_paths import RepositoryPaths | ||||
| @ -443,6 +444,26 @@ def test_trim_package() -> None: | ||||
|     assert trim_package("package: a description") == "package" | ||||
|  | ||||
|  | ||||
| def test_unquote() -> None: | ||||
|     """ | ||||
|     must remove quotation marks | ||||
|     """ | ||||
|     for source in ( | ||||
|         "abc", | ||||
|         "ab'c", | ||||
|         "ab\"c", | ||||
|     ): | ||||
|         assert unquote(shlex.quote(source)) == source | ||||
|  | ||||
|  | ||||
| def test_unquote_error() -> None: | ||||
|     """ | ||||
|     must raise value error on invalid quotation | ||||
|     """ | ||||
|     with pytest.raises(ValueError): | ||||
|         unquote("ab'c") | ||||
|  | ||||
|  | ||||
| def test_utcnow() -> None: | ||||
|     """ | ||||
|     must generate correct timestamp | ||||
|  | ||||
| @ -34,9 +34,18 @@ def test_quote() -> None: | ||||
|     """ | ||||
|     must quote strings if unsafe flag is not set | ||||
|     """ | ||||
|     assert PkgbuildPatch("key", "value").quote("value") == """value""" | ||||
|     assert PkgbuildPatch("key", "va'lue").quote("va'lue") == """'va'"'"'lue'""" | ||||
|     assert PkgbuildPatch("key", "va'lue", unsafe=True).quote("va'lue") == """va'lue""" | ||||
|     assert PkgbuildPatch.quote("value") == """value""" | ||||
|     assert PkgbuildPatch.quote("va'lue") == """'va'"'"'lue'""" | ||||
|  | ||||
|  | ||||
| def test_from_env() -> None: | ||||
|     """ | ||||
|     must construct patch from environment variable | ||||
|     """ | ||||
|     assert PkgbuildPatch.from_env("KEY=VALUE") == PkgbuildPatch("KEY", "VALUE") | ||||
|     assert PkgbuildPatch.from_env("KEY=VA=LUE") == PkgbuildPatch("KEY", "VA=LUE") | ||||
|     assert PkgbuildPatch.from_env("KEY=") == PkgbuildPatch("KEY", "") | ||||
|     assert PkgbuildPatch.from_env("KEY") == PkgbuildPatch("KEY", "") | ||||
|  | ||||
|  | ||||
| def test_serialize() -> None: | ||||
| @ -46,7 +55,19 @@ def test_serialize() -> None: | ||||
|     assert PkgbuildPatch("key", "value").serialize() == "key=value" | ||||
|     assert PkgbuildPatch("key", "42").serialize() == "key=42" | ||||
|     assert PkgbuildPatch("key", "4'2").serialize() == """key='4'"'"'2'""" | ||||
|     assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2" | ||||
|  | ||||
|  | ||||
| def test_from_env_serialize() -> None: | ||||
|     """ | ||||
|     must serialize and parse back | ||||
|     """ | ||||
|     for patch in ( | ||||
|             PkgbuildPatch("key", "value"), | ||||
|             PkgbuildPatch("key", "4'2"), | ||||
|             PkgbuildPatch("arch", ["i686", "x86_64"]), | ||||
|             PkgbuildPatch("key", ["val'ue", "val\"ue2"]), | ||||
|     ): | ||||
|         assert PkgbuildPatch.from_env(patch.serialize()) == patch | ||||
|  | ||||
|  | ||||
| def test_serialize_plain_diff() -> None: | ||||
| @ -60,7 +81,7 @@ def test_serialize_function() -> None: | ||||
|     """ | ||||
|     must correctly serialize function values | ||||
|     """ | ||||
|     assert PkgbuildPatch("key()", "{ value }", unsafe=True).serialize() == "key() { value }" | ||||
|     assert PkgbuildPatch("key()", "{ value }").serialize() == "key() { value }" | ||||
|  | ||||
|  | ||||
| def test_serialize_list() -> None: | ||||
| @ -69,7 +90,13 @@ def test_serialize_list() -> None: | ||||
|     """ | ||||
|     assert PkgbuildPatch("arch", ["i686", "x86_64"]).serialize() == """arch=(i686 x86_64)""" | ||||
|     assert PkgbuildPatch("key", ["val'ue", "val\"ue2"]).serialize() == """key=('val'"'"'ue' 'val"ue2')""" | ||||
|     assert PkgbuildPatch("key", ["val'ue", "val\"ue2"], unsafe=True).serialize() == """key=(val'ue val"ue2)""" | ||||
|  | ||||
|  | ||||
| def test_view() -> None: | ||||
|     """ | ||||
|     must correctly serialize to json | ||||
|     """ | ||||
|     assert PkgbuildPatch("key", "value").view() == {"key": "key", "value": "value"} | ||||
|  | ||||
|  | ||||
| def test_write(mocker: MockerFixture) -> None: | ||||
|  | ||||
| @ -17,7 +17,7 @@ def test_id() -> None: | ||||
|     """ | ||||
|     must correctly generate id | ||||
|     """ | ||||
|     assert RepositoryId("", "").id == "-" | ||||
|     assert RepositoryId("", "").id == "" | ||||
|     assert RepositoryId("arch", "repo").id == "arch-repo" | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										1
									
								
								tests/ahriman/web/schemas/test_package_patch_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/ahriman/web/schemas/test_package_patch_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| # schema testing goes in view class tests | ||||
							
								
								
									
										1
									
								
								tests/ahriman/web/schemas/test_patch_name_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/ahriman/web/schemas/test_patch_name_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| # schema testing goes in view class tests | ||||
							
								
								
									
										1
									
								
								tests/ahriman/web/schemas/test_patch_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/ahriman/web/schemas/test_patch_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| # schema testing goes in view class tests | ||||
							
								
								
									
										1
									
								
								tests/ahriman/web/schemas/test_versioned_log_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/ahriman/web/schemas/test_versioned_log_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| # schema testing goes in view class tests | ||||
| @ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient | ||||
| from pytest_mock import MockerFixture | ||||
| from unittest.mock import AsyncMock | ||||
|  | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.repository_id import RepositoryId | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.views.v1.service.add import AddView | ||||
| @ -40,13 +41,42 @@ async def test_post(client: TestClient, repository_id: RepositoryId, mocker: Moc | ||||
|     assert not request_schema.validate(payload) | ||||
|     response = await client.post("/api/v1/service/add", json=payload) | ||||
|     assert response.ok | ||||
|     add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=True) | ||||
|     add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", patches=[], now=True) | ||||
|  | ||||
|     json = await response.json() | ||||
|     assert json["process_id"] == "abc" | ||||
|     assert not response_schema.validate(json) | ||||
|  | ||||
|  | ||||
| async def test_post_patches(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must call post request with patches correctly | ||||
|     """ | ||||
|     add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add", return_value="abc") | ||||
|     user_mock = AsyncMock() | ||||
|     user_mock.return_value = "username" | ||||
|     mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock) | ||||
|     request_schema = pytest.helpers.schema_request(AddView.post) | ||||
|  | ||||
|     payload = { | ||||
|         "packages": ["ahriman"], | ||||
|         "patches": [ | ||||
|             { | ||||
|                 "key": "k", | ||||
|                 "value": "v", | ||||
|             }, | ||||
|             { | ||||
|                 "key": "k2", | ||||
|             }, | ||||
|         ] | ||||
|     } | ||||
|     assert not request_schema.validate(payload) | ||||
|     response = await client.post("/api/v1/service/add", json=payload) | ||||
|     assert response.ok | ||||
|     add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", | ||||
|                                      patches=[PkgbuildPatch("k", "v"), PkgbuildPatch("k2", "")], now=True) | ||||
|  | ||||
|  | ||||
| async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must call raise 400 on empty request | ||||
|  | ||||
| @ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient | ||||
| from pytest_mock import MockerFixture | ||||
| from unittest.mock import AsyncMock | ||||
|  | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.repository_id import RepositoryId | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.views.v1.service.request import RequestView | ||||
| @ -40,13 +41,42 @@ async def test_post(client: TestClient, repository_id: RepositoryId, mocker: Moc | ||||
|     assert not request_schema.validate(payload) | ||||
|     response = await client.post("/api/v1/service/request", json=payload) | ||||
|     assert response.ok | ||||
|     add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=False) | ||||
|     add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", patches=[], now=False) | ||||
|  | ||||
|     json = await response.json() | ||||
|     assert json["process_id"] == "abc" | ||||
|     assert not response_schema.validate(json) | ||||
|  | ||||
|  | ||||
| async def test_post_patches(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must call post request with patches correctly | ||||
|     """ | ||||
|     add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add", return_value="abc") | ||||
|     user_mock = AsyncMock() | ||||
|     user_mock.return_value = "username" | ||||
|     mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock) | ||||
|     request_schema = pytest.helpers.schema_request(RequestView.post) | ||||
|  | ||||
|     payload = { | ||||
|         "packages": ["ahriman"], | ||||
|         "patches": [ | ||||
|             { | ||||
|                 "key": "k", | ||||
|                 "value": "v", | ||||
|             }, | ||||
|             { | ||||
|                 "key": "k2", | ||||
|             }, | ||||
|         ] | ||||
|     } | ||||
|     assert not request_schema.validate(payload) | ||||
|     response = await client.post("/api/v1/service/request", json=payload) | ||||
|     assert response.ok | ||||
|     add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", | ||||
|                                      patches=[PkgbuildPatch("k", "v"), PkgbuildPatch("k2", "")], now=False) | ||||
|  | ||||
|  | ||||
| async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must raise exception on missing packages payload | ||||
|  | ||||
| @ -0,0 +1,81 @@ | ||||
| import pytest | ||||
|  | ||||
| from aiohttp.test_utils import TestClient | ||||
|  | ||||
| from ahriman.models.build_status import BuildStatusEnum | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.views.v1.status.patch import PatchView | ||||
|  | ||||
|  | ||||
| async def test_get_permission() -> None: | ||||
|     """ | ||||
|     must return correct permission for the request | ||||
|     """ | ||||
|     for method in ("GET",): | ||||
|         request = pytest.helpers.request("", "", method) | ||||
|         assert await PatchView.get_permission(request) == UserAccess.Reporter | ||||
|     for method in ("DELETE",): | ||||
|         request = pytest.helpers.request("", "", method) | ||||
|         assert await PatchView.get_permission(request) == UserAccess.Full | ||||
|  | ||||
|  | ||||
| def test_routes() -> None: | ||||
|     """ | ||||
|     must return correct routes | ||||
|     """ | ||||
|     assert PatchView.ROUTES == ["/api/v1/packages/{package}/patches/{patch}"] | ||||
|  | ||||
|  | ||||
| async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None: | ||||
|     """ | ||||
|     must delete patch for package | ||||
|     """ | ||||
|     await client.post(f"/api/v1/packages/{package_ahriman.base}", | ||||
|                       json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) | ||||
|     await client.post(f"/api/v1/packages/{package_python_schedule.base}", | ||||
|                       json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()}) | ||||
|  | ||||
|     patch_key = "k" | ||||
|     await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json={"key": patch_key, "value": "v"}) | ||||
|     await client.post(f"/api/v1/packages/{package_python_schedule.base}/patches", | ||||
|                       json={"key": patch_key, "value": "v2"}) | ||||
|  | ||||
|     response = await client.delete(f"/api/v1/packages/{package_ahriman.base}/patches/{patch_key}") | ||||
|     assert response.status == 204 | ||||
|  | ||||
|     response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches") | ||||
|     assert not await response.json() | ||||
|  | ||||
|     response = await client.get(f"/api/v1/packages/{package_python_schedule.base}/patches") | ||||
|     assert await response.json() | ||||
|  | ||||
|  | ||||
| async def test_get(client: TestClient, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must get patch for package | ||||
|     """ | ||||
|     patch = PkgbuildPatch("k", "v") | ||||
|     await client.post(f"/api/v1/packages/{package_ahriman.base}", | ||||
|                       json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) | ||||
|     await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json=patch.view()) | ||||
|     response_schema = pytest.helpers.schema_response(PatchView.get) | ||||
|  | ||||
|     response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches/{patch.key}") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     patches = await response.json() | ||||
|     assert not response_schema.validate(patches) | ||||
|     assert patches == patch.view() | ||||
|  | ||||
|  | ||||
| async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must return not found for missing package | ||||
|     """ | ||||
|     response_schema = pytest.helpers.schema_response(PatchView.get, code=404) | ||||
|  | ||||
|     response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches/random") | ||||
|     assert response.status == 404 | ||||
|     assert not response_schema.validate(await response.json()) | ||||
| @ -0,0 +1,75 @@ | ||||
| import pytest | ||||
|  | ||||
| from aiohttp.test_utils import TestClient | ||||
|  | ||||
| from ahriman.models.build_status import BuildStatusEnum | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.pkgbuild_patch import PkgbuildPatch | ||||
| from ahriman.models.user_access import UserAccess | ||||
| from ahriman.web.views.v1.status.patches import PatchesView | ||||
|  | ||||
|  | ||||
| async def test_get_permission() -> None: | ||||
|     """ | ||||
|     must return correct permission for the request | ||||
|     """ | ||||
|     for method in ("GET",): | ||||
|         request = pytest.helpers.request("", "", method) | ||||
|         assert await PatchesView.get_permission(request) == UserAccess.Reporter | ||||
|     for method in ("POST",): | ||||
|         request = pytest.helpers.request("", "", method) | ||||
|         assert await PatchesView.get_permission(request) == UserAccess.Full | ||||
|  | ||||
|  | ||||
| def test_routes() -> None: | ||||
|     """ | ||||
|     must return correct routes | ||||
|     """ | ||||
|     assert PatchesView.ROUTES == ["/api/v1/packages/{package}/patches"] | ||||
|  | ||||
|  | ||||
| async def test_get(client: TestClient, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must get patch for package | ||||
|     """ | ||||
|     patch = PkgbuildPatch("k", "v") | ||||
|     await client.post(f"/api/v1/packages/{package_ahriman.base}", | ||||
|                       json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) | ||||
|     await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json=patch.view()) | ||||
|     response_schema = pytest.helpers.schema_response(PatchesView.get) | ||||
|  | ||||
|     response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches") | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     patches = await response.json() | ||||
|     assert not response_schema.validate(patches) | ||||
|     assert patches == [patch.view()] | ||||
|  | ||||
|  | ||||
| async def test_post(client: TestClient, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must create patch | ||||
|     """ | ||||
|     await client.post(f"/api/v1/packages/{package_ahriman.base}", | ||||
|                       json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) | ||||
|     request_schema = pytest.helpers.schema_request(PatchesView.post) | ||||
|  | ||||
|     payload = {"key": "k", "value": "v"} | ||||
|     assert not request_schema.validate(payload) | ||||
|     response = await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json=payload) | ||||
|     assert response.status == 204 | ||||
|  | ||||
|     response = await client.get(f"/api/v1/packages/{package_ahriman.base}/patches") | ||||
|     patches = await response.json() | ||||
|     assert patches == [payload] | ||||
|  | ||||
|  | ||||
| async def test_post_exception(client: TestClient, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must raise exception on invalid payload | ||||
|     """ | ||||
|     response_schema = pytest.helpers.schema_response(PatchesView.post, code=400) | ||||
|  | ||||
|     response = await client.post(f"/api/v1/packages/{package_ahriman.base}/patches", json={}) | ||||
|     assert response.status == 400 | ||||
|     assert not response_schema.validate(await response.json()) | ||||
| @ -44,7 +44,16 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None: | ||||
|  | ||||
|     logs = await response.json() | ||||
|     assert not response_schema.validate(logs) | ||||
|     assert logs["logs"] == [[42.0, "message 1"], [43.0, "message 2"]] | ||||
|     assert logs == [ | ||||
|         { | ||||
|             "created": 42.0, | ||||
|             "message": "message 1", | ||||
|         }, | ||||
|         { | ||||
|             "created": 43.0, | ||||
|             "message": "message 2", | ||||
|         }, | ||||
|     ] | ||||
|  | ||||
|  | ||||
| async def test_get_with_pagination(client: TestClient, package_ahriman: Package) -> None: | ||||
| @ -67,18 +76,7 @@ async def test_get_with_pagination(client: TestClient, package_ahriman: Package) | ||||
|  | ||||
|     logs = await response.json() | ||||
|     assert not response_schema.validate(logs) | ||||
|     assert logs["logs"] == [[43.0, "message 2"]] | ||||
|  | ||||
|  | ||||
| async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must return not found for missing package | ||||
|     """ | ||||
|     response_schema = pytest.helpers.schema_response(LogsView.get, code=404) | ||||
|  | ||||
|     response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs") | ||||
|     assert response.status == 404 | ||||
|     assert not response_schema.validate(await response.json()) | ||||
|     assert logs == [{"created": 43.0, "message": "message 2"}] | ||||
|  | ||||
|  | ||||
| async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None: | ||||
|  | ||||
		Reference in New Issue
	
	Block a user