Merge branch 'master' into live

This commit is contained in:
Kevin Morris 2022-03-30 12:31:53 -07:00
commit cef3316d05
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
5 changed files with 132 additions and 49 deletions

View file

@ -9,7 +9,7 @@ You can add a git hook to do this by installing `python-pre-commit` and running
`pre-commit install`. `pre-commit install`.
[1]: https://lists.archlinux.org/listinfo/aur-dev [1]: https://lists.archlinux.org/listinfo/aur-dev
[2]: https://gitlab.archlinunx.org/archlinux/aurweb [2]: https://gitlab.archlinux.org/archlinux/aurweb
### Coding Guidelines ### Coding Guidelines
@ -23,6 +23,76 @@ development.
3. Use four space indentation 3. Use four space indentation
4. Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 4. Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
5. DRY: Don't Repeat Yourself 5. DRY: Don't Repeat Yourself
6. All code should be tested for good _and_ bad cases 6. All code should be tested for good _and_ bad cases (see [test/README.md][3])
[3]: https://gitlab.archlinux.org/archlinux/aurweb/-/blob/master/test/README.md
Test patches that increase coverage in the codebase are always welcome. Test patches that increase coverage in the codebase are always welcome.
### Coding Style
We use the `flake8` and `isort` tools to manage PEP-8 coherence and
import ordering in this project.
There are plugins for editors or IDEs which automate this process. Some
example plugins:
- [tell-k/vim-autopep8](https://github.com/tell-k/vim-autopep8)
- [fisadev/vim-isort](https://github.com/fisadev/vim-isort)
- [prabirshrestha/vim-lsp](https://github.com/prabirshrestha/vim-lsp)
See `setup.cfg` for flake8 and isort specific rules.
Note: We are planning on switching to [psf/black](https://github.com/psf/black).
For now, developers should ensure that flake8 and isort passes when submitting
merge requests or patch sets.
### Development Environment
To get started with local development, an instance of aurweb must be
brought up. This can be done using the following sections:
- [Using Docker](#using-docker)
- [Using INSTALL](#using-install)
There are a number of services aurweb employs to run the application
in its entirety:
- ssh
- cron jobs
- starlette/fastapi asgi server
Project structure:
- `./aurweb`: `aurweb` Python package
- `./templates`: Jinja2 templates
- `./docker`: Docker scripts and configuration files
#### Using Docker
Using Docker, we can run the entire infrastructure in two steps:
# Build the aurweb:latest image
$ docker-compose build
# Start all services in the background
$ docker-compose up -d nginx
`docker-compose` services will generate a locally signed root certificate
at `./data/root_ca.crt`. Users can import this into ca-certificates or their
browser if desired.
Accessible services (on the host):
- https://localhost:8444 (python via nginx)
- https://localhost:8443 (php via nginx)
- localhost:13306 (mariadb)
- localhost:16379 (redis)
Docker services, by default, are setup to be hot reloaded when source code
is changed.
#### Using INSTALL
The [INSTALL](INSTALL) file describes steps to install the application on
bare-metal systems.

View file

@ -6,7 +6,7 @@ from typing import Any
# Publicly visible version of aurweb. This is used to display # Publicly visible version of aurweb. This is used to display
# aurweb versioning in the footer and must be maintained. # aurweb versioning in the footer and must be maintained.
# Todo: Make this dynamic/automated. # Todo: Make this dynamic/automated.
AURWEB_VERSION = "v6.0.25" AURWEB_VERSION = "v6.0.26"
_parser = None _parser = None

View file

@ -12,7 +12,7 @@ from aurweb.models.package_vote import PackageVote
class PackageSearch: class PackageSearch:
""" A Package search query builder. """ """A Package search query builder."""
# A constant mapping of short to full name sort orderings. # A constant mapping of short to full name sort orderings.
FULL_SORT_ORDER = {"d": "desc", "a": "asc"} FULL_SORT_ORDER = {"d": "desc", "a": "asc"}
@ -24,14 +24,18 @@ class PackageSearch:
if self.user: if self.user:
self.query = self.query.join( self.query = self.query.join(
PackageVote, PackageVote,
and_(PackageVote.PackageBaseID == PackageBase.ID, and_(
PackageVote.UsersID == self.user.ID), PackageVote.PackageBaseID == PackageBase.ID,
isouter=True PackageVote.UsersID == self.user.ID,
),
isouter=True,
).join( ).join(
PackageNotification, PackageNotification,
and_(PackageNotification.PackageBaseID == PackageBase.ID, and_(
PackageNotification.UserID == self.user.ID), PackageNotification.PackageBaseID == PackageBase.ID,
isouter=True PackageNotification.UserID == self.user.ID,
),
isouter=True,
) )
self.ordering = "d" self.ordering = "d"
@ -47,7 +51,7 @@ class PackageSearch:
"m": self._search_by_maintainer, "m": self._search_by_maintainer,
"c": self._search_by_comaintainer, "c": self._search_by_comaintainer,
"M": self._search_by_co_or_maintainer, "M": self._search_by_co_or_maintainer,
"s": self._search_by_submitter "s": self._search_by_submitter,
} }
# Setup SB (Sort By) callbacks. # Setup SB (Sort By) callbacks.
@ -58,7 +62,7 @@ class PackageSearch:
"w": self._sort_by_voted, "w": self._sort_by_voted,
"o": self._sort_by_notify, "o": self._sort_by_notify,
"m": self._sort_by_maintainer, "m": self._sort_by_maintainer,
"l": self._sort_by_last_modified "l": self._sort_by_last_modified,
} }
self._joined_user = False self._joined_user = False
@ -66,12 +70,10 @@ class PackageSearch:
self._joined_comaint = False self._joined_comaint = False
def _join_user(self, outer: bool = True) -> orm.Query: def _join_user(self, outer: bool = True) -> orm.Query:
""" Centralized joining of a package base's maintainer. """ """Centralized joining of a package base's maintainer."""
if not self._joined_user: if not self._joined_user:
self.query = self.query.join( self.query = self.query.join(
User, User, User.ID == PackageBase.MaintainerUID, isouter=outer
User.ID == PackageBase.MaintainerUID,
isouter=outer
) )
self._joined_user = True self._joined_user = True
return self.query return self.query
@ -87,7 +89,7 @@ class PackageSearch:
self.query = self.query.join( self.query = self.query.join(
PackageComaintainer, PackageComaintainer,
PackageComaintainer.PackageBaseID == PackageBase.ID, PackageComaintainer.PackageBaseID == PackageBase.ID,
isouter=isouter isouter=isouter,
) )
self._joined_comaint = True self._joined_comaint = True
return self.query return self.query
@ -95,8 +97,10 @@ class PackageSearch:
def _search_by_namedesc(self, keywords: str) -> orm.Query: def _search_by_namedesc(self, keywords: str) -> orm.Query:
self._join_user() self._join_user()
self.query = self.query.filter( self.query = self.query.filter(
or_(Package.Name.like(f"%{keywords}%"), or_(
Package.Description.like(f"%{keywords}%")) Package.Name.like(f"%{keywords}%"),
Package.Description.like(f"%{keywords}%"),
)
) )
return self return self
@ -132,8 +136,7 @@ class PackageSearch:
self._join_user() self._join_user()
if keywords: if keywords:
self.query = self.query.filter( self.query = self.query.filter(
and_(User.Username == keywords, and_(User.Username == keywords, User.ID == PackageBase.MaintainerUID)
User.ID == PackageBase.MaintainerUID)
) )
else: else:
self.query = self.query.filter(PackageBase.MaintainerUID.is_(None)) self.query = self.query.filter(PackageBase.MaintainerUID.is_(None))
@ -197,8 +200,7 @@ class PackageSearch:
# in terms of performance. We should improve this; there's no # in terms of performance. We should improve this; there's no
# reason it should take _longer_. # reason it should take _longer_.
column = getattr( column = getattr(
case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), order
order
) )
name = getattr(models.Package.Name, order) name = getattr(models.Package.Name, order)
self.query = self.query.order_by(column(), name()) self.query = self.query.order_by(column(), name())
@ -209,9 +211,8 @@ class PackageSearch:
# in terms of performance. We should improve this; there's no # in terms of performance. We should improve this; there's no
# reason it should take _longer_. # reason it should take _longer_.
column = getattr( column = getattr(
case([(models.PackageNotification.UserID == self.user.ID, 1)], case([(models.PackageNotification.UserID == self.user.ID, 1)], else_=0),
else_=0), order,
order
) )
name = getattr(models.Package.Name, order) name = getattr(models.Package.Name, order)
self.query = self.query.order_by(column(), name()) self.query = self.query.order_by(column(), name())
@ -239,16 +240,16 @@ class PackageSearch:
return callback(ordering) return callback(ordering)
def count(self) -> int: def count(self) -> int:
""" Return internal query's count. """ """Return internal query's count."""
return self.query.count() return self.query.count()
def results(self) -> orm.Query: def results(self) -> orm.Query:
""" Return internal query. """ """Return internal query."""
return self.query return self.query
class RPCSearch(PackageSearch): class RPCSearch(PackageSearch):
""" A PackageSearch-derived RPC package search query builder. """A PackageSearch-derived RPC package search query builder.
With RPC search, we need a subset of PackageSearch's handlers, With RPC search, we need a subset of PackageSearch's handlers,
with a few additional handlers added. So, within the RPCSearch with a few additional handlers added. So, within the RPCSearch
@ -270,52 +271,60 @@ class RPCSearch(PackageSearch):
# We keep: "nd", "n" and "m". We also overlay four new by params # We keep: "nd", "n" and "m". We also overlay four new by params
# on top: "depends", "makedepends", "optdepends" and "checkdepends". # on top: "depends", "makedepends", "optdepends" and "checkdepends".
self.search_by_cb = { self.search_by_cb = {
k: v for k, v in self.search_by_cb.items() k: v
for k, v in self.search_by_cb.items()
if k not in RPCSearch.keys_removed if k not in RPCSearch.keys_removed
} }
self.search_by_cb.update({ self.search_by_cb.update(
{
"depends": self._search_by_depends, "depends": self._search_by_depends,
"makedepends": self._search_by_makedepends, "makedepends": self._search_by_makedepends,
"optdepends": self._search_by_optdepends, "optdepends": self._search_by_optdepends,
"checkdepends": self._search_by_checkdepends "checkdepends": self._search_by_checkdepends,
}) }
)
# We always want an optional Maintainer in the RPC. # We always want an optional Maintainer in the RPC.
self._join_user() self._join_user()
def _join_depends(self, dep_type_id: int) -> orm.Query: def _join_depends(self, dep_type_id: int) -> orm.Query:
""" Join Package with PackageDependency and filter results """Join Package with PackageDependency and filter results
based on `dep_type_id`. based on `dep_type_id`.
:param dep_type_id: DependencyType ID :param dep_type_id: DependencyType ID
:returns: PackageDependency-joined orm.Query :returns: PackageDependency-joined orm.Query
""" """
self.query = self.query.join(models.PackageDependency).filter( self.query = self.query.join(models.PackageDependency).filter(
models.PackageDependency.DepTypeID == dep_type_id) models.PackageDependency.DepTypeID == dep_type_id
)
return self.query return self.query
def _search_by_depends(self, keywords: str) -> "RPCSearch": def _search_by_depends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(DEPENDS_ID).filter( self.query = self._join_depends(DEPENDS_ID).filter(
models.PackageDependency.DepName == keywords) models.PackageDependency.DepName == keywords
)
return self return self
def _search_by_makedepends(self, keywords: str) -> "RPCSearch": def _search_by_makedepends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(MAKEDEPENDS_ID).filter( self.query = self._join_depends(MAKEDEPENDS_ID).filter(
models.PackageDependency.DepName == keywords) models.PackageDependency.DepName == keywords
)
return self return self
def _search_by_optdepends(self, keywords: str) -> "RPCSearch": def _search_by_optdepends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(OPTDEPENDS_ID).filter( self.query = self._join_depends(OPTDEPENDS_ID).filter(
models.PackageDependency.DepName == keywords) models.PackageDependency.DepName == keywords
)
return self return self
def _search_by_checkdepends(self, keywords: str) -> "RPCSearch": def _search_by_checkdepends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(CHECKDEPENDS_ID).filter( self.query = self._join_depends(CHECKDEPENDS_ID).filter(
models.PackageDependency.DepName == keywords) models.PackageDependency.DepName == keywords
)
return self return self
def search_by(self, by: str, keywords: str) -> "RPCSearch": def search_by(self, by: str, keywords: str) -> "RPCSearch":
""" Override inherited search_by. In this override, we reduce the """Override inherited search_by. In this override, we reduce the
scope of what we handle within this function. We do not set `by` scope of what we handle within this function. We do not set `by`
to a default of "nd" in the RPC, as the RPC returns an error when to a default of "nd" in the RPC, as the RPC returns an error when
incorrect `by` fields are specified. incorrect `by` fields are specified.
@ -329,6 +338,4 @@ class RPCSearch(PackageSearch):
return result return result
def results(self) -> orm.Query: def results(self) -> orm.Query:
return self.query.filter( return self.query.filter(models.PackageBase.PackagerUID.isnot(None))
models.PackageBase.PackagerUID.isnot(None)
)

View file

@ -60,6 +60,9 @@ def http_requests_total() -> Callable[[Info], None]:
labelnames=("method", "path", "status")) labelnames=("method", "path", "status"))
def instrumentation(info: Info) -> None: def instrumentation(info: Info) -> None:
if info.request.method.lower() in ("head", "options"): # pragma: no cover
return
scope = info.request.scope scope = info.request.scope
# Taken from https://github.com/stephenhillier/starlette_exporter # Taken from https://github.com/stephenhillier/starlette_exporter
@ -70,8 +73,8 @@ def http_requests_total() -> Callable[[Info], None]:
if not (scope.get("endpoint", None) and scope.get("router", None)): if not (scope.get("endpoint", None) and scope.get("router", None)):
return None return None
root_path = scope.get("root_path", "") root_path = scope.get("root_path", str())
app = scope.get("app", {}) app = scope.get("app", dict())
if hasattr(app, "root_path"): if hasattr(app, "root_path"):
app_root_path = getattr(app, "root_path") app_root_path = getattr(app, "root_path")
@ -102,6 +105,9 @@ def http_api_requests_total() -> Callable[[Info], None]:
labelnames=("type", "status")) labelnames=("type", "status"))
def instrumentation(info: Info) -> None: def instrumentation(info: Info) -> None:
if info.request.method.lower() in ("head", "options"): # pragma: no cover
return
if info.request.url.path.rstrip("/") == "/rpc": if info.request.url.path.rstrip("/") == "/rpc":
type = info.request.query_params.get("type", "None") type = info.request.query_params.get("type", "None")
if info.response: if info.response:

View file

@ -8,7 +8,7 @@
# #
[tool.poetry] [tool.poetry]
name = "aurweb" name = "aurweb"
version = "v6.0.25" version = "v6.0.26"
license = "GPL-2.0-only" license = "GPL-2.0-only"
description = "Source code for the Arch User Repository's website" description = "Source code for the Arch User Repository's website"
homepage = "https://aur.archlinux.org" homepage = "https://aur.archlinux.org"