launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28720
[Merge] ~cjwatson/launchpad:black-charms into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:black-charms into launchpad:master.
Commit message:
lp.charms: Apply black
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/426189
--
The attached diff has been truncated due to its size.
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:black-charms into launchpad:master.
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 1ab33b5..1a2bd19 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -68,3 +68,5 @@ c606443bdb2f342593c9a7c9437cb70c01f85f29
2edd6d52841e03ba11ad431e40828f2f0b6a7e17
# apply black to lp.buildmaster
95cf83968d59453397dfbdcbe30556fc8004479a
+# apply black to lp.charms
+a6bed71f3d2fdbceae20c2d435c993e8bededdce
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index db91bfe..accf87d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -48,6 +48,7 @@ repos:
|blueprints
|bugs
|buildmaster
+ |charms
)/
- repo: https://github.com/PyCQA/isort
rev: 5.9.2
@@ -72,6 +73,7 @@ repos:
|blueprints
|bugs
|buildmaster
+ |charms
)/
- id: isort
alias: isort-black
@@ -86,6 +88,7 @@ repos:
|blueprints
|bugs
|buildmaster
+ |charms
)/
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
diff --git a/lib/lp/charms/adapters/buildarch.py b/lib/lp/charms/adapters/buildarch.py
index bcb7cad..615a9f0 100644
--- a/lib/lp/charms/adapters/buildarch.py
+++ b/lib/lp/charms/adapters/buildarch.py
@@ -3,13 +3,10 @@
__all__ = [
"determine_instances_to_build",
- ]
+]
-from collections import (
- Counter,
- OrderedDict,
- )
import json
+from collections import Counter, OrderedDict
from lp.services.helpers import english_list
@@ -23,7 +20,8 @@ class MissingPropertyError(CharmBasesParserError):
def __init__(self, prop):
super().__init__(
- "Base specification is missing the {!r} property".format(prop))
+ "Base specification is missing the {!r} property".format(prop)
+ )
self.property = prop
@@ -38,7 +36,9 @@ class DuplicateRunOnError(CharmBasesParserError):
super().__init__(
"{} {} present in the 'run-on' of multiple items".format(
english_list([str(d) for d in duplicates]),
- "is" if len(duplicates) == 1 else "are"))
+ "is" if len(duplicates) == 1 else "are",
+ )
+ )
class CharmBase:
@@ -49,7 +49,9 @@ class CharmBase:
if not isinstance(channel, str):
raise BadPropertyError(
"Channel {!r} is not a string (missing quotes?)".format(
- channel))
+ channel
+ )
+ )
self.channel = channel
self.architectures = architectures
@@ -65,26 +67,31 @@ class CharmBase:
except KeyError:
raise MissingPropertyError("channel")
return cls(
- name=name, channel=channel,
- architectures=properties.get("architectures"))
+ name=name,
+ channel=channel,
+ architectures=properties.get("architectures"),
+ )
def __eq__(self, other):
return (
- self.name == other.name and
- self.channel == other.channel and
- self.architectures == other.architectures)
+ self.name == other.name
+ and self.channel == other.channel
+ and self.architectures == other.architectures
+ )
def __ne__(self, other):
return not self == other
def __hash__(self):
architectures = (
- None if self.architectures is None else tuple(self.architectures))
+ None if self.architectures is None else tuple(self.architectures)
+ )
return hash((self.name, self.channel, architectures))
def __str__(self):
return "{} {} {}".format(
- self.name, self.channel, json.dumps(self.architectures))
+ self.name, self.channel, json.dumps(self.architectures)
+ )
class CharmBaseConfiguration:
@@ -101,8 +108,9 @@ class CharmBaseConfiguration:
# common typos in case the user intends to use long-form but did so
# incorrectly (for better error message handling).
if not any(
- item in properties
- for item in ("run-on", "run_on", "build-on", "build_on")):
+ item in properties
+ for item in ("run-on", "run_on", "build-on", "build_on")
+ ):
base = CharmBase.from_dict(properties)
return cls([base], run_on=[base])
@@ -117,8 +125,9 @@ class CharmBaseConfiguration:
return cls(build_on, run_on=run_on)
-def determine_instances_to_build(charmcraft_data, supported_arches,
- default_distro_series):
+def determine_instances_to_build(
+ charmcraft_data, supported_arches, default_distro_series
+):
"""Return a list of instances to build based on charmcraft.yaml.
:param charmcraft_data: A parsed charmcraft.yaml.
@@ -133,18 +142,24 @@ def determine_instances_to_build(charmcraft_data, supported_arches,
if bases_list:
configs = [
- CharmBaseConfiguration.from_dict(item) for item in bases_list]
+ CharmBaseConfiguration.from_dict(item) for item in bases_list
+ ]
else:
# If no bases are specified, build one for each supported
# architecture for the default series.
configs = [
- CharmBaseConfiguration([
- CharmBase(
- default_distro_series.distribution.name,
- default_distro_series.version, das.architecturetag),
- ])
+ CharmBaseConfiguration(
+ [
+ CharmBase(
+ default_distro_series.distribution.name,
+ default_distro_series.version,
+ das.architecturetag,
+ ),
+ ]
+ )
for das in supported_arches
- if das.distroseries == default_distro_series]
+ if das.distroseries == default_distro_series
+ ]
# Ensure that multiple `run-on` items don't overlap; this is ambiguous
# and forbidden by charmcraft.
@@ -166,7 +181,9 @@ def determine_instances_to_build(charmcraft_data, supported_arches,
if das.distroseries.distribution.name != build_on.name:
continue
if build_on.channel not in (
- das.distroseries.name, das.distroseries.version):
+ das.distroseries.name,
+ das.distroseries.version,
+ ):
continue
if build_on.architectures is None:
# Build on all supported architectures for the requested
diff --git a/lib/lp/charms/adapters/tests/test_buildarch.py b/lib/lp/charms/adapters/tests/test_buildarch.py
index 6281def..c7659ba 100644
--- a/lib/lp/charms/adapters/tests/test_buildarch.py
+++ b/lib/lp/charms/adapters/tests/test_buildarch.py
@@ -3,98 +3,113 @@
from functools import partial
-from testscenarios import (
- load_tests_apply_scenarios,
- WithScenarios,
- )
+from testscenarios import WithScenarios, load_tests_apply_scenarios
from testtools.matchers import (
Equals,
MatchesException,
MatchesListwise,
MatchesStructure,
Raises,
- )
+)
from zope.component import getUtility
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.buildmaster.interfaces.processor import (
IProcessorSet,
ProcessorNotFound,
- )
+)
from lp.charms.adapters.buildarch import (
CharmBase,
CharmBaseConfiguration,
- determine_instances_to_build,
DuplicateRunOnError,
- )
-from lp.testing import (
- TestCase,
- TestCaseWithFactory,
- )
+ determine_instances_to_build,
+)
+from lp.testing import TestCase, TestCaseWithFactory
from lp.testing.layers import LaunchpadZopelessLayer
class TestCharmBaseConfiguration(WithScenarios, TestCase):
scenarios = [
- ("expanded", {
- "base": {
- "build-on": [{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- }],
- "run-on": [
- {
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64", "arm64"],
+ (
+ "expanded",
+ {
+ "base": {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64", "arm64"],
},
- {
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
},
],
},
- "expected_build_on": [
- CharmBase(
- name="ubuntu", channel="18.04", architectures=["amd64"]),
+ "expected_build_on": [
+ CharmBase(
+ name="ubuntu", channel="18.04", architectures=["amd64"]
+ ),
],
- "expected_run_on": [
- CharmBase(
- name="ubuntu", channel="20.04",
- architectures=["amd64", "arm64"]),
- CharmBase(
- name="ubuntu", channel="18.04", architectures=["amd64"]),
+ "expected_run_on": [
+ CharmBase(
+ name="ubuntu",
+ channel="20.04",
+ architectures=["amd64", "arm64"],
+ ),
+ CharmBase(
+ name="ubuntu", channel="18.04", architectures=["amd64"]
+ ),
],
- }),
- ("short form", {
- "base": {
- "name": "ubuntu",
- "channel": "20.04",
- },
- "expected_build_on": [CharmBase(name="ubuntu", channel="20.04")],
- "expected_run_on": [CharmBase(name="ubuntu", channel="20.04")],
- }),
- ("no run-on", {
- "base": {
- "build-on": [{
+ },
+ ),
+ (
+ "short form",
+ {
+ "base": {
"name": "ubuntu",
"channel": "20.04",
- "architectures": ["amd64"],
- }],
},
- "expected_build_on": [
- CharmBase(
- name="ubuntu", channel="20.04", architectures=["amd64"]),
+ "expected_build_on": [
+ CharmBase(name="ubuntu", channel="20.04")
],
- "expected_run_on": [
- CharmBase(
- name="ubuntu", channel="20.04", architectures=["amd64"]),
+ "expected_run_on": [CharmBase(name="ubuntu", channel="20.04")],
+ },
+ ),
+ (
+ "no run-on",
+ {
+ "base": {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ },
+ "expected_build_on": [
+ CharmBase(
+ name="ubuntu", channel="20.04", architectures=["amd64"]
+ ),
],
- }),
- ]
+ "expected_run_on": [
+ CharmBase(
+ name="ubuntu", channel="20.04", architectures=["amd64"]
+ ),
+ ],
+ },
+ ),
+ ]
def test_base(self):
config = CharmBaseConfiguration.from_dict(self.base)
@@ -109,230 +124,337 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
# Scenarios taken from the charmcraft build providers specification:
# https://discourse.charmhub.io/t/charmcraft-bases-provider-support/4713
scenarios = [
- ("single entry, single arch", {
- "bases": [{
- "build-on": [{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- }],
- }],
- "expected": [("18.04", "amd64")],
- }),
- ("multiple entries, single arch", {
- "bases": [
- {
- "build-on": [{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- }],
+ (
+ "single entry, single arch",
+ {
+ "bases": [
+ {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ }
+ ],
+ "expected": [("18.04", "amd64")],
+ },
+ ),
+ (
+ "multiple entries, single arch",
+ {
+ "bases": [
+ {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ }
+ ],
},
- {
- "build-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
+ {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
},
- {
- "build-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["riscv64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["riscv64"],
- }],
+ {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["riscv64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["riscv64"],
+ }
+ ],
},
],
- "expected": [
- ("18.04", "amd64"), ("20.04", "amd64"), ("20.04", "riscv64")],
- }),
- ("single entry, multiple arches", {
- "bases": [{
- "build-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64", "riscv64"],
- }],
- }],
- "expected": [("20.04", "amd64")],
- }),
- ("multiple entries, with cross-arch", {
- "bases": [
- {
- "build-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["riscv64"],
- }],
+ "expected": [
+ ("18.04", "amd64"),
+ ("20.04", "amd64"),
+ ("20.04", "riscv64"),
+ ],
+ },
+ ),
+ (
+ "single entry, multiple arches",
+ {
+ "bases": [
+ {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64", "riscv64"],
+ }
+ ],
+ }
+ ],
+ "expected": [("20.04", "amd64")],
+ },
+ ),
+ (
+ "multiple entries, with cross-arch",
+ {
+ "bases": [
+ {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["riscv64"],
+ }
+ ],
},
- {
- "build-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
+ {
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
},
],
- "expected": [("20.04", "amd64")],
- }),
- ("multiple run-on entries", {
- "bases": [{
- "build-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- "run-on": [
+ "expected": [("20.04", "amd64")],
+ },
+ ),
+ (
+ "multiple run-on entries",
+ {
+ "bases": [
{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- },
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ },
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64", "riscv64"],
+ },
+ ],
+ }
+ ],
+ "expected": [("20.04", "amd64")],
+ },
+ ),
+ (
+ "multiple build-on entries",
+ {
+ "bases": [
{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64", "riscv64"],
- },
- ],
- }],
- "expected": [("20.04", "amd64")],
- }),
- ("multiple build-on entries", {
- "bases": [{
- "build-on": [
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ },
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ },
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ }
+ ],
+ "expected": [("18.04", "amd64")],
+ },
+ ),
+ (
+ "redundant outputs",
+ {
+ "bases": [
{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- },
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "18.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ },
{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- },
- ],
- "run-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- }],
- "expected": [("18.04", "amd64")],
- }),
- ("redundant outputs", {
- "bases": [
- {
- "build-on": [{
- "name": "ubuntu",
- "channel": "18.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
+ "build-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
+ "run-on": [
+ {
+ "name": "ubuntu",
+ "channel": "20.04",
+ "architectures": ["amd64"],
+ }
+ ],
},
- {
- "build-on": [{
- "name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- "run-on": [{
+ ],
+ "expected_exception": MatchesException(
+ DuplicateRunOnError,
+ r"ubuntu 20\.04 \[\"amd64\"\] is present in the 'run-on' "
+ r"of multiple items",
+ ),
+ },
+ ),
+ (
+ "no bases specified",
+ {
+ "bases": None,
+ "expected": [
+ ("20.04", "amd64"),
+ ("20.04", "arm64"),
+ ("20.04", "riscv64"),
+ ],
+ },
+ ),
+ (
+ "abbreviated, no architectures specified",
+ {
+ "bases": [
+ {
"name": "ubuntu",
- "channel": "20.04",
- "architectures": ["amd64"],
- }],
- },
+ "channel": "18.04",
+ }
],
- "expected_exception": MatchesException(
- DuplicateRunOnError,
- r"ubuntu 20\.04 \[\"amd64\"\] is present in the 'run-on' of "
- r"multiple items"),
- }),
- ("no bases specified", {
- "bases": None,
- "expected": [
- ("20.04", "amd64"), ("20.04", "arm64"), ("20.04", "riscv64")],
- }),
- ("abbreviated, no architectures specified", {
- "bases": [{
- "name": "ubuntu",
- "channel": "18.04",
- }],
- "expected": [
- ("18.04", "amd64"), ("18.04", "arm64"), ("18.04", "riscv64")],
- }),
- ]
+ "expected": [
+ ("18.04", "amd64"),
+ ("18.04", "arm64"),
+ ("18.04", "riscv64"),
+ ],
+ },
+ ),
+ ]
def test_parser(self):
distro_serieses = [
self.factory.makeDistroSeries(
distribution=getUtility(ILaunchpadCelebrities).ubuntu,
- version=version)
- for version in ("20.04", "18.04")]
+ version=version,
+ )
+ for version in ("20.04", "18.04")
+ ]
dases = []
for arch_tag in ("amd64", "arm64", "riscv64"):
try:
processor = getUtility(IProcessorSet).getByName(arch_tag)
except ProcessorNotFound:
processor = self.factory.makeProcessor(
- name=arch_tag, supports_virtualized=True)
+ name=arch_tag, supports_virtualized=True
+ )
for distro_series in distro_serieses:
- dases.append(self.factory.makeDistroArchSeries(
- distroseries=distro_series, architecturetag=arch_tag,
- processor=processor))
+ dases.append(
+ self.factory.makeDistroArchSeries(
+ distroseries=distro_series,
+ architecturetag=arch_tag,
+ processor=processor,
+ )
+ )
charmcraft_data = {}
if self.bases is not None:
charmcraft_data["bases"] = self.bases
build_instances_factory = partial(
determine_instances_to_build,
- charmcraft_data, dases, distro_serieses[0])
+ charmcraft_data,
+ dases,
+ distro_serieses[0],
+ )
if hasattr(self, "expected_exception"):
self.assertThat(
- build_instances_factory, Raises(self.expected_exception))
+ build_instances_factory, Raises(self.expected_exception)
+ )
else:
- self.assertThat(build_instances_factory(), MatchesListwise([
- MatchesStructure(
- distroseries=MatchesStructure.byEquality(version=version),
- architecturetag=Equals(arch_tag))
- for version, arch_tag in self.expected]))
+ self.assertThat(
+ build_instances_factory(),
+ MatchesListwise(
+ [
+ MatchesStructure(
+ distroseries=MatchesStructure.byEquality(
+ version=version
+ ),
+ architecturetag=Equals(arch_tag),
+ )
+ for version, arch_tag in self.expected
+ ]
+ ),
+ )
load_tests = load_tests_apply_scenarios
diff --git a/lib/lp/charms/browser/charmbase.py b/lib/lp/charms/browser/charmbase.py
index d882be6..6ab8fd3 100644
--- a/lib/lp/charms/browser/charmbase.py
+++ b/lib/lp/charms/browser/charmbase.py
@@ -5,7 +5,7 @@
__all__ = [
"CharmBaseSetNavigation",
- ]
+]
from zope.component import getUtility
@@ -15,6 +15,7 @@ from lp.services.webapp.publisher import Navigation
class CharmBaseSetNavigation(Navigation):
"""Navigation methods for `ICharmBaseSet`."""
+
usedfor = ICharmBaseSet
def traverse(self, name):
diff --git a/lib/lp/charms/browser/charmrecipe.py b/lib/lp/charms/browser/charmrecipe.py
index 675e517..72321f9 100644
--- a/lib/lp/charms/browser/charmrecipe.py
+++ b/lib/lp/charms/browser/charmrecipe.py
@@ -15,45 +15,34 @@ __all__ = [
"CharmRecipeRequestBuildsView",
"CharmRecipeURL",
"CharmRecipeView",
- ]
+]
-from lazr.restful.interface import (
- copy_field,
- use_template,
- )
+from lazr.restful.interface import copy_field, use_template
from zope.component import getUtility
from zope.error.interfaces import IErrorReportingUtility
-from zope.interface import (
- implementer,
- Interface,
- )
-from zope.schema import (
- Dict,
- TextLine,
- )
+from zope.interface import Interface, implementer
+from zope.schema import Dict, TextLine
from zope.security.interfaces import Unauthorized
from lp import _
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
- )
+ action,
+)
from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
from lp.app.browser.tales import format_link
from lp.charms.browser.widgets.charmrecipebuildchannels import (
CharmRecipeBuildChannelsWidget,
- )
-from lp.charms.interfaces.charmhubclient import (
- BadRequestPackageUploadResponse,
- )
+)
+from lp.charms.interfaces.charmhubclient import BadRequestPackageUploadResponse
from lp.charms.interfaces.charmrecipe import (
- CannotAuthorizeCharmhubUploads,
CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG,
+ CannotAuthorizeCharmhubUploads,
ICharmRecipe,
ICharmRecipeSet,
NoSuchCharmRecipe,
- )
+)
from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet
from lp.code.browser.widgets.gitref import GitRefWidget
from lp.code.interfaces.gitref import IGitRef
@@ -63,20 +52,17 @@ from lp.services.features import getFeatureFlag
from lp.services.propertycache import cachedproperty
from lp.services.utils import seconds_since_epoch
from lp.services.webapp import (
- canonical_url,
ContextMenu,
- enabled_with_permission,
LaunchpadView,
Link,
Navigation,
NavigationMenu,
+ canonical_url,
+ enabled_with_permission,
stepthrough,
structured,
- )
-from lp.services.webapp.breadcrumb import (
- Breadcrumb,
- NameBreadcrumb,
- )
+)
+from lp.services.webapp.breadcrumb import Breadcrumb, NameBreadcrumb
from lp.services.webapp.candid import request_candid_discharge
from lp.services.webapp.interfaces import ICanonicalUrlData
from lp.services.webhooks.browser import WebhookTargetNavigationMixin
@@ -87,6 +73,7 @@ from lp.soyuz.browser.build import get_build_by_id_str
@implementer(ICanonicalUrlData)
class CharmRecipeURL:
"""Charm recipe URL creation rules."""
+
rootsite = "mainsite"
def __init__(self, recipe):
@@ -123,14 +110,15 @@ class CharmRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
class CharmRecipeBreadcrumb(NameBreadcrumb):
-
@property
def inside(self):
# XXX cjwatson 2021-06-04: This should probably link to an
# appropriate listing view, but we don't have one of those yet.
return Breadcrumb(
- self.context.project, text=self.context.project.display_name,
- inside=self.context.project)
+ self.context.project,
+ text=self.context.project.display_name,
+ inside=self.context.project,
+ )
class CharmRecipeNavigationMenu(NavigationMenu):
@@ -153,10 +141,14 @@ class CharmRecipeNavigationMenu(NavigationMenu):
@enabled_with_permission("launchpad.Edit")
def webhooks(self):
return Link(
- "+webhooks", "Manage webhooks", icon="edit",
+ "+webhooks",
+ "Manage webhooks",
+ icon="edit",
enabled=(
- bool(getFeatureFlag(CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG)) and
- bool(getFeatureFlag("webhooks.new.enabled"))))
+ bool(getFeatureFlag(CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG))
+ and bool(getFeatureFlag("webhooks.new.enabled"))
+ ),
+ )
@enabled_with_permission("launchpad.Edit")
def authorize(self):
@@ -196,10 +188,15 @@ class CharmRecipeView(LaunchpadView):
def person_picker(self):
field = copy_field(
ICharmRecipe["owner"],
- vocabularyName="AllUserTeamsParticipationPlusSelfSimpleDisplay")
+ vocabularyName="AllUserTeamsParticipationPlusSelfSimpleDisplay",
+ )
return InlinePersonEditPickerWidget(
- self.context, field, format_link(self.context.owner),
- header="Change owner", step_title="Select a new owner")
+ self.context,
+ field,
+ format_link(self.context.owner),
+ header="Change owner",
+ step_title="Select a new owner",
+ )
@property
def build_frequency(self):
@@ -253,34 +250,42 @@ def builds_and_requests_for_recipe(recipe):
items = sorted(
list(recipe.pending_builds) + list(recipe.pending_build_requests),
- key=make_sort_key("date_created", "date_requested"))
+ key=make_sort_key("date_created", "date_requested"),
+ )
if len(items) < 10:
# We need to interleave two unbounded result sets, but we only need
# enough items from them to make the total count up to 10. It's
# simplest to just fetch the upper bound from each set and do our
# own sorting.
recent_items = sorted(
- list(recipe.completed_builds[:10 - len(items)]) +
- list(recipe.failed_build_requests[:10 - len(items)]),
+ list(recipe.completed_builds[: 10 - len(items)])
+ + list(recipe.failed_build_requests[: 10 - len(items)]),
key=make_sort_key(
- "date_finished", "date_started",
- "date_created", "date_requested"))
- items.extend(recent_items[:10 - len(items)])
+ "date_finished",
+ "date_started",
+ "date_created",
+ "date_requested",
+ ),
+ )
+ items.extend(recent_items[: 10 - len(items)])
return items
class ICharmRecipeEditSchema(Interface):
"""Schema for adding or editing a charm recipe."""
- use_template(ICharmRecipe, include=[
- "owner",
- "name",
- "project",
- "require_virtualized",
- "auto_build",
- "auto_build_channels",
- "store_upload",
- ])
+ use_template(
+ ICharmRecipe,
+ include=[
+ "owner",
+ "name",
+ "project",
+ "require_virtualized",
+ "auto_build",
+ "auto_build_channels",
+ "store_upload",
+ ],
+ )
git_ref = copy_field(ICharmRecipe["git_ref"], required=True)
@@ -297,15 +302,16 @@ def log_oops(error, request):
class CharmRecipeAuthorizeMixin:
-
def requestAuthorization(self, recipe):
try:
self.next_url = CharmRecipeAuthorizeView.requestAuthorization(
- recipe, self.request)
+ recipe, self.request
+ )
except BadRequestPackageUploadResponse as e:
self.setFieldError(
"store_upload",
- "Cannot get permission from Charmhub to upload this package.")
+ "Cannot get permission from Charmhub to upload this package.",
+ )
log_oops(e, self.request)
@@ -333,7 +339,7 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
"store_upload",
"store_name",
"store_channels",
- ]
+ ]
@property
def is_project_context(self):
@@ -346,8 +352,9 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
@property
def initial_values(self):
initial_values = {"owner": self.user}
- if (IGitRef.providedBy(self.context) and
- IProduct.providedBy(self.context.target)):
+ if IGitRef.providedBy(self.context) and IProduct.providedBy(
+ self.context.target
+ ):
initial_values["project"] = self.context.target
return initial_values
@@ -372,14 +379,20 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
git_ref = data["git_ref"]
else:
raise NotImplementedError(
- "Unknown context for charm recipe creation.")
+ "Unknown context for charm recipe creation."
+ )
recipe = getUtility(ICharmRecipeSet).new(
- self.user, data["owner"], project, data["name"], git_ref=git_ref,
+ self.user,
+ data["owner"],
+ project,
+ data["name"],
+ git_ref=git_ref,
auto_build=data["auto_build"],
auto_build_channels=data["auto_build_channels"],
store_upload=data["store_upload"],
store_name=data["store_name"],
- store_channels=data.get("store_channels"))
+ store_channels=data.get("store_channels"),
+ )
if data["store_upload"]:
self.requestAuthorization(recipe)
else:
@@ -398,11 +411,13 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
self.setFieldError(
"name",
"There is already a charm recipe owned by %s in %s with "
- "this name." % (owner.display_name, project.display_name))
+ "this name." % (owner.display_name, project.display_name),
+ )
class BaseCharmRecipeEditView(
- CharmRecipeAuthorizeMixin, LaunchpadEditFormView):
+ CharmRecipeAuthorizeMixin, LaunchpadEditFormView
+):
schema = ICharmRecipeEditSchema
@@ -429,13 +444,15 @@ class BaseCharmRecipeEditView(
if owner is not None and owner.private:
self.setFieldError(
"owner",
- "A public charm recipe cannot have a private owner.")
+ "A public charm recipe cannot have a private owner.",
+ )
if "git_ref" in data:
ref = data.get("git_ref", self.context.git_ref)
if ref is not None and ref.private:
self.setFieldError(
"git_ref",
- "A public charm recipe cannot have a private repository.")
+ "A public charm recipe cannot have a private repository.",
+ )
def _needCharmhubReauth(self, data):
"""Does this change require reauthorizing to Charmhub?"""
@@ -444,8 +461,9 @@ class BaseCharmRecipeEditView(
if not store_upload or store_name is None:
return False
return (
- not self.context.store_upload or
- store_name != self.context.store_name)
+ not self.context.store_upload
+ or store_name != self.context.store_name
+ )
@action("Update charm recipe", name="update")
def request_action(self, action, data):
@@ -502,7 +520,7 @@ class CharmRecipeEditView(BaseCharmRecipeEditView):
"store_upload",
"store_name",
"store_channels",
- ]
+ ]
custom_widget_git_ref = GitRefWidget
custom_widget_auto_build_channels = CharmRecipeBuildChannelsWidget
custom_widget_store_channels = StoreChannelsWidget
@@ -515,13 +533,15 @@ class CharmRecipeEditView(BaseCharmRecipeEditView):
if owner and project and name:
try:
recipe = getUtility(ICharmRecipeSet).getByName(
- owner, project, name)
+ owner, project, name
+ )
if recipe != self.context:
self.setFieldError(
"name",
"There is already a charm recipe owned by %s in %s "
- "with this name." %
- (owner.display_name, project.display_name))
+ "with this name."
+ % (owner.display_name, project.display_name),
+ )
except NoSuchCharmRecipe:
pass
@@ -539,7 +559,8 @@ class CharmRecipeAuthorizeView(LaunchpadEditFormView):
"""Schema for authorizing charm recipe uploads to Charmhub."""
discharge_macaroon = TextLine(
- title="Serialized discharge macaroon", required=True)
+ title="Serialized discharge macaroon", required=True
+ )
render_context = False
@@ -560,9 +581,12 @@ class CharmRecipeAuthorizeView(LaunchpadEditFormView):
else:
base_url = canonical_url(recipe, view_name="+authorize")
return request_candid_discharge(
- request, root_macaroon_raw, base_url,
+ request,
+ root_macaroon_raw,
+ base_url,
"field.discharge_macaroon",
- discharge_macaroon_action="field.actions.complete")
+ discharge_macaroon_action="field.actions.complete",
+ )
@action("Begin authorization", name="begin")
def begin_action(self, action, data):
@@ -573,14 +597,23 @@ class CharmRecipeAuthorizeView(LaunchpadEditFormView):
@action("Complete authorization", name="complete")
def complete_action(self, action, data):
if not data.get("discharge_macaroon"):
- self.addError(structured(
- _("Uploads of %(recipe)s to Charmhub were not authorized."),
- recipe=self.context.name))
+ self.addError(
+ structured(
+ _(
+ "Uploads of %(recipe)s to Charmhub were not "
+ "authorized."
+ ),
+ recipe=self.context.name,
+ )
+ )
return
self.context.completeAuthorization(data["discharge_macaroon"])
- self.request.response.addInfoNotification(structured(
- _("Uploads of %(recipe)s to Charmhub are now authorized."),
- recipe=self.context.name))
+ self.request.response.addInfoNotification(
+ structured(
+ _("Uploads of %(recipe)s to Charmhub are now authorized."),
+ recipe=self.context.name,
+ )
+ )
self.request.response.redirect(canonical_url(self.context))
@property
@@ -620,8 +653,11 @@ class CharmRecipeRequestBuildsView(LaunchpadFormView):
"""Schema for requesting a build."""
channels = Dict(
- title="Source snap channels", key_type=TextLine(), required=True,
- description=ICharmRecipe["auto_build_channels"].description)
+ title="Source snap channels",
+ key_type=TextLine(),
+ required=True,
+ description=ICharmRecipe["auto_build_channels"].description,
+ )
custom_widget_channels = CharmRecipeBuildChannelsWidget
@@ -634,11 +670,12 @@ class CharmRecipeRequestBuildsView(LaunchpadFormView):
"""See `LaunchpadFormView`."""
return {
"channels": self.context.auto_build_channels,
- }
+ }
@action("Request builds", name="request")
def request_action(self, action, data):
self.context.requestBuilds(self.user, channels=data["channels"])
self.request.response.addNotification(
- _("Builds will be dispatched soon."))
+ _("Builds will be dispatched soon.")
+ )
self.next_url = self.cancel_url
diff --git a/lib/lp/charms/browser/charmrecipebuild.py b/lib/lp/charms/browser/charmrecipebuild.py
index 4c6ae4e..0b90fcf 100644
--- a/lib/lp/charms/browser/charmrecipebuild.py
+++ b/lib/lp/charms/browser/charmrecipebuild.py
@@ -7,30 +7,27 @@ __all__ = [
"CharmRecipeBuildContextMenu",
"CharmRecipeBuildNavigation",
"CharmRecipeBuildView",
- ]
+]
from zope.interface import Interface
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
from lp.charms.interfaces.charmrecipebuild import (
CannotScheduleStoreUpload,
ICharmRecipeBuild,
- )
+)
from lp.services.librarian.browser import (
FileNavigationMixin,
ProxiedLibraryFileAlias,
- )
+)
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
- canonical_url,
ContextMenu,
- enabled_with_permission,
Link,
Navigation,
- )
+ canonical_url,
+ enabled_with_permission,
+)
from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm
@@ -50,20 +47,29 @@ class CharmRecipeBuildContextMenu(ContextMenu):
@enabled_with_permission("launchpad.Edit")
def retry(self):
return Link(
- "+retry", "Retry this build", icon="retry",
- enabled=self.context.can_be_retried)
+ "+retry",
+ "Retry this build",
+ icon="retry",
+ enabled=self.context.can_be_retried,
+ )
@enabled_with_permission("launchpad.Edit")
def cancel(self):
return Link(
- "+cancel", "Cancel build", icon="remove",
- enabled=self.context.can_be_cancelled)
+ "+cancel",
+ "Cancel build",
+ icon="remove",
+ enabled=self.context.can_be_cancelled,
+ )
@enabled_with_permission("launchpad.Admin")
def rescore(self):
return Link(
- "+rescore", "Rescore build", icon="edit",
- enabled=self.context.can_be_rescored)
+ "+rescore",
+ "Rescore build",
+ icon="edit",
+ enabled=self.context.can_be_rescored,
+ )
class CharmRecipeBuildView(LaunchpadFormView):
@@ -86,7 +92,9 @@ class CharmRecipeBuildView(LaunchpadFormView):
return [
ProxiedLibraryFileAlias(alias, self.context)
- for _, alias, _ in self.context.getFiles() if not alias.deleted]
+ for _, alias, _ in self.context.getFiles()
+ if not alias.deleted
+ ]
@cachedproperty
def has_files(self):
@@ -106,7 +114,8 @@ class CharmRecipeBuildView(LaunchpadFormView):
else:
self.request.response.addInfoNotification(
"An upload has been scheduled and will run as soon as "
- "possible.")
+ "possible."
+ )
class CharmRecipeBuildRetryView(LaunchpadFormView):
@@ -120,6 +129,7 @@ class CharmRecipeBuildRetryView(LaunchpadFormView):
@property
def cancel_url(self):
return canonical_url(self.context)
+
next_url = cancel_url
@action("Retry build", name="retry")
@@ -127,7 +137,8 @@ class CharmRecipeBuildRetryView(LaunchpadFormView):
"""Retry the build."""
if not self.context.can_be_retried:
self.request.response.addErrorNotification(
- "Build cannot be retried")
+ "Build cannot be retried"
+ )
else:
self.context.retry()
self.request.response.addInfoNotification("Build has been queued")
@@ -146,6 +157,7 @@ class CharmRecipeBuildCancelView(LaunchpadFormView):
@property
def cancel_url(self):
return canonical_url(self.context)
+
next_url = cancel_url
@action("Cancel build", name="cancel")
@@ -165,12 +177,14 @@ class CharmRecipeBuildRescoreView(LaunchpadFormView):
if self.context.can_be_rescored:
return super().__call__()
self.request.response.addWarningNotification(
- "Cannot rescore this build because it is not queued.")
+ "Cannot rescore this build because it is not queued."
+ )
self.request.response.redirect(canonical_url(self.context))
@property
def cancel_url(self):
return canonical_url(self.context)
+
next_url = cancel_url
@action("Rescore build", name="rescore")
diff --git a/lib/lp/charms/browser/charmrecipelisting.py b/lib/lp/charms/browser/charmrecipelisting.py
index 90fa225..4a47a81 100644
--- a/lib/lp/charms/browser/charmrecipelisting.py
+++ b/lib/lp/charms/browser/charmrecipelisting.py
@@ -7,7 +7,7 @@ __all__ = [
"GitCharmRecipeListingView",
"PersonCharmRecipeListingView",
"ProjectCharmRecipeListingView",
- ]
+]
from functools import partial
@@ -35,14 +35,17 @@ class CharmRecipeListingView(LaunchpadView, FeedsMixin):
@property
def label(self):
return "Charm recipes for %(displayname)s" % {
- "displayname": self.context.displayname}
+ "displayname": self.context.displayname
+ }
def initialize(self):
super().initialize()
recipes = getUtility(ICharmRecipeSet).findByContext(
- self.context, visible_by_user=self.user)
+ self.context, visible_by_user=self.user
+ )
loader = partial(
- getUtility(ICharmRecipeSet).preloadDataForRecipes, user=self.user)
+ getUtility(ICharmRecipeSet).preloadDataForRecipes, user=self.user
+ )
self.recipes = DecoratedResultSet(recipes, pre_iter_hook=loader)
@cachedproperty
@@ -57,7 +60,8 @@ class GitCharmRecipeListingView(CharmRecipeListingView):
@property
def label(self):
return "Charm recipes for %(display_name)s" % {
- "display_name": self.context.display_name}
+ "display_name": self.context.display_name
+ }
class PersonCharmRecipeListingView(CharmRecipeListingView):
diff --git a/lib/lp/charms/browser/hascharmrecipes.py b/lib/lp/charms/browser/hascharmrecipes.py
index b73f866..854f269 100644
--- a/lib/lp/charms/browser/hascharmrecipes.py
+++ b/lib/lp/charms/browser/hascharmrecipes.py
@@ -6,7 +6,7 @@
__all__ = [
"HasCharmRecipesMenuMixin",
"HasCharmRecipesViewMixin",
- ]
+]
from zope.component import getUtility
@@ -14,13 +14,10 @@ from lp.charms.interfaces.charmrecipe import (
CHARM_RECIPE_ALLOW_CREATE,
CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
ICharmRecipeSet,
- )
+)
from lp.code.interfaces.gitrepository import IGitRepository
from lp.services.features import getFeatureFlag
-from lp.services.webapp import (
- canonical_url,
- Link,
- )
+from lp.services.webapp import Link, canonical_url
from lp.services.webapp.escaping import structured
@@ -29,17 +26,20 @@ class HasCharmRecipesMenuMixin:
def view_charm_recipes(self):
text = "View charm recipes"
- enabled = not getUtility(ICharmRecipeSet).findByContext(
- self.context, visible_by_user=self.user).is_empty()
+ enabled = (
+ not getUtility(ICharmRecipeSet)
+ .findByContext(self.context, visible_by_user=self.user)
+ .is_empty()
+ )
return Link("+charm-recipes", text, icon="info", enabled=enabled)
def create_charm_recipe(self):
# Only enabled for private contexts if the
# charm.recipe.allow_private flag is enabled.
- enabled = (
- bool(getFeatureFlag(CHARM_RECIPE_ALLOW_CREATE)) and (
- not self.context.private or
- bool(getFeatureFlag(CHARM_RECIPE_PRIVATE_FEATURE_FLAG))))
+ enabled = bool(getFeatureFlag(CHARM_RECIPE_ALLOW_CREATE)) and (
+ not self.context.private
+ or bool(getFeatureFlag(CHARM_RECIPE_PRIVATE_FEATURE_FLAG))
+ )
text = "Create charm recipe"
return Link("+new-charm-recipe", text, enabled=enabled, icon="add")
@@ -51,7 +51,8 @@ class HasCharmRecipesViewMixin:
@property
def charm_recipes(self):
return getUtility(ICharmRecipeSet).findByContext(
- self.context, visible_by_user=self.user)
+ self.context, visible_by_user=self.user
+ )
@property
def charm_recipes_link(self):
@@ -69,9 +70,12 @@ class HasCharmRecipesViewMixin:
return structured(
'<a href="%s">1 charm recipe</a> using this %s.',
canonical_url(self.charm_recipes.one()),
- context_type).escapedtext
+ context_type,
+ ).escapedtext
else:
# Link to a charm recipe listing.
return structured(
'<a href="+charm-recipes">%s charm recipes</a> using this %s.',
- count, context_type).escapedtext
+ count,
+ context_type,
+ ).escapedtext
diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py
index 2324fa6..0a0a377 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipe.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipe.py
@@ -4,24 +4,19 @@
"""Test charm recipe views."""
import base64
-from datetime import (
- datetime,
- timedelta,
- )
import json
import re
-from urllib.parse import (
- parse_qs,
- urlsplit,
- )
+from datetime import datetime, timedelta
+from urllib.parse import parse_qs, urlsplit
+import pytz
+import responses
+import soupmatchers
+import transaction
from fixtures import FakeLogger
from nacl.public import PrivateKey
from pymacaroons import Macaroon
from pymacaroons.serializers import JsonSerializer
-import pytz
-import responses
-import soupmatchers
from testtools.matchers import (
AfterPreprocessing,
ContainsDict,
@@ -30,8 +25,7 @@ from testtools.matchers import (
MatchesDict,
MatchesListwise,
MatchesStructure,
- )
-import transaction
+)
from zope.component import getUtility
from zope.publisher.interfaces import NotFound
from zope.security.interfaces import Unauthorized
@@ -46,12 +40,12 @@ from lp.charms.browser.charmrecipe import (
CharmRecipeAdminView,
CharmRecipeEditView,
CharmRecipeView,
- )
+)
from lp.charms.interfaces.charmrecipe import (
CHARM_RECIPE_ALLOW_CREATE,
CharmRecipeBuildRequestStatus,
ICharmRecipeSet,
- )
+)
from lp.registry.enums import PersonVisibility
from lp.services.crypto.interfaces import IEncryptedContainer
from lp.services.database.constants import UTC_NOW
@@ -62,33 +56,24 @@ from lp.services.webapp import canonical_url
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.testing import (
BrowserTestCase,
+ TestCaseWithFactory,
login,
login_admin,
login_person,
person_logged_in,
- TestCaseWithFactory,
time_counter,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
-from lp.testing.matchers import (
- MatchesPickerText,
- MatchesTagText,
- )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
+from lp.testing.matchers import MatchesPickerText, MatchesTagText
from lp.testing.pages import (
extract_text,
find_main_content,
find_tag_by_id,
find_tags_by_class,
get_feedback_messages,
- )
+)
from lp.testing.publication import test_traverse
-from lp.testing.views import (
- create_initialized_view,
- create_view,
- )
+from lp.testing.views import create_initialized_view, create_view
class TestCharmRecipeNavigation(TestCaseWithFactory):
@@ -103,16 +88,19 @@ class TestCharmRecipeNavigation(TestCaseWithFactory):
owner = self.factory.makePerson(name="person")
project = self.factory.makeProduct(name="project")
recipe = self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, project=project, name="charm")
+ registrant=owner, owner=owner, project=project, name="charm"
+ )
self.assertEqual(
"http://launchpad.test/~person/project/+charm/charm",
- canonical_url(recipe))
+ canonical_url(recipe),
+ )
def test_charm_recipe(self):
recipe = self.factory.makeCharmRecipe()
obj, _, _ = test_traverse(
- "http://launchpad.test/~%s/%s/+charm/%s" % (
- recipe.owner.name, recipe.project.name, recipe.name))
+ "http://launchpad.test/~%s/%s/+charm/%s"
+ % (recipe.owner.name, recipe.project.name, recipe.name)
+ )
self.assertEqual(recipe, obj)
@@ -125,25 +113,32 @@ class BaseTestCharmRecipeView(BrowserTestCase):
self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
self.useFixture(FakeLogger())
self.person = self.factory.makePerson(
- name="test-person", displayname="Test Person")
+ name="test-person", displayname="Test Person"
+ )
class TestCharmRecipeAddView(BaseTestCharmRecipeView):
-
def test_create_new_recipe_not_logged_in(self):
[git_ref] = self.factory.makeGitRefs()
self.assertRaises(
- Unauthorized, self.getViewBrowser, git_ref,
- view_name="+new-charm-recipe", no_login=True)
+ Unauthorized,
+ self.getViewBrowser,
+ git_ref,
+ view_name="+new-charm-recipe",
+ no_login=True,
+ )
def test_create_new_recipe_git(self):
self.factory.makeProduct(
- name="test-project", displayname="Test Project")
+ name="test-project", displayname="Test Project"
+ )
[git_ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.person)
+ owner=self.person, target=self.person
+ )
source_display = git_ref.display_name
browser = self.getViewBrowser(
- git_ref, view_name="+new-charm-recipe", user=self.person)
+ git_ref, view_name="+new-charm-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "charm-name"
self.assertEqual("", browser.getControl(name="field.project").value)
browser.getControl(name="field.project").value = "test-project"
@@ -152,129 +147,161 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
content = find_main_content(browser.contents)
self.assertEqual("charm-name", extract_text(content.h1))
self.assertThat(
- "Test Person", MatchesPickerText(content, "edit-owner"))
+ "Test Person", MatchesPickerText(content, "edit-owner")
+ )
self.assertThat(
"Project:\nTest Project\nEdit charm recipe",
- MatchesTagText(content, "project"))
+ MatchesTagText(content, "project"),
+ )
self.assertThat(
"Source:\n%s\nEdit charm recipe" % source_display,
- MatchesTagText(content, "source"))
+ MatchesTagText(content, "source"),
+ )
self.assertThat(
"Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n",
- MatchesTagText(content, "auto_build"))
+ MatchesTagText(content, "auto_build"),
+ )
self.assertIsNone(find_tag_by_id(content, "auto_build_channels"))
self.assertThat(
"Builds of this charm recipe are not automatically uploaded to "
"the store.\nEdit charm recipe",
- MatchesTagText(content, "store_upload"))
+ MatchesTagText(content, "store_upload"),
+ )
def test_create_new_recipe_git_project_namespace(self):
# If the Git repository is already in a project namespace, then that
# project is the default for the new recipe.
project = self.factory.makeProduct(
- name="test-project", displayname="Test Project")
+ name="test-project", displayname="Test Project"
+ )
[git_ref] = self.factory.makeGitRefs(target=project)
source_display = git_ref.display_name
browser = self.getViewBrowser(
- git_ref, view_name="+new-charm-recipe", user=self.person)
+ git_ref, view_name="+new-charm-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "charm-name"
self.assertEqual(
- "test-project", browser.getControl(name="field.project").value)
+ "test-project", browser.getControl(name="field.project").value
+ )
browser.getControl("Create charm recipe").click()
content = find_main_content(browser.contents)
self.assertEqual("charm-name", extract_text(content.h1))
self.assertThat(
- "Test Person", MatchesPickerText(content, "edit-owner"))
+ "Test Person", MatchesPickerText(content, "edit-owner")
+ )
self.assertThat(
"Project:\nTest Project\nEdit charm recipe",
- MatchesTagText(content, "project"))
+ MatchesTagText(content, "project"),
+ )
self.assertThat(
"Source:\n%s\nEdit charm recipe" % source_display,
- MatchesTagText(content, "source"))
+ MatchesTagText(content, "source"),
+ )
self.assertThat(
"Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n",
- MatchesTagText(content, "auto_build"))
+ MatchesTagText(content, "auto_build"),
+ )
self.assertIsNone(find_tag_by_id(content, "auto_build_channels"))
self.assertThat(
"Builds of this charm recipe are not automatically uploaded to "
"the store.\nEdit charm recipe",
- MatchesTagText(content, "store_upload"))
+ MatchesTagText(content, "store_upload"),
+ )
def test_create_new_recipe_project(self):
project = self.factory.makeProduct(displayname="Test Project")
[git_ref] = self.factory.makeGitRefs()
source_display = git_ref.display_name
browser = self.getViewBrowser(
- project, view_name="+new-charm-recipe", user=self.person)
+ project, view_name="+new-charm-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "charm-name"
- browser.getControl(name="field.git_ref.repository").value = (
- git_ref.repository.shortened_path)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = git_ref.repository.shortened_path
browser.getControl(name="field.git_ref.path").value = git_ref.path
browser.getControl("Create charm recipe").click()
content = find_main_content(browser.contents)
self.assertEqual("charm-name", extract_text(content.h1))
self.assertThat(
- "Test Person", MatchesPickerText(content, "edit-owner"))
+ "Test Person", MatchesPickerText(content, "edit-owner")
+ )
self.assertThat(
"Project:\nTest Project\nEdit charm recipe",
- MatchesTagText(content, "project"))
+ MatchesTagText(content, "project"),
+ )
self.assertThat(
"Source:\n%s\nEdit charm recipe" % source_display,
- MatchesTagText(content, "source"))
+ MatchesTagText(content, "source"),
+ )
self.assertThat(
"Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n",
- MatchesTagText(content, "auto_build"))
+ MatchesTagText(content, "auto_build"),
+ )
self.assertIsNone(find_tag_by_id(content, "auto_build_channels"))
self.assertThat(
"Builds of this charm recipe are not automatically uploaded to "
"the store.\nEdit charm recipe",
- MatchesTagText(content, "store_upload"))
+ MatchesTagText(content, "store_upload"),
+ )
def test_create_new_recipe_users_teams_as_owner_options(self):
# Teams that the user is in are options for the charm recipe owner.
self.factory.makeTeam(
- name="test-team", displayname="Test Team", members=[self.person])
+ name="test-team", displayname="Test Team", members=[self.person]
+ )
[git_ref] = self.factory.makeGitRefs()
browser = self.getViewBrowser(
- git_ref, view_name="+new-charm-recipe", user=self.person)
+ git_ref, view_name="+new-charm-recipe", user=self.person
+ )
options = browser.getControl("Owner").displayOptions
self.assertEqual(
["Test Person (test-person)", "Test Team (test-team)"],
- sorted(str(option) for option in options))
+ sorted(str(option) for option in options),
+ )
def test_create_new_recipe_auto_build(self):
# Creating a new recipe and asking for it to be automatically built
# sets all the appropriate fields.
self.factory.makeProduct(
- name="test-project", displayname="Test Project")
+ name="test-project", displayname="Test Project"
+ )
[git_ref] = self.factory.makeGitRefs()
browser = self.getViewBrowser(
- git_ref, view_name="+new-charm-recipe", user=self.person)
+ git_ref, view_name="+new-charm-recipe", user=self.person
+ )
browser.getControl(name="field.name").value = "charm-name"
browser.getControl(name="field.project").value = "test-project"
browser.getControl(
- "Automatically build when branch changes").selected = True
+ "Automatically build when branch changes"
+ ).selected = True
browser.getControl(
- name="field.auto_build_channels.charmcraft").value = "edge"
+ name="field.auto_build_channels.charmcraft"
+ ).value = "edge"
browser.getControl(
- name="field.auto_build_channels.core").value = "stable"
+ name="field.auto_build_channels.core"
+ ).value = "stable"
browser.getControl(
- name="field.auto_build_channels.core18").value = "beta"
+ name="field.auto_build_channels.core18"
+ ).value = "beta"
browser.getControl(
- name="field.auto_build_channels.core20").value = "edge/feature"
+ name="field.auto_build_channels.core20"
+ ).value = "edge/feature"
browser.getControl("Create charm recipe").click()
content = find_main_content(browser.contents)
self.assertThat(
"Build schedule:\n(?)\nBuilt automatically\nEdit charm recipe\n",
- MatchesTagText(content, "auto_build"))
+ MatchesTagText(content, "auto_build"),
+ )
self.assertThat(
"Source snap channels for automatic builds:\nEdit charm recipe\n"
"charmcraft\nedge\ncore\nstable\ncore18\nbeta\n"
"core20\nedge/feature\n",
- MatchesTagText(content, "auto_build_channels"))
+ MatchesTagText(content, "auto_build_channels"),
+ )
@responses.activate
def test_create_new_recipe_store_upload(self):
@@ -285,9 +312,11 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
self.pushConfig(
"launchpad",
candid_service_root="https://candid.test/",
- csrf_secret="test secret")
+ csrf_secret="test secret",
+ )
project = self.factory.makeProduct(
- name="test-project", displayname="Test Project")
+ name="test-project", displayname="Test Project"
+ )
[git_ref] = self.factory.makeGitRefs()
view_url = canonical_url(git_ref, view_name="+new-charm-recipe")
browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
@@ -300,95 +329,147 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
browser.getControl("Edge").selected = True
root_macaroon = Macaroon(version=2)
root_macaroon.add_third_party_caveat(
- "https://candid.test/", "", "identity")
+ "https://candid.test/", "", "identity"
+ )
caveat = root_macaroon.caveats[0]
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
responses.add(
- "POST", "http://charmhub.example/v1/tokens",
- json={"macaroon": root_macaroon_raw})
+ "POST",
+ "http://charmhub.example/v1/tokens",
+ json={"macaroon": root_macaroon_raw},
+ )
responses.add(
- "POST", "https://candid.test/discharge", status=401,
+ "POST",
+ "https://candid.test/discharge",
+ status=401,
json={
"Code": "interaction required",
"Message": (
- "macaroon discharge required: authentication required"),
+ "macaroon discharge required: authentication required"
+ ),
"Info": {
"InteractionMethods": {
"browser-redirect": {
"LoginURL": "https://candid.test/login-redirect",
- },
},
},
- })
+ },
+ },
+ )
browser.getControl("Create charm recipe").click()
login_person(self.person)
recipe = getUtility(ICharmRecipeSet).getByName(
- self.person, project, "charm-name")
- self.assertThat(recipe, MatchesStructure.byEquality(
- owner=self.person, project=project, name="charm-name",
- source=git_ref, store_upload=True, store_name="charmhub-name",
- store_secrets={"root": root_macaroon_raw},
- store_channels=["track/edge"]))
- self.assertThat(responses.calls, MatchesListwise([
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("http://charmhub.example/v1/tokens"),
- method=Equals("POST"),
- body=AfterPreprocessing(
- lambda b: json.loads(b.decode()),
- Equals({
- "description": "charmhub-name for launchpad.test",
- "packages": [
- {"type": "charm", "name": "charmhub-name"},
- ],
- "permissions": [
- "package-manage-releases",
- "package-manage-revisions",
- "package-view-revisions",
- ],
- })))),
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("https://candid.test/discharge"),
- headers=ContainsDict({
- "Content-Type": Equals(
- "application/x-www-form-urlencoded"),
- }),
- body=AfterPreprocessing(parse_qs, MatchesDict({
+ self.person, project, "charm-name"
+ )
+ self.assertThat(
+ recipe,
+ MatchesStructure.byEquality(
+ owner=self.person,
+ project=project,
+ name="charm-name",
+ source=git_ref,
+ store_upload=True,
+ store_name="charmhub-name",
+ store_secrets={"root": root_macaroon_raw},
+ store_channels=["track/edge"],
+ ),
+ )
+ tokens_matcher = MatchesStructure(
+ url=Equals("http://charmhub.example/v1/tokens"),
+ method=Equals("POST"),
+ body=AfterPreprocessing(
+ lambda b: json.loads(b.decode()),
+ Equals(
+ {
+ "description": ("charmhub-name for launchpad.test"),
+ "packages": [
+ {
+ "type": "charm",
+ "name": "charmhub-name",
+ },
+ ],
+ "permissions": [
+ "package-manage-releases",
+ "package-manage-revisions",
+ "package-view-revisions",
+ ],
+ }
+ ),
+ ),
+ )
+ discharge_matcher = MatchesStructure(
+ url=Equals("https://candid.test/discharge"),
+ headers=ContainsDict(
+ {
+ "Content-Type": Equals(
+ "application/x-www-form-urlencoded"
+ ),
+ }
+ ),
+ body=AfterPreprocessing(
+ parse_qs,
+ MatchesDict(
+ {
"id64": Equals(
- [base64.b64encode(
- caveat.caveat_id_bytes).decode()]),
- })))),
- ]))
+ [base64.b64encode(caveat.caveat_id_bytes).decode()]
+ ),
+ }
+ ),
+ ),
+ )
+ self.assertThat(
+ responses.calls,
+ MatchesListwise(
+ [
+ MatchesStructure(request=tokens_matcher),
+ MatchesStructure(request=discharge_matcher),
+ ]
+ ),
+ )
self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
+ return_to_matcher = AfterPreprocessing(
+ urlsplit,
+ MatchesStructure(
+ scheme=Equals("http"),
+ netloc=Equals("launchpad.test"),
+ path=Equals("/+candid-callback"),
+ query=AfterPreprocessing(
+ parse_qs,
+ MatchesDict(
+ {
+ "starting_url": Equals(
+ [canonical_url(recipe) + "/+authorize"]
+ ),
+ "discharge_macaroon_action": Equals(
+ ["field.actions.complete"]
+ ),
+ "discharge_macaroon_field": Equals(
+ ["field.discharge_macaroon"]
+ ),
+ }
+ ),
+ ),
+ fragment=Equals(""),
+ ),
+ )
self.assertThat(
urlsplit(browser.headers["Location"]),
MatchesStructure(
scheme=Equals("https"),
netloc=Equals("candid.test"),
path=Equals("/login-redirect"),
- query=AfterPreprocessing(parse_qs, ContainsDict({
- "return_to": MatchesListwise([
- AfterPreprocessing(urlsplit, MatchesStructure(
- scheme=Equals("http"),
- netloc=Equals("launchpad.test"),
- path=Equals("/+candid-callback"),
- query=AfterPreprocessing(parse_qs, MatchesDict({
- "starting_url": Equals(
- [canonical_url(recipe) + "/+authorize"]),
- "discharge_macaroon_action": Equals(
- ["field.actions.complete"]),
- "discharge_macaroon_field": Equals(
- ["field.discharge_macaroon"]),
- })),
- fragment=Equals(""))),
- ]),
- })),
- fragment=Equals("")))
+ query=AfterPreprocessing(
+ parse_qs,
+ ContainsDict(
+ {"return_to": MatchesListwise([return_to_matcher])}
+ ),
+ ),
+ fragment=Equals(""),
+ ),
+ )
class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
-
def test_unauthorized(self):
# A non-admin user cannot administer a charm recipe.
login_person(self.person)
@@ -396,16 +477,21 @@ class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
recipe_url = canonical_url(recipe)
browser = self.getViewBrowser(recipe, user=self.person)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Administer charm recipe")
+ LinkNotFoundError, browser.getLink, "Administer charm recipe"
+ )
self.assertRaises(
- Unauthorized, self.getUserBrowser, recipe_url + "/+admin",
- user=self.person)
+ Unauthorized,
+ self.getUserBrowser,
+ recipe_url + "/+admin",
+ user=self.person,
+ )
def test_admin_recipe(self):
# Admins can change require_virtualized.
login("admin@xxxxxxxxxxxxx")
admin = self.factory.makePerson(
- member_of=[getUtility(ILaunchpadCelebrities).admin])
+ member_of=[getUtility(ILaunchpadCelebrities).admin]
+ )
login_person(self.person)
recipe = self.factory.makeCharmRecipe(registrant=self.person)
self.assertTrue(recipe.require_virtualized)
@@ -422,40 +508,47 @@ class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
# Administering a charm recipe sets the date_last_modified property.
login("admin@xxxxxxxxxxxxx")
ppa_admin = self.factory.makePerson(
- member_of=[getUtility(ILaunchpadCelebrities).ppa_admin])
+ member_of=[getUtility(ILaunchpadCelebrities).ppa_admin]
+ )
login_person(self.person)
date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, date_created=date_created)
+ registrant=self.person, date_created=date_created
+ )
login_person(ppa_admin)
view = CharmRecipeAdminView(recipe, LaunchpadTestRequest())
view.initialize()
view.request_action.success({"require_virtualized": False})
self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW)
+ recipe, "date_last_modified", UTC_NOW
+ )
class TestCharmRecipeEditView(BaseTestCharmRecipeView):
-
def test_edit_recipe(self):
[old_git_ref] = self.factory.makeGitRefs()
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person, git_ref=old_git_ref)
+ registrant=self.person, owner=self.person, git_ref=old_git_ref
+ )
self.factory.makeTeam(
- name="new-team", displayname="New Team", members=[self.person])
+ name="new-team", displayname="New Team", members=[self.person]
+ )
[new_git_ref] = self.factory.makeGitRefs()
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit charm recipe").click()
browser.getControl("Owner").value = ["new-team"]
browser.getControl(name="field.name").value = "new-name"
- browser.getControl(name="field.git_ref.repository").value = (
- new_git_ref.repository.identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = new_git_ref.repository.identity
browser.getControl(name="field.git_ref.path").value = new_git_ref.path
browser.getControl(
- "Automatically build when branch changes").selected = True
+ "Automatically build when branch changes"
+ ).selected = True
browser.getControl(
- name="field.auto_build_channels.charmcraft").value = "edge"
+ name="field.auto_build_channels.charmcraft"
+ ).value = "edge"
browser.getControl("Update charm recipe").click()
content = find_main_content(browser.contents)
@@ -463,42 +556,56 @@ class TestCharmRecipeEditView(BaseTestCharmRecipeView):
self.assertThat("New Team", MatchesPickerText(content, "edit-owner"))
self.assertThat(
"Source:\n%s\nEdit charm recipe" % new_git_ref.display_name,
- MatchesTagText(content, "source"))
+ MatchesTagText(content, "source"),
+ )
self.assertThat(
"Build schedule:\n(?)\nBuilt automatically\nEdit charm recipe\n",
- MatchesTagText(content, "auto_build"))
+ MatchesTagText(content, "auto_build"),
+ )
self.assertThat(
"Source snap channels for automatic builds:\nEdit charm recipe\n"
"charmcraft\nedge",
- MatchesTagText(content, "auto_build_channels"))
+ MatchesTagText(content, "auto_build_channels"),
+ )
self.assertThat(
"Builds of this charm recipe are not automatically uploaded to "
"the store.\nEdit charm recipe",
- MatchesTagText(content, "store_upload"))
+ MatchesTagText(content, "store_upload"),
+ )
def test_edit_recipe_sets_date_last_modified(self):
# Editing a charm recipe sets the date_last_modified property.
date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, date_created=date_created)
+ registrant=self.person, date_created=date_created
+ )
with person_logged_in(self.person):
view = CharmRecipeEditView(recipe, LaunchpadTestRequest())
view.initialize()
- view.request_action.success({
- "owner": recipe.owner,
- "name": "changed",
- })
+ view.request_action.success(
+ {
+ "owner": recipe.owner,
+ "name": "changed",
+ }
+ )
self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW)
+ recipe, "date_last_modified", UTC_NOW
+ )
def test_edit_recipe_already_exists(self):
project = self.factory.makeProduct(displayname="Test Project")
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, project=project, owner=self.person,
- name="one")
+ registrant=self.person,
+ project=project,
+ owner=self.person,
+ name="one",
+ )
self.factory.makeCharmRecipe(
- registrant=self.person, project=project, owner=self.person,
- name="two")
+ registrant=self.person,
+ project=project,
+ owner=self.person,
+ name="two",
+ )
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit charm recipe").click()
browser.getControl(name="field.name").value = "two"
@@ -506,14 +613,17 @@ class TestCharmRecipeEditView(BaseTestCharmRecipeView):
self.assertEqual(
"There is already a charm recipe owned by Test Person in "
"Test Project with this name.",
- extract_text(find_tags_by_class(browser.contents, "message")[1]))
+ extract_text(find_tags_by_class(browser.contents, "message")[1]),
+ )
def test_edit_public_recipe_private_owner(self):
login_person(self.person)
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person)
+ registrant=self.person, owner=self.person
+ )
private_team = self.factory.makeTeam(
- owner=self.person, visibility=PersonVisibility.PRIVATE)
+ owner=self.person, visibility=PersonVisibility.PRIVATE
+ )
private_team_name = private_team.name
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit charm recipe").click()
@@ -521,49 +631,60 @@ class TestCharmRecipeEditView(BaseTestCharmRecipeView):
browser.getControl("Update charm recipe").click()
self.assertEqual(
"A public charm recipe cannot have a private owner.",
- extract_text(find_tags_by_class(browser.contents, "message")[1]))
+ extract_text(find_tags_by_class(browser.contents, "message")[1]),
+ )
def test_edit_public_recipe_private_git_ref(self):
login_person(self.person)
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person,
- git_ref=self.factory.makeGitRefs()[0])
+ registrant=self.person,
+ owner=self.person,
+ git_ref=self.factory.makeGitRefs()[0],
+ )
login_person(self.person)
[private_ref] = self.factory.makeGitRefs(
- owner=self.person,
- information_type=InformationType.PRIVATESECURITY)
+ owner=self.person, information_type=InformationType.PRIVATESECURITY
+ )
private_ref_identity = private_ref.repository.identity
private_ref_path = private_ref.path
browser = self.getViewBrowser(recipe, user=self.person)
browser.getLink("Edit charm recipe").click()
- browser.getControl(name="field.git_ref.repository").value = (
- private_ref_identity)
+ browser.getControl(
+ name="field.git_ref.repository"
+ ).value = private_ref_identity
browser.getControl(name="field.git_ref.path").value = private_ref_path
browser.getControl("Update charm recipe").click()
self.assertEqual(
"A public charm recipe cannot have a private repository.",
- extract_text(find_tags_by_class(browser.contents, "message")[1]))
+ extract_text(find_tags_by_class(browser.contents, "message")[1]),
+ )
class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
-
def setUp(self):
super().setUp()
self.pushConfig("charms", charmhub_url="http://charmhub.example/")
self.pushConfig(
"launchpad",
candid_service_root="https://candid.test/",
- csrf_secret="test secret")
+ csrf_secret="test secret",
+ )
self.recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person, store_upload=True,
- store_name=self.factory.getUniqueUnicode())
+ registrant=self.person,
+ owner=self.person,
+ store_upload=True,
+ store_name=self.factory.getUniqueUnicode(),
+ )
def test_unauthorized(self):
# A user without edit access cannot authorize charm recipe uploads.
other_person = self.factory.makePerson()
self.assertRaises(
- Unauthorized, self.getUserBrowser,
- canonical_url(self.recipe) + "/+authorize", user=other_person)
+ Unauthorized,
+ self.getUserBrowser,
+ canonical_url(self.recipe) + "/+authorize",
+ user=other_person,
+ )
@responses.activate
def test_begin_authorization(self):
@@ -572,93 +693,139 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
# an existing charm recipe without having to edit it.
recipe_url = canonical_url(self.recipe)
owner = self.recipe.owner
+ store_name = self.recipe.store_name
browser = self.getNonRedirectingBrowser(
- url=recipe_url + "/+authorize", user=self.recipe.owner)
+ url=recipe_url + "/+authorize", user=self.recipe.owner
+ )
root_macaroon = Macaroon(version=2)
root_macaroon.add_third_party_caveat(
- "https://candid.test/", "", "identity")
+ "https://candid.test/", "", "identity"
+ )
caveat = root_macaroon.caveats[0]
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
responses.add(
- "POST", "http://charmhub.example/v1/tokens",
- json={"macaroon": root_macaroon_raw})
+ "POST",
+ "http://charmhub.example/v1/tokens",
+ json={"macaroon": root_macaroon_raw},
+ )
responses.add(
- "POST", "https://candid.test/discharge", status=401,
+ "POST",
+ "https://candid.test/discharge",
+ status=401,
json={
"Code": "interaction required",
"Message": (
- "macaroon discharge required: authentication required"),
+ "macaroon discharge required: authentication required"
+ ),
"Info": {
"InteractionMethods": {
"browser-redirect": {
"LoginURL": "https://candid.test/login-redirect",
- },
},
},
- })
+ },
+ },
+ )
browser.getControl("Begin authorization").click()
+ tokens_matcher = MatchesStructure(
+ url=Equals("http://charmhub.example/v1/tokens"),
+ method=Equals("POST"),
+ body=AfterPreprocessing(
+ lambda b: json.loads(b.decode()),
+ Equals(
+ {
+ "description": (
+ "{} for launchpad.test".format(store_name)
+ ),
+ "packages": [
+ {
+ "type": "charm",
+ "name": store_name,
+ },
+ ],
+ "permissions": [
+ "package-manage-releases",
+ "package-manage-revisions",
+ "package-view-revisions",
+ ],
+ }
+ ),
+ ),
+ )
+ discharge_matcher = MatchesStructure(
+ url=Equals("https://candid.test/discharge"),
+ headers=ContainsDict(
+ {
+ "Content-Type": Equals(
+ "application/x-www-form-urlencoded"
+ ),
+ }
+ ),
+ body=AfterPreprocessing(
+ parse_qs,
+ MatchesDict(
+ {
+ "id64": Equals(
+ [base64.b64encode(caveat.caveat_id_bytes).decode()]
+ ),
+ }
+ ),
+ ),
+ )
+ self.assertThat(
+ responses.calls,
+ MatchesListwise(
+ [
+ MatchesStructure(request=tokens_matcher),
+ MatchesStructure(request=discharge_matcher),
+ ]
+ ),
+ )
with person_logged_in(owner):
- self.assertThat(responses.calls, MatchesListwise([
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("http://charmhub.example/v1/tokens"),
- method=Equals("POST"),
- body=AfterPreprocessing(
- lambda b: json.loads(b.decode()),
- Equals({
- "description": (
- "{} for launchpad.test".format(
- self.recipe.store_name)),
- "packages": [
- {"type": "charm",
- "name": self.recipe.store_name},
- ],
- "permissions": [
- "package-manage-releases",
- "package-manage-revisions",
- "package-view-revisions",
- ],
- })))),
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("https://candid.test/discharge"),
- headers=ContainsDict({
- "Content-Type": Equals(
- "application/x-www-form-urlencoded"),
- }),
- body=AfterPreprocessing(parse_qs, MatchesDict({
- "id64": Equals(
- [base64.b64encode(
- caveat.caveat_id_bytes).decode()]),
- })))),
- ]))
self.assertEqual(
- {"root": root_macaroon_raw}, self.recipe.store_secrets)
+ {"root": root_macaroon_raw}, self.recipe.store_secrets
+ )
self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
+ return_to_matcher = AfterPreprocessing(
+ urlsplit,
+ MatchesStructure(
+ scheme=Equals("http"),
+ netloc=Equals("launchpad.test"),
+ path=Equals("/+candid-callback"),
+ query=AfterPreprocessing(
+ parse_qs,
+ MatchesDict(
+ {
+ "starting_url": Equals(
+ [recipe_url + "/+authorize"]
+ ),
+ "discharge_macaroon_action": Equals(
+ ["field.actions.complete"]
+ ),
+ "discharge_macaroon_field": Equals(
+ ["field.discharge_macaroon"]
+ ),
+ }
+ ),
+ ),
+ fragment=Equals(""),
+ ),
+ )
self.assertThat(
urlsplit(browser.headers["Location"]),
MatchesStructure(
scheme=Equals("https"),
netloc=Equals("candid.test"),
path=Equals("/login-redirect"),
- query=AfterPreprocessing(parse_qs, ContainsDict({
- "return_to": MatchesListwise([
- AfterPreprocessing(urlsplit, MatchesStructure(
- scheme=Equals("http"),
- netloc=Equals("launchpad.test"),
- path=Equals("/+candid-callback"),
- query=AfterPreprocessing(parse_qs, MatchesDict({
- "starting_url": Equals(
- [recipe_url + "/+authorize"]),
- "discharge_macaroon_action": Equals(
- ["field.actions.complete"]),
- "discharge_macaroon_field": Equals(
- ["field.discharge_macaroon"]),
- })),
- fragment=Equals(""))),
- ]),
- })),
- fragment=Equals("")))
+ query=AfterPreprocessing(
+ parse_qs,
+ ContainsDict(
+ {"return_to": MatchesListwise([return_to_matcher])}
+ ),
+ ),
+ fragment=Equals(""),
+ ),
+ )
def test_complete_authorization_missing_discharge_macaroon(self):
# If the form does not include a discharge macaroon, the "complete"
@@ -666,17 +833,22 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
with person_logged_in(self.recipe.owner):
self.recipe.store_secrets = {
"root": Macaroon(version=2).serialize(JsonSerializer()),
- }
+ }
transaction.commit()
form = {"field.actions.complete": "1"}
view = create_initialized_view(
- self.recipe, "+authorize", form=form, method="POST",
- principal=self.recipe.owner)
+ self.recipe,
+ "+authorize",
+ form=form,
+ method="POST",
+ principal=self.recipe.owner,
+ )
html = view()
self.assertEqual(
- "Uploads of %s to Charmhub were not authorized." %
- self.recipe.name,
- get_feedback_messages(html)[1])
+ "Uploads of %s to Charmhub were not authorized."
+ % self.recipe.name,
+ get_feedback_messages(html)[1],
+ )
self.assertNotIn("exchanged_encrypted", self.recipe.store_secrets)
@responses.activate
@@ -688,86 +860,122 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
self.pushConfig(
"charms",
charmhub_secrets_public_key=base64.b64encode(
- bytes(private_key.public_key)).decode())
+ bytes(private_key.public_key)
+ ).decode(),
+ )
root_macaroon = Macaroon(version=2)
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
unbound_discharge_macaroon = Macaroon(version=2)
unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
- JsonSerializer())
+ JsonSerializer()
+ )
discharge_macaroon_raw = root_macaroon.prepare_for_request(
- unbound_discharge_macaroon).serialize(JsonSerializer())
+ unbound_discharge_macaroon
+ ).serialize(JsonSerializer())
exchanged_macaroon = Macaroon(version=2)
exchanged_macaroon_raw = exchanged_macaroon.serialize(JsonSerializer())
responses.add(
- "POST", "http://charmhub.example/v1/tokens/exchange",
- json={"macaroon": exchanged_macaroon_raw})
+ "POST",
+ "http://charmhub.example/v1/tokens/exchange",
+ json={"macaroon": exchanged_macaroon_raw},
+ )
with person_logged_in(self.recipe.owner):
self.recipe.store_secrets = {"root": root_macaroon_raw}
transaction.commit()
form = {
"field.actions.complete": "1",
"field.discharge_macaroon": unbound_discharge_macaroon_raw,
- }
+ }
view = create_initialized_view(
- self.recipe, "+authorize", form=form, method="POST",
- principal=self.recipe.owner)
+ self.recipe,
+ "+authorize",
+ form=form,
+ method="POST",
+ principal=self.recipe.owner,
+ )
self.assertEqual(302, view.request.response.getStatus())
self.assertEqual(
canonical_url(self.recipe),
- view.request.response.getHeader("Location"))
+ view.request.response.getHeader("Location"),
+ )
self.assertEqual(
- "Uploads of %s to Charmhub are now authorized." %
- self.recipe.name,
- view.request.response.notifications[0].message)
+ "Uploads of %s to Charmhub are now authorized."
+ % self.recipe.name,
+ view.request.response.notifications[0].message,
+ )
self.pushConfig(
"charms",
charmhub_secrets_private_key=base64.b64encode(
- bytes(private_key)).decode())
+ bytes(private_key)
+ ).decode(),
+ )
container = getUtility(IEncryptedContainer, "charmhub-secrets")
- self.assertThat(self.recipe.store_secrets, MatchesDict({
- "exchanged_encrypted": AfterPreprocessing(
- lambda data: container.decrypt(data).decode(),
- Equals(exchanged_macaroon_raw)),
- }))
- self.assertThat(responses.calls, MatchesListwise([
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("http://charmhub.example/v1/tokens/exchange"),
- method=Equals("POST"),
- headers=ContainsDict({
- "Macaroons": AfterPreprocessing(
- lambda v: json.loads(
- base64.b64decode(v.encode()).decode()),
- Equals([
- json.loads(m) for m in (
+ self.assertThat(
+ self.recipe.store_secrets,
+ MatchesDict(
+ {
+ "exchanged_encrypted": AfterPreprocessing(
+ lambda data: container.decrypt(data).decode(),
+ Equals(exchanged_macaroon_raw),
+ ),
+ }
+ ),
+ )
+ exchange_matcher = MatchesStructure(
+ url=Equals("http://charmhub.example/v1/tokens/exchange"),
+ method=Equals("POST"),
+ headers=ContainsDict(
+ {
+ "Macaroons": AfterPreprocessing(
+ lambda v: json.loads(
+ base64.b64decode(v.encode()).decode()
+ ),
+ Equals(
+ [
+ json.loads(m)
+ for m in (
root_macaroon_raw,
- discharge_macaroon_raw)])),
- }),
- body=AfterPreprocessing(
- lambda b: json.loads(b.decode()),
- Equals({})))),
- ]))
+ discharge_macaroon_raw,
+ )
+ ]
+ ),
+ ),
+ }
+ ),
+ body=AfterPreprocessing(
+ lambda b: json.loads(b.decode()), Equals({})
+ ),
+ )
+ self.assertThat(
+ responses.calls,
+ MatchesListwise([MatchesStructure(request=exchange_matcher)]),
+ )
class TestCharmRecipeDeleteView(BaseTestCharmRecipeView):
-
def test_unauthorized(self):
# A user without edit access cannot delete a charm recipe.
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person)
+ registrant=self.person, owner=self.person
+ )
recipe_url = canonical_url(recipe)
other_person = self.factory.makePerson()
browser = self.getViewBrowser(recipe, user=other_person)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Delete charm recipe")
+ LinkNotFoundError, browser.getLink, "Delete charm recipe"
+ )
self.assertRaises(
- Unauthorized, self.getUserBrowser, recipe_url + "/+delete",
- user=other_person)
+ Unauthorized,
+ self.getUserBrowser,
+ recipe_url + "/+delete",
+ user=other_person,
+ )
def test_delete_recipe_without_builds(self):
# A charm recipe without builds can be deleted.
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person)
+ registrant=self.person, owner=self.person
+ )
recipe_url = canonical_url(recipe)
owner_url = canonical_url(self.person)
browser = self.getViewBrowser(recipe, user=self.person)
@@ -779,7 +987,8 @@ class TestCharmRecipeDeleteView(BaseTestCharmRecipeView):
def test_delete_recipe_with_builds(self):
# A charm recipe with builds can be deleted.
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person)
+ registrant=self.person, owner=self.person
+ )
build = self.factory.makeCharmRecipeBuild(recipe=recipe)
self.factory.makeCharmFile(build=build)
recipe_url = canonical_url(recipe)
@@ -792,18 +1001,21 @@ class TestCharmRecipeDeleteView(BaseTestCharmRecipeView):
class TestCharmRecipeView(BaseTestCharmRecipeView):
-
def setUp(self):
super().setUp()
self.project = self.factory.makeProduct(
- name="test-project", displayname="Test Project")
+ name="test-project", displayname="Test Project"
+ )
self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
self.distroseries = self.factory.makeDistroSeries(
- distribution=self.ubuntu)
+ distribution=self.ubuntu
+ )
processor = getUtility(IProcessorSet).getByName("386")
self.distroarchseries = self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag="i386",
- processor=processor)
+ distroseries=self.distroseries,
+ architecturetag="i386",
+ processor=processor,
+ )
self.factory.makeBuilder(virtualized=True)
def makeCharmRecipe(self, **kwargs):
@@ -812,8 +1024,11 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
if "git_ref" not in kwargs:
kwargs["git_ref"] = self.factory.makeGitRefs()[0]
return self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person, name="charm-name",
- **kwargs)
+ registrant=self.person,
+ owner=self.person,
+ name="charm-name",
+ **kwargs,
+ )
def makeBuild(self, recipe=None, date_created=None, **kwargs):
if recipe is None:
@@ -821,11 +1036,15 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
if date_created is None:
date_created = datetime.now(pytz.UTC) - timedelta(hours=1)
build = self.factory.makeCharmRecipeBuild(
- requester=self.person, recipe=recipe,
+ requester=self.person,
+ recipe=recipe,
distro_arch_series=self.distroarchseries,
- date_created=date_created, **kwargs)
+ date_created=date_created,
+ **kwargs,
+ )
job = removeSecurityProxy(
- removeSecurityProxy(build.build_request)._job)
+ removeSecurityProxy(build.build_request)._job
+ )
job.job._status = JobStatus.COMPLETED
return build
@@ -834,34 +1053,53 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
view = create_view(recipe, "+index")
# To test the breadcrumbs we need a correct traversal stack.
view.request.traversed_objects = [
- recipe.owner, recipe.project, recipe, view]
+ recipe.owner,
+ recipe.project,
+ recipe,
+ view,
+ ]
view.initialize()
breadcrumbs_tag = soupmatchers.Tag(
- "breadcrumbs", "ol", attrs={"class": "breadcrumbs"})
+ "breadcrumbs", "ol", attrs={"class": "breadcrumbs"}
+ )
self.assertThat(
view(),
soupmatchers.HTMLContains(
soupmatchers.Within(
breadcrumbs_tag,
soupmatchers.Tag(
- "project breadcrumb", "a",
+ "project breadcrumb",
+ "a",
text="Test Project",
- attrs={"href": re.compile(r"/test-project$")})),
+ attrs={"href": re.compile(r"/test-project$")},
+ ),
+ ),
soupmatchers.Within(
breadcrumbs_tag,
soupmatchers.Tag(
- "charm breadcrumb", "li",
- text=re.compile(r"\scharm-name\s")))))
+ "charm breadcrumb",
+ "li",
+ text=re.compile(r"\scharm-name\s"),
+ ),
+ ),
+ ),
+ )
def test_index_git(self):
[ref] = self.factory.makeGitRefs(
- owner=self.person, target=self.project, name="charm-repository",
- paths=["refs/heads/master"])
+ owner=self.person,
+ target=self.project,
+ name="charm-repository",
+ paths=["refs/heads/master"],
+ )
recipe = self.makeCharmRecipe(git_ref=ref)
build = self.makeBuild(
- recipe=recipe, status=BuildStatus.FULLYBUILT,
- duration=timedelta(minutes=30))
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+ recipe=recipe,
+ status=BuildStatus.FULLYBUILT,
+ duration=timedelta(minutes=30),
+ )
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""\
Test Project
charm-name
.*
@@ -876,46 +1114,59 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
Latest builds
Status When complete Architecture
Successfully built 30 minutes ago i386
- """, self.getMainText(build.recipe))
+ """,
+ self.getMainText(build.recipe),
+ )
def test_index_success_with_buildlog(self):
# The build log is shown if it is there.
build = self.makeBuild(
- status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30))
+ status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30)
+ )
build.setLog(self.factory.makeLibraryFileAlias())
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""\
Latest builds
Status When complete Architecture
Successfully built 30 minutes ago buildlog \(.*\) i386
- """, self.getMainText(build.recipe))
+ """,
+ self.getMainText(build.recipe),
+ )
def test_index_no_builds(self):
# A message is shown when there are no builds.
recipe = self.makeCharmRecipe()
self.assertIn(
"This charm recipe has not been built yet.",
- self.getMainText(recipe))
+ self.getMainText(recipe),
+ )
def test_index_pending_build(self):
# A pending build is listed as such.
build = self.makeBuild()
build.queueBuild()
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""\
Latest builds
Status When complete Architecture
Needs building in .* \(estimated\) i386
- """, self.getMainText(build.recipe))
+ """,
+ self.getMainText(build.recipe),
+ )
def test_index_pending_build_request(self):
# A pending build request is listed as such.
recipe = self.makeCharmRecipe()
with person_logged_in(recipe.owner):
recipe.requestBuilds(recipe.owner)
- self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """\
Latest builds
Status When complete Architecture
Pending build request
- """, self.getMainText(recipe))
+ """,
+ self.getMainText(recipe),
+ )
def test_index_failed_build_request(self):
# A failed build request is listed as such, with its error message.
@@ -926,17 +1177,22 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
job.job._status = JobStatus.FAILED
job.job.date_finished = datetime.now(pytz.UTC) - timedelta(hours=1)
job.error_message = "Boom"
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""\
Latest builds
Status When complete Architecture
Failed build request 1 hour ago \(Boom\)
- """, self.getMainText(recipe))
+ """,
+ self.getMainText(recipe),
+ )
def setStatus(self, build, status):
build.updateStatus(
- BuildStatus.BUILDING, date_started=build.date_created)
+ BuildStatus.BUILDING, date_started=build.date_created
+ )
build.updateStatus(
- status, date_finished=build.date_started + timedelta(minutes=30))
+ status, date_finished=build.date_started + timedelta(minutes=30)
+ )
def test_builds_and_requests(self):
# CharmRecipeView.builds_and_requests produces reasonable results,
@@ -944,10 +1200,12 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
recipe = self.makeCharmRecipe()
# Create oldest builds first so that they sort properly by id.
date_gen = time_counter(
- datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1))
+ datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1)
+ )
builds = [
self.makeBuild(recipe=recipe, date_created=next(date_gen))
- for i in range(3)]
+ for i in range(3)
+ ]
self.setStatus(builds[2], BuildStatus.FULLYBUILT)
with person_logged_in(recipe.owner):
request = recipe.requestBuilds(recipe.owner)
@@ -956,34 +1214,49 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
view = CharmRecipeView(recipe, None)
# The pending build request is interleaved in date order with
# pending builds, and these are followed by completed builds.
- self.assertThat(view.builds_and_requests, MatchesListwise([
- MatchesStructure.byEquality(id=request.id),
- Equals(builds[1]),
- Equals(builds[0]),
- Equals(builds[2]),
- ]))
+ self.assertThat(
+ view.builds_and_requests,
+ MatchesListwise(
+ [
+ MatchesStructure.byEquality(id=request.id),
+ Equals(builds[1]),
+ Equals(builds[0]),
+ Equals(builds[2]),
+ ]
+ ),
+ )
transaction.commit()
builds.append(self.makeBuild(recipe=recipe))
del get_property_cache(view).builds_and_requests
- self.assertThat(view.builds_and_requests, MatchesListwise([
- Equals(builds[3]),
- MatchesStructure.byEquality(id=request.id),
- Equals(builds[1]),
- Equals(builds[0]),
- Equals(builds[2]),
- ]))
+ self.assertThat(
+ view.builds_and_requests,
+ MatchesListwise(
+ [
+ Equals(builds[3]),
+ MatchesStructure.byEquality(id=request.id),
+ Equals(builds[1]),
+ Equals(builds[0]),
+ Equals(builds[2]),
+ ]
+ ),
+ )
# If we pretend that the job failed, it is still listed, but after
# any pending builds.
job.job._status = JobStatus.FAILED
job.job.date_finished = job.date_created + timedelta(minutes=30)
del get_property_cache(view).builds_and_requests
- self.assertThat(view.builds_and_requests, MatchesListwise([
- Equals(builds[3]),
- Equals(builds[1]),
- Equals(builds[0]),
- MatchesStructure.byEquality(id=request.id),
- Equals(builds[2]),
- ]))
+ self.assertThat(
+ view.builds_and_requests,
+ MatchesListwise(
+ [
+ Equals(builds[3]),
+ Equals(builds[1]),
+ Equals(builds[0]),
+ MatchesStructure.byEquality(id=request.id),
+ Equals(builds[2]),
+ ]
+ ),
+ )
def test_store_channels_empty(self):
recipe = self.factory.makeCharmRecipe()
@@ -992,35 +1265,44 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
def test_store_channels_display(self):
recipe = self.factory.makeCharmRecipe(
- store_channels=["track/stable/fix-123", "track/edge/fix-123"])
+ store_channels=["track/stable/fix-123", "track/edge/fix-123"]
+ )
view = create_initialized_view(recipe, "+index")
self.assertEqual(
- "track/stable/fix-123, track/edge/fix-123", view.store_channels)
+ "track/stable/fix-123, track/edge/fix-123", view.store_channels
+ )
class TestCharmRecipeRequestBuildsView(BaseTestCharmRecipeView):
-
def setUp(self):
super().setUp()
self.project = self.factory.makeProduct(
- name="test-project", displayname="Test Project")
+ name="test-project", displayname="Test Project"
+ )
self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
self.distroseries = self.factory.makeDistroSeries(
- distribution=self.ubuntu)
+ distribution=self.ubuntu
+ )
self.architectures = []
for processor, architecture in ("386", "i386"), ("amd64", "amd64"):
das = self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag=architecture,
- processor=getUtility(IProcessorSet).getByName(processor))
+ distroseries=self.distroseries,
+ architecturetag=architecture,
+ processor=getUtility(IProcessorSet).getByName(processor),
+ )
das.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
self.architectures.append(das)
self.recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person, project=self.project,
- name="charm-name")
+ registrant=self.person,
+ owner=self.person,
+ project=self.project,
+ name="charm-name",
+ )
def test_request_builds_page(self):
# The +request-builds page is sensible.
- self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ r"""
Request builds for charm-name
Test Project
charm-name
@@ -1036,36 +1318,45 @@ class TestCharmRecipeRequestBuildsView(BaseTestCharmRecipeView):
or
Cancel
""",
- self.getMainText(self.recipe, "+request-builds", user=self.person))
+ self.getMainText(self.recipe, "+request-builds", user=self.person),
+ )
def test_request_builds_not_owner(self):
# A user without launchpad.Edit cannot request builds.
self.assertRaises(
- Unauthorized, self.getViewBrowser, self.recipe, "+request-builds")
+ Unauthorized, self.getViewBrowser, self.recipe, "+request-builds"
+ )
def test_request_builds_action(self):
# Requesting a build creates a pending build request.
browser = self.getViewBrowser(
- self.recipe, "+request-builds", user=self.person)
+ self.recipe, "+request-builds", user=self.person
+ )
browser.getControl("Request builds").click()
login_person(self.person)
[request] = self.recipe.pending_build_requests
- self.assertThat(removeSecurityProxy(request), MatchesStructure(
- recipe=Equals(self.recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- builds=AfterPreprocessing(list, Equals([])),
- _job=MatchesStructure(
- requester=Equals(self.person),
- channels=Equals({}),
- architectures=Is(None))))
+ self.assertThat(
+ removeSecurityProxy(request),
+ MatchesStructure(
+ recipe=Equals(self.recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ error_message=Is(None),
+ builds=AfterPreprocessing(list, Equals([])),
+ _job=MatchesStructure(
+ requester=Equals(self.person),
+ channels=Equals({}),
+ architectures=Is(None),
+ ),
+ ),
+ )
def test_request_builds_channels(self):
# Selecting different channels creates a build request using those
# channels.
browser = self.getViewBrowser(
- self.recipe, "+request-builds", user=self.person)
+ self.recipe, "+request-builds", user=self.person
+ )
browser.getControl(name="field.channels.core").value = "edge"
browser.getControl("Request builds").click()
diff --git a/lib/lp/charms/browser/tests/test_charmrecipebuild.py b/lib/lp/charms/browser/tests/test_charmrecipebuild.py
index 0cbc484..83df915 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipebuild.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipebuild.py
@@ -5,12 +5,12 @@
import re
+import soupmatchers
+import transaction
from fixtures import FakeLogger
from pymacaroons import Macaroon
-import soupmatchers
from storm.locals import Store
from testtools.matchers import StartsWith
-import transaction
from zope.component import getUtility
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
@@ -27,19 +27,16 @@ from lp.services.webapp import canonical_url
from lp.testing import (
ANONYMOUS,
BrowserTestCase,
+ TestCaseWithFactory,
login,
person_logged_in,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
from lp.testing.pages import (
extract_text,
find_main_content,
find_tags_by_class,
- )
+)
from lp.testing.views import create_initialized_view
@@ -55,14 +52,18 @@ class TestCanonicalUrlForCharmRecipeBuild(TestCaseWithFactory):
owner = self.factory.makePerson(name="person")
project = self.factory.makeProduct(name="charm-project")
recipe = self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, project=project, name="charm")
+ registrant=owner, owner=owner, project=project, name="charm"
+ )
build = self.factory.makeCharmRecipeBuild(
- requester=owner, recipe=recipe)
+ requester=owner, recipe=recipe
+ )
self.assertThat(
canonical_url(build),
StartsWith(
"http://launchpad.test/~person/charm-project/+charm/charm/"
- "+build/"))
+ "+build/"
+ ),
+ )
class TestCharmRecipeBuildView(TestCaseWithFactory):
@@ -76,12 +77,14 @@ class TestCharmRecipeBuildView(TestCaseWithFactory):
def test_files(self):
# CharmRecipeBuildView.files returns all the associated files.
build = self.factory.makeCharmRecipeBuild(
- status=BuildStatus.FULLYBUILT)
+ status=BuildStatus.FULLYBUILT
+ )
charm_file = self.factory.makeCharmFile(build=build)
build_view = create_initialized_view(build, "+index")
self.assertEqual(
[charm_file.library_file.filename],
- [lfa.filename for lfa in build_view.files])
+ [lfa.filename for lfa in build_view.files],
+ )
# Deleted files won't be included.
self.assertFalse(charm_file.library_file.deleted)
removeSecurityProxy(charm_file.library_file).content = None
@@ -92,69 +95,106 @@ class TestCharmRecipeBuildView(TestCaseWithFactory):
def test_revision_id(self):
build = self.factory.makeCharmRecipeBuild()
build.updateStatus(
- BuildStatus.FULLYBUILT, worker_status={"revision_id": "dummy"})
+ BuildStatus.FULLYBUILT, worker_status={"revision_id": "dummy"}
+ )
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "revision ID", "li", attrs={"id": "revision-id"},
- text=re.compile(r"^\s*Revision: dummy\s*$"))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "revision ID",
+ "li",
+ attrs={"id": "revision-id"},
+ text=re.compile(r"^\s*Revision: dummy\s*$"),
+ )
+ ),
+ )
def test_store_upload_status_in_progress(self):
build = self.factory.makeCharmRecipeBuild(
- status=BuildStatus.FULLYBUILT)
+ status=BuildStatus.FULLYBUILT
+ )
getUtility(ICharmhubUploadJobSource).create(build)
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "store upload status", "li",
- attrs={"id": "store-upload-status"},
- text=re.compile(r"^\s*Charmhub upload in progress\s*$"))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "store upload status",
+ "li",
+ attrs={"id": "store-upload-status"},
+ text=re.compile(r"^\s*Charmhub upload in progress\s*$"),
+ )
+ ),
+ )
def test_store_upload_status_completed(self):
build = self.factory.makeCharmRecipeBuild(
- status=BuildStatus.FULLYBUILT)
+ status=BuildStatus.FULLYBUILT
+ )
job = getUtility(ICharmhubUploadJobSource).create(build)
naked_job = removeSecurityProxy(job)
naked_job.job._status = JobStatus.COMPLETED
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "store upload status", "li",
- attrs={"id": "store-upload-status"},
- text=re.compile(r"^\s*Uploaded to Charmhub\s*$"))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "store upload status",
+ "li",
+ attrs={"id": "store-upload-status"},
+ text=re.compile(r"^\s*Uploaded to Charmhub\s*$"),
+ )
+ ),
+ )
def test_store_upload_status_failed(self):
build = self.factory.makeCharmRecipeBuild(
- status=BuildStatus.FULLYBUILT)
+ status=BuildStatus.FULLYBUILT
+ )
job = getUtility(ICharmhubUploadJobSource).create(build)
naked_job = removeSecurityProxy(job)
naked_job.job._status = JobStatus.FAILED
naked_job.error_message = "Review failed."
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "store upload status", "li",
- attrs={"id": "store-upload-status"},
- text=re.compile(
- r"^\s*Charmhub upload failed:\s+"
- r"Review failed.\s*$"))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "store upload status",
+ "li",
+ attrs={"id": "store-upload-status"},
+ text=re.compile(
+ r"^\s*Charmhub upload failed:\s+" r"Review failed.\s*$"
+ ),
+ )
+ ),
+ )
def test_store_upload_status_release_failed(self):
build = self.factory.makeCharmRecipeBuild(
- status=BuildStatus.FULLYBUILT)
+ status=BuildStatus.FULLYBUILT
+ )
job = getUtility(ICharmhubUploadJobSource).create(build)
naked_job = removeSecurityProxy(job)
naked_job.job._status = JobStatus.FAILED
naked_job.store_revision = 1
naked_job.error_message = "Failed to publish"
build_view = create_initialized_view(build, "+index")
- self.assertThat(build_view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "store upload status", "li",
- attrs={"id": "store-upload-status"},
- text=re.compile(
- r"^\s*Charmhub release failed:\s+"
- r"Failed to publish\s*$"))))
+ self.assertThat(
+ build_view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "store upload status",
+ "li",
+ attrs={"id": "store-upload-status"},
+ text=re.compile(
+ r"^\s*Charmhub release failed:\s+"
+ r"Failed to publish\s*$"
+ ),
+ )
+ ),
+ )
class TestCharmRecipeBuildOperations(BrowserTestCase):
@@ -169,7 +209,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
self.build_url = canonical_url(self.build)
self.requester = self.build.requester
self.buildd_admin = self.factory.makePerson(
- member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
+ member_of=[getUtility(ILaunchpadCelebrities).buildd_admin]
+ )
def test_retry_build(self):
# The requester of a build can retry it.
@@ -190,10 +231,14 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
user = self.factory.makePerson()
browser = self.getViewBrowser(self.build, user=user)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Retry this build")
+ LinkNotFoundError, browser.getLink, "Retry this build"
+ )
self.assertRaises(
- Unauthorized, self.getUserBrowser, self.build_url + "/+retry",
- user=user)
+ Unauthorized,
+ self.getUserBrowser,
+ self.build_url + "/+retry",
+ user=user,
+ )
def test_retry_build_wrong_state(self):
# If the build isn't in an unsuccessful terminal state, you can't
@@ -201,7 +246,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
self.build.updateStatus(BuildStatus.FULLYBUILT)
browser = self.getViewBrowser(self.build, user=self.requester)
self.assertRaises(
- LinkNotFoundError, browser.getLink, "Retry this build")
+ LinkNotFoundError, browser.getLink, "Retry this build"
+ )
def test_cancel_build(self):
# The requester of a build can cancel it.
@@ -223,8 +269,11 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
browser = self.getViewBrowser(self.build, user=user)
self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
self.assertRaises(
- Unauthorized, self.getUserBrowser, self.build_url + "/+cancel",
- user=user)
+ Unauthorized,
+ self.getUserBrowser,
+ self.build_url + "/+cancel",
+ user=user,
+ )
def test_cancel_build_wrong_state(self):
# If the build isn't queued, you can't cancel it.
@@ -255,7 +304,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
browser.getControl("Rescore build").click()
self.assertEqual(
"Invalid integer data",
- extract_text(find_tags_by_class(browser.contents, "message")[1]))
+ extract_text(find_tags_by_class(browser.contents, "message")[1]),
+ )
def test_rescore_build_not_admin(self):
# A non-admin user cannot cancel a build.
@@ -265,8 +315,11 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
browser = self.getViewBrowser(self.build, user=user)
self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
self.assertRaises(
- Unauthorized, self.getUserBrowser, self.build_url + "/+rescore",
- user=user)
+ Unauthorized,
+ self.getUserBrowser,
+ self.build_url + "/+rescore",
+ user=user,
+ )
def test_rescore_build_wrong_state(self):
# If the build isn't NEEDSBUILD, you can't rescore it.
@@ -283,12 +336,20 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
with person_logged_in(self.requester):
self.build.cancel()
browser = self.getViewBrowser(
- self.build, "+rescore", user=self.buildd_admin)
+ self.build, "+rescore", user=self.buildd_admin
+ )
self.assertEqual(self.build_url, browser.url)
- self.assertThat(browser.contents, soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "notification", "div", attrs={"class": "warning message"},
- text="Cannot rescore this build because it is not queued.")))
+ self.assertThat(
+ browser.contents,
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "notification",
+ "div",
+ attrs={"class": "warning message"},
+ text="Cannot rescore this build because it is not queued.",
+ )
+ ),
+ )
def setUpStoreUpload(self):
self.pushConfig("charms", charmhub_url="http://charmhub.example/")
@@ -298,7 +359,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
# "exchanged_encrypted" is present, so don't bother setting up
# encryption keys here.
self.build.recipe.store_secrets = {
- "exchanged_encrypted": Macaroon().serialize()}
+ "exchanged_encrypted": Macaroon().serialize()
+ }
def test_store_upload(self):
# A build not previously uploaded to Charmhub can be uploaded
@@ -307,7 +369,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
self.build.updateStatus(BuildStatus.FULLYBUILT)
self.factory.makeCharmFile(
build=self.build,
- library_file=self.factory.makeLibraryFileAlias(db_only=True))
+ library_file=self.factory.makeLibraryFileAlias(db_only=True),
+ )
browser = self.getViewBrowser(self.build, user=self.requester)
browser.getControl("Upload this charm to Charmhub").click()
self.assertEqual(self.build_url, browser.url)
@@ -317,7 +380,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
self.assertEqual(self.build, job.build)
self.assertEqual(
"An upload has been scheduled and will run as soon as possible.",
- extract_text(find_tags_by_class(browser.contents, "message")[0]))
+ extract_text(find_tags_by_class(browser.contents, "message")[0]),
+ )
def test_store_upload_retry(self):
# A build with a previously-failed Charmhub upload can have the
@@ -326,7 +390,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
self.build.updateStatus(BuildStatus.FULLYBUILT)
self.factory.makeCharmFile(
build=self.build,
- library_file=self.factory.makeLibraryFileAlias(db_only=True))
+ library_file=self.factory.makeLibraryFileAlias(db_only=True),
+ )
old_job = getUtility(ICharmhubUploadJobSource).create(self.build)
removeSecurityProxy(old_job).job._status = JobStatus.FAILED
browser = self.getViewBrowser(self.build, user=self.requester)
@@ -338,7 +403,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
self.assertEqual(self.build, job.build)
self.assertEqual(
"An upload has been scheduled and will run as soon as possible.",
- extract_text(find_tags_by_class(browser.contents, "message")[0]))
+ extract_text(find_tags_by_class(browser.contents, "message")[0]),
+ )
def test_store_upload_error_notifies(self):
# If a build cannot be scheduled for uploading to Charmhub, we issue
@@ -350,26 +416,31 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
self.assertEqual(self.build_url, browser.url)
login(ANONYMOUS)
self.assertEqual(
- [], list(getUtility(ICharmhubUploadJobSource).iterReady()))
+ [], list(getUtility(ICharmhubUploadJobSource).iterReady())
+ )
self.assertEqual(
"Cannot upload this charm because it has no files.",
- extract_text(find_tags_by_class(browser.contents, "message")[0]))
+ extract_text(find_tags_by_class(browser.contents, "message")[0]),
+ )
def test_builder_history(self):
Store.of(self.build).flush()
self.build.updateStatus(
- BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder())
+ BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder()
+ )
title = self.build.title
browser = self.getViewBrowser(self.build.builder, "+history")
self.assertTextMatchesExpressionIgnoreWhitespace(
r"Build history.*%s" % re.escape(title),
- extract_text(find_main_content(browser.contents)))
+ extract_text(find_main_content(browser.contents)),
+ )
self.assertEqual(self.build_url, browser.getLink(title).url)
def makeBuildingRecipe(self, information_type=InformationType.PUBLIC):
builder = self.factory.makeBuilder()
build = self.factory.makeCharmRecipeBuild(
- information_type=information_type)
+ information_type=information_type
+ )
build.updateStatus(BuildStatus.BUILDING, builder=builder)
build.queueBuild()
build.buildqueue_record.builder = builder
diff --git a/lib/lp/charms/browser/tests/test_charmrecipelisting.py b/lib/lp/charms/browser/tests/test_charmrecipelisting.py
index ceba165..cf94126 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipelisting.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipelisting.py
@@ -3,26 +3,17 @@
"""Test charm recipe listings."""
-from datetime import (
- datetime,
- timedelta,
- )
+from datetime import datetime, timedelta
from functools import partial
import pytz
import soupmatchers
-from testtools.matchers import (
- MatchesAll,
- Not,
- )
+from testtools.matchers import MatchesAll, Not
from zope.security.proxy import removeSecurityProxy
from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
from lp.code.tests.helpers import GitHostingFixture
-from lp.services.database.constants import (
- ONE_DAY_AGO,
- UTC_NOW,
- )
+from lp.services.database.constants import ONE_DAY_AGO, UTC_NOW
from lp.services.features.testing import MemoryFeatureFixture
from lp.services.webapp import canonical_url
from lp.testing import (
@@ -31,7 +22,7 @@ from lp.testing import (
login,
person_logged_in,
record_two_runs,
- )
+)
from lp.testing.layers import LaunchpadFunctionalLayer
from lp.testing.matchers import HasQueryCount
from lp.testing.views import create_initialized_view
@@ -41,16 +32,21 @@ class TestCharmRecipeListing(BrowserTestCase):
layer = LaunchpadFunctionalLayer
- def assertCharmRecipesLink(self, context, link_text,
- link_has_context=False, **kwargs):
+ def assertCharmRecipesLink(
+ self, context, link_text, link_has_context=False, **kwargs
+ ):
if link_has_context:
expected_href = canonical_url(context, view_name="+charm-recipes")
else:
expected_href = "+charm-recipes"
matcher = soupmatchers.HTMLContains(
soupmatchers.Tag(
- "View charm recipes link", "a", text=link_text,
- attrs={"href": expected_href}))
+ "View charm recipes link",
+ "a",
+ text=link_text,
+ attrs={"href": expected_href},
+ )
+ )
self.assertThat(self.getViewBrowser(context).contents, Not(matcher))
login(ANONYMOUS)
with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
@@ -71,14 +67,19 @@ class TestCharmRecipeListing(BrowserTestCase):
def test_person_links_to_recipes(self):
person = self.factory.makePerson()
self.assertCharmRecipesLink(
- person, "View charm recipes", link_has_context=True,
- registrant=person, owner=person)
+ person,
+ "View charm recipes",
+ link_has_context=True,
+ registrant=person,
+ owner=person,
+ )
def test_project_links_to_recipes(self):
project = self.factory.makeProduct()
[ref] = self.factory.makeGitRefs(target=project)
self.assertCharmRecipesLink(
- project, "View charm recipes", link_has_context=True, git_ref=ref)
+ project, "View charm recipes", link_has_context=True, git_ref=ref
+ )
def test_git_repository_recipe_listing(self):
# We can see charm recipes for a Git repository.
@@ -87,10 +88,13 @@ class TestCharmRecipeListing(BrowserTestCase):
with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
self.factory.makeCharmRecipe(git_ref=ref)
text = self.getMainText(repository, "+charm-recipes")
- self.assertTextMatchesExpressionIgnoreWhitespace("""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """
Charm recipes for lp:~.*
Name Owner Registered
- charm-name.* Team Name.* .*""", text)
+ charm-name.* Team Name.* .*""",
+ text,
+ )
def test_git_ref_recipe_listing(self):
# We can see charm recipes for a Git reference.
@@ -98,10 +102,13 @@ class TestCharmRecipeListing(BrowserTestCase):
with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
self.factory.makeCharmRecipe(git_ref=ref)
text = self.getMainText(ref, "+charm-recipes")
- self.assertTextMatchesExpressionIgnoreWhitespace("""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """
Charm recipes for ~.*:.*
Name Owner Registered
- charm-name.* Team Name.* .*""", text)
+ charm-name.* Team Name.* .*""",
+ text,
+ )
def test_person_recipe_listing(self):
# We can see charm recipes for a person.
@@ -109,13 +116,19 @@ class TestCharmRecipeListing(BrowserTestCase):
[ref] = self.factory.makeGitRefs()
with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, git_ref=ref,
- date_created=ONE_DAY_AGO)
+ registrant=owner,
+ owner=owner,
+ git_ref=ref,
+ date_created=ONE_DAY_AGO,
+ )
text = self.getMainText(owner, "+charm-recipes")
- self.assertTextMatchesExpressionIgnoreWhitespace("""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """
Charm recipes for Charm Owner
Name Source Registered
- charm-name.* ~.*:.* .*""", text)
+ charm-name.* ~.*:.* .*""",
+ text,
+ )
def test_project_recipe_listing(self):
# We can see charm recipes for a project.
@@ -124,16 +137,21 @@ class TestCharmRecipeListing(BrowserTestCase):
with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
self.factory.makeCharmRecipe(git_ref=ref, date_created=UTC_NOW)
text = self.getMainText(project, "+charm-recipes")
- self.assertTextMatchesExpressionIgnoreWhitespace("""
+ self.assertTextMatchesExpressionIgnoreWhitespace(
+ """
Charm recipes for Charmable
Name Owner Source Registered
- charm-name.* Team Name.* ~.*:.* .*""", text)
+ charm-name.* Team Name.* ~.*:.* .*""",
+ text,
+ )
def assertCharmRecipesQueryCount(self, context, item_creator):
self.pushConfig("launchpad", default_batch_size=10)
recorder1, recorder2 = record_two_runs(
lambda: self.getMainText(context, "+charm-recipes"),
- item_creator, 5)
+ item_creator,
+ 5,
+ )
self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
def test_git_repository_query_count(self):
@@ -199,34 +217,48 @@ class TestCharmRecipeListing(BrowserTestCase):
with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
recipes = [create_recipe() for i in range(count)]
for i, recipe in enumerate(recipes):
- removeSecurityProxy(recipe).date_last_modified = (
- start_time - timedelta(seconds=i))
+ removeSecurityProxy(
+ recipe
+ ).date_last_modified = start_time - timedelta(seconds=i)
return [
soupmatchers.Tag(
- "charm recipe link", "a", text=recipe.name,
+ "charm recipe link",
+ "a",
+ text=recipe.name,
attrs={
- "href": canonical_url(recipe, path_only_if_possible=True)})
- for recipe in recipes]
+ "href": canonical_url(recipe, path_only_if_possible=True)
+ },
+ )
+ for recipe in recipes
+ ]
def assertBatches(self, context, link_matchers, batched, start, size):
view = create_initialized_view(context, "+charm-recipes")
listing_tag = soupmatchers.Tag(
- "charm recipe listing", "table",
- attrs={"class": "listing sortable"})
+ "charm recipe listing",
+ "table",
+ attrs={"class": "listing sortable"},
+ )
batch_nav_tag = soupmatchers.Tag(
- "batch nav links", "td",
- attrs={"class": "batch-navigation-links"})
+ "batch nav links", "td", attrs={"class": "batch-navigation-links"}
+ )
present_links = ([batch_nav_tag] if batched else []) + [
- matcher for i, matcher in enumerate(link_matchers)
- if i in range(start, start + size)]
+ matcher
+ for i, matcher in enumerate(link_matchers)
+ if i in range(start, start + size)
+ ]
absent_links = ([] if batched else [batch_nav_tag]) + [
- matcher for i, matcher in enumerate(link_matchers)
- if i not in range(start, start + size)]
+ matcher
+ for i, matcher in enumerate(link_matchers)
+ if i not in range(start, start + size)
+ ]
self.assertThat(
view.render(),
MatchesAll(
soupmatchers.HTMLContains(listing_tag, *present_links),
- Not(soupmatchers.HTMLContains(*absent_links))))
+ Not(soupmatchers.HTMLContains(*absent_links)),
+ ),
+ )
def test_git_repository_batches_recipes(self):
repository = self.factory.makeGitRepository()
@@ -235,8 +267,11 @@ class TestCharmRecipeListing(BrowserTestCase):
now = datetime.now(pytz.UTC)
link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
self.assertBatches(repository, link_matchers, False, 0, 3)
- link_matchers.extend(self.makeCharmRecipesAndMatchers(
- create_recipe, 7, now - timedelta(seconds=3)))
+ link_matchers.extend(
+ self.makeCharmRecipesAndMatchers(
+ create_recipe, 7, now - timedelta(seconds=3)
+ )
+ )
self.assertBatches(repository, link_matchers, True, 0, 5)
def test_git_ref_batches_recipes(self):
@@ -245,19 +280,26 @@ class TestCharmRecipeListing(BrowserTestCase):
now = datetime.now(pytz.UTC)
link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
self.assertBatches(ref, link_matchers, False, 0, 3)
- link_matchers.extend(self.makeCharmRecipesAndMatchers(
- create_recipe, 7, now - timedelta(seconds=3)))
+ link_matchers.extend(
+ self.makeCharmRecipesAndMatchers(
+ create_recipe, 7, now - timedelta(seconds=3)
+ )
+ )
self.assertBatches(ref, link_matchers, True, 0, 5)
def test_person_batches_recipes(self):
owner = self.factory.makePerson()
create_recipe = partial(
- self.factory.makeCharmRecipe, registrant=owner, owner=owner)
+ self.factory.makeCharmRecipe, registrant=owner, owner=owner
+ )
now = datetime.now(pytz.UTC)
link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
self.assertBatches(owner, link_matchers, False, 0, 3)
- link_matchers.extend(self.makeCharmRecipesAndMatchers(
- create_recipe, 7, now - timedelta(seconds=3)))
+ link_matchers.extend(
+ self.makeCharmRecipesAndMatchers(
+ create_recipe, 7, now - timedelta(seconds=3)
+ )
+ )
self.assertBatches(owner, link_matchers, True, 0, 5)
def test_project_batches_recipes(self):
@@ -267,6 +309,9 @@ class TestCharmRecipeListing(BrowserTestCase):
now = datetime.now(pytz.UTC)
link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
self.assertBatches(project, link_matchers, False, 0, 3)
- link_matchers.extend(self.makeCharmRecipesAndMatchers(
- create_recipe, 7, now - timedelta(seconds=3)))
+ link_matchers.extend(
+ self.makeCharmRecipesAndMatchers(
+ create_recipe, 7, now - timedelta(seconds=3)
+ )
+ )
self.assertBatches(project, link_matchers, True, 0, 5)
diff --git a/lib/lp/charms/browser/tests/test_hascharmrecipes.py b/lib/lp/charms/browser/tests/test_hascharmrecipes.py
index e114153..19adca7 100644
--- a/lib/lp/charms/browser/tests/test_hascharmrecipes.py
+++ b/lib/lp/charms/browser/tests/test_hascharmrecipes.py
@@ -4,10 +4,7 @@
"""Test views for objects that have charm recipes."""
import soupmatchers
-from testscenarios import (
- load_tests_apply_scenarios,
- WithScenarios,
- )
+from testscenarios import WithScenarios, load_tests_apply_scenarios
from testtools.matchers import Not
from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
@@ -33,15 +30,21 @@ class TestHasCharmRecipesView(WithScenarios, TestCaseWithFactory):
layer = DatabaseFunctionalLayer
scenarios = [
- ("GitRepository", {
- "context_type": "repository",
- "context_factory": make_git_repository,
- }),
- ("GitRef", {
- "context_type": "branch",
- "context_factory": make_git_ref,
- }),
- ]
+ (
+ "GitRepository",
+ {
+ "context_type": "repository",
+ "context_factory": make_git_repository,
+ },
+ ),
+ (
+ "GitRef",
+ {
+ "context_type": "branch",
+ "context_factory": make_git_ref,
+ },
+ ),
+ ]
def setUp(self):
super().setUp()
@@ -58,16 +61,18 @@ class TestHasCharmRecipesView(WithScenarios, TestCaseWithFactory):
view = create_initialized_view(context, "+index")
self.assertEqual(
"No charm recipes using this %s." % self.context_type,
- view.charm_recipes_link)
+ view.charm_recipes_link,
+ )
def test_charm_recipes_link_one_recipe(self):
# An object with one charm recipe shows a link to that recipe.
context = self.context_factory(self)
recipe = self.makeCharmRecipe(context)
view = create_initialized_view(context, "+index")
- expected_link = (
- '<a href="%s">1 charm recipe</a> using this %s.' %
- (canonical_url(recipe), self.context_type))
+ expected_link = '<a href="%s">1 charm recipe</a> using this %s.' % (
+ canonical_url(recipe),
+ self.context_type,
+ )
self.assertEqual(expected_link, view.charm_recipes_link)
def test_charm_recipes_link_more_recipes(self):
@@ -77,8 +82,9 @@ class TestHasCharmRecipesView(WithScenarios, TestCaseWithFactory):
self.makeCharmRecipe(context)
view = create_initialized_view(context, "+index")
expected_link = (
- '<a href="+charm-recipes">2 charm recipes</a> using this %s.' %
- self.context_type)
+ '<a href="+charm-recipes">2 charm recipes</a> using this %s.'
+ % self.context_type
+ )
self.assertEqual(expected_link, view.charm_recipes_link)
@@ -88,7 +94,7 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
scenarios = [
("GitRef", {"context_factory": make_git_ref}),
- ]
+ ]
def setUp(self):
super().setUp()
@@ -103,11 +109,21 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
context = self.context_factory(self)
view = create_initialized_view(context, "+index")
new_charm_recipe_url = canonical_url(
- context, view_name="+new-charm-recipe")
- self.assertThat(view(), Not(soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "creation link", "a", attrs={"href": new_charm_recipe_url},
- text="Create charm recipe"))))
+ context, view_name="+new-charm-recipe"
+ )
+ self.assertThat(
+ view(),
+ Not(
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "creation link",
+ "a",
+ attrs={"href": new_charm_recipe_url},
+ text="Create charm recipe",
+ )
+ )
+ ),
+ )
def test_creation_link_no_recipes(self):
# An object with no charm recipes shows a creation link.
@@ -115,11 +131,19 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
context = self.context_factory(self)
view = create_initialized_view(context, "+index")
new_charm_recipe_url = canonical_url(
- context, view_name="+new-charm-recipe")
- self.assertThat(view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "creation link", "a", attrs={"href": new_charm_recipe_url},
- text="Create charm recipe")))
+ context, view_name="+new-charm-recipe"
+ )
+ self.assertThat(
+ view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "creation link",
+ "a",
+ attrs={"href": new_charm_recipe_url},
+ text="Create charm recipe",
+ )
+ ),
+ )
def test_creation_link_recipes(self):
# An object with charm recipes shows a creation link.
@@ -128,11 +152,19 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
self.makeCharmRecipe(context)
view = create_initialized_view(context, "+index")
new_charm_recipe_url = canonical_url(
- context, view_name="+new-charm-recipe")
- self.assertThat(view(), soupmatchers.HTMLContains(
- soupmatchers.Tag(
- "creation link", "a", attrs={"href": new_charm_recipe_url},
- text="Create charm recipe")))
+ context, view_name="+new-charm-recipe"
+ )
+ self.assertThat(
+ view(),
+ soupmatchers.HTMLContains(
+ soupmatchers.Tag(
+ "creation link",
+ "a",
+ attrs={"href": new_charm_recipe_url},
+ text="Create charm recipe",
+ )
+ ),
+ )
load_tests = load_tests_apply_scenarios
diff --git a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py b/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
index 92af76e..5a15feb 100644
--- a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
+++ b/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
@@ -5,16 +5,12 @@
__all__ = [
"CharmRecipeBuildChannelsWidget",
- ]
+]
from zope.browserpage import ViewPageTemplateFile
from zope.formlib.interfaces import IInputWidget
from zope.formlib.utility import setUpWidget
-from zope.formlib.widget import (
- BrowserWidget,
- InputErrors,
- InputWidget,
- )
+from zope.formlib.widget import BrowserWidget, InputErrors, InputWidget
from zope.interface import implementer
from zope.schema import TextLine
from zope.security.proxy import isinstance as zope_isinstance
@@ -23,7 +19,7 @@ from lp.app.errors import UnexpectedFormData
from lp.services.webapp.interfaces import (
IAlwaysSubmittedWidget,
ISingleLineWidgetLayout,
- )
+)
@implementer(ISingleLineWidgetLayout, IAlwaysSubmittedWidget, IInputWidget)
@@ -38,20 +34,24 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
super().__init__(context, request)
self.hint = (
"The channels to use for build tools when building the charm "
- "recipe.")
+ "recipe."
+ )
def setUpSubWidgets(self):
if self._widgets_set_up:
return
fields = [
TextLine(
- __name__=snap_name, title="%s channel" % snap_name,
- required=False)
+ __name__=snap_name,
+ title="%s channel" % snap_name,
+ required=False,
+ )
for snap_name in self.snap_names
- ]
+ ]
for field in fields:
setUpWidget(
- self, field.__name__, field, IInputWidget, prefix=self.name)
+ self, field.__name__, field, IInputWidget, prefix=self.name
+ )
self._widgets_set_up = True
def setRenderedValue(self, value):
@@ -61,13 +61,15 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
value = {}
for snap_name in self.snap_names:
getattr(self, "%s_widget" % snap_name).setRenderedValue(
- value.get(snap_name))
+ value.get(snap_name)
+ )
def hasInput(self):
"""See `IInputWidget`."""
return any(
"%s.%s" % (self.name, snap_name) in self.request.form
- for snap_name in self.snap_names)
+ for snap_name in self.snap_names
+ )
def hasValidInput(self):
"""See `IInputWidget`."""
diff --git a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py b/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
index a5c922b..ab4a44c 100644
--- a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
+++ b/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
@@ -3,23 +3,17 @@
import re
-from zope.formlib.interfaces import (
- IBrowserWidget,
- IInputWidget,
- )
+from zope.formlib.interfaces import IBrowserWidget, IInputWidget
from zope.schema import Dict
from lp.charms.browser.widgets.charmrecipebuildchannels import (
CharmRecipeBuildChannelsWidget,
- )
+)
from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
from lp.services.beautifulsoup import BeautifulSoup
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.servers import LaunchpadTestRequest
-from lp.testing import (
- TestCaseWithFactory,
- verifyObject,
- )
+from lp.testing import TestCaseWithFactory, verifyObject
from lp.testing.layers import DatabaseFunctionalLayer
@@ -32,7 +26,8 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
field = Dict(
__name__="auto_build_channels",
- title="Source snap channels for automatic builds")
+ title="Source snap channels for automatic builds",
+ )
self.context = self.factory.makeCharmRecipe()
self.field = field.bind(self.context)
self.request = LaunchpadTestRequest()
@@ -45,14 +40,17 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
def test_template(self):
self.assertTrue(
self.widget.template.filename.endswith(
- "charmrecipebuildchannels.pt"),
- "Template was not set up.")
+ "charmrecipebuildchannels.pt"
+ ),
+ "Template was not set up.",
+ )
def test_hint(self):
self.assertEqual(
"The channels to use for build tools when building the charm "
"recipe.",
- self.widget.hint)
+ self.widget.hint,
+ )
def test_setUpSubWidgets_first_call(self):
# The subwidgets are set up and a flag is set.
@@ -94,7 +92,8 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
def test_setRenderedValue_one_channel(self):
self.widget.setRenderedValue({"charmcraft": "stable"})
self.assertEqual(
- "stable", self.widget.charmcraft_widget._getCurrentValue())
+ "stable", self.widget.charmcraft_widget._getCurrentValue()
+ )
self.assertIsNone(self.widget.core_widget._getCurrentValue())
self.assertIsNone(self.widget.core18_widget._getCurrentValue())
self.assertIsNone(self.widget.core20_widget._getCurrentValue())
@@ -102,16 +101,25 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
def test_setRenderedValue_all_channels(self):
self.widget.setRenderedValue(
- {"charmcraft": "stable", "core": "candidate", "core18": "beta",
- "core20": "edge", "core22": "edge/feature"})
+ {
+ "charmcraft": "stable",
+ "core": "candidate",
+ "core18": "beta",
+ "core20": "edge",
+ "core22": "edge/feature",
+ }
+ )
self.assertEqual(
- "stable", self.widget.charmcraft_widget._getCurrentValue())
+ "stable", self.widget.charmcraft_widget._getCurrentValue()
+ )
self.assertEqual(
- "candidate", self.widget.core_widget._getCurrentValue())
+ "candidate", self.widget.core_widget._getCurrentValue()
+ )
self.assertEqual("beta", self.widget.core18_widget._getCurrentValue())
self.assertEqual("edge", self.widget.core20_widget._getCurrentValue())
self.assertEqual(
- "edge/feature", self.widget.core22_widget._getCurrentValue())
+ "edge/feature", self.widget.core22_widget._getCurrentValue()
+ )
def test_hasInput_false(self):
# hasInput is false when there are no channels in the form data.
@@ -121,7 +129,8 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
def test_hasInput_true(self):
# hasInput is true when there are channels in the form data.
self.widget.request = LaunchpadTestRequest(
- form={"field.auto_build_channels.charmcraft": "stable"})
+ form={"field.auto_build_channels.charmcraft": "stable"}
+ )
self.assertTrue(self.widget.hasInput())
def test_hasValidInput_true(self):
@@ -134,7 +143,7 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
"field.auto_build_channels.core18": "beta",
"field.auto_build_channels.core20": "edge",
"field.auto_build_channels.core22": "edge/feature",
- }
+ }
self.widget.request = LaunchpadTestRequest(form=form)
self.assertTrue(self.widget.hasValidInput())
@@ -145,12 +154,17 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
"field.auto_build_channels.core18": "beta",
"field.auto_build_channels.core20": "edge",
"field.auto_build_channels.core22": "edge/feature",
- }
+ }
self.widget.request = LaunchpadTestRequest(form=form)
self.assertEqual(
- {"charmcraft": "stable", "core18": "beta", "core20": "edge",
- "core22": "edge/feature"},
- self.widget.getInputValue())
+ {
+ "charmcraft": "stable",
+ "core18": "beta",
+ "core20": "edge",
+ "core22": "edge/feature",
+ },
+ self.widget.getInputValue(),
+ )
def test_call(self):
# The __call__ method sets up the widgets.
@@ -168,6 +182,6 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
"field.auto_build_channels.core18",
"field.auto_build_channels.core20",
"field.auto_build_channels.core22",
- ]
+ ]
ids = [field["id"] for field in fields]
self.assertContentEqual(expected_ids, ids)
diff --git a/lib/lp/charms/interfaces/charmbase.py b/lib/lp/charms/interfaces/charmbase.py
index f22c521..a7766fb 100644
--- a/lib/lp/charms/interfaces/charmbase.py
+++ b/lib/lp/charms/interfaces/charmbase.py
@@ -8,11 +8,12 @@ __all__ = [
"ICharmBase",
"ICharmBaseSet",
"NoSuchCharmBase",
- ]
+]
import http.client
from lazr.restful.declarations import (
+ REQUEST_USER,
call_with,
collection_default_content,
error_status,
@@ -26,20 +27,10 @@ from lazr.restful.declarations import (
operation_for_version,
operation_parameters,
operation_returns_entry,
- REQUEST_USER,
- )
-from lazr.restful.fields import (
- CollectionField,
- Reference,
- )
+)
+from lazr.restful.fields import CollectionField, Reference
from zope.interface import Interface
-from zope.schema import (
- Datetime,
- Dict,
- Int,
- List,
- TextLine,
- )
+from zope.schema import Datetime, Dict, Int, List, TextLine
from lp import _
from lp.app.errors import NotFoundError
@@ -54,7 +45,8 @@ class DuplicateCharmBase(Exception):
def __init__(self, distro_series):
super().__init__(
- "%s is already in use by another base." % distro_series)
+ "%s is already in use by another base." % distro_series
+ )
class NoSuchCharmBase(NotFoundError):
@@ -73,23 +65,37 @@ class ICharmBaseView(Interface):
id = Int(title=_("ID"), required=True, readonly=True)
- date_created = exported(Datetime(
- title=_("Date created"), required=True, readonly=True))
+ date_created = exported(
+ Datetime(title=_("Date created"), required=True, readonly=True)
+ )
- registrant = exported(PublicPersonChoice(
- title=_("Registrant"), required=True, readonly=True,
- vocabulary="ValidPersonOrTeam",
- description=_("The person who registered this base.")))
+ registrant = exported(
+ PublicPersonChoice(
+ title=_("Registrant"),
+ required=True,
+ readonly=True,
+ vocabulary="ValidPersonOrTeam",
+ description=_("The person who registered this base."),
+ )
+ )
- distro_series = exported(Reference(
- IDistroSeries, title=_("Distro series"),
- required=True, readonly=True))
+ distro_series = exported(
+ Reference(
+ IDistroSeries,
+ title=_("Distro series"),
+ required=True,
+ readonly=True,
+ )
+ )
- processors = exported(CollectionField(
- title=_("Processors"),
- description=_("The architectures that the charm base supports."),
- value_type=Reference(schema=IProcessor),
- readonly=True))
+ processors = exported(
+ CollectionField(
+ title=_("Processors"),
+ description=_("The architectures that the charm base supports."),
+ value_type=Reference(schema=IProcessor),
+ readonly=True,
+ )
+ )
class ICharmBaseEditableAttributes(Interface):
@@ -98,24 +104,30 @@ class ICharmBaseEditableAttributes(Interface):
Anyone can view these attributes, but they need launchpad.Edit to change.
"""
- build_snap_channels = exported(Dict(
- title=_("Source snap channels for builds"),
- key_type=TextLine(), required=True, readonly=False,
- description=_(
- "A dictionary mapping snap names to channels to use when building "
- "charm recipes that specify this base. The special '_byarch' key "
- "may have a mapping of architecture names to mappings of snap "
- "names to channels, which if present override the channels "
- "declared at the top level when building for those "
- "architectures.")))
+ build_snap_channels = exported(
+ Dict(
+ title=_("Source snap channels for builds"),
+ key_type=TextLine(),
+ required=True,
+ readonly=False,
+ description=_(
+ "A dictionary mapping snap names to channels to use when "
+ "building charm recipes that specify this base. The special "
+ "'_byarch' key may have a mapping of architecture names to "
+ "mappings of snap names to channels, which if present "
+ "override the channels declared at the top level when "
+ "building for those architectures."
+ ),
+ )
+ )
class ICharmBaseEdit(Interface):
"""`ICharmBase` methods that require launchpad.Edit permission."""
@operation_parameters(
- processors=List(
- value_type=Reference(schema=IProcessor), required=True))
+ processors=List(value_type=Reference(schema=IProcessor), required=True)
+ )
@export_write_operation()
@operation_for_version("devel")
def setProcessors(processors):
@@ -141,12 +153,20 @@ class ICharmBaseSetEdit(Interface):
@call_with(registrant=REQUEST_USER)
@operation_parameters(
processors=List(
- value_type=Reference(schema=IProcessor), required=False))
+ value_type=Reference(schema=IProcessor), required=False
+ )
+ )
@export_factory_operation(
- ICharmBase, ["distro_series", "build_snap_channels"])
+ ICharmBase, ["distro_series", "build_snap_channels"]
+ )
@operation_for_version("devel")
- def new(registrant, distro_series, build_snap_channels, processors=None,
- date_created=None):
+ def new(
+ registrant,
+ distro_series,
+ build_snap_channels,
+ processors=None,
+ date_created=None,
+ ):
"""Create an `ICharmBase`."""
@@ -162,7 +182,9 @@ class ICharmBaseSet(ICharmBaseSetEdit):
@operation_parameters(
distro_series=Reference(
- schema=IDistroSeries, title=_("Distro series"), required=True))
+ schema=IDistroSeries, title=_("Distro series"), required=True
+ )
+ )
@operation_returns_entry(ICharmBase)
@export_read_operation()
@operation_for_version("devel")
diff --git a/lib/lp/charms/interfaces/charmhubclient.py b/lib/lp/charms/interfaces/charmhubclient.py
index 9a7cc0f..c70b39d 100644
--- a/lib/lp/charms/interfaces/charmhubclient.py
+++ b/lib/lp/charms/interfaces/charmhubclient.py
@@ -14,7 +14,7 @@ __all__ = [
"UnauthorizedUploadResponse",
"UploadFailedResponse",
"UploadNotReviewedYetResponse",
- ]
+]
import http.client
@@ -23,7 +23,6 @@ from zope.interface import Interface
class CharmhubError(Exception):
-
def __init__(self, message="", detail=None, can_retry=False):
super().__init__(message)
self.message = message
diff --git a/lib/lp/charms/interfaces/charmrecipe.py b/lib/lp/charms/interfaces/charmrecipe.py
index 50d37ec..35c60e2 100644
--- a/lib/lp/charms/interfaces/charmrecipe.py
+++ b/lib/lp/charms/interfaces/charmrecipe.py
@@ -28,16 +28,14 @@ __all__ = [
"MissingCharmcraftYaml",
"NoSourceForCharmRecipe",
"NoSuchCharmRecipe",
- ]
+]
import http.client
-from lazr.enum import (
- EnumeratedType,
- Item,
- )
+from lazr.enum import EnumeratedType, Item
from lazr.lifecycle.snapshot import doNotSnapshot
from lazr.restful.declarations import (
+ REQUEST_USER,
call_with,
collection_default_content,
error_status,
@@ -53,18 +51,10 @@ from lazr.restful.declarations import (
operation_returns_collection_of,
operation_returns_entry,
rename_parameters_as,
- REQUEST_USER,
- )
-from lazr.restful.fields import (
- CollectionField,
- Reference,
- ReferenceChoice,
- )
+)
+from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
from lazr.restful.interface import copy_field
-from zope.interface import (
- Attribute,
- Interface,
- )
+from zope.interface import Attribute, Interface
from zope.schema import (
Bool,
Choice,
@@ -75,7 +65,7 @@ from zope.schema import (
Set,
Text,
TextLine,
- )
+)
from zope.security.interfaces import Unauthorized
from lp import _
@@ -89,14 +79,10 @@ from lp.code.interfaces.gitref import IGitRef
from lp.code.interfaces.gitrepository import IGitRepository
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.product import IProduct
-from lp.services.fields import (
- PersonChoice,
- PublicPersonChoice,
- )
+from lp.services.fields import PersonChoice, PublicPersonChoice
from lp.services.webhooks.interfaces import IWebhookTarget
from lp.snappy.validators.channels import channels_validator
-
CHARM_RECIPE_ALLOW_CREATE = "charm.recipe.create.enabled"
CHARM_RECIPE_PRIVATE_FEATURE_FLAG = "charm.recipe.allow_private"
CHARM_RECIPE_BUILD_DISTRIBUTION = "charm.default_build_distribution"
@@ -109,7 +95,8 @@ class CharmRecipeFeatureDisabled(Unauthorized):
def __init__(self):
super().__init__(
- "You do not have permission to create new charm recipes.")
+ "You do not have permission to create new charm recipes."
+ )
@error_status(http.client.UNAUTHORIZED)
@@ -118,7 +105,8 @@ class CharmRecipePrivateFeatureDisabled(Unauthorized):
def __init__(self):
super().__init__(
- "You do not have permission to create private charm recipes.")
+ "You do not have permission to create private charm recipes."
+ )
@error_status(http.client.BAD_REQUEST)
@@ -128,7 +116,8 @@ class DuplicateCharmRecipeName(Exception):
def __init__(self):
super().__init__(
"There is already a charm recipe with the same project, owner, "
- "and name.")
+ "and name."
+ )
@error_status(http.client.UNAUTHORIZED)
@@ -138,6 +127,7 @@ class CharmRecipeNotOwner(Unauthorized):
class NoSuchCharmRecipe(NameLookupFailed):
"""The requested charm recipe does not exist."""
+
_message_prefix = "No such charm recipe with this owner and project"
@@ -159,9 +149,12 @@ class CharmRecipePrivacyMismatch(Exception):
"""Charm recipe privacy does not match its content."""
def __init__(self, message=None):
- super().__init__(
- message or
- "Charm recipe contains private information and cannot be public.")
+ if message is None:
+ message = (
+ "Charm recipe contains private information and cannot be "
+ "public."
+ )
+ super().__init__(message)
class BadCharmRecipeSearchContext(Exception):
@@ -194,7 +187,8 @@ class CharmRecipeBuildAlreadyPending(Exception):
def __init__(self):
super().__init__(
- "An identical build of this charm recipe is already pending.")
+ "An identical build of this charm recipe is already pending."
+ )
@error_status(http.client.BAD_REQUEST)
@@ -203,30 +197,37 @@ class CharmRecipeBuildDisallowedArchitecture(Exception):
def __init__(self, das):
super().__init__(
- "This charm recipe is not allowed to build for %s/%s." %
- (das.distroseries.name, das.architecturetag))
+ "This charm recipe is not allowed to build for %s/%s."
+ % (das.distroseries.name, das.architecturetag)
+ )
class CharmRecipeBuildRequestStatus(EnumeratedType):
"""The status of a request to build a charm recipe."""
- PENDING = Item("""
+ PENDING = Item(
+ """
Pending
This charm recipe build request is pending.
- """)
+ """
+ )
- FAILED = Item("""
+ FAILED = Item(
+ """
Failed
This charm recipe build request failed.
- """)
+ """
+ )
- COMPLETED = Item("""
+ COMPLETED = Item(
+ """
Completed
This charm recipe build request completed successfully.
- """)
+ """
+ )
# XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL
@@ -238,43 +239,76 @@ class ICharmRecipeBuildRequest(Interface):
id = Int(title=_("ID"), required=True, readonly=True)
- date_requested = exported(Datetime(
- title=_("The time when this request was made"),
- required=True, readonly=True))
+ date_requested = exported(
+ Datetime(
+ title=_("The time when this request was made"),
+ required=True,
+ readonly=True,
+ )
+ )
- date_finished = exported(Datetime(
- title=_("The time when this request finished"),
- required=False, readonly=True))
+ date_finished = exported(
+ Datetime(
+ title=_("The time when this request finished"),
+ required=False,
+ readonly=True,
+ )
+ )
- recipe = exported(Reference(
- # Really ICharmRecipe, patched in lp.charms.interfaces.webservice.
- Interface,
- title=_("Charm recipe"), required=True, readonly=True))
+ recipe = exported(
+ Reference(
+ # Really ICharmRecipe, patched in lp.charms.interfaces.webservice.
+ Interface,
+ title=_("Charm recipe"),
+ required=True,
+ readonly=True,
+ )
+ )
- status = exported(Choice(
- title=_("Status"), vocabulary=CharmRecipeBuildRequestStatus,
- required=True, readonly=True))
+ status = exported(
+ Choice(
+ title=_("Status"),
+ vocabulary=CharmRecipeBuildRequestStatus,
+ required=True,
+ readonly=True,
+ )
+ )
- error_message = exported(TextLine(
- title=_("Error message"), required=True, readonly=True))
+ error_message = exported(
+ TextLine(title=_("Error message"), required=True, readonly=True)
+ )
- builds = exported(CollectionField(
- title=_("Builds produced by this request"),
- # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
- value_type=Reference(schema=Interface),
- required=True, readonly=True))
+ builds = exported(
+ CollectionField(
+ title=_("Builds produced by this request"),
+ # Really ICharmRecipeBuild, patched in
+ # lp.charms.interfaces.webservice.
+ value_type=Reference(schema=Interface),
+ required=True,
+ readonly=True,
+ )
+ )
requester = Reference(
- title=_("The person requesting the builds."), schema=IPerson,
- required=True, readonly=True)
+ title=_("The person requesting the builds."),
+ schema=IPerson,
+ required=True,
+ readonly=True,
+ )
channels = Dict(
title=_("Source snap channels for builds produced by this request"),
- key_type=TextLine(), required=False, readonly=True)
+ key_type=TextLine(),
+ required=False,
+ readonly=True,
+ )
architectures = Set(
title=_("If set, this request is limited to these architecture tags"),
- value_type=TextLine(), required=False, readonly=True)
+ value_type=TextLine(),
+ required=False,
+ readonly=True,
+ )
class ICharmRecipeView(Interface):
@@ -282,28 +316,47 @@ class ICharmRecipeView(Interface):
id = Int(title=_("ID"), required=True, readonly=True)
- date_created = exported(Datetime(
- title=_("Date created"), required=True, readonly=True))
- date_last_modified = exported(Datetime(
- title=_("Date last modified"), required=True, readonly=True))
+ date_created = exported(
+ Datetime(title=_("Date created"), required=True, readonly=True)
+ )
+ date_last_modified = exported(
+ Datetime(title=_("Date last modified"), required=True, readonly=True)
+ )
- registrant = exported(PublicPersonChoice(
- title=_("Registrant"), required=True, readonly=True,
- vocabulary="ValidPersonOrTeam",
- description=_("The person who registered this charm recipe.")))
+ registrant = exported(
+ PublicPersonChoice(
+ title=_("Registrant"),
+ required=True,
+ readonly=True,
+ vocabulary="ValidPersonOrTeam",
+ description=_("The person who registered this charm recipe."),
+ )
+ )
source = Attribute(
- "The source branch for this charm recipe (VCS-agnostic).")
+ "The source branch for this charm recipe (VCS-agnostic)."
+ )
- private = exported(Bool(
- title=_("Private"), required=False, readonly=False,
- description=_("Whether this charm recipe is private.")))
+ private = exported(
+ Bool(
+ title=_("Private"),
+ required=False,
+ readonly=False,
+ description=_("Whether this charm recipe is private."),
+ )
+ )
- can_upload_to_store = exported(Bool(
- title=_("Can upload to Charmhub"), required=True, readonly=True,
- description=_(
- "Whether everything is set up to allow uploading builds of this "
- "charm recipe to Charmhub.")))
+ can_upload_to_store = exported(
+ Bool(
+ title=_("Can upload to Charmhub"),
+ required=True,
+ readonly=True,
+ description=_(
+ "Whether everything is set up to allow uploading builds of "
+ "this charm recipe to Charmhub."
+ ),
+ )
+ )
def getAllowedInformationTypes(user):
"""Get a list of acceptable `InformationType`s for this charm recipe.
@@ -314,8 +367,9 @@ class ICharmRecipeView(Interface):
def visibleByUser(user):
"""Can the specified user see this charm recipe?"""
- def requestBuild(build_request, distro_arch_series, charm_base=None,
- channels=None):
+ def requestBuild(
+ build_request, distro_arch_series, charm_base=None, channels=None
+ ):
"""Request a single build of this charm recipe.
This method is for internal use; external callers should use
@@ -337,8 +391,12 @@ class ICharmRecipeView(Interface):
description=_(
"A dictionary mapping snap names to channels to use for these "
"builds. Currently only 'charmcraft', 'core', 'core18', "
- "'core20', and 'core22' keys are supported."),
- key_type=TextLine(), required=False))
+ "'core20', and 'core22' keys are supported."
+ ),
+ key_type=TextLine(),
+ required=False,
+ )
+ )
@export_factory_operation(ICharmRecipeBuildRequest, [])
@operation_for_version("devel")
def requestBuilds(requester, channels=None, architectures=None):
@@ -357,8 +415,13 @@ class ICharmRecipeView(Interface):
:return: An `ICharmRecipeBuildRequest`.
"""
- def requestBuildsFromJob(build_request, channels=None, architectures=None,
- allow_failures=False, logger=None):
+ def requestBuildsFromJob(
+ build_request,
+ channels=None,
+ architectures=None,
+ allow_failures=False,
+ logger=None,
+ ):
"""Synchronous part of `CharmRecipe.requestBuilds`.
Request that the charm recipe be built for relevant architectures.
@@ -391,39 +454,76 @@ class ICharmRecipeView(Interface):
:return: `ICharmRecipeBuildRequest`.
"""
- pending_build_requests = exported(doNotSnapshot(CollectionField(
- title=_("Pending build requests for this charm recipe."),
- value_type=Reference(ICharmRecipeBuildRequest),
- required=True, readonly=True)))
+ pending_build_requests = exported(
+ doNotSnapshot(
+ CollectionField(
+ title=_("Pending build requests for this charm recipe."),
+ value_type=Reference(ICharmRecipeBuildRequest),
+ required=True,
+ readonly=True,
+ )
+ )
+ )
- failed_build_requests = exported(doNotSnapshot(CollectionField(
- title=_("Failed build requests for this charm recipe."),
- value_type=Reference(ICharmRecipeBuildRequest),
- required=True, readonly=True)))
+ failed_build_requests = exported(
+ doNotSnapshot(
+ CollectionField(
+ title=_("Failed build requests for this charm recipe."),
+ value_type=Reference(ICharmRecipeBuildRequest),
+ required=True,
+ readonly=True,
+ )
+ )
+ )
- builds = exported(doNotSnapshot(CollectionField(
- title=_("All builds of this charm recipe."),
- description=_(
- "All builds of this charm recipe, sorted in descending order "
- "of finishing (or starting if not completed successfully)."),
- # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
- value_type=Reference(schema=Interface), readonly=True)))
+ builds = exported(
+ doNotSnapshot(
+ CollectionField(
+ title=_("All builds of this charm recipe."),
+ description=_(
+ "All builds of this charm recipe, sorted in descending "
+ "order of finishing (or starting if not completed "
+ "successfully)."
+ ),
+ # Really ICharmRecipeBuild, patched in
+ # lp.charms.interfaces.webservice.
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
+ )
+ )
- completed_builds = exported(doNotSnapshot(CollectionField(
- title=_("Completed builds of this charm recipe."),
- description=_(
- "Completed builds of this charm recipe, sorted in descending "
- "order of finishing."),
- # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
- value_type=Reference(schema=Interface), readonly=True)))
+ completed_builds = exported(
+ doNotSnapshot(
+ CollectionField(
+ title=_("Completed builds of this charm recipe."),
+ description=_(
+ "Completed builds of this charm recipe, sorted in "
+ "descending order of finishing."
+ ),
+ # Really ICharmRecipeBuild, patched in
+ # lp.charms.interfaces.webservice.
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
+ )
+ )
- pending_builds = exported(doNotSnapshot(CollectionField(
- title=_("Pending builds of this charm recipe."),
- description=_(
- "Pending builds of this charm recipe, sorted in descending "
- "order of creation."),
- # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
- value_type=Reference(schema=Interface), readonly=True)))
+ pending_builds = exported(
+ doNotSnapshot(
+ CollectionField(
+ title=_("Pending builds of this charm recipe."),
+ description=_(
+ "Pending builds of this charm recipe, sorted in "
+ "descending order of creation."
+ ),
+ # Really ICharmRecipeBuild, patched in
+ # lp.charms.interfaces.webservice.
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
+ )
+ )
class ICharmRecipeEdit(IWebhookTarget):
@@ -449,7 +549,9 @@ class ICharmRecipeEdit(IWebhookTarget):
@rename_parameters_as(unbound_discharge_macaroon_raw="discharge_macaroon")
@operation_parameters(
unbound_discharge_macaroon_raw=TextLine(
- title=_("Serialized discharge macaroon")))
+ title=_("Serialized discharge macaroon")
+ )
+ )
@export_write_operation()
@operation_for_version("devel")
def completeAuthorization(unbound_discharge_macaroon_raw):
@@ -473,100 +575,179 @@ class ICharmRecipeEditableAttributes(Interface):
These attributes need launchpad.View to see, and launchpad.Edit to change.
"""
- owner = exported(PersonChoice(
- title=_("Owner"), required=True, readonly=False,
- vocabulary="AllUserTeamsParticipationPlusSelf",
- description=_("The owner of this charm recipe.")))
+ owner = exported(
+ PersonChoice(
+ title=_("Owner"),
+ required=True,
+ readonly=False,
+ vocabulary="AllUserTeamsParticipationPlusSelf",
+ description=_("The owner of this charm recipe."),
+ )
+ )
- project = exported(ReferenceChoice(
- title=_("The project that this charm recipe is associated with"),
- schema=IProduct, vocabulary="Product",
- required=True, readonly=False))
+ project = exported(
+ ReferenceChoice(
+ title=_("The project that this charm recipe is associated with"),
+ schema=IProduct,
+ vocabulary="Product",
+ required=True,
+ readonly=False,
+ )
+ )
- name = exported(TextLine(
- title=_("Charm recipe name"), required=True, readonly=False,
- constraint=name_validator,
- description=_("The name of the charm recipe.")))
+ name = exported(
+ TextLine(
+ title=_("Charm recipe name"),
+ required=True,
+ readonly=False,
+ constraint=name_validator,
+ description=_("The name of the charm recipe."),
+ )
+ )
- description = exported(Text(
- title=_("Description"), required=False, readonly=False,
- description=_("A description of the charm recipe.")))
+ description = exported(
+ Text(
+ title=_("Description"),
+ required=False,
+ readonly=False,
+ description=_("A description of the charm recipe."),
+ )
+ )
git_repository = ReferenceChoice(
title=_("Git repository"),
- schema=IGitRepository, vocabulary="GitRepository",
- required=False, readonly=True,
+ schema=IGitRepository,
+ vocabulary="GitRepository",
+ required=False,
+ readonly=True,
description=_(
- "A Git repository with a branch containing a charm recipe."))
+ "A Git repository with a branch containing a charm recipe."
+ ),
+ )
git_path = TextLine(
- title=_("Git branch path"), required=False, readonly=True,
- description=_("The path of the Git branch containing a charm recipe."))
+ title=_("Git branch path"),
+ required=False,
+ readonly=True,
+ description=_("The path of the Git branch containing a charm recipe."),
+ )
- git_ref = exported(Reference(
- IGitRef, title=_("Git branch"), required=False, readonly=False,
- description=_("The Git branch containing a charm recipe.")))
+ git_ref = exported(
+ Reference(
+ IGitRef,
+ title=_("Git branch"),
+ required=False,
+ readonly=False,
+ description=_("The Git branch containing a charm recipe."),
+ )
+ )
- build_path = exported(TextLine(
- title=_("Build path"),
- description=_(
- "Subdirectory within the branch containing metadata.yaml."),
- constraint=path_does_not_escape, required=False, readonly=False))
+ build_path = exported(
+ TextLine(
+ title=_("Build path"),
+ description=_(
+ "Subdirectory within the branch containing metadata.yaml."
+ ),
+ constraint=path_does_not_escape,
+ required=False,
+ readonly=False,
+ )
+ )
- information_type = exported(Choice(
- title=_("Information type"), vocabulary=InformationType,
- required=True, readonly=False, default=InformationType.PUBLIC,
- description=_(
- "The type of information contained in this charm recipe.")))
+ information_type = exported(
+ Choice(
+ title=_("Information type"),
+ vocabulary=InformationType,
+ required=True,
+ readonly=False,
+ default=InformationType.PUBLIC,
+ description=_(
+ "The type of information contained in this charm recipe."
+ ),
+ )
+ )
- auto_build = exported(Bool(
- title=_("Automatically build when branch changes"),
- required=True, readonly=False,
- description=_(
- "Whether this charm recipe is built automatically when its branch "
- "changes.")))
+ auto_build = exported(
+ Bool(
+ title=_("Automatically build when branch changes"),
+ required=True,
+ readonly=False,
+ description=_(
+ "Whether this charm recipe is built automatically when its "
+ "branch changes."
+ ),
+ )
+ )
- auto_build_channels = exported(Dict(
- title=_("Source snap channels for automatic builds"),
- key_type=TextLine(), required=False, readonly=False,
- description=_(
- "A dictionary mapping snap names to channels to use when building "
- "this charm recipe. Currently only 'charmcraft', 'core', "
- "'core18', 'core20', and 'core22' keys are supported.")))
+ auto_build_channels = exported(
+ Dict(
+ title=_("Source snap channels for automatic builds"),
+ key_type=TextLine(),
+ required=False,
+ readonly=False,
+ description=_(
+ "A dictionary mapping snap names to channels to use when "
+ "building this charm recipe. Currently only 'charmcraft', "
+ "'core', 'core18', 'core20', and 'core22' keys are supported."
+ ),
+ )
+ )
- is_stale = exported(Bool(
- title=_("Charm recipe is stale and is due to be rebuilt."),
- required=True, readonly=True))
+ is_stale = exported(
+ Bool(
+ title=_("Charm recipe is stale and is due to be rebuilt."),
+ required=True,
+ readonly=True,
+ )
+ )
- store_upload = exported(Bool(
- title=_("Automatically upload to store"),
- required=True, readonly=False,
- description=_(
- "Whether builds of this charm recipe are automatically uploaded "
- "to the store.")))
+ store_upload = exported(
+ Bool(
+ title=_("Automatically upload to store"),
+ required=True,
+ readonly=False,
+ description=_(
+ "Whether builds of this charm recipe are automatically "
+ "uploaded to the store."
+ ),
+ )
+ )
- store_name = exported(TextLine(
- title=_("Registered store name"),
- required=False, readonly=False,
- description=_(
- "The registered name of this charm in the store.")))
+ store_name = exported(
+ TextLine(
+ title=_("Registered store name"),
+ required=False,
+ readonly=False,
+ description=_("The registered name of this charm in the store."),
+ )
+ )
store_secrets = List(
- value_type=TextLine(), title=_("Store upload tokens"),
- required=False, readonly=False,
+ value_type=TextLine(),
+ title=_("Store upload tokens"),
+ required=False,
+ readonly=False,
description=_(
"Serialized secrets issued by the store and the login service to "
- "authorize uploads of this charm recipe."))
+ "authorize uploads of this charm recipe."
+ ),
+ )
- store_channels = exported(List(
- title=_("Store channels"),
- required=False, readonly=False, constraint=channels_validator,
- description=_(
- "Channels to release this charm to after uploading it to the "
- "store. A channel is defined by a combination of an optional "
- "track, a risk, and an optional branch, e.g. "
- "'2.1/stable/fix-123', '2.1/stable', 'stable/fix-123', or "
- "'stable'.")))
+ store_channels = exported(
+ List(
+ title=_("Store channels"),
+ required=False,
+ readonly=False,
+ constraint=channels_validator,
+ description=_(
+ "Channels to release this charm to after uploading it to the "
+ "store. A channel is defined by a combination of an optional "
+ "track, a risk, and an optional branch, e.g. "
+ "'2.1/stable/fix-123', '2.1/stable', 'stable/fix-123', or "
+ "'stable'."
+ ),
+ )
+ )
class ICharmRecipeAdminAttributes(Interface):
@@ -575,9 +756,14 @@ class ICharmRecipeAdminAttributes(Interface):
These attributes need launchpad.View to see, and launchpad.Admin to change.
"""
- require_virtualized = exported(Bool(
- title=_("Require virtualized builders"), required=True, readonly=False,
- description=_("Only build this charm recipe on virtual builders.")))
+ require_virtualized = exported(
+ Bool(
+ title=_("Require virtualized builders"),
+ required=True,
+ readonly=False,
+ description=_("Only build this charm recipe on virtual builders."),
+ )
+ )
# XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL
@@ -585,8 +771,13 @@ class ICharmRecipeAdminAttributes(Interface):
# "devel".
@exported_as_webservice_entry(as_of="beta")
class ICharmRecipe(
- ICharmRecipeView, ICharmRecipeEdit, ICharmRecipeEditableAttributes,
- ICharmRecipeAdminAttributes, IPrivacy, IInformationType):
+ ICharmRecipeView,
+ ICharmRecipeEdit,
+ ICharmRecipeEditableAttributes,
+ ICharmRecipeAdminAttributes,
+ IPrivacy,
+ IInformationType,
+):
"""A buildable charm recipe."""
@@ -597,25 +788,51 @@ class ICharmRecipeSet(Interface):
@call_with(registrant=REQUEST_USER)
@operation_parameters(
information_type=copy_field(
- ICharmRecipe["information_type"], required=False))
+ ICharmRecipe["information_type"], required=False
+ )
+ )
@export_factory_operation(
- ICharmRecipe, [
- "owner", "project", "name", "description", "git_ref", "build_path",
- "auto_build", "auto_build_channels", "store_upload", "store_name",
+ ICharmRecipe,
+ [
+ "owner",
+ "project",
+ "name",
+ "description",
+ "git_ref",
+ "build_path",
+ "auto_build",
+ "auto_build_channels",
+ "store_upload",
+ "store_name",
"store_channels",
- ])
+ ],
+ )
@operation_for_version("devel")
- def new(registrant, owner, project, name, description=None, git_ref=None,
- build_path=None, require_virtualized=True,
- information_type=InformationType.PUBLIC, auto_build=False,
- auto_build_channels=None, store_upload=False, store_name=None,
- store_secrets=None, store_channels=None, date_created=None):
+ def new(
+ registrant,
+ owner,
+ project,
+ name,
+ description=None,
+ git_ref=None,
+ build_path=None,
+ require_virtualized=True,
+ information_type=InformationType.PUBLIC,
+ auto_build=False,
+ auto_build_channels=None,
+ store_upload=False,
+ store_name=None,
+ store_secrets=None,
+ store_channels=None,
+ date_created=None,
+ ):
"""Create an `ICharmRecipe`."""
@operation_parameters(
owner=Reference(IPerson, title=_("Owner"), required=True),
project=Reference(IProduct, title=_("Project"), required=True),
- name=TextLine(title=_("Recipe name"), required=True))
+ name=TextLine(title=_("Recipe name"), required=True),
+ )
@operation_returns_entry(ICharmRecipe)
@export_read_operation()
@operation_for_version("devel")
@@ -626,7 +843,8 @@ class ICharmRecipeSet(Interface):
"""Check to see if a matching charm recipe exists."""
@operation_parameters(
- owner=Reference(IPerson, title=_("Owner"), required=True))
+ owner=Reference(IPerson, title=_("Owner"), required=True)
+ )
@operation_returns_collection_of(ICharmRecipe)
@export_read_operation()
@operation_for_version("devel")
diff --git a/lib/lp/charms/interfaces/charmrecipebuild.py b/lib/lp/charms/interfaces/charmrecipebuild.py
index 2bbfe04..60574a8 100644
--- a/lib/lp/charms/interfaces/charmrecipebuild.py
+++ b/lib/lp/charms/interfaces/charmrecipebuild.py
@@ -9,52 +9,36 @@ __all__ = [
"ICharmFile",
"ICharmRecipeBuild",
"ICharmRecipeBuildSet",
- ]
+]
import http.client
-from lazr.enum import (
- EnumeratedType,
- Item,
- )
+from lazr.enum import EnumeratedType, Item
from lazr.restful.declarations import (
error_status,
export_write_operation,
exported,
exported_as_webservice_entry,
operation_for_version,
- )
-from lazr.restful.fields import (
- CollectionField,
- Reference,
- )
-from zope.interface import (
- Attribute,
- Interface,
- )
-from zope.schema import (
- Bool,
- Choice,
- Datetime,
- Dict,
- Int,
- TextLine,
- )
+)
+from lazr.restful.fields import CollectionField, Reference
+from zope.interface import Attribute, Interface
+from zope.schema import Bool, Choice, Datetime, Dict, Int, TextLine
from lp import _
from lp.buildmaster.interfaces.buildfarmjob import (
IBuildFarmJobAdmin,
IBuildFarmJobEdit,
ISpecificBuildFarmJobSource,
- )
+)
from lp.buildmaster.interfaces.packagebuild import (
IPackageBuild,
IPackageBuildView,
- )
+)
from lp.charms.interfaces.charmrecipe import (
ICharmRecipe,
ICharmRecipeBuildRequest,
- )
+)
from lp.registry.interfaces.person import IPerson
from lp.services.database.constants import DEFAULT
from lp.services.librarian.interfaces import ILibraryFileAlias
@@ -73,37 +57,47 @@ class CharmRecipeBuildStoreUploadStatus(EnumeratedType):
state of that process.
"""
- UNSCHEDULED = Item("""
+ UNSCHEDULED = Item(
+ """
Unscheduled
No upload of this charm recipe build to Charmhub is scheduled.
- """)
+ """
+ )
- PENDING = Item("""
+ PENDING = Item(
+ """
Pending
This charm recipe build is queued for upload to Charmhub.
- """)
+ """
+ )
- FAILEDTOUPLOAD = Item("""
+ FAILEDTOUPLOAD = Item(
+ """
Failed to upload
The last attempt to upload this charm recipe build to Charmhub
failed.
- """)
+ """
+ )
- FAILEDTORELEASE = Item("""
+ FAILEDTORELEASE = Item(
+ """
Failed to release to channels
The last attempt to release this charm recipe build to its intended
set of channels failed.
- """)
+ """
+ )
- UPLOADED = Item("""
+ UPLOADED = Item(
+ """
Uploaded
This charm recipe build was successfully uploaded to Charmhub.
- """)
+ """
+ )
class ICharmRecipeBuildView(IPackageBuildView):
@@ -112,89 +106,140 @@ class ICharmRecipeBuildView(IPackageBuildView):
build_request = Reference(
ICharmRecipeBuildRequest,
title=_("The build request that caused this build to be created."),
- required=True, readonly=True)
+ required=True,
+ readonly=True,
+ )
- requester = exported(Reference(
- IPerson,
- title=_("The person who requested this build."),
- required=True, readonly=True))
+ requester = exported(
+ Reference(
+ IPerson,
+ title=_("The person who requested this build."),
+ required=True,
+ readonly=True,
+ )
+ )
- recipe = exported(Reference(
- ICharmRecipe,
- title=_("The charm recipe to build."),
- required=True, readonly=True))
+ recipe = exported(
+ Reference(
+ ICharmRecipe,
+ title=_("The charm recipe to build."),
+ required=True,
+ readonly=True,
+ )
+ )
- distro_arch_series = exported(Reference(
- IDistroArchSeries,
- title=_("The series and architecture for which to build."),
- required=True, readonly=True))
+ distro_arch_series = exported(
+ Reference(
+ IDistroArchSeries,
+ title=_("The series and architecture for which to build."),
+ required=True,
+ readonly=True,
+ )
+ )
arch_tag = exported(
- TextLine(title=_("Architecture tag"), required=True, readonly=True))
+ TextLine(title=_("Architecture tag"), required=True, readonly=True)
+ )
- channels = exported(Dict(
- title=_("Source snap channels to use for this build."),
- description=_(
- "A dictionary mapping snap names to channels to use for this "
- "build. Currently only 'charmcraft', 'core', 'core18', 'core20', "
- "and 'core22' keys are supported."),
- key_type=TextLine()))
+ channels = exported(
+ Dict(
+ title=_("Source snap channels to use for this build."),
+ description=_(
+ "A dictionary mapping snap names to channels to use for this "
+ "build. Currently only 'charmcraft', 'core', 'core18', "
+ "'core20', and 'core22' keys are supported."
+ ),
+ key_type=TextLine(),
+ )
+ )
virtualized = Bool(
- title=_("If True, this build is virtualized."), readonly=True)
+ title=_("If True, this build is virtualized."), readonly=True
+ )
- score = exported(Int(
- title=_("Score of the related build farm job (if any)."),
- required=False, readonly=True))
+ score = exported(
+ Int(
+ title=_("Score of the related build farm job (if any)."),
+ required=False,
+ readonly=True,
+ )
+ )
eta = Datetime(
title=_("The datetime when the build job is estimated to complete."),
- readonly=True)
+ readonly=True,
+ )
estimate = Bool(
- title=_("If true, the date value is an estimate."), readonly=True)
+ title=_("If true, the date value is an estimate."), readonly=True
+ )
date = Datetime(
title=_(
- "The date when the build completed or is estimated to complete."),
- readonly=True)
+ "The date when the build completed or is estimated to complete."
+ ),
+ readonly=True,
+ )
- revision_id = exported(TextLine(
- title=_("Revision ID"), required=False, readonly=True,
- description=_(
- "The revision ID of the branch used for this build, if "
- "available.")))
+ revision_id = exported(
+ TextLine(
+ title=_("Revision ID"),
+ required=False,
+ readonly=True,
+ description=_(
+ "The revision ID of the branch used for this build, if "
+ "available."
+ ),
+ )
+ )
store_upload_jobs = CollectionField(
title=_("Store upload jobs for this build."),
# Really ICharmhubUploadJob.
value_type=Reference(schema=Interface),
- readonly=True)
+ readonly=True,
+ )
# Really ICharmhubUploadJob.
last_store_upload_job = Reference(
- title=_("Last store upload job for this build."), schema=Interface)
-
- store_upload_status = exported(Choice(
- title=_("Store upload status"),
- vocabulary=CharmRecipeBuildStoreUploadStatus,
- required=True, readonly=False))
-
- store_upload_revision = exported(Int(
- title=_("Store revision"),
- description=_(
- "The revision assigned to this charm recipe build by Charmhub."),
- required=False, readonly=True))
-
- store_upload_error_message = exported(TextLine(
- title=_("Store upload error message"),
- description=_(
- "The error message, if any, from the last attempt to upload "
- "this charm recipe build to Charmhub."),
- required=False, readonly=True))
+ title=_("Last store upload job for this build."), schema=Interface
+ )
+
+ store_upload_status = exported(
+ Choice(
+ title=_("Store upload status"),
+ vocabulary=CharmRecipeBuildStoreUploadStatus,
+ required=True,
+ readonly=False,
+ )
+ )
+
+ store_upload_revision = exported(
+ Int(
+ title=_("Store revision"),
+ description=_(
+ "The revision assigned to this charm recipe build by Charmhub."
+ ),
+ required=False,
+ readonly=True,
+ )
+ )
+
+ store_upload_error_message = exported(
+ TextLine(
+ title=_("Store upload error message"),
+ description=_(
+ "The error message, if any, from the last attempt to upload "
+ "this charm recipe build to Charmhub."
+ ),
+ required=False,
+ readonly=True,
+ )
+ )
store_upload_metadata = Attribute(
- _("A dict of data about store upload progress."))
+ _("A dict of data about store upload progress.")
+ )
def getFiles():
"""Retrieve the build's `ICharmFile` records.
@@ -249,16 +294,25 @@ class ICharmRecipeBuildAdmin(IBuildFarmJobAdmin):
# "devel".
@exported_as_webservice_entry(as_of="beta")
class ICharmRecipeBuild(
- ICharmRecipeBuildView, ICharmRecipeBuildEdit, ICharmRecipeBuildAdmin,
- IPackageBuild):
+ ICharmRecipeBuildView,
+ ICharmRecipeBuildEdit,
+ ICharmRecipeBuildAdmin,
+ IPackageBuild,
+):
"""A build record for a charm recipe."""
class ICharmRecipeBuildSet(ISpecificBuildFarmJobSource):
"""Utility to create and access `ICharmRecipeBuild`s."""
- def new(build_request, recipe, distro_arch_series, channels=None,
- store_upload_metadata=None, date_created=DEFAULT):
+ def new(
+ build_request,
+ recipe,
+ distro_arch_series,
+ channels=None,
+ store_upload_metadata=None,
+ date_created=DEFAULT,
+ ):
"""Create an `ICharmRecipeBuild`."""
def preloadBuildsData(builds):
@@ -271,8 +325,13 @@ class ICharmFile(Interface):
build = Reference(
ICharmRecipeBuild,
title=_("The charm recipe build producing this file."),
- required=True, readonly=True)
+ required=True,
+ readonly=True,
+ )
library_file = Reference(
- ILibraryFileAlias, title=_("the library file alias for this file."),
- required=True, readonly=True)
+ ILibraryFileAlias,
+ title=_("the library file alias for this file."),
+ required=True,
+ readonly=True,
+ )
diff --git a/lib/lp/charms/interfaces/charmrecipebuildjob.py b/lib/lp/charms/interfaces/charmrecipebuildjob.py
index 40e0ece..eeef0c6 100644
--- a/lib/lp/charms/interfaces/charmrecipebuildjob.py
+++ b/lib/lp/charms/interfaces/charmrecipebuildjob.py
@@ -4,40 +4,36 @@
"""Charm recipe build job interfaces."""
__all__ = [
- 'ICharmRecipeBuildJob',
- 'ICharmhubUploadJob',
- 'ICharmhubUploadJobSource',
- ]
+ "ICharmRecipeBuildJob",
+ "ICharmhubUploadJob",
+ "ICharmhubUploadJobSource",
+]
from lazr.restful.fields import Reference
-from zope.interface import (
- Attribute,
- Interface,
- )
-from zope.schema import (
- Int,
- TextLine,
- )
+from zope.interface import Attribute, Interface
+from zope.schema import Int, TextLine
from lp import _
from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
-from lp.services.job.interfaces.job import (
- IJob,
- IJobSource,
- IRunnableJob,
- )
+from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
class ICharmRecipeBuildJob(Interface):
"""A job related to a charm recipe build."""
job = Reference(
- title=_("The common Job attributes."), schema=IJob,
- required=True, readonly=True)
+ title=_("The common Job attributes."),
+ schema=IJob,
+ required=True,
+ readonly=True,
+ )
build = Reference(
title=_("The charm recipe build to use for this job."),
- schema=ICharmRecipeBuild, required=True, readonly=True)
+ schema=ICharmRecipeBuild,
+ required=True,
+ readonly=True,
+ )
metadata = Attribute(_("A dict of data about the job."))
@@ -46,31 +42,40 @@ class ICharmhubUploadJob(IRunnableJob):
"""A Job that uploads a charm recipe build to Charmhub."""
store_metadata = Attribute(
- _("Combined metadata for this job and the matching build"))
+ _("Combined metadata for this job and the matching build")
+ )
error_message = TextLine(
- title=_("Error message"), required=False, readonly=True)
+ title=_("Error message"), required=False, readonly=True
+ )
error_detail = TextLine(
- title=_("Error message detail"), required=False, readonly=True)
+ title=_("Error message detail"), required=False, readonly=True
+ )
upload_id = Int(
title=_(
"The ID returned by Charmhub when uploading this build's charm "
- "file."),
- required=False, readonly=True)
+ "file."
+ ),
+ required=False,
+ readonly=True,
+ )
status_url = TextLine(
title=_("The URL on Charmhub to get the status of this build"),
- required=False, readonly=True)
+ required=False,
+ readonly=True,
+ )
store_revision = Int(
title=_("The revision assigned to this build by Charmhub"),
- required=False, readonly=True)
+ required=False,
+ readonly=True,
+ )
class ICharmhubUploadJobSource(IJobSource):
-
def create(build):
"""Upload a charm recipe build to Charmhub.
diff --git a/lib/lp/charms/interfaces/charmrecipejob.py b/lib/lp/charms/interfaces/charmrecipejob.py
index d353bb6..d1fb89e 100644
--- a/lib/lp/charms/interfaces/charmrecipejob.py
+++ b/lib/lp/charms/interfaces/charmrecipejob.py
@@ -7,45 +7,38 @@ __all__ = [
"ICharmRecipeJob",
"ICharmRecipeRequestBuildsJob",
"ICharmRecipeRequestBuildsJobSource",
- ]
+]
from lazr.restful.fields import Reference
-from zope.interface import (
- Attribute,
- Interface,
- )
-from zope.schema import (
- Datetime,
- Dict,
- List,
- Set,
- TextLine,
- )
+from zope.interface import Attribute, Interface
+from zope.schema import Datetime, Dict, List, Set, TextLine
from lp import _
from lp.charms.interfaces.charmrecipe import (
ICharmRecipe,
ICharmRecipeBuildRequest,
- )
+)
from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
from lp.registry.interfaces.person import IPerson
-from lp.services.job.interfaces.job import (
- IJob,
- IJobSource,
- IRunnableJob,
- )
+from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
class ICharmRecipeJob(Interface):
"""A job related to a charm recipe."""
job = Reference(
- title=_("The common Job attributes."), schema=IJob,
- required=True, readonly=True)
+ title=_("The common Job attributes."),
+ schema=IJob,
+ required=True,
+ readonly=True,
+ )
recipe = Reference(
title=_("The charm recipe to use for this job."),
- schema=ICharmRecipe, required=True, readonly=True)
+ schema=ICharmRecipe,
+ required=True,
+ readonly=True,
+ )
metadata = Attribute(_("A dict of data about the job."))
@@ -54,45 +47,63 @@ class ICharmRecipeRequestBuildsJob(IRunnableJob):
"""A Job that processes a request for builds of a charm recipe."""
requester = Reference(
- title=_("The person requesting the builds."), schema=IPerson,
- required=True, readonly=True)
+ title=_("The person requesting the builds."),
+ schema=IPerson,
+ required=True,
+ readonly=True,
+ )
channels = Dict(
title=_("Source snap channels to use for these builds."),
description=_(
"A dictionary mapping snap names to channels to use for these "
"builds. Currently only 'charmcraft', 'core', 'core18', "
- "'core20', and 'core22' keys are supported."),
- key_type=TextLine(), required=False, readonly=True)
+ "'core20', and 'core22' keys are supported."
+ ),
+ key_type=TextLine(),
+ required=False,
+ readonly=True,
+ )
architectures = Set(
title=_("If set, limit builds to these architecture tags."),
- value_type=TextLine(), required=False, readonly=True)
+ value_type=TextLine(),
+ required=False,
+ readonly=True,
+ )
date_created = Datetime(
title=_("Time when this job was created."),
- required=True, readonly=True)
+ required=True,
+ readonly=True,
+ )
date_finished = Datetime(
- title=_("Time when this job finished."),
- required=True, readonly=True)
+ title=_("Time when this job finished."), required=True, readonly=True
+ )
error_message = TextLine(
title=_("Error message resulting from running this job."),
- required=False, readonly=True)
+ required=False,
+ readonly=True,
+ )
build_request = Reference(
title=_("The build request corresponding to this job."),
- schema=ICharmRecipeBuildRequest, required=True, readonly=True)
+ schema=ICharmRecipeBuildRequest,
+ required=True,
+ readonly=True,
+ )
builds = List(
title=_("The builds created by this request."),
value_type=Reference(schema=ICharmRecipeBuild),
- required=True, readonly=True)
+ required=True,
+ readonly=True,
+ )
class ICharmRecipeRequestBuildsJobSource(IJobSource):
-
def create(recipe, requester, channels=None, architectures=None):
"""Request builds of a charm recipe.
diff --git a/lib/lp/charms/interfaces/webservice.py b/lib/lp/charms/interfaces/webservice.py
index 5294ec1..5be9358 100644
--- a/lib/lp/charms/interfaces/webservice.py
+++ b/lib/lp/charms/interfaces/webservice.py
@@ -16,33 +16,32 @@ __all__ = [
"ICharmRecipeBuild",
"ICharmRecipeBuildRequest",
"ICharmRecipeSet",
- ]
+]
-from lp.charms.interfaces.charmbase import (
- ICharmBase,
- ICharmBaseSet,
- )
+from lp.charms.interfaces.charmbase import ICharmBase, ICharmBaseSet
from lp.charms.interfaces.charmrecipe import (
ICharmRecipe,
ICharmRecipeBuildRequest,
ICharmRecipeSet,
ICharmRecipeView,
- )
+)
from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
from lp.services.webservice.apihelpers import (
patch_collection_property,
patch_reference_property,
- )
-
+)
# ICharmRecipeBuildRequest
patch_reference_property(ICharmRecipeBuildRequest, "recipe", ICharmRecipe)
patch_collection_property(
- ICharmRecipeBuildRequest, "builds", ICharmRecipeBuild)
+ ICharmRecipeBuildRequest, "builds", ICharmRecipeBuild
+)
# ICharmRecipeView
patch_collection_property(ICharmRecipeView, "builds", ICharmRecipeBuild)
patch_collection_property(
- ICharmRecipeView, "completed_builds", ICharmRecipeBuild)
+ ICharmRecipeView, "completed_builds", ICharmRecipeBuild
+)
patch_collection_property(
- ICharmRecipeView, "pending_builds", ICharmRecipeBuild)
+ ICharmRecipeView, "pending_builds", ICharmRecipeBuild
+)
diff --git a/lib/lp/charms/mail/charmrecipebuild.py b/lib/lp/charms/mail/charmrecipebuild.py
index bb811ad..172d9af 100644
--- a/lib/lp/charms/mail/charmrecipebuild.py
+++ b/lib/lp/charms/mail/charmrecipebuild.py
@@ -3,14 +3,11 @@
__all__ = [
"CharmRecipeBuildMailer",
- ]
+]
from lp.app.browser.tales import DurationFormatterAPI
from lp.services.config import config
-from lp.services.mail.basemailer import (
- BaseMailer,
- RecipientReason,
- )
+from lp.services.mail.basemailer import BaseMailer, RecipientReason
from lp.services.webapp import canonical_url
@@ -28,9 +25,12 @@ class CharmRecipeBuildMailer(BaseMailer):
recipients = {requester: RecipientReason.forBuildRequester(requester)}
return cls(
"[Charm recipe build #%(build_id)d] %(build_title)s",
- "charmrecipebuild-notification.txt", recipients,
- config.canonical.noreply_from_address, "charm-recipe-build-status",
- build)
+ "charmrecipebuild-notification.txt",
+ recipients,
+ config.canonical.noreply_from_address,
+ "charm-recipe-build-status",
+ build,
+ )
@classmethod
def forUnauthorizedUpload(cls, build):
@@ -42,9 +42,12 @@ class CharmRecipeBuildMailer(BaseMailer):
recipients = {requester: RecipientReason.forBuildRequester(requester)}
return cls(
"Charmhub authorization failed for %(recipe_name)s",
- "charmrecipebuild-unauthorized.txt", recipients,
+ "charmrecipebuild-unauthorized.txt",
+ recipients,
config.canonical.noreply_from_address,
- "charm-recipe-build-upload-unauthorized", build)
+ "charm-recipe-build-upload-unauthorized",
+ build,
+ )
@classmethod
def forUploadFailure(cls, build):
@@ -56,9 +59,12 @@ class CharmRecipeBuildMailer(BaseMailer):
recipients = {requester: RecipientReason.forBuildRequester(requester)}
return cls(
"Charmhub upload failed for %(recipe_name)s",
- "charmrecipebuild-uploadfailed.txt", recipients,
+ "charmrecipebuild-uploadfailed.txt",
+ recipients,
config.canonical.noreply_from_address,
- "charm-recipe-build-upload-failed", build)
+ "charm-recipe-build-upload-failed",
+ build,
+ )
@classmethod
def forUploadReviewFailure(cls, build):
@@ -70,9 +76,12 @@ class CharmRecipeBuildMailer(BaseMailer):
recipients = {requester: RecipientReason.forBuildRequester(requester)}
return cls(
"Charmhub upload review failed for %(recipe_name)s",
- "charmrecipebuild-reviewfailed.txt", recipients,
+ "charmrecipebuild-reviewfailed.txt",
+ recipients,
config.canonical.noreply_from_address,
- "charm-recipe-build-upload-review-failed", build)
+ "charm-recipe-build-upload-review-failed",
+ build,
+ )
@classmethod
def forReleaseFailure(cls, build):
@@ -84,15 +93,29 @@ class CharmRecipeBuildMailer(BaseMailer):
recipients = {requester: RecipientReason.forBuildRequester(requester)}
return cls(
"Charmhub release failed for %(recipe_name)s",
- "charmrecipebuild-releasefailed.txt", recipients,
+ "charmrecipebuild-releasefailed.txt",
+ recipients,
config.canonical.noreply_from_address,
- "charm-recipe-build-release-failed", build)
-
- def __init__(self, subject, template_name, recipients, from_address,
- notification_type, build):
+ "charm-recipe-build-release-failed",
+ build,
+ )
+
+ def __init__(
+ self,
+ subject,
+ template_name,
+ recipients,
+ from_address,
+ notification_type,
+ build,
+ ):
super().__init__(
- subject, template_name, recipients, from_address,
- notification_type=notification_type)
+ subject,
+ template_name,
+ recipients,
+ from_address,
+ notification_type=notification_type,
+ )
self.build = build
def _getHeaders(self, email, recipient):
@@ -110,23 +133,26 @@ class CharmRecipeBuildMailer(BaseMailer):
else:
error_message = upload_job.error_message or ""
params = super()._getTemplateParams(email, recipient)
- params.update({
- "architecturetag": build.distro_arch_series.architecturetag,
- "build_duration": "",
- "build_id": build.id,
- "build_state": build.status.title,
- "build_title": build.title,
- "build_url": canonical_url(build),
- "builder_url": "",
- "store_error_message": error_message,
- "distroseries": build.distro_series,
- "log_url": "",
- "project_name": build.recipe.project.name,
- "recipe_authorize_url": canonical_url(
- build.recipe, view_name="+authorize"),
- "recipe_name": build.recipe.name,
- "upload_log_url": "",
- })
+ params.update(
+ {
+ "architecturetag": build.distro_arch_series.architecturetag,
+ "build_duration": "",
+ "build_id": build.id,
+ "build_state": build.status.title,
+ "build_title": build.title,
+ "build_url": canonical_url(build),
+ "builder_url": "",
+ "store_error_message": error_message,
+ "distroseries": build.distro_series,
+ "log_url": "",
+ "project_name": build.recipe.project.name,
+ "recipe_authorize_url": canonical_url(
+ build.recipe, view_name="+authorize"
+ ),
+ "recipe_name": build.recipe.name,
+ "upload_log_url": "",
+ }
+ )
if build.duration is not None:
duration_formatter = DurationFormatterAPI(build.duration)
params["build_duration"] = duration_formatter.approximateduration()
@@ -140,5 +166,4 @@ class CharmRecipeBuildMailer(BaseMailer):
def _getFooter(self, email, recipient, params):
"""See `BaseMailer`."""
- return ("%(build_url)s\n"
- "%(reason)s\n" % params)
+ return "%(build_url)s\n%(reason)s\n" % params
diff --git a/lib/lp/charms/model/charmbase.py b/lib/lp/charms/model/charmbase.py
index 6546b26..959b26c 100644
--- a/lib/lp/charms/model/charmbase.py
+++ b/lib/lp/charms/model/charmbase.py
@@ -5,17 +5,11 @@
__all__ = [
"CharmBase",
- ]
+]
import pytz
from storm.databases.postgres import JSON
-from storm.locals import (
- DateTime,
- Int,
- Reference,
- Store,
- Storm,
- )
+from storm.locals import DateTime, Int, Reference, Store, Storm
from zope.interface import implementer
from lp.buildmaster.model.processor import Processor
@@ -24,12 +18,9 @@ from lp.charms.interfaces.charmbase import (
ICharmBase,
ICharmBaseSet,
NoSuchCharmBase,
- )
+)
from lp.services.database.constants import DEFAULT
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
@implementer(ICharmBase)
@@ -41,7 +32,8 @@ class CharmBase(Storm):
id = Int(primary=True)
date_created = DateTime(
- name="date_created", tzinfo=pytz.UTC, allow_none=False)
+ name="date_created", tzinfo=pytz.UTC, allow_none=False
+ )
registrant_id = Int(name="registrant", allow_none=False)
registrant = Reference(registrant_id, "Person.id")
@@ -51,8 +43,13 @@ class CharmBase(Storm):
build_snap_channels = JSON(name="build_snap_channels", allow_none=False)
- def __init__(self, registrant, distro_series, build_snap_channels,
- date_created=DEFAULT):
+ def __init__(
+ self,
+ registrant,
+ distro_series,
+ build_snap_channels,
+ date_created=DEFAULT,
+ ):
super().__init__()
self.registrant = registrant
self.distro_series = distro_series
@@ -60,17 +57,23 @@ class CharmBase(Storm):
self.date_created = date_created
def _getProcessors(self):
- return list(Store.of(self).find(
- Processor,
- Processor.id == CharmBaseArch.processor_id,
- CharmBaseArch.charm_base == self))
+ return list(
+ Store.of(self).find(
+ Processor,
+ Processor.id == CharmBaseArch.processor_id,
+ CharmBaseArch.charm_base == self,
+ )
+ )
def setProcessors(self, processors):
"""See `ICharmBase`."""
- enablements = dict(Store.of(self).find(
- (Processor, CharmBaseArch),
- Processor.id == CharmBaseArch.processor_id,
- CharmBaseArch.charm_base == self))
+ enablements = dict(
+ Store.of(self).find(
+ (Processor, CharmBaseArch),
+ Processor.id == CharmBaseArch.processor_id,
+ CharmBaseArch.charm_base == self,
+ )
+ )
for proc in enablements:
if proc not in processors:
Store.of(self).remove(enablements[proc])
@@ -105,8 +108,14 @@ class CharmBaseArch(Storm):
class CharmBaseSet:
"""See `ICharmBaseSet`."""
- def new(self, registrant, distro_series, build_snap_channels,
- processors=None, date_created=DEFAULT):
+ def new(
+ self,
+ registrant,
+ distro_series,
+ build_snap_channels,
+ processors=None,
+ date_created=DEFAULT,
+ ):
"""See `ICharmBaseSet`."""
try:
self.getByDistroSeries(distro_series)
@@ -116,12 +125,16 @@ class CharmBaseSet:
raise DuplicateCharmBase(distro_series)
store = IMasterStore(CharmBase)
charm_base = CharmBase(
- registrant, distro_series, build_snap_channels,
- date_created=date_created)
+ registrant,
+ distro_series,
+ build_snap_channels,
+ date_created=date_created,
+ )
store.add(charm_base)
if processors is None:
processors = [
- das.processor for das in distro_series.enabled_architectures]
+ das.processor for das in distro_series.enabled_architectures
+ ]
charm_base.setProcessors(processors)
return charm_base
@@ -135,13 +148,19 @@ class CharmBaseSet:
def getByDistroSeries(self, distro_series):
"""See `ICharmBaseSet`."""
- charm_base = IStore(CharmBase).find(
- CharmBase, distro_series=distro_series).one()
+ charm_base = (
+ IStore(CharmBase)
+ .find(CharmBase, distro_series=distro_series)
+ .one()
+ )
if charm_base is None:
raise NoSuchCharmBase(distro_series)
return charm_base
def getAll(self):
"""See `ICharmBaseSet`."""
- return IStore(CharmBase).find(CharmBase).order_by(
- CharmBase.distro_series_id)
+ return (
+ IStore(CharmBase)
+ .find(CharmBase)
+ .order_by(CharmBase.distro_series_id)
+ )
diff --git a/lib/lp/charms/model/charmhubclient.py b/lib/lp/charms/model/charmhubclient.py
index 1ac2f6c..cfc9567 100644
--- a/lib/lp/charms/model/charmhubclient.py
+++ b/lib/lp/charms/model/charmhubclient.py
@@ -5,15 +5,15 @@
__all__ = [
"CharmhubClient",
- ]
+]
from base64 import b64encode
from urllib.parse import quote
+import requests
from lazr.restful.utils import get_current_browser_request
from pymacaroons import Macaroon
from pymacaroons.serializers import JsonSerializer
-import requests
from requests_toolbelt import MultipartEncoder
from zope.component import getUtility
from zope.interface import implementer
@@ -28,12 +28,9 @@ from lp.charms.interfaces.charmhubclient import (
UnauthorizedUploadResponse,
UploadFailedResponse,
UploadNotReviewedYetResponse,
- )
+)
from lp.services.config import config
-from lp.services.crypto.interfaces import (
- CryptoError,
- IEncryptedContainer,
- )
+from lp.services.crypto.interfaces import CryptoError, IEncryptedContainer
from lp.services.librarian.utils import EncodableLibraryFileAlias
from lp.services.timeline.requesttimeline import get_request_timeline
from lp.services.timeout import urlfetch
@@ -46,13 +43,15 @@ def _get_macaroon(recipe):
macaroon_raw = store_secrets.get("exchanged_encrypted")
if macaroon_raw is None:
raise UnauthorizedUploadResponse(
- "{} is not authorized for upload to Charmhub".format(recipe))
+ "{} is not authorized for upload to Charmhub".format(recipe)
+ )
container = getUtility(IEncryptedContainer, "charmhub-secrets")
try:
return container.decrypt(macaroon_raw).decode()
except CryptoError as e:
raise UnauthorizedUploadResponse(
- "Failed to decrypt macaroon: {}".format(e))
+ "Failed to decrypt macaroon: {}".format(e)
+ )
@implementer(ICharmhubClient)
@@ -80,7 +79,8 @@ class CharmhubClient:
if "error-list" in response_data:
error_message = "\n".join(
error["message"]
- for error in response_data["error-list"])
+ for error in response_data["error-list"]
+ )
detail = requests_error.response.content.decode(errors="replace")
can_retry = requests_error.response.status_code in (502, 503, 504)
return error_class(error_message, detail=detail, can_retry=can_retry)
@@ -92,20 +92,24 @@ class CharmhubClient:
request_url = urlappend(config.charms.charmhub_url, "v1/tokens")
request = get_current_browser_request()
timeline_action = get_request_timeline(request).start(
- "request-charm-upload-macaroon", package_name, allow_nested=True)
+ "request-charm-upload-macaroon", package_name, allow_nested=True
+ )
try:
response = urlfetch(
- request_url, method="POST",
+ request_url,
+ method="POST",
json={
"description": "{} for {}".format(
- package_name, config.vhost.mainsite.hostname),
+ package_name, config.vhost.mainsite.hostname
+ ),
"packages": [{"type": "charm", "name": package_name}],
"permissions": [
"package-manage-releases",
"package-manage-revisions",
"package-view-revisions",
- ],
- })
+ ],
+ },
+ )
response_data = response.json()
if "macaroon" not in response_data:
raise BadRequestPackageUploadResponse(response.text)
@@ -116,27 +120,39 @@ class CharmhubClient:
timeline_action.finish()
@classmethod
- def exchangeMacaroons(cls, root_macaroon_raw,
- unbound_discharge_macaroon_raw):
+ def exchangeMacaroons(
+ cls, root_macaroon_raw, unbound_discharge_macaroon_raw
+ ):
"""See `ICharmhubClient`."""
assert config.charms.charmhub_url is not None
root_macaroon = Macaroon.deserialize(
- root_macaroon_raw, JsonSerializer())
+ root_macaroon_raw, JsonSerializer()
+ )
unbound_discharge_macaroon = Macaroon.deserialize(
- unbound_discharge_macaroon_raw, JsonSerializer())
+ unbound_discharge_macaroon_raw, JsonSerializer()
+ )
discharge_macaroon_raw = root_macaroon.prepare_for_request(
- unbound_discharge_macaroon).serialize(JsonSerializer())
+ unbound_discharge_macaroon
+ ).serialize(JsonSerializer())
request_url = urlappend(
- config.charms.charmhub_url, "v1/tokens/exchange")
+ config.charms.charmhub_url, "v1/tokens/exchange"
+ )
request = get_current_browser_request()
timeline_action = get_request_timeline(request).start(
- "exchange-macaroons", "", allow_nested=True)
+ "exchange-macaroons", "", allow_nested=True
+ )
try:
- all_macaroons = b64encode("[{}, {}]".format(
- root_macaroon_raw, discharge_macaroon_raw).encode()).decode()
+ all_macaroons = b64encode(
+ "[{}, {}]".format(
+ root_macaroon_raw, discharge_macaroon_raw
+ ).encode()
+ ).decode()
response = urlfetch(
- request_url, method="POST",
- headers={"Macaroons": all_macaroons}, json={})
+ request_url,
+ method="POST",
+ headers={"Macaroons": all_macaroons},
+ json={},
+ )
response_data = response.json()
if "macaroon" not in response_data:
raise BadExchangeMacaroonsResponse(response.text)
@@ -151,25 +167,34 @@ class CharmhubClient:
"""Upload a single file to Charmhub's storage."""
assert config.charms.charmhub_storage_url is not None
unscanned_upload_url = urlappend(
- config.charms.charmhub_storage_url, "unscanned-upload/")
+ config.charms.charmhub_storage_url, "unscanned-upload/"
+ )
lfa.open()
try:
lfa_wrapper = EncodableLibraryFileAlias(lfa)
encoder = MultipartEncoder(
fields={
"binary": (
- lfa.filename, lfa_wrapper, "application/octet-stream"),
- })
+ lfa.filename,
+ lfa_wrapper,
+ "application/octet-stream",
+ ),
+ }
+ )
request = get_current_browser_request()
timeline_action = get_request_timeline(request).start(
- "charm-storage-push", lfa.filename, allow_nested=True)
+ "charm-storage-push", lfa.filename, allow_nested=True
+ )
try:
response = urlfetch(
- unscanned_upload_url, method="POST", data=encoder,
+ unscanned_upload_url,
+ method="POST",
+ data=encoder,
headers={
"Content-Type": encoder.content_type,
"Accept": "application/json",
- })
+ },
+ )
response_data = response.json()
if not response_data.get("successful", False):
raise UploadFailedResponse(response.text)
@@ -188,17 +213,21 @@ class CharmhubClient:
assert recipe.can_upload_to_store
push_url = urlappend(
config.charms.charmhub_url,
- "v1/charm/{}/revisions".format(quote(recipe.store_name)))
+ "v1/charm/{}/revisions".format(quote(recipe.store_name)),
+ )
macaroon_raw = _get_macaroon(recipe)
data = {"upload-id": upload_id}
request = get_current_browser_request()
timeline_action = get_request_timeline(request).start(
- "charm-push", recipe.store_name, allow_nested=True)
+ "charm-push", recipe.store_name, allow_nested=True
+ )
try:
response = urlfetch(
- push_url, method="POST",
+ push_url,
+ method="POST",
headers={"Authorization": "Macaroon {}".format(macaroon_raw)},
- json=data)
+ json=data,
+ )
response_data = response.json()
return response_data["status-url"]
except requests.HTTPError as e:
@@ -213,15 +242,18 @@ class CharmhubClient:
def checkStatus(cls, build, status_url):
"""See `ICharmhubClient`."""
status_url = urlappend(
- config.charms.charmhub_url, status_url.lstrip("/"))
+ config.charms.charmhub_url, status_url.lstrip("/")
+ )
macaroon_raw = _get_macaroon(build.recipe)
request = get_current_browser_request()
timeline_action = get_request_timeline(request).start(
- "charm-check-status", status_url, allow_nested=True)
+ "charm-check-status", status_url, allow_nested=True
+ )
try:
response = urlfetch(
status_url,
- headers={"Authorization": "Macaroon {}".format(macaroon_raw)})
+ headers={"Authorization": "Macaroon {}".format(macaroon_raw)},
+ )
response_data = response.json()
# We're asking for a single upload ID, so the response should
# only have one revision.
@@ -231,11 +263,13 @@ class CharmhubClient:
if revision["status"] == "approved":
if revision["revision"] is None:
raise ReviewFailedResponse(
- "Review passed but did not assign a revision.")
+ "Review passed but did not assign a revision."
+ )
return revision["revision"]
elif revision["status"] == "rejected":
error_message = "\n".join(
- error["message"] for error in revision["errors"])
+ error["message"] for error in revision["errors"]
+ )
raise ReviewFailedResponse(error_message)
else:
raise UploadNotReviewedYetResponse()
@@ -254,19 +288,24 @@ class CharmhubClient:
assert recipe.store_channels
release_url = urlappend(
config.charms.charmhub_url,
- "v1/charm/{}/releases".format(quote(recipe.store_name)))
+ "v1/charm/{}/releases".format(quote(recipe.store_name)),
+ )
macaroon_raw = _get_macaroon(recipe)
data = [
{"channel": channel, "revision": revision}
- for channel in recipe.store_channels]
+ for channel in recipe.store_channels
+ ]
request = get_current_browser_request()
timeline_action = get_request_timeline(request).start(
- "charm-release", recipe.store_name, allow_nested=True)
+ "charm-release", recipe.store_name, allow_nested=True
+ )
try:
urlfetch(
- release_url, method="POST",
+ release_url,
+ method="POST",
headers={"Authorization": "Macaroon {}".format(macaroon_raw)},
- json=data)
+ json=data,
+ )
except requests.HTTPError as e:
raise cls._makeCharmhubError(ReleaseFailedResponse, e)
finally:
diff --git a/lib/lp/charms/model/charmrecipe.py b/lib/lp/charms/model/charmrecipe.py
index f1bc919..1b4f0bb 100644
--- a/lib/lp/charms/model/charmrecipe.py
+++ b/lib/lp/charms/model/charmrecipe.py
@@ -6,28 +6,19 @@
__all__ = [
"CharmRecipe",
"get_charm_recipe_privacy_filter",
- ]
+]
import base64
-from datetime import (
- datetime,
- timedelta,
- )
-from operator import (
- attrgetter,
- itemgetter,
- )
+from datetime import datetime, timedelta
+from operator import attrgetter, itemgetter
+import pytz
+import yaml
from lazr.lifecycle.event import ObjectCreatedEvent
from pymacaroons import Macaroon
from pymacaroons.serializers import JsonSerializer
-import pytz
from storm.databases.postgres import JSON
-from storm.expr import (
- Cast,
- Coalesce,
- Except,
- )
+from storm.expr import Cast, Coalesce, Except
from storm.locals import (
And,
Bool,
@@ -41,8 +32,7 @@ from storm.locals import (
Select,
Store,
Unicode,
- )
-import yaml
+)
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
@@ -50,9 +40,9 @@ from zope.security.proxy import removeSecurityProxy
from lp.app.enums import (
FREE_INFORMATION_TYPES,
- InformationType,
PUBLIC_INFORMATION_TYPES,
- )
+ InformationType,
+)
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
@@ -60,19 +50,16 @@ from lp.buildmaster.model.builder import Builder
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.buildmaster.model.buildqueue import BuildQueue
from lp.charms.adapters.buildarch import determine_instances_to_build
-from lp.charms.interfaces.charmbase import (
- ICharmBaseSet,
- NoSuchCharmBase,
- )
+from lp.charms.interfaces.charmbase import ICharmBaseSet, NoSuchCharmBase
from lp.charms.interfaces.charmhubclient import ICharmhubClient
from lp.charms.interfaces.charmrecipe import (
+ CHARM_RECIPE_ALLOW_CREATE,
+ CHARM_RECIPE_BUILD_DISTRIBUTION,
+ CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
BadCharmRecipeSearchContext,
CannotAuthorizeCharmhubUploads,
CannotFetchCharmcraftYaml,
CannotParseCharmcraftYaml,
- CHARM_RECIPE_ALLOW_CREATE,
- CHARM_RECIPE_BUILD_DISTRIBUTION,
- CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
CharmRecipeBuildAlreadyPending,
CharmRecipeBuildDisallowedArchitecture,
CharmRecipeBuildRequestStatus,
@@ -87,25 +74,19 @@ from lp.charms.interfaces.charmrecipe import (
MissingCharmcraftYaml,
NoSourceForCharmRecipe,
NoSuchCharmRecipe,
- )
+)
from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet
from lp.charms.interfaces.charmrecipejob import (
ICharmRecipeRequestBuildsJobSource,
- )
+)
from lp.charms.model.charmbase import CharmBase
from lp.charms.model.charmrecipebuild import CharmRecipeBuild
-from lp.charms.model.charmrecipejob import (
- CharmRecipeJob,
- CharmRecipeJobType,
- )
-from lp.code.errors import (
- GitRepositoryBlobNotFound,
- GitRepositoryScanFault,
- )
+from lp.charms.model.charmrecipejob import CharmRecipeJob, CharmRecipeJobType
+from lp.code.errors import GitRepositoryBlobNotFound, GitRepositoryScanFault
from lp.code.interfaces.gitcollection import (
IAllGitRepositories,
IGitCollection,
- )
+)
from lp.code.interfaces.gitref import IGitRef
from lp.code.interfaces.gitrepository import IGitRepository
from lp.code.model.gitcollection import GenericGitCollection
@@ -117,7 +98,7 @@ from lp.registry.interfaces.person import (
IPerson,
IPersonSet,
validate_public_person,
- )
+)
from lp.registry.interfaces.product import IProduct
from lp.registry.model.distribution import Distribution
from lp.registry.model.distroseries import DistroSeries
@@ -127,38 +108,26 @@ from lp.services.config import config
from lp.services.crypto.interfaces import IEncryptedContainer
from lp.services.crypto.model import NaClEncryptedContainerBase
from lp.services.database.bulk import load_related
-from lp.services.database.constants import (
- DEFAULT,
- UTC_NOW,
- )
+from lp.services.database.constants import DEFAULT, UTC_NOW
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
from lp.services.database.stormbase import StormBase
from lp.services.database.stormexpr import (
Greatest,
IsTrue,
JSONExtract,
NullsLast,
- )
+)
from lp.services.features import getFeatureFlag
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
from lp.services.librarian.model import LibraryFileAlias
-from lp.services.propertycache import (
- cachedproperty,
- get_property_cache,
- )
+from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.webapp.candid import extract_candid_caveat
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.webhooks.model import WebhookTargetMixin
-from lp.soyuz.model.distroarchseries import (
- DistroArchSeries,
- PocketChroot,
- )
+from lp.soyuz.model.distroarchseries import DistroArchSeries, PocketChroot
def charm_recipe_modified(recipe, event):
@@ -213,7 +182,7 @@ class CharmRecipeBuildRequest:
JobStatus.COMPLETED: CharmRecipeBuildRequestStatus.COMPLETED,
JobStatus.FAILED: CharmRecipeBuildRequestStatus.FAILED,
JobStatus.SUSPENDED: CharmRecipeBuildRequestStatus.PENDING,
- }
+ }
return status_map[self._job.job.status]
@property
@@ -251,9 +220,11 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
id = Int(primary=True)
date_created = DateTime(
- name="date_created", tzinfo=pytz.UTC, allow_none=False)
+ name="date_created", tzinfo=pytz.UTC, allow_none=False
+ )
date_last_modified = DateTime(
- name="date_last_modified", tzinfo=pytz.UTC, allow_none=False)
+ name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+ )
registrant_id = Int(name="registrant", allow_none=False)
registrant = Reference(registrant_id, "Person.id")
@@ -264,7 +235,8 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
validate_public_person(self, attr, value)
except PrivatePersonLinkageError:
raise CharmRecipePrivacyMismatch(
- "A public charm recipe cannot have a private owner.")
+ "A public charm recipe cannot have a private owner."
+ )
return value
owner_id = Int(name="owner", allow_none=False, validator=_validate_owner)
@@ -281,12 +253,15 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
if not self.private and value is not None:
if IStore(GitRepository).get(GitRepository, value).private:
raise CharmRecipePrivacyMismatch(
- "A public charm recipe cannot have a private repository.")
+ "A public charm recipe cannot have a private repository."
+ )
return value
git_repository_id = Int(
- name="git_repository", allow_none=True,
- validator=_validate_git_repository)
+ name="git_repository",
+ allow_none=True,
+ validator=_validate_git_repository,
+ )
git_repository = Reference(git_repository_id, "GitRepository.id")
git_path = Unicode(name="git_path", allow_none=True)
@@ -297,14 +272,18 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
def _valid_information_type(self, attr, value):
if not getUtility(ICharmRecipeSet).isValidInformationType(
- value, self.owner, self.git_ref):
+ value, self.owner, self.git_ref
+ ):
raise CharmRecipePrivacyMismatch
return value
information_type = DBEnum(
- enum=InformationType, default=InformationType.PUBLIC,
- name="information_type", validator=_valid_information_type,
- allow_none=False)
+ enum=InformationType,
+ default=InformationType.PUBLIC,
+ name="information_type",
+ validator=_valid_information_type,
+ allow_none=False,
+ )
auto_build = Bool(name="auto_build", allow_none=False)
@@ -320,12 +299,25 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
_store_channels = JSON("store_channels", allow_none=True)
- def __init__(self, registrant, owner, project, name, description=None,
- git_ref=None, build_path=None, require_virtualized=True,
- information_type=InformationType.PUBLIC, auto_build=False,
- auto_build_channels=None, store_upload=False,
- store_name=None, store_secrets=None, store_channels=None,
- date_created=DEFAULT):
+ def __init__(
+ self,
+ registrant,
+ owner,
+ project,
+ name,
+ description=None,
+ git_ref=None,
+ build_path=None,
+ require_virtualized=True,
+ information_type=InformationType.PUBLIC,
+ auto_build=False,
+ auto_build_channels=None,
+ store_upload=False,
+ store_name=None,
+ store_secrets=None,
+ store_channels=None,
+ date_created=DEFAULT,
+ ):
"""Construct a `CharmRecipe`."""
if not getFeatureFlag(CHARM_RECIPE_ALLOW_CREATE):
raise CharmRecipeFeatureDisabled()
@@ -353,7 +345,10 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
def __repr__(self):
return "<CharmRecipe ~%s/%s/+charm/%s>" % (
- self.owner.name, self.project.name, self.name)
+ self.owner.name,
+ self.project.name,
+ self.name,
+ )
@property
def private(self):
@@ -409,8 +404,9 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
distro = getUtility(IDistributionSet).getByName(distro_name)
if not distro:
raise ValueError(
- "'%s' is not a valid value for feature rule '%s'" % (
- distro_name, CHARM_RECIPE_BUILD_DISTRIBUTION))
+ "'%s' is not a valid value for feature rule '%s'"
+ % (distro_name, CHARM_RECIPE_BUILD_DISTRIBUTION)
+ )
return distro
@cachedproperty
@@ -419,7 +415,8 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
# Use the series set by this feature rule, or the current series of
# the default distribution if the feature rule is not set.
series_name = getFeatureFlag(
- "charm.default_build_series.%s" % self._default_distribution.name)
+ "charm.default_build_series.%s" % self._default_distribution.name
+ )
if series_name:
return self._default_distribution.getSeries(series_name)
else:
@@ -437,10 +434,15 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
return True
if user is None:
return False
- return not IStore(CharmRecipe).find(
- CharmRecipe,
- CharmRecipe.id == self.id,
- get_charm_recipe_privacy_filter(user)).is_empty()
+ return (
+ not IStore(CharmRecipe)
+ .find(
+ CharmRecipe,
+ CharmRecipe.id == self.id,
+ get_charm_recipe_privacy_filter(user),
+ )
+ .is_empty()
+ )
def _isBuildableArchitectureAllowed(self, das, charm_base=None):
"""Check whether we may build for a buildable `DistroArchSeries`.
@@ -453,55 +455,74 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
das.enabled
and (
das.processor.supports_virtualized
- or not self.require_virtualized)
- and (charm_base is None or das.processor in charm_base.processors))
+ or not self.require_virtualized
+ )
+ and (charm_base is None or das.processor in charm_base.processors)
+ )
def _isArchitectureAllowed(self, das, charm_base=None):
"""Check whether we may build for a `DistroArchSeries`."""
return (
das.getChroot() is not None
and self._isBuildableArchitectureAllowed(
- das, charm_base=charm_base))
+ das, charm_base=charm_base
+ )
+ )
def getAllowedArchitectures(self):
"""See `ICharmRecipe`."""
store = Store.of(self)
origin = [
DistroArchSeries,
- Join(DistroSeries,
- DistroArchSeries.distroseries == DistroSeries.id),
+ Join(
+ DistroSeries, DistroArchSeries.distroseries == DistroSeries.id
+ ),
Join(Distribution, DistroSeries.distribution == Distribution.id),
- Join(PocketChroot,
- PocketChroot.distroarchseries == DistroArchSeries.id),
- Join(LibraryFileAlias,
- PocketChroot.chroot == LibraryFileAlias.id),
- ]
+ Join(
+ PocketChroot,
+ PocketChroot.distroarchseries == DistroArchSeries.id,
+ ),
+ Join(LibraryFileAlias, PocketChroot.chroot == LibraryFileAlias.id),
+ ]
# Preload DistroSeries and Distribution, since we'll need those in
# determine_architectures_to_build.
- results = store.using(*origin).find(
- (DistroArchSeries, DistroSeries, Distribution),
- DistroSeries.status.is_in(ACTIVE_STATUSES)).config(distinct=True)
+ results = (
+ store.using(*origin)
+ .find(
+ (DistroArchSeries, DistroSeries, Distribution),
+ DistroSeries.status.is_in(ACTIVE_STATUSES),
+ )
+ .config(distinct=True)
+ )
all_buildable_dases = DecoratedResultSet(results, itemgetter(0))
charm_bases = {
charm_base.distro_series_id: charm_base
for charm_base in store.find(
CharmBase,
CharmBase.distro_series_id.is_in(
- {das.distroseriesID for das in all_buildable_dases}))}
+ {das.distroseriesID for das in all_buildable_dases}
+ ),
+ )
+ }
return [
- das for das in all_buildable_dases
+ das
+ for das in all_buildable_dases
if self._isBuildableArchitectureAllowed(
- das, charm_base=charm_bases.get(das.distroseriesID))]
+ das, charm_base=charm_bases.get(das.distroseriesID)
+ )
+ ]
def _checkRequestBuild(self, requester):
"""May `requester` request builds of this charm recipe?"""
if not requester.inTeam(self.owner):
raise CharmRecipeNotOwner(
- "%s cannot create charm recipe builds owned by %s." %
- (requester.display_name, self.owner.display_name))
+ "%s cannot create charm recipe builds owned by %s."
+ % (requester.display_name, self.owner.display_name)
+ )
- def requestBuild(self, build_request, distro_arch_series, charm_base=None,
- channels=None):
+ def requestBuild(
+ self, build_request, distro_arch_series, charm_base=None, channels=None
+ ):
"""Request a single build of this charm recipe.
This method is for internal use; external callers should use
@@ -516,13 +537,15 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
"""
self._checkRequestBuild(build_request.requester)
if not self._isArchitectureAllowed(
- distro_arch_series, charm_base=charm_base):
+ distro_arch_series, charm_base=charm_base
+ ):
raise CharmRecipeBuildDisallowedArchitecture(distro_arch_series)
if not channels:
channels_clause = Or(
CharmRecipeBuild.channels == None,
- CharmRecipeBuild.channels == {})
+ CharmRecipeBuild.channels == {},
+ )
else:
channels_clause = CharmRecipeBuild.channels == channels
pending = IStore(self).find(
@@ -530,12 +553,14 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
CharmRecipeBuild.recipe == self,
CharmRecipeBuild.processor == distro_arch_series.processor,
channels_clause,
- CharmRecipeBuild.status == BuildStatus.NEEDSBUILD)
+ CharmRecipeBuild.status == BuildStatus.NEEDSBUILD,
+ )
if pending.any() is not None:
raise CharmRecipeBuildAlreadyPending
build = getUtility(ICharmRecipeBuildSet).new(
- build_request, self, distro_arch_series, channels=channels)
+ build_request, self, distro_arch_series, channels=channels
+ )
build.queueBuild()
notify(ObjectCreatedEvent(build, user=build_request.requester))
return build
@@ -544,17 +569,24 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
"""See `ICharmRecipe`."""
self._checkRequestBuild(requester)
job = getUtility(ICharmRecipeRequestBuildsJobSource).create(
- self, requester, channels=channels, architectures=architectures)
+ self, requester, channels=channels, architectures=architectures
+ )
return self.getBuildRequest(job.job_id)
- def requestBuildsFromJob(self, build_request, channels=None,
- architectures=None, allow_failures=False,
- logger=None):
+ def requestBuildsFromJob(
+ self,
+ build_request,
+ channels=None,
+ architectures=None,
+ allow_failures=False,
+ logger=None,
+ ):
"""See `ICharmRecipe`."""
try:
try:
charmcraft_data = removeSecurityProxy(
- getUtility(ICharmRecipeSet).getCharmcraftYaml(self))
+ getUtility(ICharmRecipeSet).getCharmcraftYaml(self)
+ )
except MissingCharmcraftYaml:
# charmcraft doesn't currently require charmcraft.yaml, and
# we have reasonable defaults without it.
@@ -564,28 +596,41 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
# determinism. This is chosen to be a similar order as in
# BinaryPackageBuildSet.createForSource, to minimize confusion.
supported_arches = [
- das for das in sorted(
+ das
+ for das in sorted(
self.getAllowedArchitectures(),
key=attrgetter(
- "distroseries.distribution.id", "distroseries.id",
- "processor.id"))
- if (architectures is None or
- das.architecturetag in architectures)]
+ "distroseries.distribution.id",
+ "distroseries.id",
+ "processor.id",
+ ),
+ )
+ if (
+ architectures is None
+ or das.architecturetag in architectures
+ )
+ ]
instances_to_build = determine_instances_to_build(
- charmcraft_data, supported_arches, self._default_distro_series)
+ charmcraft_data, supported_arches, self._default_distro_series
+ )
except Exception as e:
if not allow_failures:
raise
elif logger is not None:
logger.exception(
" - %s/%s/%s: %s",
- self.owner.name, self.project.name, self.name, e)
+ self.owner.name,
+ self.project.name,
+ self.name,
+ e,
+ )
builds = []
for das in instances_to_build:
try:
charm_base = getUtility(ICharmBaseSet).getByDistroSeries(
- das.distroseries)
+ das.distroseries
+ )
except NoSuchCharmBase:
charm_base = None
if charm_base is not None:
@@ -599,13 +644,18 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
arch_channels = channels
try:
build = self.requestBuild(
- build_request, das, channels=arch_channels)
+ build_request, das, channels=arch_channels
+ )
if logger is not None:
logger.debug(
" - %s/%s/%s %s/%s/%s: Build requested.",
- self.owner.name, self.project.name, self.name,
+ self.owner.name,
+ self.project.name,
+ self.name,
das.distroseries.distribution.name,
- das.distroseries.name, das.architecturetag)
+ das.distroseries.name,
+ das.architecturetag,
+ )
builds.append(build)
except CharmRecipeBuildAlreadyPending:
pass
@@ -615,9 +665,14 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
elif logger is not None:
logger.exception(
" - %s/%s/%s %s/%s/%s: %s",
- self.owner.name, self.project.name, self.name,
+ self.owner.name,
+ self.project.name,
+ self.name,
das.distroseries.distribution.name,
- das.distroseries.name, das.architecturetag, e)
+ das.distroseries.name,
+ das.architecturetag,
+ e,
+ )
return builds
def requestAutoBuilds(self, logger=None):
@@ -626,9 +681,13 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
if logger is not None:
logger.debug(
"Scheduling builds of charm recipe %s/%s/%s",
- self.owner.name, self.project.name, self.name)
+ self.owner.name,
+ self.project.name,
+ self.name,
+ )
return self.requestBuilds(
- requester=self.owner, channels=self.auto_build_channels)
+ requester=self.owner, channels=self.auto_build_channels
+ )
def getBuildRequest(self, job_id):
"""See `ICharmRecipe`."""
@@ -640,9 +699,11 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
job_source = getUtility(ICharmRecipeRequestBuildsJobSource)
# The returned jobs are ordered by descending ID.
jobs = job_source.findByRecipe(
- self, statuses=(JobStatus.WAITING, JobStatus.RUNNING))
+ self, statuses=(JobStatus.WAITING, JobStatus.RUNNING)
+ )
return DecoratedResultSet(
- jobs, result_decorator=CharmRecipeBuildRequest.fromJob)
+ jobs, result_decorator=CharmRecipeBuildRequest.fromJob
+ )
@property
def failed_build_requests(self):
@@ -651,13 +712,14 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
# The returned jobs are ordered by descending ID.
jobs = job_source.findByRecipe(self, statuses=(JobStatus.FAILED,))
return DecoratedResultSet(
- jobs, result_decorator=CharmRecipeBuildRequest.fromJob)
+ jobs, result_decorator=CharmRecipeBuildRequest.fromJob
+ )
def _getBuilds(self, filter_term, order_by):
"""The actual query to get the builds."""
query_args = [
CharmRecipeBuild.recipe == self,
- ]
+ ]
if filter_term is not None:
query_args.append(filter_term)
result = Store.of(self).find(CharmRecipeBuild, *query_args)
@@ -674,11 +736,17 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
def builds(self):
"""See `ICharmRecipe`."""
order_by = (
- NullsLast(Desc(Greatest(
- CharmRecipeBuild.date_started,
- CharmRecipeBuild.date_finished))),
+ NullsLast(
+ Desc(
+ Greatest(
+ CharmRecipeBuild.date_started,
+ CharmRecipeBuild.date_finished,
+ )
+ )
+ ),
Desc(CharmRecipeBuild.date_created),
- Desc(CharmRecipeBuild.id))
+ Desc(CharmRecipeBuild.id),
+ )
return self._getBuilds(None, order_by)
@property
@@ -689,23 +757,29 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
BuildStatus.BUILDING,
BuildStatus.UPLOADING,
BuildStatus.CANCELLING,
- ]
+ ]
@property
def completed_builds(self):
"""See `ICharmRecipe`."""
filter_term = Not(CharmRecipeBuild.status.is_in(self._pending_states))
order_by = (
- NullsLast(Desc(Greatest(
- CharmRecipeBuild.date_started,
- CharmRecipeBuild.date_finished))),
- Desc(CharmRecipeBuild.id))
+ NullsLast(
+ Desc(
+ Greatest(
+ CharmRecipeBuild.date_started,
+ CharmRecipeBuild.date_finished,
+ )
+ )
+ ),
+ Desc(CharmRecipeBuild.id),
+ )
return self._getBuilds(filter_term, order_by)
@property
def pending_builds(self):
"""See `ICharmRecipe`."""
- filter_term = (CharmRecipeBuild.status.is_in(self._pending_states))
+ filter_term = CharmRecipeBuild.status.is_in(self._pending_states)
# We want to order by date_created but this is the same as ordering
# by id (since id increases monotonically) and is less expensive.
order_by = Desc(CharmRecipeBuild.id)
@@ -716,13 +790,16 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
if self.store_name is None:
raise CannotAuthorizeCharmhubUploads(
"Cannot authorize uploads of a charm recipe with no store "
- "name.")
+ "name."
+ )
charmhub_client = getUtility(ICharmhubClient)
root_macaroon_raw = charmhub_client.requestPackageUploadPermission(
- self.store_name)
+ self.store_name
+ )
# Check that the macaroon has exactly one Candid caveat.
extract_candid_caveat(
- Macaroon.deserialize(root_macaroon_raw, JsonSerializer()))
+ Macaroon.deserialize(root_macaroon_raw, JsonSerializer())
+ )
self.store_secrets = {"root": root_macaroon_raw}
return root_macaroon_raw
@@ -731,29 +808,35 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
if self.store_secrets is None or "root" not in self.store_secrets:
raise CannotAuthorizeCharmhubUploads(
"beginAuthorization must be called before "
- "completeAuthorization.")
+ "completeAuthorization."
+ )
try:
Macaroon.deserialize(
- unbound_discharge_macaroon_raw, JsonSerializer())
+ unbound_discharge_macaroon_raw, JsonSerializer()
+ )
except Exception:
raise CannotAuthorizeCharmhubUploads(
- "Discharge macaroon is invalid.")
+ "Discharge macaroon is invalid."
+ )
charmhub_client = getUtility(ICharmhubClient)
exchanged_macaroon_raw = charmhub_client.exchangeMacaroons(
- self.store_secrets["root"], unbound_discharge_macaroon_raw)
+ self.store_secrets["root"], unbound_discharge_macaroon_raw
+ )
container = getUtility(IEncryptedContainer, "charmhub-secrets")
assert container.can_encrypt
self.store_secrets["exchanged_encrypted"] = removeSecurityProxy(
- container.encrypt(exchanged_macaroon_raw.encode()))
+ container.encrypt(exchanged_macaroon_raw.encode())
+ )
self.store_secrets.pop("root", None)
@property
def can_upload_to_store(self):
return (
- config.charms.charmhub_url is not None and
- self.store_name is not None and
- self.store_secrets is not None and
- "exchanged_encrypted" in self.store_secrets)
+ config.charms.charmhub_url is not None
+ and self.store_name is not None
+ and self.store_secrets is not None
+ and "exchanged_encrypted" in self.store_secrets
+ )
@property
def valid_webhook_event_types(self):
@@ -767,58 +850,86 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
# rather than in bulk.
buildqueue_records = store.find(
BuildQueue,
- BuildQueue._build_farm_job_id ==
- CharmRecipeBuild.build_farm_job_id,
- CharmRecipeBuild.recipe == self)
+ BuildQueue._build_farm_job_id
+ == CharmRecipeBuild.build_farm_job_id,
+ CharmRecipeBuild.recipe == self,
+ )
for buildqueue_record in buildqueue_records:
buildqueue_record.destroySelf()
- build_farm_job_ids = list(store.find(
- CharmRecipeBuild.build_farm_job_id,
- CharmRecipeBuild.recipe == self))
- store.execute("""
+ build_farm_job_ids = list(
+ store.find(
+ CharmRecipeBuild.build_farm_job_id,
+ CharmRecipeBuild.recipe == self,
+ )
+ )
+ store.execute(
+ """
DELETE FROM CharmFile
USING CharmRecipeBuild
WHERE
CharmFile.build = CharmRecipeBuild.id AND
CharmRecipeBuild.recipe = ?
- """, (self.id,))
- store.execute("""
+ """,
+ (self.id,),
+ )
+ store.execute(
+ """
DELETE FROM CharmRecipeBuildJob
USING CharmRecipeBuild
WHERE
CharmRecipeBuildJob.build = CharmRecipeBuild.id AND
CharmRecipeBuild.recipe = ?
- """, (self.id,))
+ """,
+ (self.id,),
+ )
store.find(CharmRecipeBuild, CharmRecipeBuild.recipe == self).remove()
affected_jobs = Select(
[CharmRecipeJob.job_id],
- And(CharmRecipeJob.job == Job.id, CharmRecipeJob.recipe == self))
+ And(CharmRecipeJob.job == Job.id, CharmRecipeJob.recipe == self),
+ )
store.find(Job, Job.id.is_in(affected_jobs)).remove()
getUtility(IWebhookSet).delete(self.webhooks)
store.remove(self)
store.find(
- BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)).remove()
+ BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)
+ ).remove()
@implementer(ICharmRecipeSet)
class CharmRecipeSet:
"""See `ICharmRecipeSet`."""
- def new(self, registrant, owner, project, name, description=None,
- git_ref=None, build_path=None, require_virtualized=True,
- information_type=InformationType.PUBLIC, auto_build=False,
- auto_build_channels=None, store_upload=False, store_name=None,
- store_secrets=None, store_channels=None, date_created=DEFAULT):
+ def new(
+ self,
+ registrant,
+ owner,
+ project,
+ name,
+ description=None,
+ git_ref=None,
+ build_path=None,
+ require_virtualized=True,
+ information_type=InformationType.PUBLIC,
+ auto_build=False,
+ auto_build_channels=None,
+ store_upload=False,
+ store_name=None,
+ store_secrets=None,
+ store_channels=None,
+ date_created=DEFAULT,
+ ):
"""See `ICharmRecipeSet`."""
if not registrant.inTeam(owner):
if owner.is_team:
raise CharmRecipeNotOwner(
- "%s is not a member of %s." %
- (registrant.displayname, owner.displayname))
+ "%s is not a member of %s."
+ % (registrant.displayname, owner.displayname)
+ )
else:
raise CharmRecipeNotOwner(
- "%s cannot create charm recipes owned by %s." %
- (registrant.displayname, owner.displayname))
+ "%s cannot create charm recipes owned by %s."
+ % (registrant.displayname, owner.displayname)
+ )
if git_ref is None:
raise NoSourceForCharmRecipe
@@ -830,27 +941,38 @@ class CharmRecipeSet:
# IntegrityError due to exceptions being raised during object
# creation and to ensure that everything relevant is in the Storm
# cache.
- if not self.isValidInformationType(
- information_type, owner, git_ref):
+ if not self.isValidInformationType(information_type, owner, git_ref):
raise CharmRecipePrivacyMismatch
store = IMasterStore(CharmRecipe)
recipe = CharmRecipe(
- registrant, owner, project, name, description=description,
- git_ref=git_ref, build_path=build_path,
+ registrant,
+ owner,
+ project,
+ name,
+ description=description,
+ git_ref=git_ref,
+ build_path=build_path,
require_virtualized=require_virtualized,
- information_type=information_type, auto_build=auto_build,
+ information_type=information_type,
+ auto_build=auto_build,
auto_build_channels=auto_build_channels,
- store_upload=store_upload, store_name=store_name,
- store_secrets=store_secrets, store_channels=store_channels,
- date_created=date_created)
+ store_upload=store_upload,
+ store_name=store_name,
+ store_secrets=store_secrets,
+ store_channels=store_channels,
+ date_created=date_created,
+ )
store.add(recipe)
return recipe
def _getByName(self, owner, project, name):
- return IStore(CharmRecipe).find(
- CharmRecipe, owner=owner, project=project, name=name).one()
+ return (
+ IStore(CharmRecipe)
+ .find(CharmRecipe, owner=owner, project=project, name=name)
+ .one()
+ )
def exists(self, owner, project, name):
"""See `ICharmRecipeSet`."""
@@ -863,8 +985,9 @@ class CharmRecipeSet:
raise NoSuchCharmRecipe(name)
return recipe
- def _getRecipesFromCollection(self, collection, owner=None,
- visible_by_user=None):
+ def _getRecipesFromCollection(
+ self, collection, owner=None, visible_by_user=None
+ ):
id_column = CharmRecipe.git_repository_id
ids = collection.getRepositoryIds()
expressions = [id_column.is_in(ids._get_select())]
@@ -879,12 +1002,15 @@ class CharmRecipeSet:
def findByPerson(self, person, visible_by_user=None):
"""See `ICharmRecipeSet`."""
+
def _getRecipes(collection):
collection = collection.visibleByUser(visible_by_user)
owned = self._getRecipesFromCollection(
- collection.ownedBy(person), visible_by_user=visible_by_user)
+ collection.ownedBy(person), visible_by_user=visible_by_user
+ )
packaged = self._getRecipesFromCollection(
- collection, owner=person, visible_by_user=visible_by_user)
+ collection, owner=person, visible_by_user=visible_by_user
+ )
return owned.union(packaged)
git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
@@ -893,20 +1019,28 @@ class CharmRecipeSet:
def findByProject(self, project, visible_by_user=None):
"""See `ICharmRecipeSet`."""
+
def _getRecipes(collection):
return self._getRecipesFromCollection(
collection.visibleByUser(visible_by_user),
- visible_by_user=visible_by_user)
+ visible_by_user=visible_by_user,
+ )
recipes_for_project = IStore(CharmRecipe).find(
CharmRecipe,
CharmRecipe.project == project,
- get_charm_recipe_privacy_filter(visible_by_user))
+ get_charm_recipe_privacy_filter(visible_by_user),
+ )
git_collection = removeSecurityProxy(IGitCollection(project))
return recipes_for_project.union(_getRecipes(git_collection))
- def findByGitRepository(self, repository, paths=None,
- visible_by_user=None, check_permissions=True):
+ def findByGitRepository(
+ self,
+ repository,
+ paths=None,
+ visible_by_user=None,
+ check_permissions=True,
+ ):
"""See `ICharmRecipeSet`."""
clauses = [CharmRecipe.git_repository == repository]
if paths is not None:
@@ -921,22 +1055,27 @@ class CharmRecipeSet:
CharmRecipe,
CharmRecipe.git_repository == ref.repository,
CharmRecipe.git_path == ref.path,
- get_charm_recipe_privacy_filter(visible_by_user))
+ get_charm_recipe_privacy_filter(visible_by_user),
+ )
def findByContext(self, context, visible_by_user=None, order_by_date=True):
"""See `ICharmRecipeSet`."""
if IPerson.providedBy(context):
recipes = self.findByPerson(
- context, visible_by_user=visible_by_user)
+ context, visible_by_user=visible_by_user
+ )
elif IProduct.providedBy(context):
recipes = self.findByProject(
- context, visible_by_user=visible_by_user)
+ context, visible_by_user=visible_by_user
+ )
elif IGitRepository.providedBy(context):
recipes = self.findByGitRepository(
- context, visible_by_user=visible_by_user)
+ context, visible_by_user=visible_by_user
+ )
elif IGitRef.providedBy(context):
recipes = self.findByGitRef(
- context, visible_by_user=visible_by_user)
+ context, visible_by_user=visible_by_user
+ )
else:
raise BadCharmRecipeSearchContext(context)
if order_by_date:
@@ -974,12 +1113,14 @@ class CharmRecipeSet:
person_ids.add(recipe.owner_id)
repositories = load_related(
- GitRepository, recipes, ["git_repository_id"])
+ GitRepository, recipes, ["git_repository_id"]
+ )
if repositories:
GenericGitCollection.preloadDataForRepositories(repositories)
git_refs = GitRef.findByReposAndPaths(
- [(recipe.git_repository, recipe.git_path) for recipe in recipes])
+ [(recipe.git_repository, recipe.git_path) for recipe in recipes]
+ )
for recipe in recipes:
git_ref = git_refs.get((recipe.git_repository, recipe.git_path))
if git_ref is not None:
@@ -990,8 +1131,11 @@ class CharmRecipeSet:
# aren't trigger-maintained.
person_ids.update(repository.owner_id for repository in repositories)
- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- person_ids, need_validity=True))
+ list(
+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ person_ids, need_validity=True
+ )
+ )
def getCharmcraftYaml(self, context, logger=None):
"""See `ICharmRecipeSet`."""
@@ -1012,58 +1156,68 @@ class CharmRecipeSet:
except GitRepositoryBlobNotFound:
if logger is not None:
logger.exception(
- "Cannot find charmcraft.yaml in %s",
- source.unique_name)
+ "Cannot find charmcraft.yaml in %s", source.unique_name
+ )
raise MissingCharmcraftYaml(source.unique_name)
except GitRepositoryScanFault as e:
msg = "Failed to get charmcraft.yaml from %s"
if logger is not None:
logger.exception(msg, source.unique_name)
raise CannotFetchCharmcraftYaml(
- "%s: %s" % (msg % source.unique_name, e))
+ "%s: %s" % (msg % source.unique_name, e)
+ )
try:
charmcraft_data = yaml.safe_load(blob)
except Exception as e:
# Don't bother logging parsing errors from user-supplied YAML.
raise CannotParseCharmcraftYaml(
- "Cannot parse charmcraft.yaml from %s: %s" %
- (source.unique_name, e))
+ "Cannot parse charmcraft.yaml from %s: %s"
+ % (source.unique_name, e)
+ )
if not isinstance(charmcraft_data, dict):
raise CannotParseCharmcraftYaml(
- "The top level of charmcraft.yaml from %s is not a mapping" %
- source.unique_name)
+ "The top level of charmcraft.yaml from %s is not a mapping"
+ % source.unique_name
+ )
return charmcraft_data
@staticmethod
def _findStaleRecipes():
"""Find recipes that need to be rebuilt."""
- threshold_date = (
- datetime.now(pytz.UTC) -
- timedelta(minutes=config.charms.auto_build_frequency))
+ threshold_date = datetime.now(pytz.UTC) - timedelta(
+ minutes=config.charms.auto_build_frequency
+ )
stale_clauses = [
IsTrue(CharmRecipe.is_stale),
IsTrue(CharmRecipe.auto_build),
- ]
+ ]
recent_clauses = [
CharmRecipeJob.recipe_id == CharmRecipe.id,
CharmRecipeJob.job_type == CharmRecipeJobType.REQUEST_BUILDS,
- JSONExtract(CharmRecipeJob.metadata, "channels") == Coalesce(
- CharmRecipe.auto_build_channels, Cast("null", "jsonb")),
+ JSONExtract(CharmRecipeJob.metadata, "channels")
+ == Coalesce(
+ CharmRecipe.auto_build_channels, Cast("null", "jsonb")
+ ),
CharmRecipeJob.job_id == Job.id,
# We only want recipes that haven't had an automatic build
# requested for them recently.
Job.date_created >= threshold_date,
- ]
+ ]
return IStore(CharmRecipe).find(
CharmRecipe,
- CharmRecipe.id.is_in(Except(
- Select(CharmRecipe.id, where=And(*stale_clauses)),
- Select(
- CharmRecipe.id,
- where=And(*(stale_clauses + recent_clauses))))))
+ CharmRecipe.id.is_in(
+ Except(
+ Select(CharmRecipe.id, where=And(*stale_clauses)),
+ Select(
+ CharmRecipe.id,
+ where=And(*(stale_clauses + recent_clauses)),
+ ),
+ )
+ ),
+ )
@classmethod
def makeAutoBuilds(cls, logger=None):
@@ -1086,7 +1240,8 @@ class CharmRecipeSet:
for recipe in recipes:
get_property_cache(recipe)._git_ref = None
recipes.set(
- git_repository_id=None, git_path=None, date_last_modified=UTC_NOW)
+ git_repository_id=None, git_path=None, date_last_modified=UTC_NOW
+ )
def empty_list(self):
"""See `ICharmRecipeSet`."""
@@ -1095,12 +1250,12 @@ class CharmRecipeSet:
@implementer(IEncryptedContainer)
class CharmhubSecretsEncryptedContainer(NaClEncryptedContainerBase):
-
@property
def public_key_bytes(self):
if config.charms.charmhub_secrets_public_key is not None:
return base64.b64decode(
- config.charms.charmhub_secrets_public_key.encode())
+ config.charms.charmhub_secrets_public_key.encode()
+ )
else:
return None
@@ -1108,7 +1263,8 @@ class CharmhubSecretsEncryptedContainer(NaClEncryptedContainerBase):
def private_key_bytes(self):
if config.charms.charmhub_secrets_private_key is not None:
return base64.b64decode(
- config.charms.charmhub_secrets_private_key.encode())
+ config.charms.charmhub_secrets_private_key.encode()
+ )
else:
return None
@@ -1116,7 +1272,8 @@ class CharmhubSecretsEncryptedContainer(NaClEncryptedContainerBase):
def get_charm_recipe_privacy_filter(user):
"""Return a Storm query filter to find charm recipes visible to `user`."""
public_filter = CharmRecipe.information_type.is_in(
- PUBLIC_INFORMATION_TYPES)
+ PUBLIC_INFORMATION_TYPES
+ )
# XXX cjwatson 2021-06-07: Flesh this out once we have more privacy
# infrastructure.
diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
index 3e39c63..c7f830f 100644
--- a/lib/lp/charms/model/charmrecipebuild.py
+++ b/lib/lp/charms/model/charmrecipebuild.py
@@ -6,7 +6,7 @@
__all__ = [
"CharmFile",
"CharmRecipeBuild",
- ]
+]
from datetime import timedelta
from operator import attrgetter
@@ -14,12 +14,9 @@ from operator import attrgetter
import pytz
import six
from storm.databases.postgres import JSON
-from storm.expr import (
- Column,
- Table,
- With,
- )
+from storm.expr import Column, Table, With
from storm.locals import (
+ SQL,
And,
Bool,
DateTime,
@@ -27,10 +24,9 @@ from storm.locals import (
Int,
Reference,
Select,
- SQL,
Store,
Unicode,
- )
+)
from storm.store import EmptyResultSet
from zope.component import getUtility
from zope.interface import implementer
@@ -40,7 +36,7 @@ from lp.buildmaster.enums import (
BuildFarmJobType,
BuildQueueStatus,
BuildStatus,
- )
+)
from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
from lp.buildmaster.model.packagebuild import PackageBuildMixin
@@ -51,13 +47,13 @@ from lp.charms.interfaces.charmrecipebuild import (
ICharmFile,
ICharmRecipeBuild,
ICharmRecipeBuildSet,
- )
+)
from lp.charms.interfaces.charmrecipebuildjob import ICharmhubUploadJobSource
from lp.charms.mail.charmrecipebuild import CharmRecipeBuildMailer
from lp.charms.model.charmrecipebuildjob import (
CharmRecipeBuildJob,
CharmRecipeBuildJobType,
- )
+)
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.series import SeriesStatus
from lp.registry.model.distribution import Distribution
@@ -68,21 +64,12 @@ from lp.services.database.bulk import load_related
from lp.services.database.constants import DEFAULT
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
from lp.services.database.stormbase import StormBase
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
-from lp.services.librarian.model import (
- LibraryFileAlias,
- LibraryFileContent,
- )
-from lp.services.propertycache import (
- cachedproperty,
- get_property_cache,
- )
+from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
+from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.webapp.snapshot import notify_modified
from lp.soyuz.model.distroarchseries import DistroArchSeries
@@ -107,7 +94,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
distro_arch_series_id = Int(name="distro_arch_series", allow_none=False)
distro_arch_series = Reference(
- distro_arch_series_id, "DistroArchSeries.id")
+ distro_arch_series_id, "DistroArchSeries.id"
+ )
channels = JSON("channels", allow_none=True)
@@ -117,13 +105,17 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
virtualized = Bool(name="virtualized", allow_none=False)
date_created = DateTime(
- name="date_created", tzinfo=pytz.UTC, allow_none=False)
+ name="date_created", tzinfo=pytz.UTC, allow_none=False
+ )
date_started = DateTime(
- name="date_started", tzinfo=pytz.UTC, allow_none=True)
+ name="date_started", tzinfo=pytz.UTC, allow_none=True
+ )
date_finished = DateTime(
- name="date_finished", tzinfo=pytz.UTC, allow_none=True)
+ name="date_finished", tzinfo=pytz.UTC, allow_none=True
+ )
date_first_dispatched = DateTime(
- name="date_first_dispatched", tzinfo=pytz.UTC, allow_none=True)
+ name="date_first_dispatched", tzinfo=pytz.UTC, allow_none=True
+ )
builder_id = Int(name="builder", allow_none=True)
builder = Reference(builder_id, "Builder.id")
@@ -147,9 +139,18 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
store_upload_metadata = JSON("store_upload_json_data", allow_none=True)
- def __init__(self, build_farm_job, build_request, recipe,
- distro_arch_series, processor, virtualized, channels=None,
- store_upload_metadata=None, date_created=DEFAULT):
+ def __init__(
+ self,
+ build_farm_job,
+ build_request,
+ recipe,
+ distro_arch_series,
+ processor,
+ virtualized,
+ channels=None,
+ store_upload_metadata=None,
+ date_created=DEFAULT,
+ ):
"""Construct a `CharmRecipeBuild`."""
requester = build_request.requester
super().__init__()
@@ -176,14 +177,20 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
def __repr__(self):
return "<CharmRecipeBuild ~%s/%s/+charm/%s/+build/%d>" % (
- self.recipe.owner.name, self.recipe.project.name, self.recipe.name,
- self.id)
+ self.recipe.owner.name,
+ self.recipe.project.name,
+ self.recipe.name,
+ self.id,
+ )
@property
def title(self):
return "%s build of /~%s/%s/+charm/%s" % (
- self.distro_arch_series.architecturetag, self.recipe.owner.name,
- self.recipe.project.name, self.recipe.name)
+ self.distro_arch_series.architecturetag,
+ self.recipe.owner.name,
+ self.recipe.project.name,
+ self.recipe.name,
+ )
@property
def distribution(self):
@@ -240,7 +247,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
(CharmRecipeBuild.date_started, CharmRecipeBuild.date_finished),
CharmRecipeBuild.recipe == self.recipe,
CharmRecipeBuild.processor == self.processor,
- CharmRecipeBuild.status == BuildStatus.FULLYBUILT)
+ CharmRecipeBuild.status == BuildStatus.FULLYBUILT,
+ )
result.order_by(Desc(CharmRecipeBuild.date_finished))
durations = [row[1] - row[0] for row in result[:9]]
if len(durations) == 0:
@@ -293,15 +301,17 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
jobs = Store.of(self).find(
CharmRecipeBuildJob,
CharmRecipeBuildJob.build == self,
- CharmRecipeBuildJob.job_type ==
- CharmRecipeBuildJobType.CHARMHUB_UPLOAD)
+ CharmRecipeBuildJob.job_type
+ == CharmRecipeBuildJobType.CHARMHUB_UPLOAD,
+ )
jobs.order_by(Desc(CharmRecipeBuildJob.job_id))
def preload_jobs(rows):
load_related(Job, rows, ["job_id"])
return DecoratedResultSet(
- jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs)
+ jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs
+ )
@cachedproperty
def last_store_upload_job(self):
@@ -337,19 +347,27 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
if not self.recipe.can_upload_to_store:
raise CannotScheduleStoreUpload(
"Cannot upload this charm to Charmhub because it is not "
- "properly configured.")
+ "properly configured."
+ )
if not self.was_built or self.getFiles().is_empty():
raise CannotScheduleStoreUpload(
- "Cannot upload this charm because it has no files.")
- if (self.store_upload_status ==
- CharmRecipeBuildStoreUploadStatus.PENDING):
+ "Cannot upload this charm because it has no files."
+ )
+ if (
+ self.store_upload_status
+ == CharmRecipeBuildStoreUploadStatus.PENDING
+ ):
raise CannotScheduleStoreUpload(
- "An upload of this charm is already in progress.")
- elif (self.store_upload_status ==
- CharmRecipeBuildStoreUploadStatus.UPLOADED):
+ "An upload of this charm is already in progress."
+ )
+ elif (
+ self.store_upload_status
+ == CharmRecipeBuildStoreUploadStatus.UPLOADED
+ ):
raise CannotScheduleStoreUpload(
"Cannot upload this charm because it has already been "
- "uploaded.")
+ "uploaded."
+ )
getUtility(ICharmhubUploadJobSource).create(self)
def getFiles(self):
@@ -358,7 +376,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
(CharmFile, LibraryFileAlias, LibraryFileContent),
CharmFile.build == self.id,
LibraryFileAlias.id == CharmFile.library_file_id,
- LibraryFileContent.id == LibraryFileAlias.contentID)
+ LibraryFileContent.id == LibraryFileAlias.contentID,
+ )
return result.order_by([LibraryFileAlias.filename, CharmFile.id])
def getFileByName(self, filename):
@@ -368,11 +387,16 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
elif filename.endswith("_log.txt"):
file_object = self.upload_log
else:
- file_object = Store.of(self).find(
- LibraryFileAlias,
- CharmFile.build == self.id,
- LibraryFileAlias.id == CharmFile.library_file_id,
- LibraryFileAlias.filename == filename).one()
+ file_object = (
+ Store.of(self)
+ .find(
+ LibraryFileAlias,
+ CharmFile.build == self.id,
+ LibraryFileAlias.id == CharmFile.library_file_id,
+ LibraryFileAlias.filename == filename,
+ )
+ .one()
+ )
if file_object is not None and file_object.filename == filename:
return file_object
@@ -389,18 +413,28 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
"""See `IPackageBuild`."""
return not self.getFiles().is_empty()
- def updateStatus(self, status, builder=None, worker_status=None,
- date_started=None, date_finished=None,
- force_invalid_transition=False):
+ def updateStatus(
+ self,
+ status,
+ builder=None,
+ worker_status=None,
+ date_started=None,
+ date_finished=None,
+ force_invalid_transition=False,
+ ):
"""See `IBuildFarmJob`."""
edited_fields = set()
with notify_modified(
- self, edited_fields,
- snapshot_names=("status", "revision_id")) as previous_obj:
+ self, edited_fields, snapshot_names=("status", "revision_id")
+ ) as previous_obj:
super().updateStatus(
- status, builder=builder, worker_status=worker_status,
- date_started=date_started, date_finished=date_finished,
- force_invalid_transition=force_invalid_transition)
+ status,
+ builder=builder,
+ worker_status=worker_status,
+ date_started=date_started,
+ date_finished=date_finished,
+ force_invalid_transition=force_invalid_transition,
+ )
if self.status != previous_obj.status:
edited_fields.add("status")
if worker_status is not None:
@@ -427,20 +461,35 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
"""See `ICharmRecipeBuildSet`."""
- def new(self, build_request, recipe, distro_arch_series, channels=None,
- store_upload_metadata=None, date_created=DEFAULT):
+ def new(
+ self,
+ build_request,
+ recipe,
+ distro_arch_series,
+ channels=None,
+ store_upload_metadata=None,
+ date_created=DEFAULT,
+ ):
"""See `ICharmRecipeBuildSet`."""
store = IMasterStore(CharmRecipeBuild)
build_farm_job = getUtility(IBuildFarmJobSource).new(
- CharmRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created)
+ CharmRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created
+ )
virtualized = (
not distro_arch_series.processor.supports_nonvirtualized
- or recipe.require_virtualized)
+ or recipe.require_virtualized
+ )
build = CharmRecipeBuild(
- build_farm_job, build_request, recipe, distro_arch_series,
- distro_arch_series.processor, virtualized, channels=channels,
+ build_farm_job,
+ build_request,
+ recipe,
+ distro_arch_series,
+ distro_arch_series.processor,
+ virtualized,
+ channels=channels,
store_upload_metadata=store_upload_metadata,
- date_created=date_created)
+ date_created=date_created,
+ )
store.add(build)
return build
@@ -451,45 +500,65 @@ class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
def getByBuildFarmJob(self, build_farm_job):
"""See `ISpecificBuildFarmJobSource`."""
- return Store.of(build_farm_job).find(
- CharmRecipeBuild, build_farm_job_id=build_farm_job.id).one()
+ return (
+ Store.of(build_farm_job)
+ .find(CharmRecipeBuild, build_farm_job_id=build_farm_job.id)
+ .one()
+ )
def preloadBuildsData(self, builds):
# Circular import.
from lp.charms.model.charmrecipe import CharmRecipe
+
load_related(Person, builds, ["requester_id"])
lfas = load_related(LibraryFileAlias, builds, ["log_id"])
load_related(LibraryFileContent, lfas, ["contentID"])
distroarchserieses = load_related(
- DistroArchSeries, builds, ["distro_arch_series_id"])
+ DistroArchSeries, builds, ["distro_arch_series_id"]
+ )
distroserieses = load_related(
- DistroSeries, distroarchserieses, ["distroseriesID"])
+ DistroSeries, distroarchserieses, ["distroseriesID"]
+ )
load_related(Distribution, distroserieses, ["distributionID"])
recipes = load_related(CharmRecipe, builds, ["recipe_id"])
getUtility(ICharmRecipeSet).preloadDataForRecipes(recipes)
build_ids = set(map(attrgetter("id"), builds))
- latest_jobs_cte = With("LatestJobs", Select(
- (CharmRecipeBuildJob.job_id,
- SQL(
- "rank() OVER "
- "(PARTITION BY build ORDER BY job DESC) AS rank")),
- tables=CharmRecipeBuildJob,
- where=And(
- CharmRecipeBuildJob.build_id.is_in(build_ids),
- CharmRecipeBuildJob.job_type ==
- CharmRecipeBuildJobType.CHARMHUB_UPLOAD)))
+ latest_jobs_cte = With(
+ "LatestJobs",
+ Select(
+ (
+ CharmRecipeBuildJob.job_id,
+ SQL(
+ "rank() OVER "
+ "(PARTITION BY build ORDER BY job DESC) AS rank"
+ ),
+ ),
+ tables=CharmRecipeBuildJob,
+ where=And(
+ CharmRecipeBuildJob.build_id.is_in(build_ids),
+ CharmRecipeBuildJob.job_type
+ == CharmRecipeBuildJobType.CHARMHUB_UPLOAD,
+ ),
+ ),
+ )
LatestJobs = Table("LatestJobs")
- crbjs = list(IStore(CharmRecipeBuildJob).with_(latest_jobs_cte).using(
- CharmRecipeBuildJob, LatestJobs).find(
+ crbjs = list(
+ IStore(CharmRecipeBuildJob)
+ .with_(latest_jobs_cte)
+ .using(CharmRecipeBuildJob, LatestJobs)
+ .find(
CharmRecipeBuildJob,
CharmRecipeBuildJob.job_id == Column("job", LatestJobs),
- Column("rank", LatestJobs) == 1))
+ Column("rank", LatestJobs) == 1,
+ )
+ )
crbj_map = {}
for crbj in crbjs:
crbj_map[crbj.build] = crbj.makeDerived()
for build in builds:
- get_property_cache(build).last_store_upload_job = (
- crbj_map.get(build))
+ get_property_cache(build).last_store_upload_job = crbj_map.get(
+ build
+ )
load_related(Job, crbjs, ["job_id"])
def getByBuildFarmJobs(self, build_farm_jobs):
@@ -497,8 +566,11 @@ class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
if len(build_farm_jobs) == 0:
return EmptyResultSet()
rows = Store.of(build_farm_jobs[0]).find(
- CharmRecipeBuild, CharmRecipeBuild.build_farm_job_id.is_in(
- bfj.id for bfj in build_farm_jobs))
+ CharmRecipeBuild,
+ CharmRecipeBuild.build_farm_job_id.is_in(
+ bfj.id for bfj in build_farm_jobs
+ ),
+ )
return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
diff --git a/lib/lp/charms/model/charmrecipebuildbehaviour.py b/lib/lp/charms/model/charmrecipebuildbehaviour.py
index 7c8dabc..fffe7cf 100644
--- a/lib/lp/charms/model/charmrecipebuildbehaviour.py
+++ b/lib/lp/charms/model/charmrecipebuildbehaviour.py
@@ -8,7 +8,7 @@ Dispatches charm recipe build jobs to build-farm workers.
__all__ = [
"CharmRecipeBuildBehaviour",
- ]
+]
from twisted.internet import defer
from zope.component import adapter
@@ -20,15 +20,13 @@ from lp.buildmaster.enums import BuildBaseImageType
from lp.buildmaster.interfaces.builder import CannotBuild
from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
IBuildFarmJobBehaviour,
- )
+)
from lp.buildmaster.model.buildfarmjobbehaviour import (
BuildFarmJobBehaviourBase,
- )
+)
from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
from lp.registry.interfaces.series import SeriesStatus
-from lp.soyuz.adapters.archivedependencies import (
- get_sources_list_for_building,
- )
+from lp.soyuz.adapters.archivedependencies import get_sources_list_for_building
@adapter(ICharmRecipeBuild)
@@ -45,9 +43,12 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
# Examples:
# buildlog_charm_ubuntu_wily_amd64_name_FULLYBUILT.txt
return "buildlog_charm_%s_%s_%s_%s_%s.txt" % (
- das.distroseries.distribution.name, das.distroseries.name,
- das.architecturetag, self.build.recipe.name,
- self.build.status.name)
+ das.distroseries.distribution.name,
+ das.distroseries.name,
+ das.architecturetag,
+ self.build.recipe.name,
+ self.build.status.name,
+ )
def verifyBuildRequest(self, logger):
"""Assert some pre-build checks.
@@ -59,12 +60,14 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
build = self.build
if build.virtualized and not self._builder.virtualized:
raise AssertionError(
- "Attempt to build virtual item on a non-virtual builder.")
+ "Attempt to build virtual item on a non-virtual builder."
+ )
chroot = build.distro_arch_series.getChroot()
if chroot is None:
raise CannotBuild(
- "Missing chroot for %s" % build.distro_arch_series.displayname)
+ "Missing chroot for %s" % build.distro_arch_series.displayname
+ )
@defer.inlineCallbacks
def extraBuildArgs(self, logger=None):
@@ -79,9 +82,12 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
# We have to remove the security proxy that Zope applies to this
# dict, since otherwise we'll be unable to serialise it to XML-RPC.
args["channels"] = removeSecurityProxy(channels)
- args["archives"], args["trusted_keys"] = (
- yield get_sources_list_for_building(
- self, build.distro_arch_series, None, logger=logger))
+ (
+ args["archives"],
+ args["trusted_keys"],
+ ) = yield get_sources_list_for_building(
+ self, build.distro_arch_series, None, logger=logger
+ )
if build.recipe.build_path is not None:
args["build_path"] = build.recipe.build_path
if build.recipe.git_ref is not None:
@@ -93,9 +99,13 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
args["git_path"] = build.recipe.git_ref.name
else:
raise CannotBuild(
- "Source repository for ~%s/%s/+charm/%s has been deleted." % (
- build.recipe.owner.name, build.recipe.project.name,
- build.recipe.name))
+ "Source repository for ~%s/%s/+charm/%s has been deleted."
+ % (
+ build.recipe.owner.name,
+ build.recipe.project.name,
+ build.recipe.name,
+ )
+ )
args["private"] = build.is_private
return args
diff --git a/lib/lp/charms/model/charmrecipebuildjob.py b/lib/lp/charms/model/charmrecipebuildjob.py
index d3fdd5a..41638e9 100644
--- a/lib/lp/charms/model/charmrecipebuildjob.py
+++ b/lib/lp/charms/model/charmrecipebuildjob.py
@@ -7,26 +7,17 @@ __all__ = [
"CharmhubUploadJob",
"CharmRecipeBuildJob",
"CharmRecipeBuildJobType",
- ]
+]
from datetime import timedelta
+import transaction
from lazr.delegates import delegate_to
-from lazr.enum import (
- DBEnumeratedType,
- DBItem,
- )
+from lazr.enum import DBEnumeratedType, DBItem
from storm.databases.postgres import JSON
-from storm.locals import (
- Int,
- Reference,
- )
-import transaction
+from storm.locals import Int, Reference
from zope.component import getUtility
-from zope.interface import (
- implementer,
- provider,
- )
+from zope.interface import implementer, provider
from lp.app.errors import NotFoundError
from lp.charms.interfaces.charmhubclient import (
@@ -38,24 +29,18 @@ from lp.charms.interfaces.charmhubclient import (
UnauthorizedUploadResponse,
UploadFailedResponse,
UploadNotReviewedYetResponse,
- )
+)
from lp.charms.interfaces.charmrecipebuildjob import (
ICharmhubUploadJob,
ICharmhubUploadJobSource,
ICharmRecipeBuildJob,
- )
+)
from lp.charms.mail.charmrecipebuild import CharmRecipeBuildMailer
from lp.services.config import config
from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
from lp.services.database.stormbase import StormBase
-from lp.services.job.model.job import (
- EnumeratedSubclass,
- Job,
- )
+from lp.services.job.model.job import EnumeratedSubclass, Job
from lp.services.job.runner import BaseRunnableJob
from lp.services.propertycache import get_property_cache
from lp.services.webapp.snapshot import notify_modified
@@ -64,11 +49,14 @@ from lp.services.webapp.snapshot import notify_modified
class CharmRecipeBuildJobType(DBEnumeratedType):
"""Values that `ICharmRecipeBuildJob.job_type` can take."""
- CHARMHUB_UPLOAD = DBItem(0, """
+ CHARMHUB_UPLOAD = DBItem(
+ 0,
+ """
Charmhub upload
This job uploads a charm recipe build to Charmhub.
- """)
+ """,
+ )
@implementer(ICharmRecipeBuildJob)
@@ -110,8 +98,8 @@ class CharmRecipeBuildJob(StormBase):
@delegate_to(ICharmRecipeBuildJob)
class CharmRecipeBuildJobDerived(
- BaseRunnableJob, metaclass=EnumeratedSubclass):
-
+ BaseRunnableJob, metaclass=EnumeratedSubclass
+):
def __init__(self, charm_recipe_build_job):
self.context = charm_recipe_build_job
@@ -119,8 +107,12 @@ class CharmRecipeBuildJobDerived(
"""An informative representation of the job."""
recipe = self.build.recipe
return "<%s for ~%s/%s/+charm/%s/+build/%d>" % (
- self.__class__.__name__, recipe.owner.name, recipe.project.name,
- recipe.name, self.build.id)
+ self.__class__.__name__,
+ recipe.owner.name,
+ recipe.project.name,
+ recipe.name,
+ self.build.id,
+ )
@classmethod
def get(cls, job_id):
@@ -132,11 +124,13 @@ class CharmRecipeBuildJobDerived(
or its `job_type` does not match the desired subclass.
"""
charm_recipe_build_job = IStore(CharmRecipeBuildJob).get(
- CharmRecipeBuildJob, job_id)
+ CharmRecipeBuildJob, job_id
+ )
if charm_recipe_build_job.job_type != cls.class_job_type:
raise NotFoundError(
- "No object found with id %d and type %s" %
- (job_id, cls.class_job_type.title))
+ "No object found with id %d and type %s"
+ % (job_id, cls.class_job_type.title)
+ )
return cls(charm_recipe_build_job)
@classmethod
@@ -146,21 +140,24 @@ class CharmRecipeBuildJobDerived(
CharmRecipeBuildJob,
CharmRecipeBuildJob.job_type == cls.class_job_type,
CharmRecipeBuildJob.job == Job.id,
- Job.id.is_in(Job.ready_jobs))
+ Job.id.is_in(Job.ready_jobs),
+ )
return (cls(job) for job in jobs)
def getOopsVars(self):
"""See `IRunnableJob`."""
oops_vars = super().getOopsVars()
recipe = self.context.build.recipe
- oops_vars.extend([
- ("job_id", self.context.job.id),
- ("job_type", self.context.job_type.title),
- ("build_id", self.context.build.id),
- ("recipe_owner_name", recipe.owner.name),
- ("recipe_project_name", recipe.project.name),
- ("recipe_name", recipe.name),
- ])
+ oops_vars.extend(
+ [
+ ("job_id", self.context.job.id),
+ ("job_type", self.context.job_type.title),
+ ("build_id", self.context.build.id),
+ ("recipe_owner_name", recipe.owner.name),
+ ("recipe_project_name", recipe.project.name),
+ ("recipe_name", recipe.name),
+ ]
+ )
return oops_vars
@@ -179,7 +176,7 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
UnauthorizedUploadResponse,
ReviewFailedResponse,
ReleaseFailedResponse,
- )
+ )
retry_error_types = (UploadNotReviewedYetResponse, RetryableCharmhubError)
max_retries = 30
@@ -192,7 +189,8 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
edited_fields = set()
with notify_modified(build, edited_fields) as before_modification:
charm_recipe_build_job = CharmRecipeBuildJob(
- build, cls.class_job_type, {})
+ build, cls.class_job_type, {}
+ )
job = cls(charm_recipe_build_job)
job.celeryRunOnCommit()
del get_property_cache(build).last_store_upload_job
@@ -267,12 +265,14 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
# Ideally we'd just override Job._set_status or similar, but
# lazr.delegates makes that difficult, so we use this to override all
# the individual Job lifecycle methods instead.
- def _do_lifecycle(self, method_name, manage_transaction=False,
- *args, **kwargs):
+ def _do_lifecycle(
+ self, method_name, manage_transaction=False, *args, **kwargs
+ ):
edited_fields = set()
with notify_modified(self.build, edited_fields) as before_modification:
getattr(super(), method_name)(
- *args, manage_transaction=manage_transaction, **kwargs)
+ *args, manage_transaction=manage_transaction, **kwargs
+ )
upload_status = self.build.store_upload_status
if upload_status != before_modification.store_upload_status:
edited_fields.add("store_upload_status")
@@ -324,9 +324,13 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
try:
try:
charm_lfa = next(
- (lfa for _, lfa, _ in self.build.getFiles()
- if lfa.filename.endswith(".charm")),
- None)
+ (
+ lfa
+ for _, lfa, _ in self.build.getFiles()
+ if lfa.filename.endswith(".charm")
+ ),
+ None,
+ )
if charm_lfa is None:
# Nothing to do.
self.error_message = None
@@ -341,11 +345,13 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
self.attempt_count = 1
if self.store_revision is None:
self.store_revision = client.checkStatus(
- self.build, self.status_url)
+ self.build, self.status_url
+ )
if self.store_revision is None:
raise AssertionError(
"checkStatus returned successfully but with no "
- "revision")
+ "revision"
+ )
# We made progress, so reset attempt_count.
self.attempt_count = 1
if self.build.recipe.store_channels:
@@ -354,21 +360,27 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
except self.retry_error_types:
raise
except Exception as e:
- if (isinstance(e, CharmhubError) and e.can_retry and
- self.attempt_count <= self.max_retries):
+ if (
+ isinstance(e, CharmhubError)
+ and e.can_retry
+ and self.attempt_count <= self.max_retries
+ ):
raise RetryableCharmhubError(e.args[0], detail=e.detail)
self.error_message = str(e)
self.error_detail = getattr(e, "detail", None)
mailer_factory = None
if isinstance(e, UnauthorizedUploadResponse):
mailer_factory = (
- CharmRecipeBuildMailer.forUnauthorizedUpload)
+ CharmRecipeBuildMailer.forUnauthorizedUpload
+ )
elif isinstance(e, UploadFailedResponse):
mailer_factory = CharmRecipeBuildMailer.forUploadFailure
elif isinstance(
- e, (BadReviewStatusResponse, ReviewFailedResponse)):
+ e, (BadReviewStatusResponse, ReviewFailedResponse)
+ ):
mailer_factory = (
- CharmRecipeBuildMailer.forUploadReviewFailure)
+ CharmRecipeBuildMailer.forUploadReviewFailure
+ )
elif isinstance(e, ReleaseFailedResponse):
mailer_factory = CharmRecipeBuildMailer.forReleaseFailure
if mailer_factory is not None:
diff --git a/lib/lp/charms/model/charmrecipejob.py b/lib/lp/charms/model/charmrecipejob.py
index 61a4818..a54e0c5 100644
--- a/lib/lp/charms/model/charmrecipejob.py
+++ b/lib/lp/charms/model/charmrecipejob.py
@@ -7,53 +7,37 @@ __all__ = [
"CharmRecipeJob",
"CharmRecipeJobType",
"CharmRecipeRequestBuildsJob",
- ]
+]
+import transaction
from lazr.delegates import delegate_to
-from lazr.enum import (
- DBEnumeratedType,
- DBItem,
- )
+from lazr.enum import DBEnumeratedType, DBItem
from storm.databases.postgres import JSON
-from storm.locals import (
- Desc,
- Int,
- Reference,
- )
+from storm.locals import Desc, Int, Reference
from storm.store import EmptyResultSet
-import transaction
from zope.component import getUtility
-from zope.interface import (
- implementer,
- provider,
- )
+from zope.interface import implementer, provider
from lp.app.errors import NotFoundError
from lp.charms.interfaces.charmrecipe import (
CannotFetchCharmcraftYaml,
CannotParseCharmcraftYaml,
MissingCharmcraftYaml,
- )
+)
from lp.charms.interfaces.charmrecipejob import (
ICharmRecipeJob,
ICharmRecipeRequestBuildsJob,
ICharmRecipeRequestBuildsJobSource,
- )
+)
from lp.charms.model.charmrecipebuild import CharmRecipeBuild
from lp.registry.interfaces.person import IPersonSet
from lp.services.config import config
from lp.services.database.bulk import load_related
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
- IMasterStore,
- IStore,
- )
+from lp.services.database.interfaces import IMasterStore, IStore
from lp.services.database.stormbase import StormBase
-from lp.services.job.model.job import (
- EnumeratedSubclass,
- Job,
- )
+from lp.services.job.model.job import EnumeratedSubclass, Job
from lp.services.job.runner import BaseRunnableJob
from lp.services.mail.sendmail import format_address_for_person
from lp.services.propertycache import cachedproperty
@@ -63,11 +47,14 @@ from lp.services.scripts import log
class CharmRecipeJobType(DBEnumeratedType):
"""Values that `ICharmRecipeJob.job_type` can take."""
- REQUEST_BUILDS = DBItem(0, """
+ REQUEST_BUILDS = DBItem(
+ 0,
+ """
Request builds
This job requests builds of a charm recipe.
- """)
+ """,
+ )
@implementer(ICharmRecipeJob)
@@ -83,7 +70,8 @@ class CharmRecipeJob(StormBase):
recipe = Reference(recipe_id, "CharmRecipe.id")
job_type = DBEnum(
- name="job_type", enum=CharmRecipeJobType, allow_none=False)
+ name="job_type", enum=CharmRecipeJobType, allow_none=False
+ )
metadata = JSON("json_data", allow_none=False)
@@ -110,15 +98,17 @@ class CharmRecipeJob(StormBase):
@delegate_to(ICharmRecipeJob)
class CharmRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
-
def __init__(self, recipe_job):
self.context = recipe_job
def __repr__(self):
"""An informative representation of the job."""
return "<%s for ~%s/%s/+charm/%s>" % (
- self.__class__.__name__, self.recipe.owner.name,
- self.recipe.project.name, self.recipe.name)
+ self.__class__.__name__,
+ self.recipe.owner.name,
+ self.recipe.project.name,
+ self.recipe.name,
+ )
@classmethod
def get(cls, job_id):
@@ -132,8 +122,9 @@ class CharmRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
recipe_job = IStore(CharmRecipeJob).get(CharmRecipeJob, job_id)
if recipe_job.job_type != cls.class_job_type:
raise NotFoundError(
- "No object found with id %d and type %s" %
- (job_id, cls.class_job_type.title))
+ "No object found with id %d and type %s"
+ % (job_id, cls.class_job_type.title)
+ )
return cls(recipe_job)
@classmethod
@@ -143,19 +134,22 @@ class CharmRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
CharmRecipeJob,
CharmRecipeJob.job_type == cls.class_job_type,
CharmRecipeJob.job == Job.id,
- Job.id.is_in(Job.ready_jobs))
+ Job.id.is_in(Job.ready_jobs),
+ )
return (cls(job) for job in jobs)
def getOopsVars(self):
"""See `IRunnableJob`."""
oops_vars = super().getOopsVars()
- oops_vars.extend([
- ("job_id", self.context.job.id),
- ("job_type", self.context.job_type.title),
- ("recipe_owner_name", self.context.recipe.owner.name),
- ("recipe_project_name", self.context.recipe.project.name),
- ("recipe_name", self.context.recipe.name),
- ])
+ oops_vars.extend(
+ [
+ ("job_id", self.context.job.id),
+ ("job_type", self.context.job_type.title),
+ ("recipe_owner_name", self.context.recipe.owner.name),
+ ("recipe_project_name", self.context.recipe.project.name),
+ ("recipe_name", self.context.recipe.name),
+ ]
+ )
return oops_vars
@@ -169,7 +163,7 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
user_error_types = (
CannotParseCharmcraftYaml,
MissingCharmcraftYaml,
- )
+ )
retry_error_types = (CannotFetchCharmcraftYaml,)
max_retries = 5
@@ -185,8 +179,9 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
# Really a set or None, but sets aren't directly
# JSON-serialisable.
"architectures": (
- list(architectures) if architectures is not None else None),
- }
+ list(architectures) if architectures is not None else None
+ ),
+ }
recipe_job = CharmRecipeJob(recipe, cls.class_job_type, metadata)
job = cls(recipe_job)
job.celeryRunOnCommit()
@@ -198,36 +193,49 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
clauses = [
CharmRecipeJob.recipe == recipe,
CharmRecipeJob.job_type == cls.class_job_type,
- ]
+ ]
if statuses is not None:
- clauses.extend([
- CharmRecipeJob.job == Job.id,
- Job._status.is_in(statuses),
- ])
+ clauses.extend(
+ [
+ CharmRecipeJob.job == Job.id,
+ Job._status.is_in(statuses),
+ ]
+ )
if job_ids is not None:
clauses.append(CharmRecipeJob.job_id.is_in(job_ids))
- recipe_jobs = IStore(CharmRecipeJob).find(
- CharmRecipeJob, *clauses).order_by(Desc(CharmRecipeJob.job_id))
+ recipe_jobs = (
+ IStore(CharmRecipeJob)
+ .find(CharmRecipeJob, *clauses)
+ .order_by(Desc(CharmRecipeJob.job_id))
+ )
def preload_jobs(rows):
load_related(Job, rows, ["job_id"])
return DecoratedResultSet(
- recipe_jobs, lambda recipe_job: cls(recipe_job),
- pre_iter_hook=preload_jobs)
+ recipe_jobs,
+ lambda recipe_job: cls(recipe_job),
+ pre_iter_hook=preload_jobs,
+ )
@classmethod
def getByRecipeAndID(cls, recipe, job_id):
"""See `ICharmRecipeRequestBuildsJobSource`."""
- recipe_job = IStore(CharmRecipeJob).find(
- CharmRecipeJob,
- CharmRecipeJob.job_id == job_id,
- CharmRecipeJob.recipe == recipe,
- CharmRecipeJob.job_type == cls.class_job_type).one()
+ recipe_job = (
+ IStore(CharmRecipeJob)
+ .find(
+ CharmRecipeJob,
+ CharmRecipeJob.job_id == job_id,
+ CharmRecipeJob.recipe == recipe,
+ CharmRecipeJob.job_type == cls.class_job_type,
+ )
+ .one()
+ )
if recipe_job is None:
raise NotFoundError(
- "No REQUEST_BUILDS job with ID %d found for %r" %
- (job_id, recipe))
+ "No REQUEST_BUILDS job with ID %d found for %r"
+ % (job_id, recipe)
+ )
return cls(recipe_job)
def getOperationDescription(self):
@@ -286,7 +294,8 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
build_ids = self.metadata.get("builds")
if build_ids:
return IStore(CharmRecipeBuild).find(
- CharmRecipeBuild, CharmRecipeBuild.id.is_in(build_ids))
+ CharmRecipeBuild, CharmRecipeBuild.id.is_in(build_ids)
+ )
else:
return EmptyResultSet()
@@ -300,12 +309,16 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
requester = self.requester
if requester is None:
log.info(
- "Skipping %r because the requester has been deleted." % self)
+ "Skipping %r because the requester has been deleted." % self
+ )
return
try:
self.builds = self.recipe.requestBuildsFromJob(
- self.build_request, channels=self.channels,
- architectures=self.architectures, logger=log)
+ self.build_request,
+ channels=self.channels,
+ architectures=self.architectures,
+ logger=log,
+ )
self.error_message = None
except Exception as e:
self.error_message = str(e)
diff --git a/lib/lp/charms/subscribers/charmrecipebuild.py b/lib/lp/charms/subscribers/charmrecipebuild.py
index a0272a3..883e345 100644
--- a/lib/lp/charms/subscribers/charmrecipebuild.py
+++ b/lib/lp/charms/subscribers/charmrecipebuild.py
@@ -6,9 +6,7 @@
from zope.component import getUtility
from lp.buildmaster.enums import BuildStatus
-from lp.charms.interfaces.charmrecipe import (
- CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG,
- )
+from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG
from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
from lp.charms.interfaces.charmrecipebuildjob import ICharmhubUploadJobSource
from lp.services.features import getFeatureFlag
@@ -23,12 +21,17 @@ def _trigger_charm_recipe_build_webhook(build, action):
payload = {
"recipe_build": canonical_url(build, force_local_path=True),
"action": action,
- }
- payload.update(compose_webhook_payload(
- ICharmRecipeBuild, build,
- ["recipe", "build_request", "status", "store_upload_status"]))
+ }
+ payload.update(
+ compose_webhook_payload(
+ ICharmRecipeBuild,
+ build,
+ ["recipe", "build_request", "status", "store_upload_status"],
+ )
+ )
getUtility(IWebhookSet).trigger(
- build.recipe, "charm-recipe:build:0.1", payload)
+ build.recipe, "charm-recipe:build:0.1", payload
+ )
def charm_recipe_build_created(build, event):
@@ -41,16 +44,20 @@ def charm_recipe_build_modified(build, event):
if event.edited_fields is not None:
status_changed = "status" in event.edited_fields
store_upload_status_changed = (
- "store_upload_status" in event.edited_fields)
+ "store_upload_status" in event.edited_fields
+ )
if status_changed or store_upload_status_changed:
_trigger_charm_recipe_build_webhook(build, "status-changed")
if status_changed:
if build.status == BuildStatus.FULLYBUILT:
- if (build.recipe.can_upload_to_store and
- build.recipe.store_upload):
+ if (
+ build.recipe.can_upload_to_store
+ and build.recipe.store_upload
+ ):
log.info("Scheduling upload of %r to the store." % build)
getUtility(ICharmhubUploadJobSource).create(build)
else:
log.info(
- "%r is not configured for upload to the store." %
- build.recipe)
+ "%r is not configured for upload to the store."
+ % build.recipe
+ )
diff --git a/lib/lp/charms/tests/test_charmbase.py b/lib/lp/charms/tests/test_charmbase.py
index 4d6d175..c49bdce 100644
--- a/lib/lp/charms/tests/test_charmbase.py
+++ b/lib/lp/charms/tests/test_charmbase.py
@@ -3,34 +3,25 @@
"""Test bases for charms."""
-from testtools.matchers import (
- ContainsDict,
- Equals,
- )
-from zope.component import (
- getAdapter,
- getUtility,
- )
+from testtools.matchers import ContainsDict, Equals
+from zope.component import getAdapter, getUtility
from lp.app.interfaces.security import IAuthorization
from lp.charms.interfaces.charmbase import (
ICharmBase,
ICharmBaseSet,
NoSuchCharmBase,
- )
+)
from lp.services.webapp.interfaces import OAuthPermission
from lp.testing import (
+ TestCaseWithFactory,
admin_logged_in,
api_url,
celebrity_logged_in,
logout,
person_logged_in,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- ZopelessDatabaseLayer,
- )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, ZopelessDatabaseLayer
from lp.testing.pages import webservice_for_person
@@ -54,10 +45,12 @@ class TestCharmBase(TestCaseWithFactory):
charm_base = self.factory.makeCharmBase(distro_series=distro_series)
charm_base_set = getUtility(ICharmBaseSet)
self.assertEqual(
- charm_base, charm_base_set.getByDistroSeries(distro_series))
+ charm_base, charm_base_set.getByDistroSeries(distro_series)
+ )
charm_base.destroySelf()
self.assertRaises(
- NoSuchCharmBase, charm_base_set.getByDistroSeries, distro_series)
+ NoSuchCharmBase, charm_base_set.getByDistroSeries, distro_series
+ )
class TestCharmBaseProcessors(TestCaseWithFactory):
@@ -67,32 +60,40 @@ class TestCharmBaseProcessors(TestCaseWithFactory):
def setUp(self):
super().setUp(user="foo.bar@xxxxxxxxxxxxx")
self.unrestricted_procs = [
- self.factory.makeProcessor() for _ in range(3)]
+ self.factory.makeProcessor() for _ in range(3)
+ ]
self.restricted_procs = [
self.factory.makeProcessor(restricted=True, build_by_default=False)
- for _ in range(2)]
+ for _ in range(2)
+ ]
self.procs = self.unrestricted_procs + self.restricted_procs
self.factory.makeProcessor()
self.distroseries = self.factory.makeDistroSeries()
for processor in self.procs:
self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag=processor.name,
- processor=processor)
+ distroseries=self.distroseries,
+ architecturetag=processor.name,
+ processor=processor,
+ )
def test_new_default_processors(self):
# CharmBaseSet.new creates a CharmBaseArch for each available
# Processor for the corresponding series.
charm_base = getUtility(ICharmBaseSet).new(
registrant=self.factory.makePerson(),
- distro_series=self.distroseries, build_snap_channels={})
+ distro_series=self.distroseries,
+ build_snap_channels={},
+ )
self.assertContentEqual(self.procs, charm_base.processors)
def test_new_override_processors(self):
# CharmBaseSet.new can be given a custom set of processors.
charm_base = getUtility(ICharmBaseSet).new(
registrant=self.factory.makePerson(),
- distro_series=self.distroseries, build_snap_channels={},
- processors=self.procs[:2])
+ distro_series=self.distroseries,
+ build_snap_channels={},
+ processors=self.procs[:2],
+ )
self.assertContentEqual(self.procs[:2], charm_base.processors)
def test_set(self):
@@ -116,15 +117,19 @@ class TestCharmBaseSet(TestCaseWithFactory):
charm_base = self.factory.makeCharmBase(distro_series=distro_series)
self.factory.makeCharmBase()
self.assertEqual(
- charm_base, charm_base_set.getByDistroSeries(distro_series))
+ charm_base, charm_base_set.getByDistroSeries(distro_series)
+ )
self.assertRaises(
- NoSuchCharmBase, charm_base_set.getByDistroSeries,
- self.factory.makeDistroSeries())
+ NoSuchCharmBase,
+ charm_base_set.getByDistroSeries,
+ self.factory.makeDistroSeries(),
+ )
def test_getAll(self):
charm_bases = [self.factory.makeCharmBase() for _ in range(3)]
self.assertContentEqual(
- charm_bases, getUtility(ICharmBaseSet).getAll())
+ charm_bases, getUtility(ICharmBaseSet).getAll()
+ )
class TestCharmBaseWebservice(TestCaseWithFactory):
@@ -137,11 +142,15 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
distroseries = self.factory.makeDistroSeries()
distroseries_url = api_url(distroseries)
webservice = webservice_for_person(
- person, permission=OAuthPermission.WRITE_PUBLIC)
+ person, permission=OAuthPermission.WRITE_PUBLIC
+ )
webservice.default_api_version = "devel"
response = webservice.named_post(
- "/+charm-bases", "new", distro_series=distroseries_url,
- build_snap_channels={"charmcraft": "stable"})
+ "/+charm-bases",
+ "new",
+ distro_series=distroseries_url,
+ build_snap_channels={"charmcraft": "stable"},
+ )
self.assertEqual(401, response.status)
def test_new(self):
@@ -150,22 +159,35 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
distroseries = self.factory.makeDistroSeries()
distroseries_url = api_url(distroseries)
webservice = webservice_for_person(
- person, permission=OAuthPermission.WRITE_PUBLIC)
+ person, permission=OAuthPermission.WRITE_PUBLIC
+ )
webservice.default_api_version = "devel"
logout()
response = webservice.named_post(
- "/+charm-bases", "new", distro_series=distroseries_url,
- build_snap_channels={"charmcraft": "stable"})
+ "/+charm-bases",
+ "new",
+ distro_series=distroseries_url,
+ build_snap_channels={"charmcraft": "stable"},
+ )
self.assertEqual(201, response.status)
charm_base = webservice.get(response.getHeader("Location")).jsonBody()
with person_logged_in(person):
- self.assertThat(charm_base, ContainsDict({
- "registrant_link": Equals(
- webservice.getAbsoluteUrl(api_url(person))),
- "distro_series_link": Equals(
- webservice.getAbsoluteUrl(distroseries_url)),
- "build_snap_channels": Equals({"charmcraft": "stable"}),
- }))
+ self.assertThat(
+ charm_base,
+ ContainsDict(
+ {
+ "registrant_link": Equals(
+ webservice.getAbsoluteUrl(api_url(person))
+ ),
+ "distro_series_link": Equals(
+ webservice.getAbsoluteUrl(distroseries_url)
+ ),
+ "build_snap_channels": Equals(
+ {"charmcraft": "stable"}
+ ),
+ }
+ ),
+ )
def test_new_duplicate_distro_series(self):
# An attempt to create a CharmBase with a duplicate distro series is
@@ -175,21 +197,30 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
distroseries_str = str(distroseries)
distroseries_url = api_url(distroseries)
webservice = webservice_for_person(
- person, permission=OAuthPermission.WRITE_PUBLIC)
+ person, permission=OAuthPermission.WRITE_PUBLIC
+ )
webservice.default_api_version = "devel"
logout()
response = webservice.named_post(
- "/+charm-bases", "new", distro_series=distroseries_url,
- build_snap_channels={"charmcraft": "stable"})
+ "/+charm-bases",
+ "new",
+ distro_series=distroseries_url,
+ build_snap_channels={"charmcraft": "stable"},
+ )
self.assertEqual(201, response.status)
response = webservice.named_post(
- "/+charm-bases", "new", distro_series=distroseries_url,
- build_snap_channels={"charmcraft": "stable"})
+ "/+charm-bases",
+ "new",
+ distro_series=distroseries_url,
+ build_snap_channels={"charmcraft": "stable"},
+ )
self.assertEqual(400, response.status)
self.assertEqual(
- ("%s is already in use by another base." %
- distroseries_str).encode(),
- response.body)
+ (
+ "%s is already in use by another base." % distroseries_str
+ ).encode(),
+ response.body,
+ )
def test_getByDistroSeries(self):
# lp.charm_bases.getByDistroSeries returns a matching CharmBase.
@@ -197,17 +228,21 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
distroseries = self.factory.makeDistroSeries()
distroseries_url = api_url(distroseries)
webservice = webservice_for_person(
- person, permission=OAuthPermission.READ_PUBLIC)
+ person, permission=OAuthPermission.READ_PUBLIC
+ )
webservice.default_api_version = "devel"
with celebrity_logged_in("registry_experts"):
self.factory.makeCharmBase(distro_series=distroseries)
response = webservice.named_get(
- "/+charm-bases", "getByDistroSeries",
- distro_series=distroseries_url)
+ "/+charm-bases",
+ "getByDistroSeries",
+ distro_series=distroseries_url,
+ )
self.assertEqual(200, response.status)
self.assertEqual(
webservice.getAbsoluteUrl(distroseries_url),
- response.jsonBody()["distro_series_link"])
+ response.jsonBody()["distro_series_link"],
+ )
def test_getByDistroSeries_missing(self):
# lp.charm_bases.getByDistroSeries returns 404 for a non-existent
@@ -217,47 +252,64 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
distroseries_str = str(distroseries)
distroseries_url = api_url(distroseries)
webservice = webservice_for_person(
- person, permission=OAuthPermission.READ_PUBLIC)
+ person, permission=OAuthPermission.READ_PUBLIC
+ )
webservice.default_api_version = "devel"
logout()
response = webservice.named_get(
- "/+charm-bases", "getByDistroSeries",
- distro_series=distroseries_url)
+ "/+charm-bases",
+ "getByDistroSeries",
+ distro_series=distroseries_url,
+ )
self.assertEqual(404, response.status)
self.assertEqual(
- ("No base for %s." % distroseries_str).encode(), response.body)
+ ("No base for %s." % distroseries_str).encode(), response.body
+ )
def setUpProcessors(self):
self.unrestricted_procs = [
- self.factory.makeProcessor() for _ in range(3)]
+ self.factory.makeProcessor() for _ in range(3)
+ ]
self.unrestricted_proc_names = [
- processor.name for processor in self.unrestricted_procs]
+ processor.name for processor in self.unrestricted_procs
+ ]
self.restricted_procs = [
self.factory.makeProcessor(restricted=True, build_by_default=False)
- for _ in range(2)]
+ for _ in range(2)
+ ]
self.restricted_proc_names = [
- processor.name for processor in self.restricted_procs]
+ processor.name for processor in self.restricted_procs
+ ]
self.procs = self.unrestricted_procs + self.restricted_procs
self.factory.makeProcessor()
self.distroseries = self.factory.makeDistroSeries()
for processor in self.procs:
self.factory.makeDistroArchSeries(
- distroseries=self.distroseries, architecturetag=processor.name,
- processor=processor)
+ distroseries=self.distroseries,
+ architecturetag=processor.name,
+ processor=processor,
+ )
def setProcessors(self, user, charm_base_url, names):
ws = webservice_for_person(
- user, permission=OAuthPermission.WRITE_PUBLIC)
+ user, permission=OAuthPermission.WRITE_PUBLIC
+ )
return ws.named_post(
- charm_base_url, "setProcessors",
+ charm_base_url,
+ "setProcessors",
processors=["/+processors/%s" % name for name in names],
- api_version="devel")
+ api_version="devel",
+ )
def assertProcessors(self, user, charm_base_url, names):
- body = webservice_for_person(user).get(
- charm_base_url + "/processors", api_version="devel").jsonBody()
+ body = (
+ webservice_for_person(user)
+ .get(charm_base_url + "/processors", api_version="devel")
+ .jsonBody()
+ )
self.assertContentEqual(
- names, [entry["name"] for entry in body["entries"]])
+ names, [entry["name"] for entry in body["entries"]]
+ )
def test_setProcessors_admin(self):
"""An admin can change the supported processor set."""
@@ -265,49 +317,62 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
with admin_logged_in():
charm_base = self.factory.makeCharmBase(
distro_series=self.distroseries,
- processors=self.unrestricted_procs)
+ processors=self.unrestricted_procs,
+ )
charm_base_url = api_url(charm_base)
admin = self.factory.makeAdministrator()
self.assertProcessors(
- admin, charm_base_url, self.unrestricted_proc_names)
+ admin, charm_base_url, self.unrestricted_proc_names
+ )
response = self.setProcessors(
- admin, charm_base_url,
- [self.unrestricted_proc_names[0], self.restricted_proc_names[0]])
+ admin,
+ charm_base_url,
+ [self.unrestricted_proc_names[0], self.restricted_proc_names[0]],
+ )
self.assertEqual(200, response.status)
self.assertProcessors(
- admin, charm_base_url,
- [self.unrestricted_proc_names[0], self.restricted_proc_names[0]])
+ admin,
+ charm_base_url,
+ [self.unrestricted_proc_names[0], self.restricted_proc_names[0]],
+ )
def test_setProcessors_non_admin_forbidden(self):
"""Only admins and registry experts can call setProcessors."""
self.setUpProcessors()
with admin_logged_in():
charm_base = self.factory.makeCharmBase(
- distro_series=self.distroseries)
+ distro_series=self.distroseries
+ )
charm_base_url = api_url(charm_base)
person = self.factory.makePerson()
response = self.setProcessors(
- person, charm_base_url, [self.unrestricted_proc_names[0]])
+ person, charm_base_url, [self.unrestricted_proc_names[0]]
+ )
self.assertEqual(401, response.status)
def test_collection(self):
# lp.charm_bases is a collection of all CharmBases.
person = self.factory.makePerson()
webservice = webservice_for_person(
- person, permission=OAuthPermission.READ_PUBLIC)
+ person, permission=OAuthPermission.READ_PUBLIC
+ )
webservice.default_api_version = "devel"
distroseries_urls = []
with celebrity_logged_in("registry_experts"):
for i in range(3):
distroseries = self.factory.makeDistroSeries()
distroseries_urls.append(
- webservice.getAbsoluteUrl(api_url(distroseries)))
+ webservice.getAbsoluteUrl(api_url(distroseries))
+ )
self.factory.makeCharmBase(distro_series=distroseries)
response = webservice.get("/+charm-bases")
self.assertEqual(200, response.status)
self.assertContentEqual(
distroseries_urls,
- [entry["distro_series_link"]
- for entry in response.jsonBody()["entries"]])
+ [
+ entry["distro_series_link"]
+ for entry in response.jsonBody()["entries"]
+ ],
+ )
diff --git a/lib/lp/charms/tests/test_charmhubclient.py b/lib/lp/charms/tests/test_charmhubclient.py
index 018709d..771941f 100644
--- a/lib/lp/charms/tests/test_charmhubclient.py
+++ b/lib/lp/charms/tests/test_charmhubclient.py
@@ -9,15 +9,13 @@ import io
import json
from urllib.parse import quote
-from lazr.restful.utils import get_current_browser_request
import multipart
+import responses
+import transaction
+from lazr.restful.utils import get_current_browser_request
from nacl.public import PrivateKey
-from pymacaroons import (
- Macaroon,
- Verifier,
- )
+from pymacaroons import Macaroon, Verifier
from pymacaroons.serializers import JsonSerializer
-import responses
from testtools.matchers import (
AfterPreprocessing,
ContainsDict,
@@ -29,8 +27,7 @@ from testtools.matchers import (
MatchesListwise,
MatchesStructure,
Mismatch,
- )
-import transaction
+)
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -45,7 +42,7 @@ from lp.charms.interfaces.charmhubclient import (
UnauthorizedUploadResponse,
UploadFailedResponse,
UploadNotReviewedYetResponse,
- )
+)
from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
from lp.services.crypto.interfaces import IEncryptedContainer
from lp.services.features.testing import FeatureFixture
@@ -72,40 +69,76 @@ class MacaroonVerifies(Matcher):
class RequestMatches(MatchesAll):
"""Matches a request with the specified attributes."""
- def __init__(self, macaroons=None, auth=None, json_data=None,
- file_data=None, **kwargs):
+ def __init__(
+ self,
+ macaroons=None,
+ auth=None,
+ json_data=None,
+ file_data=None,
+ **kwargs
+ ):
matchers = []
kwargs = dict(kwargs)
if macaroons is not None:
- matchers.append(MatchesStructure(headers=ContainsDict({
- "Macaroons": AfterPreprocessing(
- lambda v: json.loads(
- base64.b64decode(v.encode()).decode()),
- Equals([json.loads(m) for m in macaroons])),
- })))
+ matchers.append(
+ MatchesStructure(
+ headers=ContainsDict(
+ {
+ "Macaroons": AfterPreprocessing(
+ lambda v: json.loads(
+ base64.b64decode(v.encode()).decode()
+ ),
+ Equals([json.loads(m) for m in macaroons]),
+ ),
+ }
+ )
+ )
+ )
if auth is not None:
auth_scheme, auth_params_matcher = auth
- matchers.append(MatchesStructure(headers=ContainsDict({
- "Authorization": AfterPreprocessing(
- lambda v: v.split(" ", 1),
- MatchesListwise([
- Equals(auth_scheme),
- auth_params_matcher,
- ])),
- })))
+ matchers.append(
+ MatchesStructure(
+ headers=ContainsDict(
+ {
+ "Authorization": AfterPreprocessing(
+ lambda v: v.split(" ", 1),
+ MatchesListwise(
+ [
+ Equals(auth_scheme),
+ auth_params_matcher,
+ ]
+ ),
+ ),
+ }
+ )
+ )
+ )
if json_data is not None:
- matchers.append(MatchesStructure(body=AfterPreprocessing(
- lambda b: json.loads(b.decode()), Equals(json_data))))
+ matchers.append(
+ MatchesStructure(
+ body=AfterPreprocessing(
+ lambda b: json.loads(b.decode()), Equals(json_data)
+ )
+ )
+ )
elif file_data is not None:
- matchers.append(AfterPreprocessing(
- lambda r: multipart.parse_form_data({
- "REQUEST_METHOD": r.method,
- "CONTENT_TYPE": r.headers["Content-Type"],
- "CONTENT_LENGTH": r.headers["Content-Length"],
- "wsgi.input": io.BytesIO(
- r.body.read() if hasattr(r.body, "read") else r.body),
- })[1],
- MatchesDict(file_data)))
+ matchers.append(
+ AfterPreprocessing(
+ lambda r: multipart.parse_form_data(
+ {
+ "REQUEST_METHOD": r.method,
+ "CONTENT_TYPE": r.headers["Content-Type"],
+ "CONTENT_LENGTH": r.headers["Content-Length"],
+ "wsgi.input": io.BytesIO(
+ r.body.read()
+ if hasattr(r.body, "read")
+ else r.body
+ ),
+ }
+ )[1],
+ MatchesDict(file_data),
+ )
+ )
if kwargs:
matchers.append(MatchesStructure(**kwargs))
super().__init__(*matchers)
@@ -121,7 +154,8 @@ class TestCharmhubClient(TestCaseWithFactory):
self.pushConfig(
"charms",
charmhub_url="http://charmhub.example/",
- charmhub_storage_url="http://storage.charmhub.example/")
+ charmhub_storage_url="http://storage.charmhub.example/",
+ )
self.client = getUtility(ICharmhubClient)
def _setUpSecretStorage(self):
@@ -129,86 +163,121 @@ class TestCharmhubClient(TestCaseWithFactory):
self.pushConfig(
"charms",
charmhub_secrets_public_key=base64.b64encode(
- bytes(self.private_key.public_key)).decode(),
+ bytes(self.private_key.public_key)
+ ).decode(),
charmhub_secrets_private_key=base64.b64encode(
- bytes(self.private_key)).decode())
+ bytes(self.private_key)
+ ).decode(),
+ )
def _makeStoreSecrets(self):
self.exchanged_key = hashlib.sha256(
- self.factory.getUniqueBytes()).hexdigest()
+ self.factory.getUniqueBytes()
+ ).hexdigest()
exchanged_macaroon = Macaroon(key=self.exchanged_key)
container = getUtility(IEncryptedContainer, "charmhub-secrets")
return {
- "exchanged_encrypted": removeSecurityProxy(container.encrypt(
- exchanged_macaroon.serialize().encode())),
- }
+ "exchanged_encrypted": removeSecurityProxy(
+ container.encrypt(exchanged_macaroon.serialize().encode())
+ ),
+ }
def _addUnscannedUploadResponse(self):
responses.add(
- "POST", "http://storage.charmhub.example/unscanned-upload/",
- json={"successful": True, "upload_id": 1})
+ "POST",
+ "http://storage.charmhub.example/unscanned-upload/",
+ json={"successful": True, "upload_id": 1},
+ )
def _addCharmPushResponse(self, name):
responses.add(
"POST",
"http://charmhub.example/v1/charm/{}/revisions".format(
- quote(name)),
+ quote(name)
+ ),
status=200,
json={
"status-url": (
"/v1/charm/{}/revisions/review?upload-id=123".format(
- quote(name))),
- })
+ quote(name)
+ )
+ ),
+ },
+ )
def _addCharmReleaseResponse(self, name):
responses.add(
"POST",
"http://charmhub.example/v1/charm/{}/releases".format(quote(name)),
- json={})
+ json={},
+ )
@responses.activate
def test_requestPackageUploadPermission(self):
responses.add(
- "POST", "http://charmhub.example/v1/tokens",
- json={"macaroon": "sentinel"})
+ "POST",
+ "http://charmhub.example/v1/tokens",
+ json={"macaroon": "sentinel"},
+ )
macaroon = self.client.requestPackageUploadPermission("test-charm")
- self.assertThat(responses.calls[-1].request, RequestMatches(
- url=Equals("http://charmhub.example/v1/tokens"),
- method=Equals("POST"),
- json_data={
- "description": "test-charm for launchpad.test",
- "packages": [{"type": "charm", "name": "test-charm"}],
- "permissions": [
- "package-manage-releases",
- "package-manage-revisions",
- "package-view-revisions",
+ self.assertThat(
+ responses.calls[-1].request,
+ RequestMatches(
+ url=Equals("http://charmhub.example/v1/tokens"),
+ method=Equals("POST"),
+ json_data={
+ "description": "test-charm for launchpad.test",
+ "packages": [{"type": "charm", "name": "test-charm"}],
+ "permissions": [
+ "package-manage-releases",
+ "package-manage-revisions",
+ "package-view-revisions",
],
- }))
+ },
+ ),
+ )
self.assertEqual("sentinel", macaroon)
request = get_current_browser_request()
start, stop = get_request_timeline(request).actions[-2:]
- self.assertThat(start, MatchesStructure.byEquality(
- category="request-charm-upload-macaroon-start",
- detail="test-charm"))
- self.assertThat(stop, MatchesStructure.byEquality(
- category="request-charm-upload-macaroon-stop",
- detail="test-charm"))
+ self.assertThat(
+ start,
+ MatchesStructure.byEquality(
+ category="request-charm-upload-macaroon-start",
+ detail="test-charm",
+ ),
+ )
+ self.assertThat(
+ stop,
+ MatchesStructure.byEquality(
+ category="request-charm-upload-macaroon-stop",
+ detail="test-charm",
+ ),
+ )
@responses.activate
def test_requestPackageUploadPermission_missing_macaroon(self):
responses.add("POST", "http://charmhub.example/v1/tokens", json={})
self.assertRaisesWithContent(
- BadRequestPackageUploadResponse, "{}",
- self.client.requestPackageUploadPermission, "test-charm")
+ BadRequestPackageUploadResponse,
+ "{}",
+ self.client.requestPackageUploadPermission,
+ "test-charm",
+ )
@responses.activate
def test_requestPackageUploadPermission_error(self):
responses.add(
- "POST", "http://charmhub.example/v1/tokens",
- status=503, json={"error-list": [{"message": "Failed"}]})
+ "POST",
+ "http://charmhub.example/v1/tokens",
+ status=503,
+ json={"error-list": [{"message": "Failed"}]},
+ )
self.assertRaisesWithContent(
- BadRequestPackageUploadResponse, "Failed",
- self.client.requestPackageUploadPermission, "test-charm")
+ BadRequestPackageUploadResponse,
+ "Failed",
+ self.client.requestPackageUploadPermission,
+ "test-charm",
+ )
@responses.activate
def test_requestPackageUploadPermission_404(self):
@@ -216,86 +285,124 @@ class TestCharmhubClient(TestCaseWithFactory):
self.assertRaisesWithContent(
BadRequestPackageUploadResponse,
"404 Client Error: Not Found",
- self.client.requestPackageUploadPermission, "test-charm")
+ self.client.requestPackageUploadPermission,
+ "test-charm",
+ )
@responses.activate
def test_exchangeMacaroons(self):
responses.add(
- "POST", "http://charmhub.example/v1/tokens/exchange",
- json={"macaroon": "sentinel"})
+ "POST",
+ "http://charmhub.example/v1/tokens/exchange",
+ json={"macaroon": "sentinel"},
+ )
root_macaroon = Macaroon(version=2)
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
unbound_discharge_macaroon = Macaroon(version=2)
unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
- JsonSerializer())
+ JsonSerializer()
+ )
discharge_macaroon_raw = root_macaroon.prepare_for_request(
- unbound_discharge_macaroon).serialize(JsonSerializer())
+ unbound_discharge_macaroon
+ ).serialize(JsonSerializer())
exchanged_macaroon_raw = self.client.exchangeMacaroons(
- root_macaroon_raw, unbound_discharge_macaroon_raw)
- self.assertThat(responses.calls[-1].request, RequestMatches(
- url=Equals("http://charmhub.example/v1/tokens/exchange"),
- method=Equals("POST"),
- macaroons=[root_macaroon_raw, discharge_macaroon_raw],
- json_data={}))
+ root_macaroon_raw, unbound_discharge_macaroon_raw
+ )
+ self.assertThat(
+ responses.calls[-1].request,
+ RequestMatches(
+ url=Equals("http://charmhub.example/v1/tokens/exchange"),
+ method=Equals("POST"),
+ macaroons=[root_macaroon_raw, discharge_macaroon_raw],
+ json_data={},
+ ),
+ )
self.assertEqual("sentinel", exchanged_macaroon_raw)
request = get_current_browser_request()
start, stop = get_request_timeline(request).actions[-2:]
- self.assertThat(start, MatchesStructure.byEquality(
- category="exchange-macaroons-start", detail=""))
- self.assertThat(stop, MatchesStructure.byEquality(
- category="exchange-macaroons-stop", detail=""))
+ self.assertThat(
+ start,
+ MatchesStructure.byEquality(
+ category="exchange-macaroons-start", detail=""
+ ),
+ )
+ self.assertThat(
+ stop,
+ MatchesStructure.byEquality(
+ category="exchange-macaroons-stop", detail=""
+ ),
+ )
@responses.activate
def test_exchangeMacaroons_missing_macaroon(self):
responses.add(
- "POST", "http://charmhub.example/v1/tokens/exchange", json={})
+ "POST", "http://charmhub.example/v1/tokens/exchange", json={}
+ )
root_macaroon_raw = Macaroon(version=2).serialize(JsonSerializer())
discharge_macaroon_raw = Macaroon(version=2).serialize(
- JsonSerializer())
+ JsonSerializer()
+ )
self.assertRaisesWithContent(
- BadExchangeMacaroonsResponse, "{}",
+ BadExchangeMacaroonsResponse,
+ "{}",
self.client.exchangeMacaroons,
- root_macaroon_raw, discharge_macaroon_raw)
+ root_macaroon_raw,
+ discharge_macaroon_raw,
+ )
@responses.activate
def test_exchangeMacaroons_error(self):
responses.add(
- "POST", "http://charmhub.example/v1/tokens/exchange",
+ "POST",
+ "http://charmhub.example/v1/tokens/exchange",
status=401,
- json={"error-list": [{"message": "Exchange window expired"}]})
+ json={"error-list": [{"message": "Exchange window expired"}]},
+ )
root_macaroon_raw = Macaroon(version=2).serialize(JsonSerializer())
discharge_macaroon_raw = Macaroon(version=2).serialize(
- JsonSerializer())
+ JsonSerializer()
+ )
self.assertRaisesWithContent(
- BadExchangeMacaroonsResponse, "Exchange window expired",
+ BadExchangeMacaroonsResponse,
+ "Exchange window expired",
self.client.exchangeMacaroons,
- root_macaroon_raw, discharge_macaroon_raw)
+ root_macaroon_raw,
+ discharge_macaroon_raw,
+ )
@responses.activate
def test_exchangeMacaroons_404(self):
responses.add(
- "POST", "http://charmhub.example/v1/tokens/exchange", status=404)
+ "POST", "http://charmhub.example/v1/tokens/exchange", status=404
+ )
root_macaroon_raw = Macaroon(version=2).serialize(JsonSerializer())
discharge_macaroon_raw = Macaroon(version=2).serialize(
- JsonSerializer())
+ JsonSerializer()
+ )
self.assertRaisesWithContent(
BadExchangeMacaroonsResponse,
"404 Client Error: Not Found",
self.client.exchangeMacaroons,
- root_macaroon_raw, discharge_macaroon_raw)
+ root_macaroon_raw,
+ discharge_macaroon_raw,
+ )
def makeUploadableCharmRecipeBuild(self, store_secrets=None):
if store_secrets is None:
store_secrets = self._makeStoreSecrets()
recipe = self.factory.makeCharmRecipe(
store_upload=True,
- store_name="test-charm", store_secrets=store_secrets)
+ store_name="test-charm",
+ store_secrets=store_secrets,
+ )
build = self.factory.makeCharmRecipeBuild(recipe=recipe)
charm_lfa = self.factory.makeLibraryFileAlias(
- filename="test-charm.charm", content="dummy charm content")
+ filename="test-charm.charm", content="dummy charm content"
+ )
self.factory.makeCharmFile(build=build, library_file=charm_lfa)
manifest_lfa = self.factory.makeLibraryFileAlias(
- filename="test-charm.manifest", content="dummy manifest content")
+ filename="test-charm.manifest", content="dummy manifest content"
+ )
self.factory.makeCharmFile(build=build, library_file=manifest_lfa)
build.updateStatus(BuildStatus.BUILDING)
build.updateStatus(BuildStatus.FULLYBUILT)
@@ -304,7 +411,8 @@ class TestCharmhubClient(TestCaseWithFactory):
@responses.activate
def test_uploadFile(self):
charm_lfa = self.factory.makeLibraryFileAlias(
- filename="test-charm.charm", content="dummy charm content")
+ filename="test-charm.charm", content="dummy charm content"
+ )
transaction.commit()
self._addUnscannedUploadResponse()
# XXX cjwatson 2021-08-19: Use
@@ -312,36 +420,45 @@ class TestCharmhubClient(TestCaseWithFactory):
with dbuser("charm-build-job"):
self.assertEqual(1, self.client.uploadFile(charm_lfa))
requests = [call.request for call in responses.calls]
- self.assertThat(requests, MatchesListwise([
- RequestMatches(
- url=Equals(
- "http://storage.charmhub.example/unscanned-upload/"),
- method=Equals("POST"),
- file_data={
- "binary": MatchesStructure.byEquality(
- name="binary", filename="test-charm.charm",
- value="dummy charm content",
- content_type="application/octet-stream",
- )}),
- ]))
+ request_matcher = RequestMatches(
+ url=Equals("http://storage.charmhub.example/unscanned-upload/"),
+ method=Equals("POST"),
+ file_data={
+ "binary": MatchesStructure.byEquality(
+ name="binary",
+ filename="test-charm.charm",
+ value="dummy charm content",
+ content_type="application/octet-stream",
+ )
+ },
+ )
+ self.assertThat(requests, MatchesListwise([request_matcher]))
@responses.activate
def test_uploadFile_error(self):
charm_lfa = self.factory.makeLibraryFileAlias(
- filename="test-charm.charm", content="dummy charm content")
+ filename="test-charm.charm", content="dummy charm content"
+ )
transaction.commit()
responses.add(
- "POST", "http://storage.charmhub.example/unscanned-upload/",
- status=502, body="The proxy exploded.\n")
+ "POST",
+ "http://storage.charmhub.example/unscanned-upload/",
+ status=502,
+ body="The proxy exploded.\n",
+ )
# XXX cjwatson 2021-08-19: Use
# config.ICharmhubUploadJobSource.dbuser once that job exists.
with dbuser("charm-build-job"):
err = self.assertRaises(
- UploadFailedResponse, self.client.uploadFile, charm_lfa)
+ UploadFailedResponse, self.client.uploadFile, charm_lfa
+ )
self.assertEqual("502 Server Error: Bad Gateway", str(err))
- self.assertThat(err, MatchesStructure(
- detail=Equals("The proxy exploded.\n"),
- can_retry=Is(True)))
+ self.assertThat(
+ err,
+ MatchesStructure(
+ detail=Equals("The proxy exploded.\n"), can_retry=Is(True)
+ ),
+ )
@responses.activate
def test_push(self):
@@ -354,18 +471,22 @@ class TestCharmhubClient(TestCaseWithFactory):
with dbuser("charm-build-job"):
self.assertEqual(
"/v1/charm/test-charm/revisions/review?upload-id=123",
- self.client.push(build, 1))
+ self.client.push(build, 1),
+ )
requests = [call.request for call in responses.calls]
- self.assertThat(requests, MatchesListwise([
- RequestMatches(
- url=Equals(
- "http://charmhub.example/v1/charm/test-charm/revisions"),
- method=Equals("POST"),
- headers=ContainsDict(
- {"Content-Type": Equals("application/json")}),
- auth=("Macaroon", MacaroonVerifies(self.exchanged_key)),
- json_data={"upload-id": 1}),
- ]))
+ request_matcher = RequestMatches(
+ url=Equals(
+ "http://charmhub.example/v1/charm/test-charm/revisions"
+ ),
+ method=Equals("POST"),
+ headers=ContainsDict({"Content-Type": Equals("application/json")}),
+ auth=(
+ "Macaroon",
+ MacaroonVerifies(self.exchanged_key),
+ ),
+ json_data={"upload-id": 1},
+ )
+ self.assertThat(requests, MatchesListwise([request_matcher]))
@responses.activate
def test_push_unauthorized(self):
@@ -375,18 +496,23 @@ class TestCharmhubClient(TestCaseWithFactory):
charm_push_error = {
"code": "permission-required",
"message": "Missing required permission: package-manage-revisions",
- }
+ }
responses.add(
- "POST", "http://charmhub.example/v1/charm/test-charm/revisions",
+ "POST",
+ "http://charmhub.example/v1/charm/test-charm/revisions",
status=401,
- json={"error-list": [charm_push_error]})
+ json={"error-list": [charm_push_error]},
+ )
# XXX cjwatson 2021-08-19: Use
# config.ICharmhubUploadJobSource.dbuser once that job exists.
with dbuser("charm-build-job"):
self.assertRaisesWithContent(
UnauthorizedUploadResponse,
"Missing required permission: package-manage-revisions",
- self.client.push, build, 1)
+ self.client.push,
+ build,
+ 1,
+ )
@responses.activate
def test_checkStatus_pending(self):
@@ -394,7 +520,8 @@ class TestCharmhubClient(TestCaseWithFactory):
build = self.makeUploadableCharmRecipeBuild()
status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
responses.add(
- "GET", "http://charmhub.example" + status_url,
+ "GET",
+ "http://charmhub.example" + status_url,
json={
"revisions": [
{
@@ -402,12 +529,16 @@ class TestCharmhubClient(TestCaseWithFactory):
"status": "new",
"revision": None,
"errors": None,
- },
- ],
- })
+ },
+ ],
+ },
+ )
self.assertRaises(
UploadNotReviewedYetResponse,
- self.client.checkStatus, build, status_url)
+ self.client.checkStatus,
+ build,
+ status_url,
+ )
@responses.activate
def test_checkStatus_error(self):
@@ -415,7 +546,8 @@ class TestCharmhubClient(TestCaseWithFactory):
build = self.makeUploadableCharmRecipeBuild()
status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
responses.add(
- "GET", "http://charmhub.example" + status_url,
+ "GET",
+ "http://charmhub.example" + status_url,
json={
"revisions": [
{
@@ -424,13 +556,18 @@ class TestCharmhubClient(TestCaseWithFactory):
"revision": None,
"errors": [
{"code": None, "message": "This charm is broken."},
- ],
- },
- ],
- })
+ ],
+ },
+ ],
+ },
+ )
self.assertRaisesWithContent(
- ReviewFailedResponse, "This charm is broken.",
- self.client.checkStatus, build, status_url)
+ ReviewFailedResponse,
+ "This charm is broken.",
+ self.client.checkStatus,
+ build,
+ status_url,
+ )
@responses.activate
def test_checkStatus_approved_no_revision(self):
@@ -438,7 +575,8 @@ class TestCharmhubClient(TestCaseWithFactory):
build = self.makeUploadableCharmRecipeBuild()
status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
responses.add(
- "GET", "http://charmhub.example" + status_url,
+ "GET",
+ "http://charmhub.example" + status_url,
json={
"revisions": [
{
@@ -446,13 +584,17 @@ class TestCharmhubClient(TestCaseWithFactory):
"status": "approved",
"revision": None,
"errors": None,
- },
- ],
- })
+ },
+ ],
+ },
+ )
self.assertRaisesWithContent(
ReviewFailedResponse,
"Review passed but did not assign a revision.",
- self.client.checkStatus, build, status_url)
+ self.client.checkStatus,
+ build,
+ status_url,
+ )
@responses.activate
def test_checkStatus_approved(self):
@@ -460,7 +602,8 @@ class TestCharmhubClient(TestCaseWithFactory):
build = self.makeUploadableCharmRecipeBuild()
status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
responses.add(
- "GET", "http://charmhub.example" + status_url,
+ "GET",
+ "http://charmhub.example" + status_url,
json={
"revisions": [
{
@@ -468,17 +611,21 @@ class TestCharmhubClient(TestCaseWithFactory):
"status": "approved",
"revision": 1,
"errors": None,
- },
- ],
- })
+ },
+ ],
+ },
+ )
self.assertEqual(1, self.client.checkStatus(build, status_url))
requests = [call.request for call in responses.calls]
- self.assertThat(requests, MatchesListwise([
- RequestMatches(
- url=Equals("http://charmhub.example" + status_url),
- method=Equals("GET"),
- auth=("Macaroon", MacaroonVerifies(self.exchanged_key))),
- ]))
+ request_matcher = RequestMatches(
+ url=Equals("http://charmhub.example" + status_url),
+ method=Equals("GET"),
+ auth=(
+ "Macaroon",
+ MacaroonVerifies(self.exchanged_key),
+ ),
+ )
+ self.assertThat(requests, MatchesListwise([request_matcher]))
@responses.activate
def test_checkStatus_404(self):
@@ -486,58 +633,89 @@ class TestCharmhubClient(TestCaseWithFactory):
build = self.makeUploadableCharmRecipeBuild()
status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
responses.add(
- "GET", "http://charmhub.example" + status_url, status=404)
+ "GET", "http://charmhub.example" + status_url, status=404
+ )
self.assertRaisesWithContent(
- BadReviewStatusResponse, "404 Client Error: Not Found",
- self.client.checkStatus, build, status_url)
+ BadReviewStatusResponse,
+ "404 Client Error: Not Found",
+ self.client.checkStatus,
+ build,
+ status_url,
+ )
@responses.activate
def test_release(self):
self._setUpSecretStorage()
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name="test-charm",
+ store_upload=True,
+ store_name="test-charm",
store_secrets=self._makeStoreSecrets(),
- store_channels=["stable", "edge"])
+ store_channels=["stable", "edge"],
+ )
build = self.factory.makeCharmRecipeBuild(recipe=recipe)
self._addCharmReleaseResponse("test-charm")
self.client.release(build, 1)
- self.assertThat(responses.calls[-1].request, RequestMatches(
- url=Equals("http://charmhub.example/v1/charm/test-charm/releases"),
- method=Equals("POST"),
- headers=ContainsDict({"Content-Type": Equals("application/json")}),
- auth=("Macaroon", MacaroonVerifies(self.exchanged_key)),
- json_data=[
- {"channel": "stable", "revision": 1},
- {"channel": "edge", "revision": 1},
- ]))
+ self.assertThat(
+ responses.calls[-1].request,
+ RequestMatches(
+ url=Equals(
+ "http://charmhub.example/v1/charm/test-charm/releases"
+ ),
+ method=Equals("POST"),
+ headers=ContainsDict(
+ {"Content-Type": Equals("application/json")}
+ ),
+ auth=("Macaroon", MacaroonVerifies(self.exchanged_key)),
+ json_data=[
+ {"channel": "stable", "revision": 1},
+ {"channel": "edge", "revision": 1},
+ ],
+ ),
+ )
@responses.activate
def test_release_error(self):
self._setUpSecretStorage()
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name="test-charm",
+ store_upload=True,
+ store_name="test-charm",
store_secrets=self._makeStoreSecrets(),
- store_channels=["stable", "edge"])
+ store_channels=["stable", "edge"],
+ )
build = self.factory.makeCharmRecipeBuild(recipe=recipe)
responses.add(
- "POST", "http://charmhub.example/v1/charm/test-charm/releases",
+ "POST",
+ "http://charmhub.example/v1/charm/test-charm/releases",
status=503,
- json={"error-list": [{"message": "Failed to publish"}]})
+ json={"error-list": [{"message": "Failed to publish"}]},
+ )
self.assertRaisesWithContent(
- ReleaseFailedResponse, "Failed to publish",
- self.client.release, build, 1)
+ ReleaseFailedResponse,
+ "Failed to publish",
+ self.client.release,
+ build,
+ 1,
+ )
@responses.activate
def test_release_404(self):
self._setUpSecretStorage()
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name="test-charm",
+ store_upload=True,
+ store_name="test-charm",
store_secrets=self._makeStoreSecrets(),
- store_channels=["stable", "edge"])
+ store_channels=["stable", "edge"],
+ )
build = self.factory.makeCharmRecipeBuild(recipe=recipe)
responses.add(
- "POST", "http://charmhub.example/v1/charm/test-charm/releases",
- status=404)
+ "POST",
+ "http://charmhub.example/v1/charm/test-charm/releases",
+ status=404,
+ )
self.assertRaisesWithContent(
- ReleaseFailedResponse, "404 Client Error: Not Found",
- self.client.release, build, 1)
+ ReleaseFailedResponse,
+ "404 Client Error: Not Found",
+ self.client.release,
+ build,
+ 1,
+ )
diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
index b67d542..f7818e8 100644
--- a/lib/lp/charms/tests/test_charmrecipe.py
+++ b/lib/lp/charms/tests/test_charmrecipe.py
@@ -4,20 +4,18 @@
"""Test charm recipes."""
import base64
-from datetime import (
- datetime,
- timedelta,
- )
import json
+from datetime import datetime, timedelta
from textwrap import dedent
-from fixtures import FakeLogger
import iso8601
+import pytz
+import responses
+import transaction
+from fixtures import FakeLogger
from nacl.public import PrivateKey
from pymacaroons import Macaroon
from pymacaroons.serializers import JsonSerializer
-import pytz
-import responses
from storm.exceptions import LostObjectError
from storm.locals import Store
from testtools.matchers import (
@@ -32,8 +30,7 @@ from testtools.matchers import (
MatchesListwise,
MatchesSetwise,
MatchesStructure,
- )
-import transaction
+)
from zope.component import getUtility
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
@@ -44,21 +41,21 @@ from lp.buildmaster.enums import (
BuildBaseImageType,
BuildQueueStatus,
BuildStatus,
- )
+)
from lp.buildmaster.interfaces.buildqueue import IBuildQueue
from lp.buildmaster.interfaces.processor import (
IProcessorSet,
ProcessorNotFound,
- )
+)
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.buildmaster.model.buildqueue import BuildQueue
from lp.charms.interfaces.charmrecipe import (
- BadCharmRecipeSearchContext,
- CannotAuthorizeCharmhubUploads,
CHARM_RECIPE_ALLOW_CREATE,
CHARM_RECIPE_BUILD_DISTRIBUTION,
CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG,
+ BadCharmRecipeSearchContext,
+ CannotAuthorizeCharmhubUploads,
CharmRecipeBuildAlreadyPending,
CharmRecipeBuildDisallowedArchitecture,
CharmRecipeBuildRequestStatus,
@@ -68,37 +65,31 @@ from lp.charms.interfaces.charmrecipe import (
ICharmRecipeSet,
ICharmRecipeView,
NoSourceForCharmRecipe,
- )
+)
from lp.charms.interfaces.charmrecipebuild import (
ICharmRecipeBuild,
ICharmRecipeBuildSet,
- )
+)
from lp.charms.interfaces.charmrecipebuildjob import ICharmhubUploadJobSource
from lp.charms.interfaces.charmrecipejob import (
ICharmRecipeRequestBuildsJobSource,
- )
+)
from lp.charms.model.charmrecipe import CharmRecipeSet
from lp.charms.model.charmrecipebuild import CharmFile
from lp.charms.model.charmrecipebuildjob import CharmRecipeBuildJob
from lp.charms.model.charmrecipejob import CharmRecipeJob
from lp.code.errors import GitRepositoryBlobNotFound
from lp.code.tests.helpers import GitHostingFixture
-from lp.registry.enums import (
- PersonVisibility,
- TeamMembershipPolicy,
- )
+from lp.registry.enums import PersonVisibility, TeamMembershipPolicy
from lp.registry.interfaces.series import SeriesStatus
from lp.services.config import config
from lp.services.crypto.interfaces import IEncryptedContainer
-from lp.services.database.constants import (
- ONE_DAY_AGO,
- UTC_NOW,
- )
+from lp.services.database.constants import ONE_DAY_AGO, UTC_NOW
from lp.services.database.interfaces import IStore
from lp.services.database.sqlbase import (
flush_database_caches,
get_transaction_timestamp,
- )
+)
from lp.services.features.testing import FeatureFixture
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.runner import JobRunner
@@ -108,26 +99,23 @@ from lp.services.webapp.publisher import canonical_url
from lp.services.webapp.snapshot import notify_modified
from lp.services.webhooks.testing import LogsScheduledWebhooks
from lp.testing import (
- admin_logged_in,
ANONYMOUS,
+ StormStatementRecorder,
+ TestCaseWithFactory,
+ admin_logged_in,
api_url,
login,
logout,
person_logged_in,
record_two_runs,
- StormStatementRecorder,
- TestCaseWithFactory,
- )
+)
from lp.testing.dbuser import dbuser
from lp.testing.layers import (
DatabaseFunctionalLayer,
LaunchpadFunctionalLayer,
LaunchpadZopelessLayer,
- )
-from lp.testing.matchers import (
- DoesNotSnapshot,
- HasQueryCount,
- )
+)
+from lp.testing.matchers import DoesNotSnapshot, HasQueryCount
from lp.testing.pages import webservice_for_person
@@ -138,15 +126,18 @@ class TestCharmRecipeFeatureFlags(TestCaseWithFactory):
def test_feature_flag_disabled(self):
# Without a feature flag, we wil not create any charm recipes.
self.assertRaises(
- CharmRecipeFeatureDisabled, self.factory.makeCharmRecipe)
+ CharmRecipeFeatureDisabled, self.factory.makeCharmRecipe
+ )
def test_private_feature_flag_disabled(self):
# Without a private feature flag, we wil not create new private
# charm recipes.
self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
self.assertRaises(
- CharmRecipePrivateFeatureDisabled, self.factory.makeCharmRecipe,
- information_type=InformationType.PROPRIETARY)
+ CharmRecipePrivateFeatureDisabled,
+ self.factory.makeCharmRecipe,
+ information_type=InformationType.PROPRIETARY,
+ )
class TestCharmRecipe(TestCaseWithFactory):
@@ -167,17 +158,25 @@ class TestCharmRecipe(TestCaseWithFactory):
# CharmRecipe objects have an informative __repr__.
recipe = self.factory.makeCharmRecipe()
self.assertEqual(
- "<CharmRecipe ~%s/%s/+charm/%s>" % (
- recipe.owner.name, recipe.project.name, recipe.name),
- repr(recipe))
+ "<CharmRecipe ~%s/%s/+charm/%s>"
+ % (recipe.owner.name, recipe.project.name, recipe.name),
+ repr(recipe),
+ )
def test_avoids_problematic_snapshots(self):
self.assertThat(
self.factory.makeCharmRecipe(),
DoesNotSnapshot(
- ["pending_build_requests", "failed_build_requests",
- "builds", "completed_builds", "pending_builds"],
- ICharmRecipeView))
+ [
+ "pending_build_requests",
+ "failed_build_requests",
+ "builds",
+ "completed_builds",
+ "pending_builds",
+ ],
+ ICharmRecipeView,
+ ),
+ )
def test_initial_date_last_modified(self):
# The initial value of date_last_modified is date_created.
@@ -191,14 +190,16 @@ class TestCharmRecipe(TestCaseWithFactory):
with notify_modified(removeSecurityProxy(recipe), ["name"]):
pass
self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW)
+ recipe, "date_last_modified", UTC_NOW
+ )
def test__default_distribution_default(self):
# If the CHARM_RECIPE_BUILD_DISTRIBUTION feature rule is not set, we
# default to Ubuntu.
recipe = self.factory.makeCharmRecipe()
self.assertEqual(
- "ubuntu", removeSecurityProxy(recipe)._default_distribution.name)
+ "ubuntu", removeSecurityProxy(recipe)._default_distribution.name
+ )
def test__default_distribution_feature_rule(self):
# If the CHARM_RECIPE_BUILD_DISTRIBUTION feature rule is set, we
@@ -208,8 +209,8 @@ class TestCharmRecipe(TestCaseWithFactory):
recipe = self.factory.makeCharmRecipe()
with FeatureFixture({CHARM_RECIPE_BUILD_DISTRIBUTION: distro_name}):
self.assertEqual(
- distribution,
- removeSecurityProxy(recipe)._default_distribution)
+ distribution, removeSecurityProxy(recipe)._default_distribution
+ )
def test__default_distribution_feature_rule_nonexistent(self):
# If we mistakenly set the rule to a non-existent distribution,
@@ -217,11 +218,16 @@ class TestCharmRecipe(TestCaseWithFactory):
recipe = self.factory.makeCharmRecipe()
with FeatureFixture({CHARM_RECIPE_BUILD_DISTRIBUTION: "nonexistent"}):
expected_msg = (
- "'nonexistent' is not a valid value for feature rule '%s'" %
- CHARM_RECIPE_BUILD_DISTRIBUTION)
+ "'nonexistent' is not a valid value for feature rule '%s'"
+ % CHARM_RECIPE_BUILD_DISTRIBUTION
+ )
self.assertRaisesWithContent(
- ValueError, expected_msg,
- getattr, removeSecurityProxy(recipe), "_default_distribution")
+ ValueError,
+ expected_msg,
+ getattr,
+ removeSecurityProxy(recipe),
+ "_default_distribution",
+ )
def test__default_distro_series_feature_rule(self):
# If the appropriate per-distribution feature rule is set, we
@@ -230,17 +236,21 @@ class TestCharmRecipe(TestCaseWithFactory):
distribution = self.factory.makeDistribution(name=distro_name)
distro_series_name = "myseries"
distro_series = self.factory.makeDistroSeries(
- distribution=distribution, name=distro_series_name)
+ distribution=distribution, name=distro_series_name
+ )
self.factory.makeDistroSeries(distribution=distribution)
recipe = self.factory.makeCharmRecipe()
- with FeatureFixture({
+ with FeatureFixture(
+ {
CHARM_RECIPE_BUILD_DISTRIBUTION: distro_name,
- "charm.default_build_series.%s" % distro_name: (
- distro_series_name),
- }):
+ "charm.default_build_series.%s"
+ % distro_name: (distro_series_name),
+ }
+ ):
self.assertEqual(
distro_series,
- removeSecurityProxy(recipe)._default_distro_series)
+ removeSecurityProxy(recipe)._default_distro_series,
+ )
def test__default_distro_series_no_feature_rule(self):
# If the appropriate per-distribution feature rule is not set, we
@@ -248,40 +258,52 @@ class TestCharmRecipe(TestCaseWithFactory):
distro_name = "mydistro"
distribution = self.factory.makeDistribution(name=distro_name)
self.factory.makeDistroSeries(
- distribution=distribution, status=SeriesStatus.SUPPORTED)
+ distribution=distribution, status=SeriesStatus.SUPPORTED
+ )
current_series = self.factory.makeDistroSeries(
- distribution=distribution, status=SeriesStatus.DEVELOPMENT)
+ distribution=distribution, status=SeriesStatus.DEVELOPMENT
+ )
recipe = self.factory.makeCharmRecipe()
with FeatureFixture({CHARM_RECIPE_BUILD_DISTRIBUTION: distro_name}):
self.assertEqual(
current_series,
- removeSecurityProxy(recipe)._default_distro_series)
-
- def makeBuildableDistroArchSeries(self, architecturetag=None,
- processor=None,
- supports_virtualized=True,
- supports_nonvirtualized=True, **kwargs):
+ removeSecurityProxy(recipe)._default_distro_series,
+ )
+
+ def makeBuildableDistroArchSeries(
+ self,
+ architecturetag=None,
+ processor=None,
+ supports_virtualized=True,
+ supports_nonvirtualized=True,
+ **kwargs
+ ):
if architecturetag is None:
architecturetag = self.factory.getUniqueUnicode("arch")
if processor is None:
try:
processor = getUtility(IProcessorSet).getByName(
- architecturetag)
+ architecturetag
+ )
except ProcessorNotFound:
processor = self.factory.makeProcessor(
name=architecturetag,
supports_virtualized=supports_virtualized,
- supports_nonvirtualized=supports_nonvirtualized)
+ supports_nonvirtualized=supports_nonvirtualized,
+ )
das = self.factory.makeDistroArchSeries(
- architecturetag=architecturetag, processor=processor, **kwargs)
+ architecturetag=architecturetag, processor=processor, **kwargs
+ )
# Add both a chroot and a LXD image to test that
# getAllowedArchitectures doesn't get confused by multiple
# PocketChroot rows for a single DistroArchSeries.
fake_chroot = self.factory.makeLibraryFileAlias(
- filename="fake_chroot.tar.gz", db_only=True)
+ filename="fake_chroot.tar.gz", db_only=True
+ )
das.addOrUpdateChroot(fake_chroot)
fake_lxd = self.factory.makeLibraryFileAlias(
- filename="fake_lxd.tar.gz", db_only=True)
+ filename="fake_lxd.tar.gz", db_only=True
+ )
das.addOrUpdateChroot(fake_lxd, image_type=BuildBaseImageType.LXD)
return das
@@ -292,18 +314,22 @@ class TestCharmRecipe(TestCaseWithFactory):
build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
build = recipe.requestBuild(build_request, das)
self.assertTrue(ICharmRecipeBuild.providedBy(build))
- self.assertThat(build, MatchesStructure(
- requester=Equals(recipe.owner.teamowner),
- distro_arch_series=Equals(das),
- channels=Is(None),
- status=Equals(BuildStatus.NEEDSBUILD),
- ))
+ self.assertThat(
+ build,
+ MatchesStructure(
+ requester=Equals(recipe.owner.teamowner),
+ distro_arch_series=Equals(das),
+ channels=Is(None),
+ status=Equals(BuildStatus.NEEDSBUILD),
+ ),
+ )
store = Store.of(build)
store.flush()
build_queue = store.find(
BuildQueue,
- BuildQueue._build_farm_job_id ==
- removeSecurityProxy(build).build_farm_job_id).one()
+ BuildQueue._build_farm_job_id
+ == removeSecurityProxy(build).build_farm_job_id,
+ ).one()
self.assertProvides(build_queue, IBuildQueue)
self.assertEqual(recipe.require_virtualized, build_queue.virtualized)
self.assertEqual(BuildQueueStatus.WAITING, build_queue.status)
@@ -324,7 +350,8 @@ class TestCharmRecipe(TestCaseWithFactory):
das = self.makeBuildableDistroArchSeries()
build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
build = recipe.requestBuild(
- build_request, das, channels={"charmcraft": "edge"})
+ build_request, das, channels={"charmcraft": "edge"}
+ )
self.assertEqual({"charmcraft": "edge"}, build.channels)
def test_requestBuild_rejects_repeats(self):
@@ -333,24 +360,37 @@ class TestCharmRecipe(TestCaseWithFactory):
distro_series = self.factory.makeDistroSeries()
arches = [
self.makeBuildableDistroArchSeries(distroseries=distro_series)
- for _ in range(2)]
+ for _ in range(2)
+ ]
build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
old_build = recipe.requestBuild(build_request, arches[0])
self.assertRaises(
- CharmRecipeBuildAlreadyPending, recipe.requestBuild,
- build_request, arches[0])
+ CharmRecipeBuildAlreadyPending,
+ recipe.requestBuild,
+ build_request,
+ arches[0],
+ )
# We can build for a different distroarchseries.
recipe.requestBuild(build_request, arches[1])
# channels=None and channels={} are treated as equivalent, but
# anything else allows a new build.
self.assertRaises(
- CharmRecipeBuildAlreadyPending, recipe.requestBuild,
- build_request, arches[0], channels={})
+ CharmRecipeBuildAlreadyPending,
+ recipe.requestBuild,
+ build_request,
+ arches[0],
+ channels={},
+ )
recipe.requestBuild(
- build_request, arches[0], channels={"core": "edge"})
+ build_request, arches[0], channels={"core": "edge"}
+ )
self.assertRaises(
- CharmRecipeBuildAlreadyPending, recipe.requestBuild,
- build_request, arches[0], channels={"core": "edge"})
+ CharmRecipeBuildAlreadyPending,
+ recipe.requestBuild,
+ build_request,
+ arches[0],
+ channels={"core": "edge"},
+ )
# Changing the status of the old build allows a new build.
old_build.updateStatus(BuildStatus.BUILDING)
old_build.updateStatus(BuildStatus.FULLYBUILT)
@@ -364,20 +404,24 @@ class TestCharmRecipe(TestCaseWithFactory):
dases = {}
for proc_nonvirt in True, False:
das = self.makeBuildableDistroArchSeries(
- distroseries=distro_series, supports_virtualized=True,
- supports_nonvirtualized=proc_nonvirt)
+ distroseries=distro_series,
+ supports_virtualized=True,
+ supports_nonvirtualized=proc_nonvirt,
+ )
dases[proc_nonvirt] = das
for proc_nonvirt, recipe_virt, build_virt in (
- (True, False, False),
- (True, True, True),
- (False, False, True),
- (False, True, True),
- ):
+ (True, False, False),
+ (True, True, True),
+ (False, False, True),
+ (False, True, True),
+ ):
das = dases[proc_nonvirt]
recipe = self.factory.makeCharmRecipe(
- require_virtualized=recipe_virt)
+ require_virtualized=recipe_virt
+ )
build_request = self.factory.makeCharmRecipeBuildRequest(
- recipe=recipe)
+ recipe=recipe
+ )
build = recipe.requestBuild(build_request, das)
self.assertEqual(build_virt, build.virtualized)
@@ -387,55 +431,78 @@ class TestCharmRecipe(TestCaseWithFactory):
recipe = self.factory.makeCharmRecipe()
distro_series = self.factory.makeDistroSeries()
das = self.makeBuildableDistroArchSeries(
- distroseries=distro_series, supports_virtualized=False,
- supports_nonvirtualized=True)
+ distroseries=distro_series,
+ supports_virtualized=False,
+ supports_nonvirtualized=True,
+ )
build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
self.assertRaises(
- CharmRecipeBuildDisallowedArchitecture, recipe.requestBuild,
- build_request, das)
+ CharmRecipeBuildDisallowedArchitecture,
+ recipe.requestBuild,
+ build_request,
+ das,
+ )
with admin_logged_in():
recipe.require_virtualized = False
recipe.requestBuild(build_request, das)
def test_requestBuild_triggers_webhooks(self):
# Requesting a build triggers webhooks.
- self.useFixture(FeatureFixture({
- CHARM_RECIPE_ALLOW_CREATE: "on",
- CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
- }))
+ self.useFixture(
+ FeatureFixture(
+ {
+ CHARM_RECIPE_ALLOW_CREATE: "on",
+ CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
+ }
+ )
+ )
logger = self.useFixture(FakeLogger())
recipe = self.factory.makeCharmRecipe()
das = self.makeBuildableDistroArchSeries()
build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
hook = self.factory.makeWebhook(
- target=recipe, event_types=["charm-recipe:build:0.1"])
+ target=recipe, event_types=["charm-recipe:build:0.1"]
+ )
build = recipe.requestBuild(build_request, das)
expected_payload = {
"recipe_build": Equals(
- canonical_url(build, force_local_path=True)),
+ canonical_url(build, force_local_path=True)
+ ),
"action": Equals("created"),
"recipe": Equals(canonical_url(recipe, force_local_path=True)),
"build_request": Equals(
- canonical_url(build_request, force_local_path=True)),
+ canonical_url(build_request, force_local_path=True)
+ ),
"status": Equals("Needs building"),
"store_upload_status": Equals("Unscheduled"),
- }
+ }
with person_logged_in(recipe.owner):
delivery = hook.deliveries.one()
self.assertThat(
- delivery, MatchesStructure(
+ delivery,
+ MatchesStructure(
event_type=Equals("charm-recipe:build:0.1"),
- payload=MatchesDict(expected_payload)))
+ payload=MatchesDict(expected_payload),
+ ),
+ )
with dbuser(config.IWebhookDeliveryJobSource.dbuser):
self.assertEqual(
- "<WebhookDeliveryJob for webhook %d on %r>" % (
- hook.id, hook.target),
- repr(delivery))
+ "<WebhookDeliveryJob for webhook %d on %r>"
+ % (hook.id, hook.target),
+ repr(delivery),
+ )
self.assertThat(
logger.output,
- LogsScheduledWebhooks([
- (hook, "charm-recipe:build:0.1",
- MatchesDict(expected_payload))]))
+ LogsScheduledWebhooks(
+ [
+ (
+ hook,
+ "charm-recipe:build:0.1",
+ MatchesDict(expected_payload),
+ )
+ ]
+ ),
+ )
def test_requestBuilds(self):
# requestBuilds schedules a job and returns a corresponding
@@ -444,22 +511,30 @@ class TestCharmRecipe(TestCaseWithFactory):
now = get_transaction_timestamp(IStore(recipe))
with person_logged_in(recipe.owner.teamowner):
request = recipe.requestBuilds(recipe.owner.teamowner)
- self.assertThat(request, MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=Is(None),
- architectures=Is(None)))
+ self.assertThat(
+ request,
+ MatchesStructure(
+ date_requested=Equals(now),
+ date_finished=Is(None),
+ recipe=Equals(recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ error_message=Is(None),
+ channels=Is(None),
+ architectures=Is(None),
+ ),
+ )
[job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(job, MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner.teamowner),
- channels=Is(None),
- architectures=Is(None)))
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job_id=Equals(request.id),
+ job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+ recipe=Equals(recipe),
+ requester=Equals(recipe.owner.teamowner),
+ channels=Is(None),
+ architectures=Is(None),
+ ),
+ )
def test_requestBuilds_with_channels(self):
# If asked to build using particular snap channels, requestBuilds
@@ -468,23 +543,32 @@ class TestCharmRecipe(TestCaseWithFactory):
now = get_transaction_timestamp(IStore(recipe))
with person_logged_in(recipe.owner.teamowner):
request = recipe.requestBuilds(
- recipe.owner.teamowner, channels={"charmcraft": "edge"})
- self.assertThat(request, MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=MatchesDict({"charmcraft": Equals("edge")}),
- architectures=Is(None)))
+ recipe.owner.teamowner, channels={"charmcraft": "edge"}
+ )
+ self.assertThat(
+ request,
+ MatchesStructure(
+ date_requested=Equals(now),
+ date_finished=Is(None),
+ recipe=Equals(recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ error_message=Is(None),
+ channels=MatchesDict({"charmcraft": Equals("edge")}),
+ architectures=Is(None),
+ ),
+ )
[job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(job, MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner.teamowner),
- channels=Equals({"charmcraft": "edge"}),
- architectures=Is(None)))
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job_id=Equals(request.id),
+ job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+ recipe=Equals(recipe),
+ requester=Equals(recipe.owner.teamowner),
+ channels=Equals({"charmcraft": "edge"}),
+ architectures=Is(None),
+ ),
+ )
def test_requestBuilds_with_architectures(self):
# If asked to build for particular architectures, requestBuilds
@@ -493,130 +577,183 @@ class TestCharmRecipe(TestCaseWithFactory):
now = get_transaction_timestamp(IStore(recipe))
with person_logged_in(recipe.owner.teamowner):
request = recipe.requestBuilds(
- recipe.owner.teamowner, architectures={"amd64", "i386"})
- self.assertThat(request, MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=Is(None),
- architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
+ recipe.owner.teamowner, architectures={"amd64", "i386"}
+ )
+ self.assertThat(
+ request,
+ MatchesStructure(
+ date_requested=Equals(now),
+ date_finished=Is(None),
+ recipe=Equals(recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ error_message=Is(None),
+ channels=Is(None),
+ architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
+ ),
+ )
[job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(job, MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner.teamowner),
- channels=Is(None),
- architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
-
- def makeRequestBuildsJob(self, distro_series_version, arch_tags,
- git_ref=None):
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job_id=Equals(request.id),
+ job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+ recipe=Equals(recipe),
+ requester=Equals(recipe.owner.teamowner),
+ channels=Is(None),
+ architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
+ ),
+ )
+
+ def makeRequestBuildsJob(
+ self, distro_series_version, arch_tags, git_ref=None
+ ):
recipe = self.factory.makeCharmRecipe(git_ref=git_ref)
distro_series = self.factory.makeDistroSeries(
distribution=getUtility(ILaunchpadCelebrities).ubuntu,
- version=distro_series_version)
+ version=distro_series_version,
+ )
for arch_tag in arch_tags:
self.makeBuildableDistroArchSeries(
- distroseries=distro_series, architecturetag=arch_tag)
+ distroseries=distro_series, architecturetag=arch_tag
+ )
return getUtility(ICharmRecipeRequestBuildsJobSource).create(
- recipe, recipe.owner.teamowner, {"charmcraft": "edge"})
-
- def assertRequestedBuildsMatch(self, builds, job, distro_series_version,
- arch_tags, channels):
- self.assertThat(builds, MatchesSetwise(
- *(MatchesStructure(
- requester=Equals(job.requester),
- recipe=Equals(job.recipe),
- distro_arch_series=MatchesStructure(
- distroseries=MatchesStructure.byEquality(
- version=distro_series_version),
- architecturetag=Equals(arch_tag)),
- channels=Equals(channels))
- for arch_tag in arch_tags)))
+ recipe, recipe.owner.teamowner, {"charmcraft": "edge"}
+ )
+
+ def assertRequestedBuildsMatch(
+ self, builds, job, distro_series_version, arch_tags, channels
+ ):
+ self.assertThat(
+ builds,
+ MatchesSetwise(
+ *(
+ MatchesStructure(
+ requester=Equals(job.requester),
+ recipe=Equals(job.recipe),
+ distro_arch_series=MatchesStructure(
+ distroseries=MatchesStructure.byEquality(
+ version=distro_series_version
+ ),
+ architecturetag=Equals(arch_tag),
+ ),
+ channels=Equals(channels),
+ )
+ for arch_tag in arch_tags
+ )
+ ),
+ )
def test_requestBuildsFromJob_restricts_explicit_list(self):
# requestBuildsFromJob limits builds targeted at an explicit list of
# architectures to those allowed for the recipe.
- self.useFixture(GitHostingFixture(blob=dedent("""\
- bases:
- - build-on:
- - name: ubuntu
- channel: "20.04"
- architectures: [sparc]
- - build-on:
- - name: ubuntu
- channel: "20.04"
- architectures: [i386]
- - build-on:
- - name: ubuntu
- channel: "20.04"
- architectures: [avr]
- """)))
+ self.useFixture(
+ GitHostingFixture(
+ blob=dedent(
+ """\
+ bases:
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [sparc]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [i386]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [avr]
+ """
+ )
+ )
+ )
job = self.makeRequestBuildsJob("20.04", ["sparc", "avr", "mips64el"])
self.assertEqual(
- get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+ get_transaction_timestamp(IStore(job.recipe)), job.date_created
+ )
transaction.commit()
with person_logged_in(job.requester):
builds = job.recipe.requestBuildsFromJob(
- job.build_request, channels=removeSecurityProxy(job.channels))
+ job.build_request, channels=removeSecurityProxy(job.channels)
+ )
self.assertRequestedBuildsMatch(
- builds, job, "20.04", ["sparc", "avr"], job.channels)
+ builds, job, "20.04", ["sparc", "avr"], job.channels
+ )
def test_requestBuildsFromJob_no_explicit_bases(self):
# If the recipe doesn't specify any bases, requestBuildsFromJob
# requests builds for all configured architectures for the default
# series.
- self.useFixture(FeatureFixture({
- CHARM_RECIPE_ALLOW_CREATE: "on",
- CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
- "charm.default_build_series.ubuntu": "20.04",
- }))
+ self.useFixture(
+ FeatureFixture(
+ {
+ CHARM_RECIPE_ALLOW_CREATE: "on",
+ CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+ "charm.default_build_series.ubuntu": "20.04",
+ }
+ )
+ )
self.useFixture(GitHostingFixture(blob="name: foo\n"))
old_distro_series = self.factory.makeDistroSeries(
distribution=getUtility(ILaunchpadCelebrities).ubuntu,
- version="18.04")
+ version="18.04",
+ )
for arch_tag in ("mips64el", "riscv64"):
self.makeBuildableDistroArchSeries(
- distroseries=old_distro_series, architecturetag=arch_tag)
+ distroseries=old_distro_series, architecturetag=arch_tag
+ )
job = self.makeRequestBuildsJob("20.04", ["mips64el", "riscv64"])
self.assertEqual(
- get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+ get_transaction_timestamp(IStore(job.recipe)), job.date_created
+ )
transaction.commit()
with person_logged_in(job.requester):
builds = job.recipe.requestBuildsFromJob(
- job.build_request, channels=removeSecurityProxy(job.channels))
+ job.build_request, channels=removeSecurityProxy(job.channels)
+ )
self.assertRequestedBuildsMatch(
- builds, job, "20.04", ["mips64el", "riscv64"], job.channels)
+ builds, job, "20.04", ["mips64el", "riscv64"], job.channels
+ )
def test_requestBuildsFromJob_no_charmcraft_yaml(self):
# If the recipe has no charmcraft.yaml file, requestBuildsFromJob
# treats this as equivalent to a charmcraft.yaml file that doesn't
# specify any bases: that is, it requests builds for all configured
# architectures for the default series.
- self.useFixture(FeatureFixture({
- CHARM_RECIPE_ALLOW_CREATE: "on",
- CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
- "charm.default_build_series.ubuntu": "20.04",
- }))
- self.useFixture(GitHostingFixture()).getBlob.failure = (
- GitRepositoryBlobNotFound("placeholder", "charmcraft.yaml"))
+ self.useFixture(
+ FeatureFixture(
+ {
+ CHARM_RECIPE_ALLOW_CREATE: "on",
+ CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+ "charm.default_build_series.ubuntu": "20.04",
+ }
+ )
+ )
+ self.useFixture(
+ GitHostingFixture()
+ ).getBlob.failure = GitRepositoryBlobNotFound(
+ "placeholder", "charmcraft.yaml"
+ )
old_distro_series = self.factory.makeDistroSeries(
distribution=getUtility(ILaunchpadCelebrities).ubuntu,
- version="18.04")
+ version="18.04",
+ )
for arch_tag in ("mips64el", "riscv64"):
self.makeBuildableDistroArchSeries(
- distroseries=old_distro_series, architecturetag=arch_tag)
+ distroseries=old_distro_series, architecturetag=arch_tag
+ )
job = self.makeRequestBuildsJob("20.04", ["mips64el", "riscv64"])
self.assertEqual(
- get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+ get_transaction_timestamp(IStore(job.recipe)), job.date_created
+ )
transaction.commit()
with person_logged_in(job.requester):
builds = job.recipe.requestBuildsFromJob(
- job.build_request, channels=removeSecurityProxy(job.channels))
+ job.build_request, channels=removeSecurityProxy(job.channels)
+ )
self.assertRequestedBuildsMatch(
- builds, job, "20.04", ["mips64el", "riscv64"], job.channels)
+ builds, job, "20.04", ["mips64el", "riscv64"], job.channels
+ )
def test_requestBuildsFromJob_architectures_parameter(self):
# If an explicit set of architectures was given as a parameter,
@@ -624,16 +761,21 @@ class TestCharmRecipe(TestCaseWithFactory):
# when requesting builds.
self.useFixture(GitHostingFixture(blob="name: foo\n"))
job = self.makeRequestBuildsJob(
- "20.04", ["avr", "mips64el", "riscv64"])
+ "20.04", ["avr", "mips64el", "riscv64"]
+ )
self.assertEqual(
- get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+ get_transaction_timestamp(IStore(job.recipe)), job.date_created
+ )
transaction.commit()
with person_logged_in(job.requester):
builds = job.recipe.requestBuildsFromJob(
- job.build_request, channels=removeSecurityProxy(job.channels),
- architectures={"avr", "riscv64"})
+ job.build_request,
+ channels=removeSecurityProxy(job.channels),
+ architectures={"avr", "riscv64"},
+ )
self.assertRequestedBuildsMatch(
- builds, job, "20.04", ["avr", "riscv64"], job.channels)
+ builds, job, "20.04", ["avr", "riscv64"], job.channels
+ )
def test_requestBuildsFromJob_charm_base_architectures(self):
# requestBuildsFromJob intersects the architectures supported by the
@@ -641,20 +783,25 @@ class TestCharmRecipe(TestCaseWithFactory):
self.useFixture(GitHostingFixture(blob="name: foo\n"))
job = self.makeRequestBuildsJob("20.04", ["sparc", "i386", "avr"])
distroseries = getUtility(ILaunchpadCelebrities).ubuntu.getSeries(
- "20.04")
+ "20.04"
+ )
with admin_logged_in():
self.factory.makeCharmBase(
distro_series=distroseries,
build_snap_channels={"charmcraft": "stable/launchpad-buildd"},
processors=[
distroseries[arch_tag].processor
- for arch_tag in ("sparc", "avr")])
+ for arch_tag in ("sparc", "avr")
+ ],
+ )
transaction.commit()
with person_logged_in(job.requester):
builds = job.recipe.requestBuildsFromJob(
- job.build_request, channels=removeSecurityProxy(job.channels))
+ job.build_request, channels=removeSecurityProxy(job.channels)
+ )
self.assertRequestedBuildsMatch(
- builds, job, "20.04", ["sparc", "avr"], job.channels)
+ builds, job, "20.04", ["sparc", "avr"], job.channels
+ )
def test_requestBuildsFromJob_charm_base_build_channels_by_arch(self):
# If the charm base declares different build channels for specific
@@ -663,72 +810,113 @@ class TestCharmRecipe(TestCaseWithFactory):
self.useFixture(GitHostingFixture(blob="name: foo\n"))
job = self.makeRequestBuildsJob("20.04", ["avr", "riscv64"])
distroseries = getUtility(ILaunchpadCelebrities).ubuntu.getSeries(
- "20.04")
+ "20.04"
+ )
with admin_logged_in():
self.factory.makeCharmBase(
distro_series=distroseries,
build_snap_channels={
"core20": "stable",
"_byarch": {"riscv64": {"core20": "candidate"}},
- })
+ },
+ )
transaction.commit()
with person_logged_in(job.requester):
builds = job.recipe.requestBuildsFromJob(
- job.build_request, channels=removeSecurityProxy(job.channels))
- self.assertThat(builds, MatchesSetwise(
- *(MatchesStructure(
- requester=Equals(job.requester),
- recipe=Equals(job.recipe),
- distro_arch_series=MatchesStructure(
- distroseries=MatchesStructure.byEquality(version="20.04"),
- architecturetag=Equals(arch_tag)),
- channels=Equals(channels))
- for arch_tag, channels in (
- ("avr", {"charmcraft": "edge", "core20": "stable"}),
- ("riscv64", {"charmcraft": "edge", "core20": "candidate"}),
- ))))
+ job.build_request, channels=removeSecurityProxy(job.channels)
+ )
+ self.assertThat(
+ builds,
+ MatchesSetwise(
+ *(
+ MatchesStructure(
+ requester=Equals(job.requester),
+ recipe=Equals(job.recipe),
+ distro_arch_series=MatchesStructure(
+ distroseries=MatchesStructure.byEquality(
+ version="20.04"
+ ),
+ architecturetag=Equals(arch_tag),
+ ),
+ channels=Equals(channels),
+ )
+ for arch_tag, channels in (
+ ("avr", {"charmcraft": "edge", "core20": "stable"}),
+ (
+ "riscv64",
+ {"charmcraft": "edge", "core20": "candidate"},
+ ),
+ )
+ )
+ ),
+ )
def test_requestBuildsFromJob_triggers_webhooks(self):
# requestBuildsFromJob triggers webhooks, and the payload includes a
# link to the build request.
- self.useFixture(FeatureFixture({
- CHARM_RECIPE_ALLOW_CREATE: "on",
- CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
- "charm.default_build_series.ubuntu": "20.04",
- CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
- }))
+ self.useFixture(
+ FeatureFixture(
+ {
+ CHARM_RECIPE_ALLOW_CREATE: "on",
+ CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+ "charm.default_build_series.ubuntu": "20.04",
+ CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
+ }
+ )
+ )
self.useFixture(GitHostingFixture(blob="name: foo\n"))
logger = self.useFixture(FakeLogger())
job = self.makeRequestBuildsJob("20.04", ["mips64el", "riscv64"])
hook = self.factory.makeWebhook(
- target=job.recipe, event_types=["charm-recipe:build:0.1"])
+ target=job.recipe, event_types=["charm-recipe:build:0.1"]
+ )
with person_logged_in(job.requester):
builds = job.recipe.requestBuildsFromJob(
- job.build_request, channels=removeSecurityProxy(job.channels))
+ job.build_request, channels=removeSecurityProxy(job.channels)
+ )
self.assertEqual(2, len(builds))
payload_matchers = [
- MatchesDict({
- "recipe_build": Equals(canonical_url(
- build, force_local_path=True)),
- "action": Equals("created"),
- "recipe": Equals(canonical_url(
- job.recipe, force_local_path=True)),
- "build_request": Equals(canonical_url(
- job.build_request, force_local_path=True)),
- "status": Equals("Needs building"),
- "store_upload_status": Equals("Unscheduled"),
- })
- for build in builds]
- self.assertThat(hook.deliveries, MatchesSetwise(*(
- MatchesStructure(
- event_type=Equals("charm-recipe:build:0.1"),
- payload=payload_matcher)
- for payload_matcher in payload_matchers)))
+ MatchesDict(
+ {
+ "recipe_build": Equals(
+ canonical_url(build, force_local_path=True)
+ ),
+ "action": Equals("created"),
+ "recipe": Equals(
+ canonical_url(job.recipe, force_local_path=True)
+ ),
+ "build_request": Equals(
+ canonical_url(
+ job.build_request, force_local_path=True
+ )
+ ),
+ "status": Equals("Needs building"),
+ "store_upload_status": Equals("Unscheduled"),
+ }
+ )
+ for build in builds
+ ]
+ self.assertThat(
+ hook.deliveries,
+ MatchesSetwise(
+ *(
+ MatchesStructure(
+ event_type=Equals("charm-recipe:build:0.1"),
+ payload=payload_matcher,
+ )
+ for payload_matcher in payload_matchers
+ )
+ ),
+ )
self.assertThat(
logger.output,
- LogsScheduledWebhooks([
- (hook, "charm-recipe:build:0.1", payload_matcher)
- for payload_matcher in payload_matchers]))
+ LogsScheduledWebhooks(
+ [
+ (hook, "charm-recipe:build:0.1", payload_matcher)
+ for payload_matcher in payload_matchers
+ ]
+ ),
+ )
def test_requestAutoBuilds(self):
# requestAutoBuilds creates a new build request with appropriate
@@ -737,59 +925,79 @@ class TestCharmRecipe(TestCaseWithFactory):
now = get_transaction_timestamp(IStore(recipe))
with person_logged_in(recipe.owner.teamowner):
request = recipe.requestAutoBuilds()
- self.assertThat(request, MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=Is(None),
- architectures=Is(None)))
+ self.assertThat(
+ request,
+ MatchesStructure(
+ date_requested=Equals(now),
+ date_finished=Is(None),
+ recipe=Equals(recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ error_message=Is(None),
+ channels=Is(None),
+ architectures=Is(None),
+ ),
+ )
[job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(job, MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner),
- channels=Is(None),
- architectures=Is(None)))
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job_id=Equals(request.id),
+ job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+ recipe=Equals(recipe),
+ requester=Equals(recipe.owner),
+ channels=Is(None),
+ architectures=Is(None),
+ ),
+ )
def test_requestAutoBuilds_channels(self):
# requestAutoBuilds honours CharmRecipe.auto_build_channels.
recipe = self.factory.makeCharmRecipe(
- auto_build_channels={"charmcraft": "edge"})
+ auto_build_channels={"charmcraft": "edge"}
+ )
now = get_transaction_timestamp(IStore(recipe))
with person_logged_in(recipe.owner.teamowner):
request = recipe.requestAutoBuilds()
- self.assertThat(request, MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=Equals({"charmcraft": "edge"}),
- architectures=Is(None)))
+ self.assertThat(
+ request,
+ MatchesStructure(
+ date_requested=Equals(now),
+ date_finished=Is(None),
+ recipe=Equals(recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ error_message=Is(None),
+ channels=Equals({"charmcraft": "edge"}),
+ architectures=Is(None),
+ ),
+ )
[job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(job, MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner),
- channels=Equals({"charmcraft": "edge"}),
- architectures=Is(None)))
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job_id=Equals(request.id),
+ job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+ recipe=Equals(recipe),
+ requester=Equals(recipe.owner),
+ channels=Equals({"charmcraft": "edge"}),
+ architectures=Is(None),
+ ),
+ )
def test_delete_without_builds(self):
# A charm recipe with no builds can be deleted.
owner = self.factory.makePerson()
project = self.factory.makeProduct()
recipe = self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, project=project, name="condemned")
+ registrant=owner, owner=owner, project=project, name="condemned"
+ )
self.assertTrue(
- getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+ )
with person_logged_in(recipe.owner):
recipe.destroySelf()
self.assertFalse(
- getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+ )
def test_related_webhooks_deleted(self):
owner = self.factory.makePerson()
@@ -811,54 +1019,69 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
self.pushConfig("charms", charmhub_url="http://charmhub.example/")
self.pushConfig(
- "launchpad", candid_service_root="https://candid.test/")
+ "launchpad", candid_service_root="https://candid.test/"
+ )
@responses.activate
def assertBeginsAuthorization(self, recipe, **kwargs):
root_macaroon = Macaroon(version=2)
root_macaroon.add_third_party_caveat(
- "https://candid.test/", "", "identity")
+ "https://candid.test/", "", "identity"
+ )
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
responses.add(
- "POST", "http://charmhub.example/v1/tokens",
- json={"macaroon": root_macaroon_raw})
+ "POST",
+ "http://charmhub.example/v1/tokens",
+ json={"macaroon": root_macaroon_raw},
+ )
self.assertEqual(root_macaroon_raw, recipe.beginAuthorization())
- self.assertThat(responses.calls, MatchesListwise([
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("http://charmhub.example/v1/tokens"),
- method=Equals("POST"),
- body=AfterPreprocessing(
- lambda b: json.loads(b.decode()),
- Equals({
- "description": (
- "{} for launchpad.test".format(
- recipe.store_name)),
- "packages": [
- {"type": "charm", "name": recipe.store_name},
- ],
- "permissions": [
- "package-manage-releases",
- "package-manage-revisions",
- "package-view-revisions",
- ],
- })))),
- ]))
+ tokens_matcher = MatchesStructure(
+ url=Equals("http://charmhub.example/v1/tokens"),
+ method=Equals("POST"),
+ body=AfterPreprocessing(
+ lambda b: json.loads(b.decode()),
+ Equals(
+ {
+ "description": (
+ "{} for launchpad.test".format(recipe.store_name)
+ ),
+ "packages": [
+ {
+ "type": "charm",
+ "name": recipe.store_name,
+ },
+ ],
+ "permissions": [
+ "package-manage-releases",
+ "package-manage-revisions",
+ "package-view-revisions",
+ ],
+ }
+ ),
+ ),
+ )
+ self.assertThat(
+ responses.calls,
+ MatchesListwise([MatchesStructure(request=tokens_matcher)]),
+ )
self.assertEqual({"root": root_macaroon_raw}, recipe.store_secrets)
def test_beginAuthorization(self):
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name=self.factory.getUniqueUnicode())
+ store_upload=True, store_name=self.factory.getUniqueUnicode()
+ )
with person_logged_in(recipe.registrant):
self.assertBeginsAuthorization(recipe)
def test_beginAuthorization_unauthorized(self):
# A user without edit access cannot authorize charm recipe uploads.
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name=self.factory.getUniqueUnicode())
+ store_upload=True, store_name=self.factory.getUniqueUnicode()
+ )
with person_logged_in(self.factory.makePerson()):
self.assertRaises(
- Unauthorized, getattr, recipe, "beginAuthorization")
+ Unauthorized, getattr, recipe, "beginAuthorization"
+ )
@responses.activate
def test_completeAuthorization(self):
@@ -866,56 +1089,84 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
self.pushConfig(
"charms",
charmhub_secrets_public_key=base64.b64encode(
- bytes(private_key.public_key)).decode())
+ bytes(private_key.public_key)
+ ).decode(),
+ )
root_macaroon = Macaroon(version=2)
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
unbound_discharge_macaroon = Macaroon(version=2)
unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
- JsonSerializer())
+ JsonSerializer()
+ )
discharge_macaroon_raw = root_macaroon.prepare_for_request(
- unbound_discharge_macaroon).serialize(JsonSerializer())
+ unbound_discharge_macaroon
+ ).serialize(JsonSerializer())
exchanged_macaroon = Macaroon(version=2)
exchanged_macaroon_raw = exchanged_macaroon.serialize(JsonSerializer())
responses.add(
- "POST", "http://charmhub.example/v1/tokens/exchange",
- json={"macaroon": exchanged_macaroon_raw})
+ "POST",
+ "http://charmhub.example/v1/tokens/exchange",
+ json={"macaroon": exchanged_macaroon_raw},
+ )
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name=self.factory.getUniqueUnicode(),
- store_secrets={"root": root_macaroon_raw})
+ store_upload=True,
+ store_name=self.factory.getUniqueUnicode(),
+ store_secrets={"root": root_macaroon_raw},
+ )
with person_logged_in(recipe.registrant):
recipe.completeAuthorization(unbound_discharge_macaroon_raw)
self.pushConfig(
"charms",
charmhub_secrets_private_key=base64.b64encode(
- bytes(private_key)).decode())
+ bytes(private_key)
+ ).decode(),
+ )
container = getUtility(IEncryptedContainer, "charmhub-secrets")
- self.assertThat(recipe.store_secrets, MatchesDict({
- "exchanged_encrypted": AfterPreprocessing(
- lambda data: container.decrypt(data).decode(),
- Equals(exchanged_macaroon_raw)),
- }))
- self.assertThat(responses.calls, MatchesListwise([
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("http://charmhub.example/v1/tokens/exchange"),
- method=Equals("POST"),
- headers=ContainsDict({
- "Macaroons": AfterPreprocessing(
- lambda v: json.loads(
- base64.b64decode(v.encode()).decode()),
- Equals([
- json.loads(m) for m in (
+ self.assertThat(
+ recipe.store_secrets,
+ MatchesDict(
+ {
+ "exchanged_encrypted": AfterPreprocessing(
+ lambda data: container.decrypt(data).decode(),
+ Equals(exchanged_macaroon_raw),
+ ),
+ }
+ ),
+ )
+ exchange_matcher = MatchesStructure(
+ url=Equals("http://charmhub.example/v1/tokens/exchange"),
+ method=Equals("POST"),
+ headers=ContainsDict(
+ {
+ "Macaroons": AfterPreprocessing(
+ lambda v: json.loads(
+ base64.b64decode(v.encode()).decode()
+ ),
+ Equals(
+ [
+ json.loads(m)
+ for m in (
root_macaroon_raw,
- discharge_macaroon_raw)])),
- }),
- body=AfterPreprocessing(
- lambda b: json.loads(b.decode()),
- Equals({})))),
- ]))
+ discharge_macaroon_raw,
+ )
+ ]
+ ),
+ ),
+ }
+ ),
+ body=AfterPreprocessing(
+ lambda b: json.loads(b.decode()), Equals({})
+ ),
+ )
+ self.assertThat(
+ responses.calls,
+ MatchesListwise([MatchesStructure(request=exchange_matcher)]),
+ )
def test_completeAuthorization_without_beginAuthorization(self):
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name=self.factory.getUniqueUnicode())
+ store_upload=True, store_name=self.factory.getUniqueUnicode()
+ )
discharge_macaroon = Macaroon(version=2)
with person_logged_in(recipe.registrant):
self.assertRaisesWithContent(
@@ -923,27 +1174,35 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
"beginAuthorization must be called before "
"completeAuthorization.",
recipe.completeAuthorization,
- discharge_macaroon.serialize(JsonSerializer()))
+ discharge_macaroon.serialize(JsonSerializer()),
+ )
def test_completeAuthorization_unauthorized(self):
root_macaroon = Macaroon(version=2)
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name=self.factory.getUniqueUnicode(),
- store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+ store_upload=True,
+ store_name=self.factory.getUniqueUnicode(),
+ store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+ )
with person_logged_in(self.factory.makePerson()):
self.assertRaises(
- Unauthorized, getattr, recipe, "completeAuthorization")
+ Unauthorized, getattr, recipe, "completeAuthorization"
+ )
def test_completeAuthorization_malformed_discharge_macaroon(self):
root_macaroon = Macaroon(version=2)
recipe = self.factory.makeCharmRecipe(
- store_upload=True, store_name=self.factory.getUniqueUnicode(),
- store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+ store_upload=True,
+ store_name=self.factory.getUniqueUnicode(),
+ store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+ )
with person_logged_in(recipe.registrant):
self.assertRaisesWithContent(
CannotAuthorizeCharmhubUploads,
"Discharge macaroon is invalid.",
- recipe.completeAuthorization, "nonsense")
+ recipe.completeAuthorization,
+ "nonsense",
+ )
class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
@@ -963,28 +1222,47 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
distroseries = self.factory.makeDistroSeries()
processor = self.factory.makeProcessor(supports_virtualized=True)
das = self.factory.makeDistroArchSeries(
- distroseries=distroseries, architecturetag=processor.name,
- processor=processor)
+ distroseries=distroseries,
+ architecturetag=processor.name,
+ processor=processor,
+ )
das.addOrUpdateChroot(
self.factory.makeLibraryFileAlias(
- filename="fake_chroot.tar.gz", db_only=True))
- self.useFixture(GitHostingFixture(blob=dedent("""\
- bases:
- - build-on:
- - name: "%s"
- channel: "%s"
- architectures: [%s]
- """ % (
- distroseries.distribution.name, distroseries.name,
- processor.name))))
+ filename="fake_chroot.tar.gz", db_only=True
+ )
+ )
+ self.useFixture(
+ GitHostingFixture(
+ blob=dedent(
+ """\
+ bases:
+ - build-on:
+ - name: "%s"
+ channel: "%s"
+ architectures: [%s]
+ """
+ % (
+ distroseries.distribution.name,
+ distroseries.name,
+ processor.name,
+ )
+ )
+ )
+ )
[git_ref] = self.factory.makeGitRefs()
condemned_recipe = self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, project=project, name="condemned",
- git_ref=git_ref)
+ registrant=owner,
+ owner=owner,
+ project=project,
+ name="condemned",
+ git_ref=git_ref,
+ )
other_recipe = self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, project=project, git_ref=git_ref)
+ registrant=owner, owner=owner, project=project, git_ref=git_ref
+ )
self.assertTrue(
- getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+ )
with person_logged_in(owner):
requests = []
jobs = []
@@ -999,10 +1277,12 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
[other_build] = requests[1].builds
charm_file = self.factory.makeCharmFile(build=build)
other_charm_file = self.factory.makeCharmFile(build=other_build)
- charm_build_job = getUtility(
- ICharmhubUploadJobSource).create(build)
- other_build_job = getUtility(
- ICharmhubUploadJobSource).create(other_build)
+ charm_build_job = getUtility(ICharmhubUploadJobSource).create(
+ build
+ )
+ other_build_job = getUtility(ICharmhubUploadJobSource).create(
+ other_build
+ )
store = Store.of(condemned_recipe)
store.flush()
job_ids = [job.job_id for job in jobs]
@@ -1017,7 +1297,8 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
# The deleted recipe, its build requests, its builds and the build job
# are gone.
self.assertFalse(
- getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+ getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+ )
self.assertIsNone(store.get(CharmRecipeJob, job_ids[0]))
self.assertIsNone(getUtility(ICharmRecipeBuildSet).getByID(build_id))
self.assertIsNone(store.get(BuildQueue, build_queue_id))
@@ -1026,16 +1307,20 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
self.assertIsNone(store.get(CharmRecipeBuildJob, charm_build_job_id))
# Unrelated build requests, build jobs and builds are still present.
self.assertIsNotNone(
- store.get(CharmRecipeBuildJob, other_build_job.job_id))
+ store.get(CharmRecipeBuildJob, other_build_job.job_id)
+ )
self.assertEqual(
removeSecurityProxy(jobs[1]).context,
- store.get(CharmRecipeJob, job_ids[1]))
+ store.get(CharmRecipeJob, job_ids[1]),
+ )
self.assertEqual(
other_build,
- getUtility(ICharmRecipeBuildSet).getByID(other_build.id))
+ getUtility(ICharmRecipeBuildSet).getByID(other_build.id),
+ )
self.assertIsNotNone(other_build.buildqueue_record)
self.assertIsNotNone(
- store.get(CharmFile, removeSecurityProxy(other_charm_file).id))
+ store.get(CharmFile, removeSecurityProxy(other_charm_file).id)
+ )
class TestCharmRecipeSet(TestCaseWithFactory):
@@ -1063,7 +1348,7 @@ class TestCharmRecipeSet(TestCaseWithFactory):
"owner": self.factory.makeTeam(owner=registrant),
"project": self.factory.makeProduct(),
"name": self.factory.getUniqueUnicode("charm-name"),
- }
+ }
if git_ref is None:
git_ref = self.factory.makeGitRefs()[0]
components["git_ref"] = git_ref
@@ -1097,22 +1382,30 @@ class TestCharmRecipeSet(TestCaseWithFactory):
# fails.
registrant = self.factory.makePerson()
self.assertRaises(
- NoSourceForCharmRecipe, getUtility(ICharmRecipeSet).new,
- registrant, registrant, self.factory.makeProduct(),
- self.factory.getUniqueUnicode("charm-name"))
+ NoSourceForCharmRecipe,
+ getUtility(ICharmRecipeSet).new,
+ registrant,
+ registrant,
+ self.factory.makeProduct(),
+ self.factory.getUniqueUnicode("charm-name"),
+ )
def test_getByName(self):
owner = self.factory.makePerson()
project = self.factory.makeProduct()
project_recipe = self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, project=project, name="proj-charm")
+ registrant=owner, owner=owner, project=project, name="proj-charm"
+ )
self.factory.makeCharmRecipe(
- registrant=owner, owner=owner, name="proj-charm")
+ registrant=owner, owner=owner, name="proj-charm"
+ )
self.assertEqual(
project_recipe,
getUtility(ICharmRecipeSet).getByName(
- owner, project, "proj-charm"))
+ owner, project, "proj-charm"
+ ),
+ )
def test_findByOwner(self):
# ICharmRecipeSet.findByOwner returns all charm recipes with the
@@ -1121,8 +1414,9 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipes = []
for owner in owners:
for i in range(2):
- recipes.append(self.factory.makeCharmRecipe(
- registrant=owner, owner=owner))
+ recipes.append(
+ self.factory.makeCharmRecipe(registrant=owner, owner=owner)
+ )
recipe_set = getUtility(ICharmRecipeSet)
self.assertContentEqual(recipes[:2], recipe_set.findByOwner(owners[0]))
self.assertContentEqual(recipes[2:], recipe_set.findByOwner(owners[1]))
@@ -1133,15 +1427,18 @@ class TestCharmRecipeSet(TestCaseWithFactory):
owners = [self.factory.makePerson() for i in range(2)]
recipes = []
for owner in owners:
- recipes.append(self.factory.makeCharmRecipe(
- registrant=owner, owner=owner))
+ recipes.append(
+ self.factory.makeCharmRecipe(registrant=owner, owner=owner)
+ )
[ref] = self.factory.makeGitRefs(owner=owner)
recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
recipe_set = getUtility(ICharmRecipeSet)
self.assertContentEqual(
- recipes[:2], recipe_set.findByPerson(owners[0]))
+ recipes[:2], recipe_set.findByPerson(owners[0])
+ )
self.assertContentEqual(
- recipes[2:], recipe_set.findByPerson(owners[1]))
+ recipes[2:], recipe_set.findByPerson(owners[1])
+ )
def test_findByProject(self):
# ICharmRecipeSet.findByProject returns all charm recipes based on
@@ -1157,9 +1454,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
recipe_set = getUtility(ICharmRecipeSet)
self.assertContentEqual(
- recipes[:2], recipe_set.findByProject(projects[0]))
+ recipes[:2], recipe_set.findByProject(projects[0])
+ )
self.assertContentEqual(
- recipes[2:4], recipe_set.findByProject(projects[1]))
+ recipes[2:4], recipe_set.findByProject(projects[1])
+ )
def test_findByGitRepository(self):
# ICharmRecipeSet.findByGitRepository returns all charm recipes with
@@ -1172,9 +1471,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
recipe_set = getUtility(ICharmRecipeSet)
self.assertContentEqual(
- recipes[:2], recipe_set.findByGitRepository(repositories[0]))
+ recipes[:2], recipe_set.findByGitRepository(repositories[0])
+ )
self.assertContentEqual(
- recipes[2:], recipe_set.findByGitRepository(repositories[1]))
+ recipes[2:], recipe_set.findByGitRepository(repositories[1])
+ )
def test_findByGitRepository_paths(self):
# ICharmRecipeSet.findByGitRepository can restrict by reference
@@ -1187,16 +1488,21 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
recipe_set = getUtility(ICharmRecipeSet)
self.assertContentEqual(
- [], recipe_set.findByGitRepository(repositories[0], paths=[]))
+ [], recipe_set.findByGitRepository(repositories[0], paths=[])
+ )
self.assertContentEqual(
[recipes[0]],
recipe_set.findByGitRepository(
- repositories[0], paths=[recipes[0].git_ref.path]))
+ repositories[0], paths=[recipes[0].git_ref.path]
+ ),
+ )
self.assertContentEqual(
recipes[:2],
recipe_set.findByGitRepository(
repositories[0],
- paths=[recipes[0].git_ref.path, recipes[1].git_ref.path]))
+ paths=[recipes[0].git_ref.path, recipes[1].git_ref.path],
+ ),
+ )
def test_findByGitRef(self):
# ICharmRecipeSet.findByGitRef returns all charm recipes with the
@@ -1205,8 +1511,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
refs = []
recipes = []
for repository in repositories:
- refs.extend(self.factory.makeGitRefs(
- paths=["refs/heads/master", "refs/heads/other"]))
+ refs.extend(
+ self.factory.makeGitRefs(
+ paths=["refs/heads/master", "refs/heads/other"]
+ )
+ )
recipes.append(self.factory.makeCharmRecipe(git_ref=refs[-2]))
recipes.append(self.factory.makeCharmRecipe(git_ref=refs[-1]))
recipe_set = getUtility(ICharmRecipeSet)
@@ -1219,33 +1528,47 @@ class TestCharmRecipeSet(TestCaseWithFactory):
person = self.factory.makePerson()
project = self.factory.makeProduct()
repository = self.factory.makeGitRepository(
- owner=person, target=project)
+ owner=person, target=project
+ )
refs = self.factory.makeGitRefs(
repository=repository,
- paths=["refs/heads/master", "refs/heads/other"])
+ paths=["refs/heads/master", "refs/heads/other"],
+ )
other_repository = self.factory.makeGitRepository()
other_refs = self.factory.makeGitRefs(
repository=other_repository,
- paths=["refs/heads/master", "refs/heads/other"])
+ paths=["refs/heads/master", "refs/heads/other"],
+ )
recipes = []
recipes.append(self.factory.makeCharmRecipe(git_ref=refs[0]))
recipes.append(self.factory.makeCharmRecipe(git_ref=refs[1]))
- recipes.append(self.factory.makeCharmRecipe(
- registrant=person, owner=person, git_ref=other_refs[0]))
- recipes.append(self.factory.makeCharmRecipe(
- project=project, git_ref=other_refs[1]))
+ recipes.append(
+ self.factory.makeCharmRecipe(
+ registrant=person, owner=person, git_ref=other_refs[0]
+ )
+ )
+ recipes.append(
+ self.factory.makeCharmRecipe(
+ project=project, git_ref=other_refs[1]
+ )
+ )
recipe_set = getUtility(ICharmRecipeSet)
self.assertContentEqual(recipes[:3], recipe_set.findByContext(person))
self.assertContentEqual(
[recipes[0], recipes[1], recipes[3]],
- recipe_set.findByContext(project))
+ recipe_set.findByContext(project),
+ )
self.assertContentEqual(
- recipes[:2], recipe_set.findByContext(repository))
+ recipes[:2], recipe_set.findByContext(repository)
+ )
self.assertContentEqual(
- [recipes[0]], recipe_set.findByContext(refs[0]))
+ [recipes[0]], recipe_set.findByContext(refs[0])
+ )
self.assertRaises(
- BadCharmRecipeSearchContext, recipe_set.findByContext,
- self.factory.makeDistribution())
+ BadCharmRecipeSearchContext,
+ recipe_set.findByContext,
+ self.factory.makeDistribution(),
+ )
def test__findStaleRecipes(self):
# Stale; not built automatically.
@@ -1254,9 +1577,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
self.factory.makeCharmRecipe(auto_build=True, is_stale=False)
# Stale; built automatically.
stale_daily = self.factory.makeCharmRecipe(
- auto_build=True, is_stale=True)
+ auto_build=True, is_stale=True
+ )
self.assertContentEqual(
- [stale_daily], CharmRecipeSet._findStaleRecipes())
+ [stale_daily], CharmRecipeSet._findStaleRecipes()
+ )
def test__findStaleRecipes_distinct(self):
# If a charm recipe has two build requests, it only returns one
@@ -1264,10 +1589,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
for _ in range(2):
build_request = self.factory.makeCharmRecipeBuildRequest(
- recipe=recipe)
+ recipe=recipe
+ )
removeSecurityProxy(
- removeSecurityProxy(build_request)._job).job.date_created = (
- datetime.now(pytz.UTC) - timedelta(days=2))
+ removeSecurityProxy(build_request)._job
+ ).job.date_created = datetime.now(pytz.UTC) - timedelta(days=2)
self.assertContentEqual([recipe], CharmRecipeSet._findStaleRecipes())
def test_makeAutoBuilds(self):
@@ -1277,17 +1603,24 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
logger = BufferLogger()
[build_request] = getUtility(ICharmRecipeSet).makeAutoBuilds(
- logger=logger)
- self.assertThat(build_request, MatchesStructure(
- recipe=Equals(recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- requester=Equals(recipe.owner), channels=Is(None)))
+ logger=logger
+ )
+ self.assertThat(
+ build_request,
+ MatchesStructure(
+ recipe=Equals(recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ requester=Equals(recipe.owner),
+ channels=Is(None),
+ ),
+ )
expected_log_entries = [
- "DEBUG Scheduling builds of charm recipe %s/%s/%s" % (
- recipe.owner.name, recipe.project.name, recipe.name),
- ]
+ "DEBUG Scheduling builds of charm recipe %s/%s/%s"
+ % (recipe.owner.name, recipe.project.name, recipe.name),
+ ]
self.assertEqual(
- expected_log_entries, logger.getLogBuffer().splitlines())
+ expected_log_entries, logger.getLogBuffer().splitlines()
+ )
self.assertFalse(recipe.is_stale)
def test_makeAutoBuild_skips_for_unexpected_exceptions(self):
@@ -1295,19 +1628,22 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
logger = BufferLogger()
recipe = removeSecurityProxy(recipe)
+
# currently there is no expected way that `makeAutoBuilds` could fail
# so we fake it
def fake_requestAutoBuilds_with_side_effect(logger=None):
raise Exception("something unexpected went wrong")
+
recipe.requestAutoBuilds = fake_requestAutoBuilds_with_side_effect
build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
- logger=logger)
+ logger=logger
+ )
self.assertEqual([], build_requests)
self.assertEqual(
- ['ERROR something unexpected went wrong'],
- logger.getLogBuffer().splitlines()
+ ["ERROR something unexpected went wrong"],
+ logger.getLogBuffer().splitlines(),
)
def test_makeAutoBuild_skips_and_no_logger_enabled(self):
@@ -1316,8 +1652,10 @@ class TestCharmRecipeSet(TestCaseWithFactory):
# but we particularly test with no logger enabled.
recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
recipe = removeSecurityProxy(recipe)
+
def fake_requestAutoBuilds_with_side_effect(logger=None):
raise Exception("something unexpected went wrong")
+
recipe.requestAutoBuilds = fake_requestAutoBuilds_with_side_effect
build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds()
@@ -1329,49 +1667,67 @@ class TestCharmRecipeSet(TestCaseWithFactory):
# recently.
recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
self.factory.makeCharmRecipeBuildRequest(
- requester=recipe.owner, recipe=recipe)
+ requester=recipe.owner, recipe=recipe
+ )
logger = BufferLogger()
build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
- logger=logger)
+ logger=logger
+ )
self.assertEqual([], build_requests)
self.assertEqual([], logger.getLogBuffer().splitlines())
def test_makeAutoBuilds_skips_if_requested_recently_matching_channels(
- self):
+ self,
+ ):
# ICharmRecipeSet.makeAutoBuilds only considers recent build
# requests to match a recipe if they match its auto_build_channels.
recipe1 = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
recipe2 = self.factory.makeCharmRecipe(
- auto_build=True, auto_build_channels={"charmcraft": "edge"},
- is_stale=True)
+ auto_build=True,
+ auto_build_channels={"charmcraft": "edge"},
+ is_stale=True,
+ )
# Create some build requests with mismatched channels.
self.factory.makeCharmRecipeBuildRequest(
- recipe=recipe1, requester=recipe1.owner,
- channels={"charmcraft": "edge"})
+ recipe=recipe1,
+ requester=recipe1.owner,
+ channels={"charmcraft": "edge"},
+ )
self.factory.makeCharmRecipeBuildRequest(
- recipe=recipe2, requester=recipe2.owner,
- channels={"charmcraft": "stable"})
+ recipe=recipe2,
+ requester=recipe2.owner,
+ channels={"charmcraft": "stable"},
+ )
logger = BufferLogger()
build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
- logger=logger)
- self.assertThat(build_requests, MatchesSetwise(
- MatchesStructure(
- recipe=Equals(recipe1),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- requester=Equals(recipe1.owner), channels=Is(None)),
- MatchesStructure(
- recipe=Equals(recipe2),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- requester=Equals(recipe2.owner),
- channels=Equals({"charmcraft": "edge"}))))
+ logger=logger
+ )
+ self.assertThat(
+ build_requests,
+ MatchesSetwise(
+ MatchesStructure(
+ recipe=Equals(recipe1),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ requester=Equals(recipe1.owner),
+ channels=Is(None),
+ ),
+ MatchesStructure(
+ recipe=Equals(recipe2),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ requester=Equals(recipe2.owner),
+ channels=Equals({"charmcraft": "edge"}),
+ ),
+ ),
+ )
log_entries = logger.getLogBuffer().splitlines()
self.assertEqual(2, len(log_entries))
for recipe in recipe1, recipe2:
self.assertIn(
- "DEBUG Scheduling builds of charm recipe %s/%s/%s" % (
- recipe.owner.name, recipe.project.name, recipe.name),
- log_entries)
+ "DEBUG Scheduling builds of charm recipe %s/%s/%s"
+ % (recipe.owner.name, recipe.project.name, recipe.name),
+ log_entries,
+ )
self.assertFalse(recipe.is_stale)
# Mark the two recipes stale and try again. There are now matching
@@ -1381,7 +1737,8 @@ class TestCharmRecipeSet(TestCaseWithFactory):
IStore(recipe).flush()
logger = BufferLogger()
build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
- logger=logger)
+ logger=logger
+ )
self.assertEqual([], build_requests)
self.assertEqual([], logger.getLogBuffer().splitlines())
@@ -1396,15 +1753,21 @@ class TestCharmRecipeSet(TestCaseWithFactory):
recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
one_day_ago = datetime.now(pytz.UTC) - timedelta(days=1)
build_request = self.factory.makeCharmRecipeBuildRequest(
- recipe=recipe, requester=recipe.owner)
+ recipe=recipe, requester=recipe.owner
+ )
removeSecurityProxy(
- removeSecurityProxy(build_request)._job).job.date_created = (
- one_day_ago)
+ removeSecurityProxy(build_request)._job
+ ).job.date_created = one_day_ago
[build_request] = getUtility(ICharmRecipeSet).makeAutoBuilds()
- self.assertThat(build_request, MatchesStructure(
- recipe=Equals(recipe),
- status=Equals(CharmRecipeBuildRequestStatus.PENDING),
- requester=Equals(recipe.owner), channels=Is(None)))
+ self.assertThat(
+ build_request,
+ MatchesStructure(
+ recipe=Equals(recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ requester=Equals(recipe.owner),
+ channels=Is(None),
+ ),
+ )
def test_makeAutoBuilds_with_older_and_newer_build_requests(self):
# If builds of a recipe have been requested twice, and the most recent
@@ -1414,10 +1777,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
for timediff in timedelta(days=1), timedelta(minutes=30):
date_created = datetime.now(pytz.UTC) - timediff
build_request = self.factory.makeCharmRecipeBuildRequest(
- recipe=recipe, requester=recipe.owner)
+ recipe=recipe, requester=recipe.owner
+ )
removeSecurityProxy(
- removeSecurityProxy(build_request)._job).job.date_created = (
- date_created)
+ removeSecurityProxy(build_request)._job
+ ).job.date_created = date_created
self.assertEqual([], getUtility(ICharmRecipeSet).makeAutoBuilds())
def test_detachFromGitRepository(self):
@@ -1432,21 +1796,28 @@ class TestCharmRecipeSet(TestCaseWithFactory):
[ref] = self.factory.makeGitRefs(repository=repository)
paths.append(ref.path)
refs.append(ref)
- recipes.append(self.factory.makeCharmRecipe(
- git_ref=ref, date_created=ONE_DAY_AGO))
+ recipes.append(
+ self.factory.makeCharmRecipe(
+ git_ref=ref, date_created=ONE_DAY_AGO
+ )
+ )
getUtility(ICharmRecipeSet).detachFromGitRepository(repositories[0])
self.assertEqual(
[None, None, repositories[1], repositories[1]],
- [recipe.git_repository for recipe in recipes])
+ [recipe.git_repository for recipe in recipes],
+ )
self.assertEqual(
[None, None, paths[2], paths[3]],
- [recipe.git_path for recipe in recipes])
+ [recipe.git_path for recipe in recipes],
+ )
self.assertEqual(
[None, None, refs[2], refs[3]],
- [recipe.git_ref for recipe in recipes])
+ [recipe.git_ref for recipe in recipes],
+ )
for recipe in recipes[:2]:
self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW)
+ recipe, "date_last_modified", UTC_NOW
+ )
class TestCharmRecipeWebservice(TestCaseWithFactory):
@@ -1455,26 +1826,39 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({
- CHARM_RECIPE_ALLOW_CREATE: "on",
- CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
- CHARM_RECIPE_PRIVATE_FEATURE_FLAG: "on",
- }))
+ self.useFixture(
+ FeatureFixture(
+ {
+ CHARM_RECIPE_ALLOW_CREATE: "on",
+ CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+ CHARM_RECIPE_PRIVATE_FEATURE_FLAG: "on",
+ }
+ )
+ )
self.pushConfig("charms", charmhub_url="http://charmhub.example/")
self.pushConfig(
- "launchpad", candid_service_root="https://candid.test/")
+ "launchpad", candid_service_root="https://candid.test/"
+ )
self.person = self.factory.makePerson(displayname="Test Person")
self.webservice = webservice_for_person(
- self.person, permission=OAuthPermission.WRITE_PUBLIC)
+ self.person, permission=OAuthPermission.WRITE_PUBLIC
+ )
self.webservice.default_api_version = "devel"
login(ANONYMOUS)
def getURL(self, obj):
return self.webservice.getAbsoluteUrl(api_url(obj))
- def makeCharmRecipe(self, owner=None, project=None, name=None,
- git_ref=None, private=False, webservice=None,
- **kwargs):
+ def makeCharmRecipe(
+ self,
+ owner=None,
+ project=None,
+ name=None,
+ git_ref=None,
+ private=False,
+ webservice=None,
+ **kwargs
+ ):
if owner is None:
owner = self.person
if project is None:
@@ -1490,58 +1874,80 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
project_url = api_url(project)
git_ref_url = api_url(git_ref)
logout()
- information_type = (InformationType.PROPRIETARY if private else
- InformationType.PUBLIC)
+ information_type = (
+ InformationType.PROPRIETARY if private else InformationType.PUBLIC
+ )
response = webservice.named_post(
- "/+charm-recipes", "new", owner=owner_url, project=project_url,
- name=name, git_ref=git_ref_url,
- information_type=information_type.title, **kwargs)
+ "/+charm-recipes",
+ "new",
+ owner=owner_url,
+ project=project_url,
+ name=name,
+ git_ref=git_ref_url,
+ information_type=information_type.title,
+ **kwargs,
+ )
self.assertEqual(201, response.status)
return webservice.get(response.getHeader("Location")).jsonBody()
def getCollectionLinks(self, entry, member):
"""Return a list of self_link attributes of entries in a collection."""
collection = self.webservice.get(
- entry["%s_collection_link" % member]).jsonBody()
+ entry["%s_collection_link" % member]
+ ).jsonBody()
return [entry["self_link"] for entry in collection["entries"]]
def test_new_git(self):
# Charm recipe creation based on a Git branch works.
team = self.factory.makeTeam(
owner=self.person,
- membership_policy=TeamMembershipPolicy.RESTRICTED)
+ membership_policy=TeamMembershipPolicy.RESTRICTED,
+ )
project = self.factory.makeProduct(owner=team)
[ref] = self.factory.makeGitRefs()
recipe = self.makeCharmRecipe(
- owner=team, project=project, name="test-charm", git_ref=ref)
+ owner=team, project=project, name="test-charm", git_ref=ref
+ )
with person_logged_in(self.person):
- self.assertThat(recipe, ContainsDict({
- "registrant_link": Equals(self.getURL(self.person)),
- "owner_link": Equals(self.getURL(team)),
- "project_link": Equals(self.getURL(project)),
- "name": Equals("test-charm"),
- "git_ref_link": Equals(self.getURL(ref)),
- "build_path": Is(None),
- "require_virtualized": Is(True),
- }))
+ self.assertThat(
+ recipe,
+ ContainsDict(
+ {
+ "registrant_link": Equals(self.getURL(self.person)),
+ "owner_link": Equals(self.getURL(team)),
+ "project_link": Equals(self.getURL(project)),
+ "name": Equals("test-charm"),
+ "git_ref_link": Equals(self.getURL(ref)),
+ "build_path": Is(None),
+ "require_virtualized": Is(True),
+ }
+ ),
+ )
def test_new_store_options(self):
# The store-related options in CharmRecipe.new work.
store_name = self.factory.getUniqueUnicode()
recipe = self.makeCharmRecipe(
- store_upload=True, store_name=store_name, store_channels=["edge"])
+ store_upload=True, store_name=store_name, store_channels=["edge"]
+ )
with person_logged_in(self.person):
- self.assertThat(recipe, ContainsDict({
- "store_upload": Is(True),
- "store_name": Equals(store_name),
- "store_channels": Equals(["edge"]),
- }))
+ self.assertThat(
+ recipe,
+ ContainsDict(
+ {
+ "store_upload": Is(True),
+ "store_name": Equals(store_name),
+ "store_channels": Equals(["edge"]),
+ }
+ ),
+ )
def test_duplicate(self):
# An attempt to create a duplicate charm recipe fails.
team = self.factory.makeTeam(
owner=self.person,
- membership_policy=TeamMembershipPolicy.RESTRICTED)
+ membership_policy=TeamMembershipPolicy.RESTRICTED,
+ )
project = self.factory.makeProduct(owner=team)
name = self.factory.getUniqueUnicode()
[git_ref] = self.factory.makeGitRefs()
@@ -1549,22 +1955,34 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
project_url = api_url(project)
git_ref_url = api_url(git_ref)
self.makeCharmRecipe(
- owner=team, project=project, name=name, git_ref=git_ref)
+ owner=team, project=project, name=name, git_ref=git_ref
+ )
response = self.webservice.named_post(
- "/+charm-recipes", "new", owner=owner_url, project=project_url,
- name=name, git_ref=git_ref_url)
- self.assertThat(response, MatchesStructure.byEquality(
- status=400,
- body=(
- b"There is already a charm recipe with the same project, "
- b"owner, and name.")))
+ "/+charm-recipes",
+ "new",
+ owner=owner_url,
+ project=project_url,
+ name=name,
+ git_ref=git_ref_url,
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=400,
+ body=(
+ b"There is already a charm recipe with the same project, "
+ b"owner, and name."
+ ),
+ ),
+ )
def test_not_owner(self):
# If the registrant is not the owner or a member of the owner team,
# charm recipe creation fails.
other_person = self.factory.makePerson(displayname="Other Person")
other_team = self.factory.makeTeam(
- owner=other_person, displayname="Other Team")
+ owner=other_person, displayname="Other Team"
+ )
project = self.factory.makeProduct(owner=self.person)
[git_ref] = self.factory.makeGitRefs()
transaction.commit()
@@ -1574,59 +1992,97 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
git_ref_url = api_url(git_ref)
logout()
response = self.webservice.named_post(
- "/+charm-recipes", "new", owner=other_person_url,
- project=project_url, name="test-charm", git_ref=git_ref_url)
- self.assertThat(response, MatchesStructure.byEquality(
- status=401,
- body=(
- b"Test Person cannot create charm recipes owned by "
- b"Other Person.")))
+ "/+charm-recipes",
+ "new",
+ owner=other_person_url,
+ project=project_url,
+ name="test-charm",
+ git_ref=git_ref_url,
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=401,
+ body=(
+ b"Test Person cannot create charm recipes owned by "
+ b"Other Person."
+ ),
+ ),
+ )
response = self.webservice.named_post(
- "/+charm-recipes", "new", owner=other_team_url,
- project=project_url, name="test-charm", git_ref=git_ref_url)
- self.assertThat(response, MatchesStructure.byEquality(
- status=401,
- body=b"Test Person is not a member of Other Team."))
+ "/+charm-recipes",
+ "new",
+ owner=other_team_url,
+ project=project_url,
+ name="test-charm",
+ git_ref=git_ref_url,
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=401, body=b"Test Person is not a member of Other Team."
+ ),
+ )
def test_cannot_set_private_components_of_public_recipe(self):
# If a charm recipe is public, then trying to change its owner or
# git_ref components to be private fails.
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, owner=self.person,
- git_ref=self.factory.makeGitRefs()[0])
+ registrant=self.person,
+ owner=self.person,
+ git_ref=self.factory.makeGitRefs()[0],
+ )
private_team = self.factory.makeTeam(
- owner=self.person, visibility=PersonVisibility.PRIVATE)
+ owner=self.person, visibility=PersonVisibility.PRIVATE
+ )
[private_ref] = self.factory.makeGitRefs(
- owner=self.person,
- information_type=InformationType.PRIVATESECURITY)
+ owner=self.person, information_type=InformationType.PRIVATESECURITY
+ )
recipe_url = api_url(recipe)
with person_logged_in(self.person):
private_team_url = api_url(private_team)
private_ref_url = api_url(private_ref)
logout()
private_webservice = webservice_for_person(
- self.person, permission=OAuthPermission.WRITE_PRIVATE)
+ self.person, permission=OAuthPermission.WRITE_PRIVATE
+ )
private_webservice.default_api_version = "devel"
response = private_webservice.patch(
- recipe_url, "application/json",
- json.dumps({"owner_link": private_team_url}))
- self.assertThat(response, MatchesStructure.byEquality(
- status=400,
- body=b"A public charm recipe cannot have a private owner."))
+ recipe_url,
+ "application/json",
+ json.dumps({"owner_link": private_team_url}),
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=400,
+ body=b"A public charm recipe cannot have a private owner.",
+ ),
+ )
response = private_webservice.patch(
- recipe_url, "application/json",
- json.dumps({"git_ref_link": private_ref_url}))
- self.assertThat(response, MatchesStructure.byEquality(
- status=400,
- body=b"A public charm recipe cannot have a private repository."))
+ recipe_url,
+ "application/json",
+ json.dumps({"git_ref_link": private_ref_url}),
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=400,
+ body=(
+ b"A public charm recipe cannot have a private repository."
+ ),
+ ),
+ )
def test_is_stale(self):
# is_stale is exported and is read-only.
recipe = self.makeCharmRecipe()
self.assertTrue(recipe["is_stale"])
response = self.webservice.patch(
- recipe["self_link"], "application/json",
- json.dumps({"is_stale": False}))
+ recipe["self_link"],
+ "application/json",
+ json.dumps({"is_stale": False}),
+ )
self.assertEqual(400, response.status)
def test_getByName(self):
@@ -1638,8 +2094,12 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
owner_url = api_url(self.person)
project_url = api_url(project)
response = self.webservice.named_get(
- "/+charm-recipes", "getByName",
- owner=owner_url, project=project_url, name=name)
+ "/+charm-recipes",
+ "getByName",
+ owner=owner_url,
+ project=project_url,
+ name=name,
+ )
self.assertEqual(200, response.status)
self.assertEqual(recipe, response.jsonBody())
@@ -1652,65 +2112,90 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
owner_url = api_url(self.person)
project_url = api_url(project)
response = self.webservice.named_get(
- "/+charm-recipes", "getByName",
- owner=owner_url, project=project_url, name="nonexistent")
- self.assertThat(response, MatchesStructure.byEquality(
- status=404,
- body=(
- b"No such charm recipe with this owner and project: "
- b"'nonexistent'.")))
+ "/+charm-recipes",
+ "getByName",
+ owner=owner_url,
+ project=project_url,
+ name="nonexistent",
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=404,
+ body=(
+ b"No such charm recipe with this owner and project: "
+ b"'nonexistent'."
+ ),
+ ),
+ )
@responses.activate
def assertBeginsAuthorization(self, recipe, **kwargs):
recipe_url = api_url(recipe)
root_macaroon = Macaroon(version=2)
root_macaroon.add_third_party_caveat(
- "https://candid.test/", "", "identity")
+ "https://candid.test/", "", "identity"
+ )
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
logout()
responses.add(
- "POST", "http://charmhub.example/v1/tokens",
- json={"macaroon": root_macaroon_raw})
+ "POST",
+ "http://charmhub.example/v1/tokens",
+ json={"macaroon": root_macaroon_raw},
+ )
response = self.webservice.named_post(
- recipe_url, "beginAuthorization", **kwargs)
+ recipe_url, "beginAuthorization", **kwargs
+ )
[call] = responses.calls
- self.assertThat(call.request, MatchesStructure.byEquality(
- url="http://charmhub.example/v1/tokens", method="POST"))
+ self.assertThat(
+ call.request,
+ MatchesStructure.byEquality(
+ url="http://charmhub.example/v1/tokens", method="POST"
+ ),
+ )
with person_logged_in(self.person):
expected_body = {
"description": (
- "{} for launchpad.test".format(recipe.store_name)),
+ "{} for launchpad.test".format(recipe.store_name)
+ ),
"packages": [{"type": "charm", "name": recipe.store_name}],
"permissions": [
"package-manage-releases",
"package-manage-revisions",
"package-view-revisions",
- ],
- }
+ ],
+ }
self.assertEqual(
- expected_body, json.loads(call.request.body.decode("UTF-8")))
+ expected_body, json.loads(call.request.body.decode("UTF-8"))
+ )
self.assertEqual({"root": root_macaroon_raw}, recipe.store_secrets)
return response, root_macaroon_raw
def test_beginAuthorization(self):
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, store_upload=True,
- store_name=self.factory.getUniqueUnicode())
+ registrant=self.person,
+ store_upload=True,
+ store_name=self.factory.getUniqueUnicode(),
+ )
response, root_macaroon_raw = self.assertBeginsAuthorization(recipe)
self.assertEqual(root_macaroon_raw, response.jsonBody())
def test_beginAuthorization_unauthorized(self):
# A user without edit access cannot authorize charm recipe uploads.
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, store_upload=True,
- store_name=self.factory.getUniqueUnicode())
+ registrant=self.person,
+ store_upload=True,
+ store_name=self.factory.getUniqueUnicode(),
+ )
recipe_url = api_url(recipe)
other_person = self.factory.makePerson()
other_webservice = webservice_for_person(
- other_person, permission=OAuthPermission.WRITE_PUBLIC)
+ other_person, permission=OAuthPermission.WRITE_PUBLIC
+ )
other_webservice.default_api_version = "devel"
response = other_webservice.named_post(
- recipe_url, "beginAuthorization")
+ recipe_url, "beginAuthorization"
+ )
self.assertEqual(401, response.status)
@responses.activate
@@ -1719,134 +2204,197 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
self.pushConfig(
"charms",
charmhub_secrets_public_key=base64.b64encode(
- bytes(private_key.public_key)).decode())
+ bytes(private_key.public_key)
+ ).decode(),
+ )
root_macaroon = Macaroon(version=2)
root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
unbound_discharge_macaroon = Macaroon(version=2)
unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
- JsonSerializer())
+ JsonSerializer()
+ )
discharge_macaroon_raw = root_macaroon.prepare_for_request(
- unbound_discharge_macaroon).serialize(JsonSerializer())
+ unbound_discharge_macaroon
+ ).serialize(JsonSerializer())
exchanged_macaroon = Macaroon(version=2)
exchanged_macaroon_raw = exchanged_macaroon.serialize(JsonSerializer())
responses.add(
- "POST", "http://charmhub.example/v1/tokens/exchange",
- json={"macaroon": exchanged_macaroon_raw})
+ "POST",
+ "http://charmhub.example/v1/tokens/exchange",
+ json={"macaroon": exchanged_macaroon_raw},
+ )
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, store_upload=True,
+ registrant=self.person,
+ store_upload=True,
store_name=self.factory.getUniqueUnicode(),
- store_secrets={"root": root_macaroon_raw})
+ store_secrets={"root": root_macaroon_raw},
+ )
recipe_url = api_url(recipe)
logout()
response = self.webservice.named_post(
- recipe_url, "completeAuthorization",
- discharge_macaroon=json.dumps(unbound_discharge_macaroon_raw))
+ recipe_url,
+ "completeAuthorization",
+ discharge_macaroon=json.dumps(unbound_discharge_macaroon_raw),
+ )
self.assertEqual(200, response.status)
self.pushConfig(
"charms",
charmhub_secrets_private_key=base64.b64encode(
- bytes(private_key)).decode())
+ bytes(private_key)
+ ).decode(),
+ )
container = getUtility(IEncryptedContainer, "charmhub-secrets")
with person_logged_in(self.person):
- self.assertThat(recipe.store_secrets, MatchesDict({
- "exchanged_encrypted": AfterPreprocessing(
- lambda data: container.decrypt(data).decode(),
- Equals(exchanged_macaroon_raw)),
- }))
- self.assertThat(responses.calls, MatchesListwise([
- MatchesStructure(
- request=MatchesStructure(
- url=Equals("http://charmhub.example/v1/tokens/exchange"),
- method=Equals("POST"),
- headers=ContainsDict({
- "Macaroons": AfterPreprocessing(
- lambda v: json.loads(
- base64.b64decode(v.encode()).decode()),
- Equals([
- json.loads(m) for m in (
+ self.assertThat(
+ recipe.store_secrets,
+ MatchesDict(
+ {
+ "exchanged_encrypted": AfterPreprocessing(
+ lambda data: container.decrypt(data).decode(),
+ Equals(exchanged_macaroon_raw),
+ ),
+ }
+ ),
+ )
+ exchange_matcher = MatchesStructure(
+ url=Equals("http://charmhub.example/v1/tokens/exchange"),
+ method=Equals("POST"),
+ headers=ContainsDict(
+ {
+ "Macaroons": AfterPreprocessing(
+ lambda v: json.loads(
+ base64.b64decode(v.encode()).decode()
+ ),
+ Equals(
+ [
+ json.loads(m)
+ for m in (
root_macaroon_raw,
- discharge_macaroon_raw)])),
- }),
- body=AfterPreprocessing(
- lambda b: json.loads(b.decode()),
- Equals({})))),
- ]))
+ discharge_macaroon_raw,
+ )
+ ]
+ ),
+ ),
+ }
+ ),
+ body=AfterPreprocessing(
+ lambda b: json.loads(b.decode()), Equals({})
+ ),
+ )
+ self.assertThat(
+ responses.calls,
+ MatchesListwise([MatchesStructure(request=exchange_matcher)]),
+ )
def test_completeAuthorization_without_beginAuthorization(self):
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, store_upload=True,
- store_name=self.factory.getUniqueUnicode())
+ registrant=self.person,
+ store_upload=True,
+ store_name=self.factory.getUniqueUnicode(),
+ )
recipe_url = api_url(recipe)
logout()
discharge_macaroon = Macaroon(version=2)
response = self.webservice.named_post(
- recipe_url, "completeAuthorization",
+ recipe_url,
+ "completeAuthorization",
discharge_macaroon=json.dumps(
- discharge_macaroon.serialize(JsonSerializer())))
- self.assertThat(response, MatchesStructure.byEquality(
- status=400,
- body=(
- b"beginAuthorization must be called before "
- b"completeAuthorization.")))
+ discharge_macaroon.serialize(JsonSerializer())
+ ),
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=400,
+ body=(
+ b"beginAuthorization must be called before "
+ b"completeAuthorization."
+ ),
+ ),
+ )
def test_completeAuthorization_unauthorized(self):
root_macaroon = Macaroon(version=2)
discharge_macaroon = Macaroon(version=2)
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, store_upload=True,
+ registrant=self.person,
+ store_upload=True,
store_name=self.factory.getUniqueUnicode(),
- store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+ store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+ )
recipe_url = api_url(recipe)
other_person = self.factory.makePerson()
other_webservice = webservice_for_person(
- other_person, permission=OAuthPermission.WRITE_PUBLIC)
+ other_person, permission=OAuthPermission.WRITE_PUBLIC
+ )
other_webservice.default_api_version = "devel"
response = other_webservice.named_post(
- recipe_url, "completeAuthorization",
+ recipe_url,
+ "completeAuthorization",
discharge_macaroon=json.dumps(
- discharge_macaroon.serialize(JsonSerializer())))
+ discharge_macaroon.serialize(JsonSerializer())
+ ),
+ )
self.assertEqual(401, response.status)
def test_completeAuthorization_malformed_discharge_macaroon(self):
root_macaroon = Macaroon(version=2)
recipe = self.factory.makeCharmRecipe(
- registrant=self.person, store_upload=True,
+ registrant=self.person,
+ store_upload=True,
store_name=self.factory.getUniqueUnicode(),
- store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+ store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+ )
recipe_url = api_url(recipe)
logout()
response = self.webservice.named_post(
- recipe_url, "completeAuthorization",
- discharge_macaroon="nonsense")
- self.assertThat(response, MatchesStructure.byEquality(
- status=400, body=b"Discharge macaroon is invalid."))
-
- def makeBuildableDistroArchSeries(self, distroseries=None,
- architecturetag=None, processor=None,
- supports_virtualized=True,
- supports_nonvirtualized=True, **kwargs):
+ recipe_url, "completeAuthorization", discharge_macaroon="nonsense"
+ )
+ self.assertThat(
+ response,
+ MatchesStructure.byEquality(
+ status=400, body=b"Discharge macaroon is invalid."
+ ),
+ )
+
+ def makeBuildableDistroArchSeries(
+ self,
+ distroseries=None,
+ architecturetag=None,
+ processor=None,
+ supports_virtualized=True,
+ supports_nonvirtualized=True,
+ **kwargs
+ ):
if architecturetag is None:
architecturetag = self.factory.getUniqueUnicode("arch")
if processor is None:
try:
processor = getUtility(IProcessorSet).getByName(
- architecturetag)
+ architecturetag
+ )
except ProcessorNotFound:
processor = self.factory.makeProcessor(
name=architecturetag,
supports_virtualized=supports_virtualized,
- supports_nonvirtualized=supports_nonvirtualized)
+ supports_nonvirtualized=supports_nonvirtualized,
+ )
das = self.factory.makeDistroArchSeries(
- distroseries=distroseries, architecturetag=architecturetag,
- processor=processor, **kwargs)
+ distroseries=distroseries,
+ architecturetag=architecturetag,
+ processor=processor,
+ **kwargs,
+ )
# Add both a chroot and a LXD image to test that
# getAllowedArchitectures doesn't get confused by multiple
# PocketChroot rows for a single DistroArchSeries.
fake_chroot = self.factory.makeLibraryFileAlias(
- filename="fake_chroot.tar.gz", db_only=True)
+ filename="fake_chroot.tar.gz", db_only=True
+ )
das.addOrUpdateChroot(fake_chroot)
fake_lxd = self.factory.makeLibraryFileAlias(
- filename="fake_lxd.tar.gz", db_only=True)
+ filename="fake_lxd.tar.gz", db_only=True
+ )
das.addOrUpdateChroot(fake_lxd, image_type=BuildBaseImageType.LXD)
return das
@@ -1856,42 +2404,58 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
# the status of the asynchronous job.
distroseries = self.factory.makeDistroSeries(
distribution=getUtility(ILaunchpadCelebrities).ubuntu,
- registrant=self.person)
+ registrant=self.person,
+ )
processors = [
self.factory.makeProcessor(supports_virtualized=True)
- for _ in range(3)]
+ for _ in range(3)
+ ]
for processor in processors:
self.makeBuildableDistroArchSeries(
- distroseries=distroseries, architecturetag=processor.name,
- processor=processor, owner=self.person)
+ distroseries=distroseries,
+ architecturetag=processor.name,
+ processor=processor,
+ owner=self.person,
+ )
[git_ref] = self.factory.makeGitRefs()
recipe = self.makeCharmRecipe(git_ref=git_ref)
now = get_transaction_timestamp(IStore(distroseries))
response = self.webservice.named_post(
- recipe["self_link"], "requestBuilds",
- channels={"charmcraft": "edge"})
+ recipe["self_link"],
+ "requestBuilds",
+ channels={"charmcraft": "edge"},
+ )
self.assertEqual(201, response.status)
build_request_url = response.getHeader("Location")
build_request = self.webservice.get(build_request_url).jsonBody()
- self.assertThat(build_request, ContainsDict({
- "date_requested": AfterPreprocessing(
- iso8601.parse_date, GreaterThan(now)),
- "date_finished": Is(None),
- "recipe_link": Equals(recipe["self_link"]),
- "status": Equals("Pending"),
- "error_message": Is(None),
- "builds_collection_link": Equals(build_request_url + "/builds"),
- }))
+ self.assertThat(
+ build_request,
+ ContainsDict(
+ {
+ "date_requested": AfterPreprocessing(
+ iso8601.parse_date, GreaterThan(now)
+ ),
+ "date_finished": Is(None),
+ "recipe_link": Equals(recipe["self_link"]),
+ "status": Equals("Pending"),
+ "error_message": Is(None),
+ "builds_collection_link": Equals(
+ build_request_url + "/builds"
+ ),
+ }
+ ),
+ )
self.assertEqual([], self.getCollectionLinks(build_request, "builds"))
with person_logged_in(self.person):
charmcraft_yaml = "bases:\n"
for processor in processors:
charmcraft_yaml += (
- ' - build-on:\n'
- ' - name: ubuntu\n'
+ " - build-on:\n"
+ " - name: ubuntu\n"
' channel: "%s"\n'
- ' architectures: [%s]\n' %
- (distroseries.version, processor.name))
+ " architectures: [%s]\n"
+ % (distroseries.version, processor.name)
+ )
self.useFixture(GitHostingFixture(blob=charmcraft_yaml))
[job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
with dbuser(config.ICharmRecipeRequestBuildsJobSource.dbuser):
@@ -1899,30 +2463,50 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
date_requested = iso8601.parse_date(build_request["date_requested"])
now = get_transaction_timestamp(IStore(distroseries))
build_request = self.webservice.get(
- build_request["self_link"]).jsonBody()
- self.assertThat(build_request, ContainsDict({
- "date_requested": AfterPreprocessing(
- iso8601.parse_date, Equals(date_requested)),
- "date_finished": AfterPreprocessing(
- iso8601.parse_date,
- MatchesAll(GreaterThan(date_requested), LessThan(now))),
- "recipe_link": Equals(recipe["self_link"]),
- "status": Equals("Completed"),
- "error_message": Is(None),
- "builds_collection_link": Equals(build_request_url + "/builds"),
- }))
+ build_request["self_link"]
+ ).jsonBody()
+ self.assertThat(
+ build_request,
+ ContainsDict(
+ {
+ "date_requested": AfterPreprocessing(
+ iso8601.parse_date, Equals(date_requested)
+ ),
+ "date_finished": AfterPreprocessing(
+ iso8601.parse_date,
+ MatchesAll(GreaterThan(date_requested), LessThan(now)),
+ ),
+ "recipe_link": Equals(recipe["self_link"]),
+ "status": Equals("Completed"),
+ "error_message": Is(None),
+ "builds_collection_link": Equals(
+ build_request_url + "/builds"
+ ),
+ }
+ ),
+ )
builds = self.webservice.get(
- build_request["builds_collection_link"]).jsonBody()["entries"]
+ build_request["builds_collection_link"]
+ ).jsonBody()["entries"]
with person_logged_in(self.person):
- self.assertThat(builds, MatchesSetwise(*(
- ContainsDict({
- "recipe_link": Equals(recipe["self_link"]),
- "archive_link": Equals(
- self.getURL(distroseries.main_archive)),
- "arch_tag": Equals(processor.name),
- "channels": Equals({"charmcraft": "edge"}),
- })
- for processor in processors)))
+ self.assertThat(
+ builds,
+ MatchesSetwise(
+ *(
+ ContainsDict(
+ {
+ "recipe_link": Equals(recipe["self_link"]),
+ "archive_link": Equals(
+ self.getURL(distroseries.main_archive)
+ ),
+ "arch_tag": Equals(processor.name),
+ "channels": Equals({"charmcraft": "edge"}),
+ }
+ )
+ for processor in processors
+ )
+ ),
+ )
def test_requestBuilds_failure(self):
# If the asynchronous build request job fails, this is reflected in
@@ -1931,41 +2515,61 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
recipe = self.makeCharmRecipe(git_ref=git_ref)
now = get_transaction_timestamp(IStore(git_ref))
response = self.webservice.named_post(
- recipe["self_link"], "requestBuilds")
+ recipe["self_link"], "requestBuilds"
+ )
self.assertEqual(201, response.status)
build_request_url = response.getHeader("Location")
build_request = self.webservice.get(build_request_url).jsonBody()
- self.assertThat(build_request, ContainsDict({
- "date_requested": AfterPreprocessing(
- iso8601.parse_date, GreaterThan(now)),
- "date_finished": Is(None),
- "recipe_link": Equals(recipe["self_link"]),
- "status": Equals("Pending"),
- "error_message": Is(None),
- "builds_collection_link": Equals(build_request_url + "/builds"),
- }))
+ self.assertThat(
+ build_request,
+ ContainsDict(
+ {
+ "date_requested": AfterPreprocessing(
+ iso8601.parse_date, GreaterThan(now)
+ ),
+ "date_finished": Is(None),
+ "recipe_link": Equals(recipe["self_link"]),
+ "status": Equals("Pending"),
+ "error_message": Is(None),
+ "builds_collection_link": Equals(
+ build_request_url + "/builds"
+ ),
+ }
+ ),
+ )
self.assertEqual([], self.getCollectionLinks(build_request, "builds"))
with person_logged_in(self.person):
self.useFixture(GitHostingFixture()).getBlob.failure = Exception(
- "Something went wrong")
+ "Something went wrong"
+ )
[job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
with dbuser(config.ICharmRecipeRequestBuildsJobSource.dbuser):
JobRunner([job]).runAll()
date_requested = iso8601.parse_date(build_request["date_requested"])
now = get_transaction_timestamp(IStore(git_ref))
build_request = self.webservice.get(
- build_request["self_link"]).jsonBody()
- self.assertThat(build_request, ContainsDict({
- "date_requested": AfterPreprocessing(
- iso8601.parse_date, Equals(date_requested)),
- "date_finished": AfterPreprocessing(
- iso8601.parse_date,
- MatchesAll(GreaterThan(date_requested), LessThan(now))),
- "recipe_link": Equals(recipe["self_link"]),
- "status": Equals("Failed"),
- "error_message": Equals("Something went wrong"),
- "builds_collection_link": Equals(build_request_url + "/builds"),
- }))
+ build_request["self_link"]
+ ).jsonBody()
+ self.assertThat(
+ build_request,
+ ContainsDict(
+ {
+ "date_requested": AfterPreprocessing(
+ iso8601.parse_date, Equals(date_requested)
+ ),
+ "date_finished": AfterPreprocessing(
+ iso8601.parse_date,
+ MatchesAll(GreaterThan(date_requested), LessThan(now)),
+ ),
+ "recipe_link": Equals(recipe["self_link"]),
+ "status": Equals("Failed"),
+ "error_message": Equals("Something went wrong"),
+ "builds_collection_link": Equals(
+ build_request_url + "/builds"
+ ),
+ }
+