Bug 1784232 - Bump taskgraph to v3.2.0 r=ahal

Differential Revision: https://phabricator.services.mozilla.com/D155978
This commit is contained in:
Johan Lorenzo 2022-09-20 09:11:45 +00:00
parent 35f28283e0
commit 0e57af4969
63 changed files with 2402 additions and 550 deletions

View file

@ -37,7 +37,7 @@
# #
# { # {
# tasks_for: 'action', # tasks_for: 'action',
# push: {owner, pushlog_id, revision}, # push: {owner, pushlog_id, revision, base_revision},
# repository: {url, project, level}, # repository: {url, project, level},
# input, # input,
# taskId, // targetted taskId # taskId, // targetted taskId
@ -196,6 +196,7 @@ tasks:
# to `mach taskgraph decision` are all on the command line. # to `mach taskgraph decision` are all on the command line.
$merge: $merge:
- GECKO_BASE_REPOSITORY: 'https://hg.mozilla.org/mozilla-unified' - GECKO_BASE_REPOSITORY: 'https://hg.mozilla.org/mozilla-unified'
GECKO_BASE_REV: '${push.base_revision}'
GECKO_HEAD_REPOSITORY: '${repoUrl}' GECKO_HEAD_REPOSITORY: '${repoUrl}'
GECKO_HEAD_REF: '${push.revision}' GECKO_HEAD_REF: '${push.revision}'
GECKO_HEAD_REV: '${push.revision}' GECKO_HEAD_REV: '${push.revision}'
@ -253,6 +254,7 @@ tasks:
--tasks-for='${tasks_for}' --tasks-for='${tasks_for}'
--repository-type=hg --repository-type=hg
--base-repository="$GECKO_BASE_REPOSITORY" --base-repository="$GECKO_BASE_REPOSITORY"
--base-rev="$GECKO_BASE_REV"
--head-repository="$GECKO_HEAD_REPOSITORY" --head-repository="$GECKO_HEAD_REPOSITORY"
--head-ref="$GECKO_HEAD_REF" --head-ref="$GECKO_HEAD_REF"
--head-rev="$GECKO_HEAD_REV" --head-rev="$GECKO_HEAD_REV"

View file

@ -75,6 +75,7 @@ vendored:third_party/python/ecdsa
vendored:third_party/python/esprima vendored:third_party/python/esprima
vendored:third_party/python/fluent.migrate vendored:third_party/python/fluent.migrate
vendored:third_party/python/fluent.syntax vendored:third_party/python/fluent.syntax
vendored:third_party/python/giturlparse
vendored:third_party/python/gyp/pylib vendored:third_party/python/gyp/pylib
vendored:third_party/python/idna vendored:third_party/python/idna
vendored:third_party/python/idna-ssl vendored:third_party/python/idna-ssl
@ -87,6 +88,7 @@ vendored:third_party/python/jsonschema
vendored:third_party/python/looseversion vendored:third_party/python/looseversion
vendored:third_party/python/MarkupSafe/src vendored:third_party/python/MarkupSafe/src
vendored:third_party/python/mohawk vendored:third_party/python/mohawk
vendored:third_party/python/mozilla_repo_urls
vendored:third_party/python/mozilla_version vendored:third_party/python/mozilla_version
vendored:third_party/python/multidict vendored:third_party/python/multidict
vendored:third_party/python/packaging vendored:third_party/python/packaging

View file

@ -36,9 +36,15 @@ Push Information
``base_repository`` in cases where ``base_repository`` is likely to be cached ``base_repository`` in cases where ``base_repository`` is likely to be cached
and only a few additional commits are needed from ``head_repository``. and only a few additional commits are needed from ``head_repository``.
``base_rev``
The previous revision before ``head_rev`` got merged into. This can be a short revision string.
``head_rev`` ``head_rev``
The revision to check out; this can be a short revision string The revision to check out; this can be a short revision string
``base_ref``
Reference where ``head_rev`` got merged into. It is usually a branch or a tag.
``head_ref`` ``head_ref``
For Mercurial repositories, this is the same as ``head_rev``. For For Mercurial repositories, this is the same as ``head_rev``. For
git repositories, which do not allow pulling explicit revisions, this gives git repositories, which do not allow pulling explicit revisions, this gives

View file

@ -183,10 +183,14 @@ def register_callback_action(
revision = parameters[ revision = parameters[
"{}head_rev".format(graph_config["project-repo-param-prefix"]) "{}head_rev".format(graph_config["project-repo-param-prefix"])
] ]
base_revision = parameters[
"{}base_rev".format(graph_config["project-repo-param-prefix"])
]
push = { push = {
"owner": "mozilla-taskcluster-maintenance@mozilla.com", "owner": "mozilla-taskcluster-maintenance@mozilla.com",
"pushlog_id": parameters["pushlog_id"], "pushlog_id": parameters["pushlog_id"],
"revision": revision, "revision": revision,
"base_revision": base_revision,
} }
match = re.match( match = re.match(

View file

@ -15,11 +15,19 @@ import yaml
from redo import retry from redo import retry
from taskgraph import create from taskgraph import create
from taskgraph.create import create_tasks from taskgraph.create import create_tasks
from taskgraph.decision import (
# TODO: Let standalone taskgraph generate parameters instead
# of calling internals
_determine_more_accurate_base_ref,
_determine_more_accurate_base_rev,
_get_env_prefix,
)
from taskgraph.parameters import Parameters from taskgraph.parameters import Parameters
from taskgraph.taskgraph import TaskGraph from taskgraph.taskgraph import TaskGraph
from taskgraph.util.python_path import find_object from taskgraph.util.python_path import find_object
from taskgraph.util.schema import Schema, validate_schema from taskgraph.util.schema import Schema, validate_schema
from taskgraph.util.taskcluster import get_artifact from taskgraph.util.taskcluster import get_artifact
from taskgraph.util.vcs import get_repository
from taskgraph.util.yaml import load_yaml from taskgraph.util.yaml import load_yaml
from voluptuous import Any, Optional, Required from voluptuous import Any, Optional, Required
@ -283,6 +291,8 @@ def get_decision_parameters(graph_config, options):
n: options[n] n: options[n]
for n in [ for n in [
"base_repository", "base_repository",
"base_ref",
"base_rev",
"head_repository", "head_repository",
"head_rev", "head_rev",
"head_ref", "head_ref",
@ -310,6 +320,23 @@ def get_decision_parameters(graph_config, options):
commit_message = get_hg_commit_message(os.path.join(GECKO, product_dir)) commit_message = get_hg_commit_message(os.path.join(GECKO, product_dir))
repo_path = os.getcwd()
repo = get_repository(repo_path)
parameters["base_ref"] = _determine_more_accurate_base_ref(
repo,
candidate_base_ref=options.get("base_ref"),
head_ref=options.get("head_ref"),
base_rev=options.get("base_rev"),
)
parameters["base_rev"] = _determine_more_accurate_base_rev(
repo,
base_ref=parameters["base_ref"],
candidate_base_rev=options.get("base_rev"),
head_rev=options.get("head_rev"),
env_prefix=_get_env_prefix(graph_config),
)
# Define default filter list, as most configurations shouldn't need # Define default filter list, as most configurations shouldn't need
# custom filters. # custom filters.
parameters["filters"] = [ parameters["filters"] = [

View file

@ -595,6 +595,15 @@ def image_digest(args):
help='Type of repository, either "hg" or "git"', help='Type of repository, either "hg" or "git"',
) )
@argument("--base-repository", required=True, help='URL for "base" repository to clone') @argument("--base-repository", required=True, help='URL for "base" repository to clone')
@argument(
"--base-ref", default="", help='Reference of the revision in the "base" repository'
)
@argument(
"--base-rev",
default="",
help="Taskgraph decides what to do based on the revision range between "
"`--base-rev` and `--head-rev`. Value is determined automatically if not provided",
)
@argument( @argument(
"--head-repository", "--head-repository",
required=True, required=True,

View file

@ -75,8 +75,12 @@ class TestGetDecisionParameters(unittest.TestCase):
} }
@patch("gecko_taskgraph.decision.get_hg_revision_branch") @patch("gecko_taskgraph.decision.get_hg_revision_branch")
def test_simple_options(self, mock_get_hg_revision_branch): @patch("gecko_taskgraph.decision._determine_more_accurate_base_rev")
def test_simple_options(
self, mock_determine_more_accurate_base_rev, mock_get_hg_revision_branch
):
mock_get_hg_revision_branch.return_value = "default" mock_get_hg_revision_branch.return_value = "default"
mock_determine_more_accurate_base_rev.return_value = "baserev"
with MockedOpen({self.ttc_file: None}): with MockedOpen({self.ttc_file: None}):
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options) params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options)
self.assertEqual(params["pushlog_id"], "143") self.assertEqual(params["pushlog_id"], "143")
@ -88,8 +92,12 @@ class TestGetDecisionParameters(unittest.TestCase):
self.assertEqual(params["try_task_config"], {}) self.assertEqual(params["try_task_config"], {})
@patch("gecko_taskgraph.decision.get_hg_revision_branch") @patch("gecko_taskgraph.decision.get_hg_revision_branch")
def test_no_email_owner(self, mock_get_hg_revision_branch): @patch("gecko_taskgraph.decision._determine_more_accurate_base_rev")
def test_no_email_owner(
self, mock_determine_more_accurate_base_rev, mock_get_hg_revision_branch
):
mock_get_hg_revision_branch.return_value = "default" mock_get_hg_revision_branch.return_value = "default"
mock_determine_more_accurate_base_rev.return_value = "baserev"
self.options["owner"] = "ffxbld" self.options["owner"] = "ffxbld"
with MockedOpen({self.ttc_file: None}): with MockedOpen({self.ttc_file: None}):
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options) params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options)
@ -97,9 +105,16 @@ class TestGetDecisionParameters(unittest.TestCase):
@patch("gecko_taskgraph.decision.get_hg_revision_branch") @patch("gecko_taskgraph.decision.get_hg_revision_branch")
@patch("gecko_taskgraph.decision.get_hg_commit_message") @patch("gecko_taskgraph.decision.get_hg_commit_message")
def test_try_options(self, mock_get_hg_commit_message, mock_get_hg_revision_branch): @patch("gecko_taskgraph.decision._determine_more_accurate_base_rev")
def test_try_options(
self,
mock_determine_more_accurate_base_rev,
mock_get_hg_commit_message,
mock_get_hg_revision_branch,
):
mock_get_hg_commit_message.return_value = "try: -b do -t all --artifact" mock_get_hg_commit_message.return_value = "try: -b do -t all --artifact"
mock_get_hg_revision_branch.return_value = "default" mock_get_hg_revision_branch.return_value = "default"
mock_determine_more_accurate_base_rev.return_value = "baserev"
self.options["project"] = "try" self.options["project"] = "try"
with MockedOpen({self.ttc_file: None}): with MockedOpen({self.ttc_file: None}):
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options) params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options)
@ -117,11 +132,16 @@ class TestGetDecisionParameters(unittest.TestCase):
@patch("gecko_taskgraph.decision.get_hg_revision_branch") @patch("gecko_taskgraph.decision.get_hg_revision_branch")
@patch("gecko_taskgraph.decision.get_hg_commit_message") @patch("gecko_taskgraph.decision.get_hg_commit_message")
@patch("gecko_taskgraph.decision._determine_more_accurate_base_rev")
def test_try_task_config( def test_try_task_config(
self, mock_get_hg_commit_message, mock_get_hg_revision_branch self,
mock_get_hg_commit_message,
mock_get_hg_revision_branch,
mock_determine_more_accurate_base_rev,
): ):
mock_get_hg_commit_message.return_value = "Fuzzy query=foo" mock_get_hg_commit_message.return_value = "Fuzzy query=foo"
mock_get_hg_revision_branch.return_value = "default" mock_get_hg_revision_branch.return_value = "default"
mock_determine_more_accurate_base_rev.return_value = "baserev"
ttc = {"tasks": ["a", "b"]} ttc = {"tasks": ["a", "b"]}
self.options["project"] = "try" self.options["project"] = "try"
with MockedOpen({self.ttc_file: json.dumps(ttc)}): with MockedOpen({self.ttc_file: json.dumps(ttc)}):

View file

@ -24,6 +24,7 @@ class TestTaskclusterYml(unittest.TestCase):
"tasks_for": "hg-push", "tasks_for": "hg-push",
"push": { "push": {
"revision": "e8d2d9aff5026ef1f1777b781b47fdcbdb9d8f20", "revision": "e8d2d9aff5026ef1f1777b781b47fdcbdb9d8f20",
"base_revision": "e8aebe488b2f2e567940577de25013d00e818f7c",
"owner": "dustin@mozilla.com", "owner": "dustin@mozilla.com",
"pushlog_id": 1556565286, "pushlog_id": 1556565286,
"pushdate": 112957, "pushdate": 112957,
@ -51,6 +52,7 @@ class TestTaskclusterYml(unittest.TestCase):
}, },
"push": { "push": {
"revision": "e8aebe488b2f2e567940577de25013d00e818f7c", "revision": "e8aebe488b2f2e567940577de25013d00e818f7c",
"base_revision": "54cbb3745cdb9a8aa0a4428d405b3b2e1c7d13c2",
"pushlog_id": -1, "pushlog_id": -1,
"pushdate": 0, "pushdate": 0,
"owner": "cron", "owner": "cron",
@ -80,6 +82,7 @@ class TestTaskclusterYml(unittest.TestCase):
}, },
"push": { "push": {
"revision": "e8d2d9aff5026ef1f1777b781b47fdcbdb9d8f20", "revision": "e8d2d9aff5026ef1f1777b781b47fdcbdb9d8f20",
"base_revision": "e8aebe488b2f2e567940577de25013d00e818f7c",
"owner": "dustin@mozilla.com", "owner": "dustin@mozilla.com",
"pushlog_id": 1556565286, "pushlog_id": 1556565286,
"pushdate": 112957, "pushdate": 112957,

View file

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,165 @@
Metadata-Version: 2.1
Name: giturlparse
Version: 0.10.0
Summary: A Git URL parsing module (supports parsing and rewriting)
Home-page: https://github.com/nephila/giturlparse
Author: Aaron O Mullan
Author-email: aaron@friendco.de
Maintainer: Iacopo Spalletti
Maintainer-email: i.spalletti@nephila.it
License: Apache v2
Keywords: giturlparse
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Requires-Python: >=3.6
Description-Content-Type: text/x-rst
===========
giturlparse
===========
Parse & rewrite git urls (supports GitHub, Bitbucket, FriendCode, Assembla, Gitlab ...)
This is a fork of giturlparse.py with updated parsers.
Original project can be found at https://github.com/FriendCode/giturlparse.py
************
Installing
************
::
pip install giturlparse
******************
Examples
******************
Exposed attributes
==================
* ``platform``: platform codename
* ``host``: server hostname
* ``resource``: same as ``host``
* ``port``: URL port (only if explicitly defined in URL)
* ``protocol``: URL protocol (git, ssh, http/https)
* ``protocols``: list of protocols explicitly defined in URL
* ``user``: repository user
* ``owner``: repository owner (user or organization)
* ``repo``: repository name
* ``name``: same as ``repo``
* ``groups``: list of groups - gitlab only
* ``path``: path to file or directory (includes the branch name) - gitlab / github only
* ``path_raw``: raw path starting from the repo name (might include platform keyword) - gitlab / github only
* ``branch``: branch name (when parseable) - gitlab / github only
Parse
==================
::
from giturlparse import parse
p = parse('git@bitbucket.org:AaronO/some-repo.git')
p.host, p.owner, p.repo
# => ('bitbucket.org', 'AaronO', 'some-repo')
Rewrite
==================
::
from giturlparse import parse
url = 'git@github.com:Org/Private-repo.git'
p = parse(url)
p.url2ssh, p.url2https, p.url2git, p.url2http
# => ('git@github.com:Org/Private-repo.git', 'https://github.com/Org/Private-repo.git', 'git://github.com/Org/Private-repo.git', None)
URLS
==================
Alternative URLs for same repo::
from giturlparse import parse
url = 'git@github.com:Org/Private-repo.git'
parse(url).urls
# => {
# 'ssh': 'git@github.com:Org/Private-repo.git',
# 'https': 'https://github.com/Org/Private-repo.git',
# 'git': 'git://github.com/Org/Private-repo.git'
# }
Validate
==================
::
from giturlparse import parse, validate
url = 'git@github.com:Org/Private-repo.git'
parse(url).valid
# => True
# Or
validate(url)
# => True
Tests
==================
::
python setup.py test
License
==================
Apache v2 (Check out LICENSE file)
.. :changelog:
*******
History
*******
.. towncrier release notes start
0.10.0 (2020-12-05)
===================
Features
--------
- General matching improvements (#18)
- Update tooling, drop python2 (#10213)
0.9.2 (2018-10-27)
==================
* Removed "s" from the base platform regex
* Fix license classifier in setup.py
* Update meta files
0.9.1 (2018-01-20)
==================
* First fork release

View file

@ -0,0 +1,18 @@
giturlparse/__init__.py,sha256=c5WMm7u1auWiuJrsY0bo1IsT6iRi8b6pGebNQC03_PI,332
giturlparse/parser.py,sha256=BTaOH--z1-odYdOwEb5iNadYpCvUM4-bKHYXGKxGIZM,1924
giturlparse/result.py,sha256=wKg1h9vYXkPseRgEAIk8TDPS1UMIU_z3t4IKbT7uD18,2765
giturlparse/platforms/__init__.py,sha256=y8xzQWxqGHwlvx0pY99Hqott-xK2Q0iBzpQ9dTehTrY,527
giturlparse/platforms/assembla.py,sha256=iPYpPOu8cNapbniD7sj63aTwPGT4DUH1U8RkvbUkiqE,498
giturlparse/platforms/base.py,sha256=cZPxEa1u1WNq6IvhUVp3XWJtks9Dy2sifDaJAdeHclI,1566
giturlparse/platforms/bitbucket.py,sha256=R6dsFBhuMlLe9-gIAP7X8hzJn-FHAjI-bBgnfNom4tc,680
giturlparse/platforms/friendcode.py,sha256=w__PNSQAkNO2Y45doOw7YMDqwuSyu_FocQTRa305VM0,389
giturlparse/platforms/github.py,sha256=G_7VRQpm5ZtvOcc1xbVF3CnC4AcCRnyK7EgkoaoqOEo,1446
giturlparse/platforms/gitlab.py,sha256=2K65zlI8CA5OdXV9eXW3SBFH7oW78lFlkhLviW3Mwyo,1794
giturlparse/tests/__init__.py,sha256=yBGT6Ycwx1AsTFYemzHoqrJ82seE0gfGti99VyrV3x0,37
giturlparse/tests/parse.py,sha256=dpFzvo40qdH7Zg6CmgMqBMeZz473GhbZotmVK_nq_pk,14594
giturlparse/tests/rewrite.py,sha256=scB7YGBUeFo3bEyI0Mvc0hK_ajlBY2RkrEGRtnrtukc,3386
giturlparse-0.10.0.dist-info/LICENSE,sha256=c7p036pSC0mkAbXSFFmoUjoUbzt1GKgz7qXvqFEwv2g,10273
giturlparse-0.10.0.dist-info/METADATA,sha256=NDWxArULRXhAAu2KttDMuZu1k35HvJ1eJHEcWfeB8lI,3511
giturlparse-0.10.0.dist-info/WHEEL,sha256=oh0NKYrTcu1i1-wgrI1cnhkjYIi8WJ-8qd9Jrr5_y4E,110
giturlparse-0.10.0.dist-info/top_level.txt,sha256=NHfX7iaRAYz-bnROU6Q0tgNInQU-YgIeeii0uznxCLA,12
giturlparse-0.10.0.dist-info/RECORD,,

View file

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.36.1)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

View file

@ -0,0 +1 @@
giturlparse

View file

@ -0,0 +1,14 @@
from .parser import parse as _parse
from .result import GitUrlParsed
__author__ = "Iacopo Spalletti"
__email__ = "i.spalletti@nephila.it"
__version__ = "0.10.0"
def parse(url, check_domain=True):
return GitUrlParsed(_parse(url, check_domain))
def validate(url, check_domain=True):
return parse(url, check_domain).valid

View file

@ -0,0 +1,69 @@
from collections import defaultdict
from .platforms import PLATFORMS
SUPPORTED_ATTRIBUTES = (
"domain",
"repo",
"owner",
"path_raw",
"groups_path",
"_user",
"port",
"url",
"platform",
"protocol",
)
def parse(url, check_domain=True):
# Values are None by default
parsed_info = defaultdict(lambda: None)
parsed_info["port"] = ""
parsed_info["path_raw"] = ""
parsed_info["groups_path"] = ""
# Defaults to all attributes
map(parsed_info.setdefault, SUPPORTED_ATTRIBUTES)
for name, platform in PLATFORMS:
for protocol, regex in platform.COMPILED_PATTERNS.items():
# print(name, protocol, regex)
# Match current regex against URL
match = regex.match(url)
# Skip if not matched
if not match:
# print("[%s] URL: %s dit not match %s" % (name, url, regex.pattern))
continue
# Skip if domain is bad
domain = match.group("domain")
# print('[%s] DOMAIN = %s' % (url, domain,))
if check_domain:
if platform.DOMAINS and not (domain in platform.DOMAINS):
continue
if platform.SKIP_DOMAINS and domain in platform.SKIP_DOMAINS:
continue
# add in platform defaults
parsed_info.update(platform.DEFAULTS)
# Get matches as dictionary
matches = platform.clean_data(match.groupdict(default=""))
# Update info with matches
parsed_info.update(matches)
# Update info with platform info
parsed_info.update(
{
"url": url,
"platform": name,
"protocol": protocol,
}
)
return parsed_info
# Empty if none matched
return parsed_info

View file

@ -0,0 +1,18 @@
from .assembla import AssemblaPlatform
from .base import BasePlatform
from .bitbucket import BitbucketPlatform
from .friendcode import FriendCodePlatform
from .github import GitHubPlatform
from .gitlab import GitLabPlatform
# Supported platforms
PLATFORMS = [
# name -> Platform object
("github", GitHubPlatform()),
("bitbucket", BitbucketPlatform()),
("friendcode", FriendCodePlatform()),
("assembla", AssemblaPlatform()),
("gitlab", GitLabPlatform()),
# Match url
("base", BasePlatform()),
]

View file

@ -0,0 +1,14 @@
from .base import BasePlatform
class AssemblaPlatform(BasePlatform):
DOMAINS = ("git.assembla.com",)
PATTERNS = {
"ssh": r"(?P<protocols>(git\+)?(?P<protocol>ssh))?(://)?git@(?P<domain>.+?):(?P<pathname>(?P<repo>.+)).git",
"git": r"(?P<protocols>(?P<protocol>git))://(?P<domain>.+?)/(?P<pathname>(?P<repo>.+)).git",
}
FORMATS = {
"ssh": r"git@%(domain)s:%(repo)s.git",
"git": r"git://%(domain)s/%(repo)s.git",
}
DEFAULTS = {"_user": "git"}

View file

@ -0,0 +1,43 @@
import itertools
import re
class BasePlatform:
FORMATS = {
"ssh": r"(?P<protocols>(git\+)?(?P<protocol>ssh))?(://)?%(_user)s@%(host)s:%(repo)s.git",
"http": r"(?P<protocols>(git\+)?(?P<protocol>http))://%(host)s/%(repo)s.git",
"https": r"(?P<protocols>(git\+)?(?P<protocol>https))://%(host)s/%(repo)s.git",
"git": r"(?P<protocols>(?P<protocol>git))://%(host)s/%(repo)s.git",
}
PATTERNS = {
"ssh": r"(?P<_user>.+)@(?P<domain>[^/]+?):(?P<repo>.+).git",
"http": r"http://(?P<domain>[^/]+?)/(?P<repo>.+).git",
"https": r"https://(?P<domain>[^/]+?)/(?P<repo>.+).git",
"git": r"git://(?P<domain>[^/]+?)/(?P<repo>.+).git",
}
# None means it matches all domains
DOMAINS = None
SKIP_DOMAINS = None
DEFAULTS = {}
def __init__(self):
# Precompile PATTERNS
self.COMPILED_PATTERNS = {proto: re.compile(regex, re.IGNORECASE) for proto, regex in self.PATTERNS.items()}
# Supported protocols
self.PROTOCOLS = self.PATTERNS.keys()
if self.__class__ == BasePlatform:
sub = [subclass.SKIP_DOMAINS for subclass in self.__class__.__subclasses__() if subclass.SKIP_DOMAINS]
if sub:
self.SKIP_DOMAINS = list(itertools.chain.from_iterable(sub))
@staticmethod
def clean_data(data):
data["path"] = ""
data["branch"] = ""
data["protocols"] = list(filter(lambda x: x, data["protocols"].split("+")))
data["pathname"] = data["pathname"].strip(":")
return data

View file

@ -0,0 +1,20 @@
from .base import BasePlatform
class BitbucketPlatform(BasePlatform):
PATTERNS = {
"https": (
r"(?P<protocols>(git\+)?(?P<protocol>https))://(?P<_user>.+)@(?P<domain>.+?)"
r"(?P<pathname>/(?P<owner>.+)/(?P<repo>.+?)(?:\.git)?)$"
),
"ssh": (
r"(?P<protocols>(git\+)?(?P<protocol>ssh))?(://)?git@(?P<domain>.+?):"
r"(?P<pathname>(?P<owner>.+)/(?P<repo>.+?)(?:\.git)?)$"
),
}
FORMATS = {
"https": r"https://%(owner)s@%(domain)s/%(owner)s/%(repo)s.git",
"ssh": r"git@%(domain)s:%(owner)s/%(repo)s.git",
}
DOMAINS = ("bitbucket.org",)
DEFAULTS = {"_user": "git"}

View file

@ -0,0 +1,14 @@
from .base import BasePlatform
class FriendCodePlatform(BasePlatform):
DOMAINS = ("friendco.de",)
PATTERNS = {
"https": (
r"(?P<protocols>(git\+)?(?P<protocol>https))://(?P<domain>.+?)/"
r"(?P<pathname>(?P<owner>.+)@user/(?P<repo>.+)).git"
),
}
FORMATS = {
"https": r"https://%(domain)s/%(owner)s@user/%(repo)s.git",
}

View file

@ -0,0 +1,39 @@
from .base import BasePlatform
class GitHubPlatform(BasePlatform):
PATTERNS = {
"https": (
r"(?P<protocols>(git\+)?(?P<protocol>https))://(?P<domain>[^/]+?)"
r"(?P<pathname>/(?P<owner>[^/]+?)/(?P<repo>[^/]+?)(?:\.git)?(?P<path_raw>(/blob/|/tree/).+)?)$"
),
"ssh": (
r"(?P<protocols>(git\+)?(?P<protocol>ssh))?(://)?git@(?P<domain>.+?)(?P<pathname>(:|/)"
r"(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)"
r"(?P<path_raw>(/blob/|/tree/).+)?)$"
),
"git": (
r"(?P<protocols>(?P<protocol>git))://(?P<domain>.+?)"
r"(?P<pathname>/(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?"
r"(?P<path_raw>(/blob/|/tree/).+)?)$"
),
}
FORMATS = {
"https": r"https://%(domain)s/%(owner)s/%(repo)s.git%(path_raw)s",
"ssh": r"git@%(domain)s:%(owner)s/%(repo)s.git%(path_raw)s",
"git": r"git://%(domain)s/%(owner)s/%(repo)s.git%(path_raw)s",
}
DOMAINS = (
"github.com",
"gist.github.com",
)
DEFAULTS = {"_user": "git"}
@staticmethod
def clean_data(data):
data = BasePlatform.clean_data(data)
if data["path_raw"].startswith("/blob/"):
data["path"] = data["path_raw"].replace("/blob/", "")
if data["path_raw"].startswith("/tree/"):
data["branch"] = data["path_raw"].replace("/tree/", "")
return data

View file

@ -0,0 +1,43 @@
from .base import BasePlatform
class GitLabPlatform(BasePlatform):
PATTERNS = {
"https": (
r"(?P<protocols>(git\+)?(?P<protocol>https))://(?P<domain>.+?)(?P<port>:[0-9]+)?"
r"(?P<pathname>/(?P<owner>[^/]+?)/"
r"(?P<groups_path>.*?)?(?(groups_path)/)?(?P<repo>[^/]+?)(?:\.git)?"
r"(?P<path_raw>(/blob/|/-/tree/).+)?)$"
),
"ssh": (
r"(?P<protocols>(git\+)?(?P<protocol>ssh))?(://)?git@(?P<domain>.+?):(?P<port>[0-9]+)?(?(port))?"
r"(?P<pathname>/?(?P<owner>[^/]+)/"
r"(?P<groups_path>.*?)?(?(groups_path)/)?(?P<repo>[^/]+?)(?:\.git)?"
r"(?P<path_raw>(/blob/|/-/tree/).+)?)$"
),
"git": (
r"(?P<protocols>(?P<protocol>git))://(?P<domain>.+?):(?P<port>[0-9]+)?(?(port))?"
r"(?P<pathname>/?(?P<owner>[^/]+)/"
r"(?P<groups_path>.*?)?(?(groups_path)/)?(?P<repo>[^/]+?)(?:\.git)?"
r"(?P<path_raw>(/blob/|/-/tree/).+)?)$"
),
}
FORMATS = {
"https": r"https://%(domain)s/%(owner)s/%(groups_slash)s%(repo)s.git%(path_raw)s",
"ssh": r"git@%(domain)s:%(port_slash)s%(owner)s/%(groups_slash)s%(repo)s.git%(path_raw)s",
"git": r"git://%(domain)s%(port)s/%(owner)s/%(groups_slash)s%(repo)s.git%(path_raw)s",
}
SKIP_DOMAINS = (
"github.com",
"gist.github.com",
)
DEFAULTS = {"_user": "git", "port": ""}
@staticmethod
def clean_data(data):
data = BasePlatform.clean_data(data)
if data["path_raw"].startswith("/blob/"):
data["path"] = data["path_raw"].replace("/blob/", "")
if data["path_raw"].startswith("/-/tree/"):
data["branch"] = data["path_raw"].replace("/-/tree/", "")
return data

View file

@ -0,0 +1,131 @@
from copy import copy
from .platforms import PLATFORMS
# Possible values to extract from a Git Url
REQUIRED_ATTRIBUTES = (
"domain",
"repo",
)
class GitUrlParsed:
platform = None
def __init__(self, parsed_info):
self._parsed = parsed_info
# Set parsed objects as attributes
for k, v in parsed_info.items():
setattr(self, k, v)
for name, platform in PLATFORMS:
if name == self.platform:
self._platform_obj = platform
break
def _valid_attrs(self):
return all([getattr(self, attr, None) for attr in REQUIRED_ATTRIBUTES]) # NOQA
@property
def valid(self):
return all(
[
self._valid_attrs(),
]
)
##
# Alias properties
##
@property
def host(self):
return self.domain
@property
def resource(self):
return self.domain
@property
def name(self):
return self.repo
@property
def user(self):
if hasattr(self, "_user"):
return self._user
return self.owner
@property
def groups(self):
if self.groups_path:
return self.groups_path.split("/")
else:
return []
def format(self, protocol): # noqa : A0003
"""Reformat URL to protocol."""
items = copy(self._parsed)
items["port_slash"] = "%s/" % self.port if self.port else ""
items["groups_slash"] = "%s/" % self.groups_path if self.groups_path else ""
return self._platform_obj.FORMATS[protocol] % items
@property
def normalized(self):
"""Normalize URL."""
return self.format(self.protocol)
##
# Rewriting
##
@property
def url2ssh(self):
return self.format("ssh")
@property
def url2http(self):
return self.format("http")
@property
def url2https(self):
return self.format("https")
@property
def url2git(self):
return self.format("git")
# All supported Urls for a repo
@property
def urls(self):
return {protocol: self.format(protocol) for protocol in self._platform_obj.PROTOCOLS}
##
# Platforms
##
@property
def github(self):
return self.platform == "github"
@property
def bitbucket(self):
return self.platform == "bitbucket"
@property
def friendcode(self):
return self.platform == "friendcode"
@property
def assembla(self):
return self.platform == "assembla"
@property
def gitlab(self):
return self.platform == "gitlab"
##
# Get data as dict
##
@property
def data(self):
return dict(self._parsed)

View file

@ -0,0 +1,16 @@
Metadata-Version: 2.1
Name: mozilla-repo-urls
Version: 0.0.3
Summary: Process Mozilla's repository URLs. The intent is to centralize URLs parsing.
Home-page: https://github.com/mozilla-releng/mozilla-repo-urls
Author: Mozilla Release Engineering
Author-email: release+python@mozilla.com
License: MPL2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Requires-Dist: giturlparse

View file

@ -0,0 +1,12 @@
mozilla_repo_urls/__init__.py,sha256=seFB5ueyozmIXZxBWVATYPbQCzNln2SWSTirc0yk_A0,108
mozilla_repo_urls/errors.py,sha256=koXTtW_axWwCtWVHkvxXjYqAq1AXYbN54Q-C8veqDNw,485
mozilla_repo_urls/parser.py,sha256=a6Q1x8o_yu4Qof86aq2M8dwPhiMeAzCV7LxAw7CzwRg,805
mozilla_repo_urls/result.py,sha256=SN8hhIc6R1xLGOYZ0h1CpQbFiRP6LIqzNoup3MjOqnc,744
mozilla_repo_urls/platforms/__init__.py,sha256=5gwGbeTZUI-0VR0HmC3913e6AUTylDkjmcXYkg8QwYc,89
mozilla_repo_urls/platforms/hgmo.py,sha256=bKpBHAqxlMOmGbfn6aSU_q5N-7LbwNDSrt0TZ0UzTvk,1043
test/__init__.py,sha256=ui4glNH_cCoz4Ex7hcZhHTcstOPJb2wcojFiNvvIALI,88
test/test_integration.py,sha256=Eq0XjKA0w0Ao50UJMcF5tuPiFqfa1stgl9T2lg1vHJE,10919
mozilla_repo_urls-0.0.3.dist-info/METADATA,sha256=4bWx4lq-nBnCq0cnpmwfmfVpmFwX0dbvBi3jg05B1s4,628
mozilla_repo_urls-0.0.3.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
mozilla_repo_urls-0.0.3.dist-info/top_level.txt,sha256=0LuRstNeetmfWdKTPvknIx8aDVzsf1KSmASCgOvKvDM,23
mozilla_repo_urls-0.0.3.dist-info/RECORD,,

View file

@ -0,0 +1,2 @@
mozilla_repo_urls
test

View file

@ -0,0 +1,2 @@
from mozilla_repo_urls.errors import * # noqa F401
from mozilla_repo_urls.parser import parse # noqa F401

View file

@ -0,0 +1,15 @@
class RepoUrlsBaseError(Exception):
pass
class InvalidRepoUrlError(RepoUrlsBaseError):
def __init__(self, url_string) -> None:
super().__init__(f"Could not parse URL: {url_string}")
class UnsupportedPlatformError(RepoUrlsBaseError):
def __init__(self, url_string, platform, supported_platforms) -> None:
super().__init__(
f"Unsupported platform. Got: {platform}. "
f"Expected: {supported_platforms}. URL: {url_string}"
)

View file

@ -0,0 +1,27 @@
import giturlparse
from mozilla_repo_urls.platforms import ADDITIONAL_PLATFORMS
from .errors import InvalidRepoUrlError, UnsupportedPlatformError
from .result import RepoUrlParsed
for i, platform in enumerate(ADDITIONAL_PLATFORMS):
giturlparse.platforms.PLATFORMS.insert(i, platform)
_SUPPORTED_PLAFORMS = ("hgmo", "github")
def parse(url_string):
# Workaround for https://github.com/nephila/giturlparse/issues/43
url_string = url_string.rstrip("/")
parsed_info = giturlparse.parser.parse(url_string)
parsed_url = RepoUrlParsed(parsed_info)
if not parsed_url.valid:
raise InvalidRepoUrlError(url_string)
if parsed_url.platform not in _SUPPORTED_PLAFORMS:
raise UnsupportedPlatformError(url_string, platform, _SUPPORTED_PLAFORMS)
return parsed_url

View file

@ -0,0 +1,5 @@
from .hgmo import HgmoPlatform
ADDITIONAL_PLATFORMS = [
("hgmo", HgmoPlatform()),
]

View file

@ -0,0 +1,34 @@
from giturlparse.platforms.base import BasePlatform
class HgmoPlatform(BasePlatform):
PATTERNS = {
"https": (
r"(?P<protocols>(?P<protocol>https))://"
r"(?P<domain>[^/]+?)"
r"(?P<pathname>/"
r"(?P<repo>(([^/]+?)(/)?){1,2}))"
r"(?P<path_raw>(/raw-file/|/file/).+)?$"
),
"ssh": (
r"(?P<protocols>(?P<protocol>ssh))(://)?"
r"(?P<domain>.+?)"
r"(?P<pathname>(:|/))"
r"(?P<repo>(([^/]+?)(/)?){1,2})/?$"
),
}
FORMATS = {
"https": r"https://%(domain)s/%(repo)s%(path_raw)s",
"ssh": r"ssh://%(domain)s/%(repo)s",
}
DOMAINS = ("hg.mozilla.org",)
DEFAULTS = {"_user": ""}
@staticmethod
def clean_data(data):
data = BasePlatform.clean_data(data)
if data["path_raw"].startswith(("/raw-file/", "/file")):
data["path"] = (
data["path_raw"].replace("/raw-file/", "").replace("/file/", "")
)
return data

View file

@ -0,0 +1,30 @@
import giturlparse
_DOT_GIT_SUFFIX = ".git"
class RepoUrlParsed(giturlparse.result.GitUrlParsed):
@property
def hgmo(self) -> bool:
return self.platform == "hgmo"
@property
def repo_name(self) -> str:
return self.repo_path.split("/")[-1]
@property
def repo_path(self) -> str:
repo_path = (
self.pathname[: -len(_DOT_GIT_SUFFIX)]
if self.pathname.endswith(_DOT_GIT_SUFFIX)
else self.pathname
)
return repo_path.strip("/")
@property
def repo_type(self) -> str:
return "hg" if self.platform == "hgmo" else "git"
@property
def taskcluster_role_prefix(self) -> str:
return f"repo:{self.host}/{self.repo_path}"

View file

@ -0,0 +1,6 @@
from contextlib import contextmanager
@contextmanager
def does_not_raise():
yield

View file

@ -0,0 +1,280 @@
from test import does_not_raise
import pytest
import mozilla_repo_urls
@pytest.mark.parametrize(
"url_string, expectation, expected",
(
(
"https://hg.mozilla.org/mozilla-central",
does_not_raise(),
{
"github": False,
"groups": [],
"hgmo": True,
"host": "hg.mozilla.org",
"name": "mozilla-central",
"normalized": "https://hg.mozilla.org/mozilla-central",
"path_raw": "",
"path": "",
"pathname": "/mozilla-central",
"platform": "hgmo",
"port": "",
"protocol": "https",
"protocols": ["https"],
"repo": "mozilla-central",
"repo_name": "mozilla-central",
"repo_path": "mozilla-central",
"repo_type": "hg",
"resource": "hg.mozilla.org",
"taskcluster_role_prefix": "repo:hg.mozilla.org/mozilla-central",
"urls": {
"https": "https://hg.mozilla.org/mozilla-central",
"ssh": "ssh://hg.mozilla.org/mozilla-central",
},
"user": "",
"valid": True,
},
),
(
"https://hg.mozilla.org/releases/mozilla-beta",
does_not_raise(),
{
"github": False,
"groups": [],
"hgmo": True,
"host": "hg.mozilla.org",
"name": "releases/mozilla-beta",
"normalized": "https://hg.mozilla.org/releases/mozilla-beta",
"path_raw": "",
"path": "",
"pathname": "/releases/mozilla-beta",
"platform": "hgmo",
"port": "",
"protocol": "https",
"protocols": ["https"],
"repo": "releases/mozilla-beta",
"repo_name": "mozilla-beta",
"repo_path": "releases/mozilla-beta",
"repo_type": "hg",
"resource": "hg.mozilla.org",
"taskcluster_role_prefix": "repo:hg.mozilla.org/releases/mozilla-beta",
"urls": {
"https": "https://hg.mozilla.org/releases/mozilla-beta",
"ssh": "ssh://hg.mozilla.org/releases/mozilla-beta",
},
"user": "",
"valid": True,
},
),
(
"https://hg.mozilla.org/releases/mozilla-release",
does_not_raise(),
{
"github": False,
"groups": [],
"hgmo": True,
"host": "hg.mozilla.org",
"name": "releases/mozilla-release",
"normalized": "https://hg.mozilla.org/releases/mozilla-release",
"path_raw": "",
"path": "",
"pathname": "/releases/mozilla-release",
"platform": "hgmo",
"port": "",
"protocol": "https",
"protocols": ["https"],
"repo": "releases/mozilla-release",
"repo_name": "mozilla-release",
"repo_path": "releases/mozilla-release",
"repo_type": "hg",
"resource": "hg.mozilla.org",
"taskcluster_role_prefix": "repo:hg.mozilla.org/releases/mozilla-release", # noqa: E501
"urls": {
"https": "https://hg.mozilla.org/releases/mozilla-release",
"ssh": "ssh://hg.mozilla.org/releases/mozilla-release",
},
"user": "",
"valid": True,
},
),
(
"https://hg.mozilla.org/try",
does_not_raise(),
{
"groups": [],
"hgmo": True,
"host": "hg.mozilla.org",
"name": "try",
"normalized": "https://hg.mozilla.org/try",
"path_raw": "",
"path": "",
"pathname": "/try",
"platform": "hgmo",
"port": "",
"protocol": "https",
"protocols": ["https"],
"repo": "try",
"repo_name": "try",
"repo_path": "try",
"repo_type": "hg",
"resource": "hg.mozilla.org",
"taskcluster_role_prefix": "repo:hg.mozilla.org/try",
"urls": {
"https": "https://hg.mozilla.org/try",
"ssh": "ssh://hg.mozilla.org/try",
},
"user": "",
"valid": True,
},
),
(
"https://hg.mozilla.org/mozilla-central/raw-file/tip/taskcluster/ci/config.yml", # noqa: E501
does_not_raise(),
{
"groups": [],
"hgmo": True,
"host": "hg.mozilla.org",
"name": "mozilla-central",
"normalized": "https://hg.mozilla.org/mozilla-central/raw-file/tip/taskcluster/ci/config.yml", # noqa: E501
"path_raw": "/raw-file/tip/taskcluster/ci/config.yml",
"path": "tip/taskcluster/ci/config.yml",
"pathname": "/mozilla-central",
"platform": "hgmo",
"port": "",
"protocol": "https",
"protocols": ["https"],
"repo": "mozilla-central",
"repo_name": "mozilla-central",
"repo_path": "mozilla-central",
"repo_type": "hg",
"resource": "hg.mozilla.org",
"taskcluster_role_prefix": "repo:hg.mozilla.org/mozilla-central",
"urls": {
"https": "https://hg.mozilla.org/mozilla-central/raw-file/tip/taskcluster/ci/config.yml", # noqa: E501
"ssh": "ssh://hg.mozilla.org/mozilla-central",
},
"user": "",
"valid": True,
},
),
(
"https://hg.mozilla.org/mozilla-central/file/tip/taskcluster/ci/config.yml", # noqa: E501
does_not_raise(),
{
"github": False,
"groups": [],
"hgmo": True,
"host": "hg.mozilla.org",
"name": "mozilla-central",
"normalized": "https://hg.mozilla.org/mozilla-central/file/tip/taskcluster/ci/config.yml", # noqa: E501
"path_raw": "/file/tip/taskcluster/ci/config.yml",
"path": "tip/taskcluster/ci/config.yml",
"pathname": "/mozilla-central",
"platform": "hgmo",
"port": "",
"protocol": "https",
"protocols": ["https"],
"repo": "mozilla-central",
"repo_name": "mozilla-central",
"repo_path": "mozilla-central",
"repo_type": "hg",
"resource": "hg.mozilla.org",
"taskcluster_role_prefix": "repo:hg.mozilla.org/mozilla-central",
"urls": {
"https": "https://hg.mozilla.org/mozilla-central/file/tip/taskcluster/ci/config.yml", # noqa: E501
"ssh": "ssh://hg.mozilla.org/mozilla-central",
},
"user": "",
"valid": True,
},
),
(
"https://github.com/mozilla-mobile/fenix",
does_not_raise(),
{
"github": True,
"groups": [],
"hgmo": False,
"host": "github.com",
"name": "fenix",
"normalized": "https://github.com/mozilla-mobile/fenix.git",
"owner": "mozilla-mobile",
"path_raw": "",
"path": "",
"pathname": "/mozilla-mobile/fenix",
"platform": "github",
"port": "",
"protocol": "https",
"protocols": ["https"],
"repo": "fenix",
"repo_name": "fenix",
"repo_path": "mozilla-mobile/fenix",
"repo_type": "git",
"resource": "github.com",
"taskcluster_role_prefix": "repo:github.com/mozilla-mobile/fenix",
"urls": {
"git": "git://github.com/mozilla-mobile/fenix.git",
"https": "https://github.com/mozilla-mobile/fenix.git",
"ssh": "git@github.com:mozilla-mobile/fenix.git",
},
"user": "git",
"valid": True,
},
),
(
"git@github.com:mozilla-mobile/firefox-android.git",
does_not_raise(),
{
"github": True,
"groups": [],
"hgmo": False,
"host": "github.com",
"name": "firefox-android",
"normalized": "git@github.com:mozilla-mobile/firefox-android.git",
"owner": "mozilla-mobile",
"path_raw": "",
"path": "",
"pathname": "mozilla-mobile/firefox-android.git",
"platform": "github",
"port": "",
"protocol": "ssh",
"protocols": [],
"repo": "firefox-android",
"repo_name": "firefox-android",
"repo_path": "mozilla-mobile/firefox-android",
"repo_type": "git",
"resource": "github.com",
"taskcluster_role_prefix": "repo:github.com/mozilla-mobile/firefox-android", # noqa: E501
"urls": {
"git": "git://github.com/mozilla-mobile/firefox-android.git",
"https": "https://github.com/mozilla-mobile/firefox-android.git",
"ssh": "git@github.com:mozilla-mobile/firefox-android.git",
},
"user": "git",
"valid": True,
},
),
(
"https://some.unknown/repo",
pytest.raises(mozilla_repo_urls.InvalidRepoUrlError),
None,
),
(
"https://gitlab.com/some-owner/some-repo",
pytest.raises(mozilla_repo_urls.UnsupportedPlatformError),
None,
),
),
)
def test_parse(url_string, expectation, expected):
with expectation:
url_object = mozilla_repo_urls.parse(url_string)
actual = {
attribute_name: getattr(url_object, attribute_name)
for attribute_name in expected.keys()
}
assert actual == expected

36
third_party/python/poetry.lock generated vendored
View file

@ -194,6 +194,14 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "giturlparse"
version = "0.10.0"
description = "A Git URL parsing module (supports parsing and rewriting)"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]] [[package]]
name = "glean-parser" name = "glean-parser"
version = "6.1.2" version = "6.1.2"
@ -334,6 +342,17 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
six = "*" six = "*"
[[package]]
name = "mozilla-repo-urls"
version = "0.0.3"
description = "Process Mozilla's repository URLs. The intent is to centralize URLs parsing."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
giturlparse = "*"
[[package]] [[package]]
name = "mozilla-version" name = "mozilla-version"
version = "0.3.4" version = "0.3.4"
@ -615,7 +634,7 @@ test = ["pytest", "pytest-cov", "pytest-mock", "httmock", "mock", "setuptools-li
[[package]] [[package]]
name = "taskcluster-taskgraph" name = "taskcluster-taskgraph"
version = "2.0.0" version = "3.2.0"
description = "Build taskcluster taskgraphs" description = "Build taskcluster taskgraphs"
category = "main" category = "main"
optional = false optional = false
@ -625,6 +644,7 @@ python-versions = "*"
appdirs = ">=1.4" appdirs = ">=1.4"
attrs = ">=19.1.0" attrs = ">=19.1.0"
json-e = ">=2.7" json-e = ">=2.7"
mozilla-repo-urls = "*"
PyYAML = ">=5.4" PyYAML = ">=5.4"
redo = ">=2.0" redo = ">=2.0"
requests = ">=2.25" requests = ">=2.25"
@ -737,7 +757,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6" python-versions = "^3.6"
content-hash = "9d5442add586f045a8bac2403afaade45b7836ae851e906fd598d48c23075eb1" content-hash = "e350bab49e867e6242ad7be2ca071a731ffe5162e79553e38f82eba26c421da3"
[metadata.files] [metadata.files]
aiohttp = [ aiohttp = [
@ -850,6 +870,10 @@ esprima = [
{file = "fluent.syntax-0.18.1-py2.py3-none-any.whl", hash = "sha256:0e63679fa4f1b3042565220a5127b4bab842424f07d6a13c12299e3b3835486a"}, {file = "fluent.syntax-0.18.1-py2.py3-none-any.whl", hash = "sha256:0e63679fa4f1b3042565220a5127b4bab842424f07d6a13c12299e3b3835486a"},
{file = "fluent.syntax-0.18.1.tar.gz", hash = "sha256:3a55f5e605d1b029a65cc8b6492c86ec4608e15447e73db1495de11fd46c104f"}, {file = "fluent.syntax-0.18.1.tar.gz", hash = "sha256:3a55f5e605d1b029a65cc8b6492c86ec4608e15447e73db1495de11fd46c104f"},
] ]
giturlparse = [
{file = "giturlparse-0.10.0-py2.py3-none-any.whl", hash = "sha256:04ba1a3a099c3093fa8d24a422913c6a9b2c2cd22bcffc939cf72e3e98f672d7"},
{file = "giturlparse-0.10.0.tar.gz", hash = "sha256:2595ab291d30717cda8474b874c9fd509f1b9802ad7f6968c36a45e4b13eb337"},
]
glean-parser = [ glean-parser = [
{file = "glean_parser-6.1.2-py3-none-any.whl", hash = "sha256:e801af6463b7e0ba79d97ddfc0a58d9d71121c93cea601417571e33fa8142270"}, {file = "glean_parser-6.1.2-py3-none-any.whl", hash = "sha256:e801af6463b7e0ba79d97ddfc0a58d9d71121c93cea601417571e33fa8142270"},
{file = "glean_parser-6.1.2.tar.gz", hash = "sha256:12a0fecedc1144d77fa571e0422ff3fea4dbadc381d631bea800a6b2f58f4f7f"}, {file = "glean_parser-6.1.2.tar.gz", hash = "sha256:12a0fecedc1144d77fa571e0422ff3fea4dbadc381d631bea800a6b2f58f4f7f"},
@ -945,6 +969,10 @@ mohawk = [
{file = "mohawk-0.3.4-py2-none-any.whl", hash = "sha256:b3f85ffa93a5c7d2f9cc591246ef9f8ac4a9fa716bfd5bae0377699a2d89d78c"}, {file = "mohawk-0.3.4-py2-none-any.whl", hash = "sha256:b3f85ffa93a5c7d2f9cc591246ef9f8ac4a9fa716bfd5bae0377699a2d89d78c"},
{file = "mohawk-0.3.4.tar.gz", hash = "sha256:e98b331d9fa9ece7b8be26094cbe2d57613ae882133cc755167268a984bc0ab3"}, {file = "mohawk-0.3.4.tar.gz", hash = "sha256:e98b331d9fa9ece7b8be26094cbe2d57613ae882133cc755167268a984bc0ab3"},
] ]
mozilla-repo-urls = [
{file = "mozilla-repo-urls-0.0.3.tar.gz", hash = "sha256:3b2f9b42111ce3d50ecdcd081a62229e1fdc8f5472adbf405abe12d3ba8e8af5"},
{file = "mozilla_repo_urls-0.0.3-py3-none-any.whl", hash = "sha256:673c80a4d0ed449093203b88e119e0bf1074026f891c1dd50aa04d534fd0658c"},
]
mozilla-version = [ mozilla-version = [
{file = "mozilla-version-0.3.4.tar.gz", hash = "sha256:3ed4deb7a6fb25c83a5346ef4de08ddff9b2ddc4d16dd8fafb4a84978cc71255"}, {file = "mozilla-version-0.3.4.tar.gz", hash = "sha256:3ed4deb7a6fb25c83a5346ef4de08ddff9b2ddc4d16dd8fafb4a84978cc71255"},
{file = "mozilla_version-0.3.4-py2.py3-none-any.whl", hash = "sha256:ce5741c2e7d12c30b53de9f79e30d6ac2a8bd4c93be711d30c7a7a08e32a094f"}, {file = "mozilla_version-0.3.4-py2.py3-none-any.whl", hash = "sha256:ce5741c2e7d12c30b53de9f79e30d6ac2a8bd4c93be711d30c7a7a08e32a094f"},
@ -1125,8 +1153,8 @@ taskcluster = [
{file = "taskcluster-44.2.2.tar.gz", hash = "sha256:0266a6a901e1a2ec838984a7f24e7adb6d58f9f2e221a7f613388f8f23f786fc"}, {file = "taskcluster-44.2.2.tar.gz", hash = "sha256:0266a6a901e1a2ec838984a7f24e7adb6d58f9f2e221a7f613388f8f23f786fc"},
] ]
taskcluster-taskgraph = [ taskcluster-taskgraph = [
{file = "taskcluster-taskgraph-2.0.0.tar.gz", hash = "sha256:93eff40ba39a29cd290fc25a2124ed9bf5806d87891edd7e8de35df568708141"}, {file = "taskcluster-taskgraph-3.2.0.tar.gz", hash = "sha256:941d96fd36c20b0e84e322d2c25be04fe5f258405793af5e086bbec698cbcb17"},
{file = "taskcluster_taskgraph-2.0.0-py3-none-any.whl", hash = "sha256:3d22ab488071ddc82997b33fc6c1c524a44bdc7e14b30a274d99dbbdd7389502"}, {file = "taskcluster_taskgraph-3.2.0-py3-none-any.whl", hash = "sha256:fafcd1bfd8ec8b2f57db4e4d87d5b881da3ad7119d78d407d22b4b71b805d1bf"},
] ]
taskcluster-urls = [ taskcluster-urls = [
{file = "taskcluster-urls-13.0.1.tar.gz", hash = "sha256:b25e122ecec249c4299ac7b20b08db76e3e2025bdaeb699a9d444556de5fd367"}, {file = "taskcluster-urls-13.0.1.tar.gz", hash = "sha256:b25e122ecec249c4299ac7b20b08db76e3e2025bdaeb699a9d444556de5fd367"},

View file

@ -1,48 +1,48 @@
appdirs==1.4.4 appdirs==1.4.4
attrs==19.2.0 attrs==19.2.0
blessings==1.7 blessings==1.7
cbor2==4.0.1 cbor2==4.0.1
colorama==0.4.5 colorama==0.4.5
compare-locales==8.2.1 compare-locales==8.2.1
cookies==2.2.1 cookies==2.2.1
cram==0.7 cram==0.7
distro==1.4.0 distro==1.4.0
ecdsa==0.15 ecdsa==0.15
esprima==4.0.1 esprima==4.0.1
fluent.migrate==0.11 fluent.migrate==0.11
fluent.syntax==0.18.1 fluent.syntax==0.18.1
glean_parser==6.1.2 glean_parser==6.1.2
# Pin importlib-metadata to a version compatible with poetry # Pin importlib-metadata to a version compatible with poetry
importlib-metadata==1.7.0 importlib-metadata==1.7.0
jsmin==2.1.0 jsmin==2.1.0
json-e==2.7.0 json-e==2.7.0
looseversion==1.0.1 looseversion==1.0.1
mozilla-version==0.3.4 mozilla-version==0.3.4
packaging==20.9 packaging==20.9
pathspec==0.9.0 pathspec==0.9.0
pip==21.2.4 pip==21.2.4
pip-tools==5.5.0 pip-tools==5.5.0
ply==3.10 ply==3.10
pyasn1==0.4.8 pyasn1==0.4.8
pyasn1-modules==0.2.8 pyasn1-modules==0.2.8
pylru==1.0.9 pylru==1.0.9
python-hglib==2.4 python-hglib==2.4
pytoml==0.1.10 pytoml==0.1.10
pyyaml==5.4.1 pyyaml==5.4.1
redo==2.0.3 redo==2.0.3
requests==2.25.1 requests==2.25.1
requests-unixsocket==0.2.0 requests-unixsocket==0.2.0
responses==0.10.6 responses==0.10.6
rsa==3.1.4 rsa==3.1.4
sentry-sdk==0.14.3 sentry-sdk==0.14.3
setuptools==51.2.0 setuptools==51.2.0
six==1.13.0 six==1.13.0
slugid==2.0.0 slugid==2.0.0
taskcluster==44.2.2 taskcluster==44.2.2
taskcluster-taskgraph==2.0.0 taskcluster-taskgraph==3.2.0
taskcluster-urls==13.0.1 taskcluster-urls==13.0.1
tqdm==4.62.3 tqdm==4.62.3
urllib3==1.26 urllib3==1.26
voluptuous==0.12.1 voluptuous==0.12.1
wheel==0.37.0 wheel==0.37.0
yamllint==1.23 yamllint==1.23

View file

@ -1,395 +1,401 @@
aiohttp==3.7.4.post0; python_version >= "3.6" \ aiohttp==3.7.4.post0; python_version >= "3.6" \
--hash=sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5 \ --hash=sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5 \
--hash=sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8 \ --hash=sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8 \
--hash=sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95 \ --hash=sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95 \
--hash=sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290 \ --hash=sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290 \
--hash=sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f \ --hash=sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f \
--hash=sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809 \ --hash=sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809 \
--hash=sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe \ --hash=sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe \
--hash=sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287 \ --hash=sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287 \
--hash=sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc \ --hash=sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc \
--hash=sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87 \ --hash=sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87 \
--hash=sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0 \ --hash=sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0 \
--hash=sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970 \ --hash=sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970 \
--hash=sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f \ --hash=sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f \
--hash=sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde \ --hash=sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde \
--hash=sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c \ --hash=sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c \
--hash=sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8 \ --hash=sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8 \
--hash=sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f \ --hash=sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f \
--hash=sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5 \ --hash=sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5 \
--hash=sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf \ --hash=sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf \
--hash=sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df \ --hash=sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df \
--hash=sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213 \ --hash=sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213 \
--hash=sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4 \ --hash=sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4 \
--hash=sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009 \ --hash=sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009 \
--hash=sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5 \ --hash=sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5 \
--hash=sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013 \ --hash=sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013 \
--hash=sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16 \ --hash=sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16 \
--hash=sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5 \ --hash=sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5 \
--hash=sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b \ --hash=sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b \
--hash=sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd \ --hash=sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd \
--hash=sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439 \ --hash=sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439 \
--hash=sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22 \ --hash=sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22 \
--hash=sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a \ --hash=sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a \
--hash=sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb \ --hash=sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb \
--hash=sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb \ --hash=sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb \
--hash=sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9 \ --hash=sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9 \
--hash=sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe \ --hash=sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe \
--hash=sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf --hash=sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf
appdirs==1.4.4 \ appdirs==1.4.4 \
--hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \
--hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41
async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" \ async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" \
--hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \ --hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \
--hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3 --hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3
attrs==19.2.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ attrs==19.2.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2 \ --hash=sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2 \
--hash=sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396 --hash=sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396
blessings==1.7; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ blessings==1.7; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e \ --hash=sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e \
--hash=sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3 \ --hash=sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3 \
--hash=sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d --hash=sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d
cbor2==4.0.1 \ cbor2==4.0.1 \
--hash=sha256:b0eb916c9ea226aa81e9091607737475d5b0e5c314fe8d5a87179fba449cd190 \ --hash=sha256:b0eb916c9ea226aa81e9091607737475d5b0e5c314fe8d5a87179fba449cd190 \
--hash=sha256:cee0d01e520563b5a73c72eace5c428bb68aefb1b3f7aee5d692d3af6a1e5172 --hash=sha256:cee0d01e520563b5a73c72eace5c428bb68aefb1b3f7aee5d692d3af6a1e5172
certifi==2018.4.16 \ certifi==2018.4.16 \
--hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0 \ --hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0 \
--hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7 --hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7
chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \ chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \ --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
click==7.1.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" \ click==7.1.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" \
--hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \
--hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a
colorama==0.4.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ colorama==0.4.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \
--hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4
compare-locales==8.2.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \ compare-locales==8.2.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \
--hash=sha256:470d50d96c68f8e147daa3d70f29a7b750adefea450c5fa07e0f666c8083d854 \ --hash=sha256:470d50d96c68f8e147daa3d70f29a7b750adefea450c5fa07e0f666c8083d854 \
--hash=sha256:e6a1610151d357e74ee6c1f5e944f1868e449f83e478c84d92f7b86132f721d7 --hash=sha256:e6a1610151d357e74ee6c1f5e944f1868e449f83e478c84d92f7b86132f721d7
cookies==2.2.1 \ cookies==2.2.1 \
--hash=sha256:15bee753002dff684987b8df8c235288eb8d45f8191ae056254812dfd42c81d3 \ --hash=sha256:15bee753002dff684987b8df8c235288eb8d45f8191ae056254812dfd42c81d3 \
--hash=sha256:d6b698788cae4cfa4e62ef8643a9ca332b79bd96cb314294b864ae8d7eb3ee8e --hash=sha256:d6b698788cae4cfa4e62ef8643a9ca332b79bd96cb314294b864ae8d7eb3ee8e
cram==0.7 \ cram==0.7 \
--hash=sha256:008e4e8b4d325cf040964b5f62460535b004a7bc816d54f8527a4d299edfe4a3 \ --hash=sha256:008e4e8b4d325cf040964b5f62460535b004a7bc816d54f8527a4d299edfe4a3 \
--hash=sha256:7da7445af2ce15b90aad5ec4792f857cef5786d71f14377e9eb994d8b8337f2f --hash=sha256:7da7445af2ce15b90aad5ec4792f857cef5786d71f14377e9eb994d8b8337f2f
diskcache==4.1.0 \ diskcache==4.1.0 \
--hash=sha256:69b253a6ffe95bb4bafb483b97c24fca3c2c6c47b82e92b36486969a7e80d47d \ --hash=sha256:69b253a6ffe95bb4bafb483b97c24fca3c2c6c47b82e92b36486969a7e80d47d \
--hash=sha256:bcee5a59f9c264e2809e58d01be6569a3bbb1e36a1e0fb83f7ef9b2075f95ce0 --hash=sha256:bcee5a59f9c264e2809e58d01be6569a3bbb1e36a1e0fb83f7ef9b2075f95ce0
distro==1.4.0 \ distro==1.4.0 \
--hash=sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4 \ --hash=sha256:eedf82a470ebe7d010f1872c17237c79ab04097948800029994fa458e52fb4b4 \
--hash=sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57 --hash=sha256:362dde65d846d23baee4b5c058c8586f219b5a54be1cf5fc6ff55c4578392f57
ecdsa==0.15; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") \ ecdsa==0.15; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") \
--hash=sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061 \ --hash=sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061 \
--hash=sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277 --hash=sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277
esprima==4.0.1 \ esprima==4.0.1 \
--hash=sha256:08db1a876d3c2910db9cfaeb83108193af5411fc3a3a66ebefacd390d21323ee --hash=sha256:08db1a876d3c2910db9cfaeb83108193af5411fc3a3a66ebefacd390d21323ee
fluent.migrate==0.11 \ fluent.migrate==0.11 \
--hash=sha256:3b93fdba9cbc8702d160367ba3a0d5c120707fdde752af35aecf516ce80ed252 --hash=sha256:3b93fdba9cbc8702d160367ba3a0d5c120707fdde752af35aecf516ce80ed252
fluent.syntax==0.18.1 \ fluent.syntax==0.18.1 \
--hash=sha256:0e63679fa4f1b3042565220a5127b4bab842424f07d6a13c12299e3b3835486a \ --hash=sha256:0e63679fa4f1b3042565220a5127b4bab842424f07d6a13c12299e3b3835486a \
--hash=sha256:3a55f5e605d1b029a65cc8b6492c86ec4608e15447e73db1495de11fd46c104f --hash=sha256:3a55f5e605d1b029a65cc8b6492c86ec4608e15447e73db1495de11fd46c104f
glean-parser==6.1.2 \ giturlparse==0.10.0; python_version >= "3.6" \
--hash=sha256:e801af6463b7e0ba79d97ddfc0a58d9d71121c93cea601417571e33fa8142270 \ --hash=sha256:04ba1a3a099c3093fa8d24a422913c6a9b2c2cd22bcffc939cf72e3e98f672d7 \
--hash=sha256:12a0fecedc1144d77fa571e0422ff3fea4dbadc381d631bea800a6b2f58f4f7f --hash=sha256:2595ab291d30717cda8474b874c9fd509f1b9802ad7f6968c36a45e4b13eb337
idna-ssl==1.1.0; python_version < "3.7" and python_version >= "3.6" \ glean-parser==6.1.2 \
--hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c --hash=sha256:e801af6463b7e0ba79d97ddfc0a58d9d71121c93cea601417571e33fa8142270 \
idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.7" or python_version < "3.7" and python_version >= "3.6" and python_full_version >= "3.4.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \ --hash=sha256:12a0fecedc1144d77fa571e0422ff3fea4dbadc381d631bea800a6b2f58f4f7f
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \ idna-ssl==1.1.0; python_version < "3.7" and python_version >= "3.6" \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 --hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c
importlib-metadata==1.7.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.7" or python_version < "3.7" and python_version >= "3.6" and python_full_version >= "3.4.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \
--hash=sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070 \ --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83 --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
iso8601==0.1.14; python_version <= "3.6" \ importlib-metadata==1.7.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:e7e1122f064d626e17d47cd5106bed2c620cb38fe464999e0ddae2b6d2de6004 \ --hash=sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070 \
--hash=sha256:8aafd56fa0290496c5edbb13c311f78fa3a241f0853540da09d9363eae3ebd79 --hash=sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83
jinja2==2.11.3; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" \ iso8601==0.1.14; python_version <= "3.6" \
--hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \ --hash=sha256:e7e1122f064d626e17d47cd5106bed2c620cb38fe464999e0ddae2b6d2de6004 \
--hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 --hash=sha256:8aafd56fa0290496c5edbb13c311f78fa3a241f0853540da09d9363eae3ebd79
jsmin==2.1.0 \ jinja2==2.11.3; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" \
--hash=sha256:5d07bf0251a4128e5e8e8eef603849b6b5741c337bff087731a248f9cc774f56 --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \
json-e==2.7.0 \ --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6
--hash=sha256:d8c1ec3f5bbc7728c3a504ebe58829f283c64eca230871e4eefe974b4cdaae4a jsmin==2.1.0 \
jsonschema==3.2.0 \ --hash=sha256:5d07bf0251a4128e5e8e8eef603849b6b5741c337bff087731a248f9cc774f56
--hash=sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163 \ json-e==2.7.0 \
--hash=sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a --hash=sha256:d8c1ec3f5bbc7728c3a504ebe58829f283c64eca230871e4eefe974b4cdaae4a
looseversion==1.0.1; python_version >= "3" \ jsonschema==3.2.0 \
--hash=sha256:a205beabd0ffd40488edb9ccb3a39134510fc7c0c2847a25079f559e59c004ac \ --hash=sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163 \
--hash=sha256:b339dfde67680e9c5c2e96673e52bee9f94d2f0e1b8f4cbfd86d32311e86b952 --hash=sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a
markupsafe==1.1.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" or python_full_version >= "3.5.0" \ looseversion==1.0.1; python_version >= "3" \
--hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \ --hash=sha256:a205beabd0ffd40488edb9ccb3a39134510fc7c0c2847a25079f559e59c004ac \
--hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ --hash=sha256:b339dfde67680e9c5c2e96673e52bee9f94d2f0e1b8f4cbfd86d32311e86b952
--hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \ markupsafe==1.1.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" or python_full_version >= "3.5.0" \
--hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \ --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
--hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \ --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
--hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \ --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
--hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \ --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
--hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \ --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
--hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \ --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
--hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \ --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
--hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \ --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
--hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \ --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
--hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \ --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
--hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \ --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
--hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \ --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
--hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \ --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
--hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \ --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
--hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \ --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
--hash=sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5 \ --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
--hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \ --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
--hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \ --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
--hash=sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f \ --hash=sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5 \
--hash=sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0 \ --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
--hash=sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7 \ --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
--hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \ --hash=sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f \
--hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \ --hash=sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0 \
--hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \ --hash=sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7 \
--hash=sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193 \ --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
--hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \ --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
--hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
--hash=sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1 \ --hash=sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193 \
--hash=sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1 \ --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
--hash=sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f \ --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
--hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \ --hash=sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1 \
--hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \ --hash=sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1 \
--hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \ --hash=sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f \
--hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \ --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
--hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \ --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
--hash=sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2 \ --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \
--hash=sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032 \ --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \
--hash=sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b \ --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \
--hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \ --hash=sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2 \
--hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ --hash=sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032 \
--hash=sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c \ --hash=sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b \
--hash=sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb \ --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \
--hash=sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014 \ --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \
--hash=sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850 \ --hash=sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c \
--hash=sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85 \ --hash=sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb \
--hash=sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621 \ --hash=sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014 \
--hash=sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39 \ --hash=sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850 \
--hash=sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8 \ --hash=sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85 \
--hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b --hash=sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621 \
mohawk==0.3.4 \ --hash=sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39 \
--hash=sha256:b3f85ffa93a5c7d2f9cc591246ef9f8ac4a9fa716bfd5bae0377699a2d89d78c \ --hash=sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8 \
--hash=sha256:e98b331d9fa9ece7b8be26094cbe2d57613ae882133cc755167268a984bc0ab3 --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b
mozilla-version==0.3.4 \ mohawk==0.3.4 \
--hash=sha256:3ed4deb7a6fb25c83a5346ef4de08ddff9b2ddc4d16dd8fafb4a84978cc71255 \ --hash=sha256:b3f85ffa93a5c7d2f9cc591246ef9f8ac4a9fa716bfd5bae0377699a2d89d78c \
--hash=sha256:ce5741c2e7d12c30b53de9f79e30d6ac2a8bd4c93be711d30c7a7a08e32a094f --hash=sha256:e98b331d9fa9ece7b8be26094cbe2d57613ae882133cc755167268a984bc0ab3
multidict==5.1.0; python_version >= "3.6" \ mozilla-repo-urls==0.0.3 \
--hash=sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f \ --hash=sha256:3b2f9b42111ce3d50ecdcd081a62229e1fdc8f5472adbf405abe12d3ba8e8af5 \
--hash=sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf \ --hash=sha256:673c80a4d0ed449093203b88e119e0bf1074026f891c1dd50aa04d534fd0658c
--hash=sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281 \ mozilla-version==0.3.4 \
--hash=sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d \ --hash=sha256:3ed4deb7a6fb25c83a5346ef4de08ddff9b2ddc4d16dd8fafb4a84978cc71255 \
--hash=sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d \ --hash=sha256:ce5741c2e7d12c30b53de9f79e30d6ac2a8bd4c93be711d30c7a7a08e32a094f
--hash=sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da \ multidict==5.1.0; python_version >= "3.6" \
--hash=sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224 \ --hash=sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f \
--hash=sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26 \ --hash=sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf \
--hash=sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6 \ --hash=sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281 \
--hash=sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76 \ --hash=sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d \
--hash=sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a \ --hash=sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d \
--hash=sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f \ --hash=sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da \
--hash=sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348 \ --hash=sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224 \
--hash=sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93 \ --hash=sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26 \
--hash=sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9 \ --hash=sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6 \
--hash=sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37 \ --hash=sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76 \
--hash=sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5 \ --hash=sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a \
--hash=sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632 \ --hash=sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f \
--hash=sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952 \ --hash=sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348 \
--hash=sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79 \ --hash=sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93 \
--hash=sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456 \ --hash=sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9 \
--hash=sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7 \ --hash=sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37 \
--hash=sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635 \ --hash=sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5 \
--hash=sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a \ --hash=sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632 \
--hash=sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea \ --hash=sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952 \
--hash=sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656 \ --hash=sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79 \
--hash=sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3 \ --hash=sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456 \
--hash=sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93 \ --hash=sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7 \
--hash=sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647 \ --hash=sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635 \
--hash=sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d \ --hash=sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a \
--hash=sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8 \ --hash=sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea \
--hash=sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1 \ --hash=sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656 \
--hash=sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841 \ --hash=sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3 \
--hash=sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda \ --hash=sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93 \
--hash=sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80 \ --hash=sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647 \
--hash=sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359 \ --hash=sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d \
--hash=sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5 --hash=sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8 \
packaging==20.9; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ --hash=sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1 \
--hash=sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a \ --hash=sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841 \
--hash=sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5 --hash=sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda \
pathspec==0.9.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ --hash=sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80 \
--hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ --hash=sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359 \
--hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 --hash=sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5
pip-tools==5.5.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ packaging==20.9; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:cb0108391366b3ef336185097b3c2c0f3fa115b15098dafbda5e78aef70ea114 \ --hash=sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a \
--hash=sha256:10841c1e56c234d610d0466447685b9ea4ee4a2c274f858c0ef3c33d9bd0d985 --hash=sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5
pip==21.2.4; python_version >= "3.6" \ pathspec==0.9.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:fa9ebb85d3fd607617c0c44aca302b1b45d87f9c2a1649b46c26167ca4296323 \ --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
--hash=sha256:0eb8a1516c3d138ae8689c0c1a60fde7143310832f9dc77e11d8a4bc62de193b --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1
ply==3.10 \ pip-tools==5.5.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:96e94af7dd7031d8d6dd6e2a8e0de593b511c211a86e28a9c9621c275ac8bacb --hash=sha256:cb0108391366b3ef336185097b3c2c0f3fa115b15098dafbda5e78aef70ea114 \
pyasn1-modules==0.2.8 \ --hash=sha256:10841c1e56c234d610d0466447685b9ea4ee4a2c274f858c0ef3c33d9bd0d985
--hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \ pip==21.2.4; python_version >= "3.6" \
--hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \ --hash=sha256:fa9ebb85d3fd607617c0c44aca302b1b45d87f9c2a1649b46c26167ca4296323 \
--hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \ --hash=sha256:0eb8a1516c3d138ae8689c0c1a60fde7143310832f9dc77e11d8a4bc62de193b
--hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \ ply==3.10 \
--hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \ --hash=sha256:96e94af7dd7031d8d6dd6e2a8e0de593b511c211a86e28a9c9621c275ac8bacb
--hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \ pyasn1-modules==0.2.8 \
--hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
--hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \ --hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \
--hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \ --hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \
--hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \ --hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \
--hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \ --hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \
--hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \
--hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd --hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \
pyasn1==0.4.8 \ --hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \
--hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \ --hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \
--hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \ --hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \
--hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \ --hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \
--hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \ --hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ --hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd
--hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \ pyasn1==0.4.8 \
--hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \ --hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \
--hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \ --hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \
--hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \ --hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \
--hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \ --hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \
--hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
--hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \ --hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba --hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \
pylru==1.0.9 \ --hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \
--hash=sha256:71376192671f0ad1690b2a7427d39a29b1df994c8469a9b46b03ed7e28c0172c --hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \
pyparsing==2.4.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" \ --hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \
--hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \ --hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \
--hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 --hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \
pyrsistent==0.16.0 \ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
--hash=sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3 pylru==1.0.9 \
python-hglib==2.4 \ --hash=sha256:71376192671f0ad1690b2a7427d39a29b1df994c8469a9b46b03ed7e28c0172c
--hash=sha256:693d6ed92a6566e78802c7a03c256cda33d08c63ad3f00fcfa11379b184b9462 pyparsing==2.4.7; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" \
pytoml==0.1.10 \ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
--hash=sha256:98399eabd927cd3e12457525315b6abbc5abf9a6f392ab578cbcec327f73890c --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1
pyyaml==5.4.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \ pyrsistent==0.16.0 \
--hash=sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922 \ --hash=sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3
--hash=sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393 \ python-hglib==2.4 \
--hash=sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8 \ --hash=sha256:693d6ed92a6566e78802c7a03c256cda33d08c63ad3f00fcfa11379b184b9462
--hash=sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185 \ pytoml==0.1.10 \
--hash=sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253 \ --hash=sha256:98399eabd927cd3e12457525315b6abbc5abf9a6f392ab578cbcec327f73890c
--hash=sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc \ pyyaml==5.4.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \
--hash=sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347 \ --hash=sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922 \
--hash=sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541 \ --hash=sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393 \
--hash=sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5 \ --hash=sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8 \
--hash=sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df \ --hash=sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185 \
--hash=sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018 \ --hash=sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253 \
--hash=sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63 \ --hash=sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc \
--hash=sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa \ --hash=sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347 \
--hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0 \ --hash=sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541 \
--hash=sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b \ --hash=sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5 \
--hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \ --hash=sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df \
--hash=sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46 \ --hash=sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018 \
--hash=sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb \ --hash=sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63 \
--hash=sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247 \ --hash=sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa \
--hash=sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc \ --hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0 \
--hash=sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc \ --hash=sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b \
--hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \
--hash=sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77 \ --hash=sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46 \
--hash=sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183 \ --hash=sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb \
--hash=sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122 \ --hash=sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247 \
--hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \ --hash=sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc \
--hash=sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10 \ --hash=sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc \
--hash=sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db \ --hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \
--hash=sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e --hash=sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77 \
redo==2.0.3 \ --hash=sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183 \
--hash=sha256:36784bf8ae766e14f9db0e377ccfa02835d648321d2007b6ae0bf4fd612c0f94 \ --hash=sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122 \
--hash=sha256:71161cb0e928d824092a5f16203939bbc0867ce4c4685db263cf22c3ae7634a8 --hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \
requests-unixsocket==0.2.0 \ --hash=sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10 \
--hash=sha256:9e5c1a20afc3cf786197ae59c79bcdb0e7565f218f27df5f891307ee8817c1ea \ --hash=sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db \
--hash=sha256:014d07bfb66dc805a011a8b4b306cf4ec96d2eddb589f6b2b5765e626f0dc0cc --hash=sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e
requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ redo==2.0.3 \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \ --hash=sha256:36784bf8ae766e14f9db0e377ccfa02835d648321d2007b6ae0bf4fd612c0f94 \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 --hash=sha256:71161cb0e928d824092a5f16203939bbc0867ce4c4685db263cf22c3ae7634a8
responses==0.10.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ requests-unixsocket==0.2.0 \
--hash=sha256:97193c0183d63fba8cd3a041c75464e4b09ea0aff6328800d1546598567dde0b \ --hash=sha256:9e5c1a20afc3cf786197ae59c79bcdb0e7565f218f27df5f891307ee8817c1ea \
--hash=sha256:502d9c0c8008439cfcdef7e251f507fcfdd503b56e8c0c87c3c3e3393953f790 --hash=sha256:014d07bfb66dc805a011a8b4b306cf4ec96d2eddb589f6b2b5765e626f0dc0cc
rsa==3.1.4 \ requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:e2b0b05936c276b1edd2e1525553233b666df9e29b5c3ba223eed738277c82a0 --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
sentry-sdk==0.14.3 \ --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
--hash=sha256:bb90a4e19c7233a580715fc986cc44be2c48fc10b31e71580a2037e1c94b6950 \ responses==0.10.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:23808d571d2461a4ce3784ec12bbee5bdb8c026c143fe79d36cef8a6d653e71f --hash=sha256:97193c0183d63fba8cd3a041c75464e4b09ea0aff6328800d1546598567dde0b \
setuptools==51.2.0; python_version >= "3.6" \ --hash=sha256:502d9c0c8008439cfcdef7e251f507fcfdd503b56e8c0c87c3c3e3393953f790
--hash=sha256:56948bf25c682e166cf2bfe7c1ad63e5745849b50d1ae7b0f8bff5decdcf34f2 \ rsa==3.1.4 \
--hash=sha256:7ef59b1790b3491f8d321f531eccc11517a07a4d7637e498465cd834d80d4c2c --hash=sha256:e2b0b05936c276b1edd2e1525553233b666df9e29b5c3ba223eed738277c82a0
six==1.13.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0") \ sentry-sdk==0.14.3 \
--hash=sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd \ --hash=sha256:bb90a4e19c7233a580715fc986cc44be2c48fc10b31e71580a2037e1c94b6950 \
--hash=sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66 --hash=sha256:23808d571d2461a4ce3784ec12bbee5bdb8c026c143fe79d36cef8a6d653e71f
slugid==2.0.0 \ setuptools==51.2.0; python_version >= "3.6" \
--hash=sha256:aec8b0e01c4ad32e38e12d609eab3ec912fd129aaf6b2ded0199b56a5f8fd67c \ --hash=sha256:56948bf25c682e166cf2bfe7c1ad63e5745849b50d1ae7b0f8bff5decdcf34f2 \
--hash=sha256:a950d98b72691178bdd4d6c52743c4a2aa039207cf7a97d71060a111ff9ba297 --hash=sha256:7ef59b1790b3491f8d321f531eccc11517a07a4d7637e498465cd834d80d4c2c
taskcluster-taskgraph==2.0.0 \ six==1.13.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0") \
--hash=sha256:93eff40ba39a29cd290fc25a2124ed9bf5806d87891edd7e8de35df568708141 \ --hash=sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd \
--hash=sha256:3d22ab488071ddc82997b33fc6c1c524a44bdc7e14b30a274d99dbbdd7389502 --hash=sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66
taskcluster-urls==13.0.1 \ slugid==2.0.0 \
--hash=sha256:b25e122ecec249c4299ac7b20b08db76e3e2025bdaeb699a9d444556de5fd367 \ --hash=sha256:aec8b0e01c4ad32e38e12d609eab3ec912fd129aaf6b2ded0199b56a5f8fd67c \
--hash=sha256:5e25e7e6818e8877178b175ff43d2e6548afad72694aa125f404a7329ece0973 \ --hash=sha256:a950d98b72691178bdd4d6c52743c4a2aa039207cf7a97d71060a111ff9ba297
--hash=sha256:f66dcbd6572a6216ab65949f0fa0b91f2df647918028436c384e6af5cd12ae2b taskcluster-taskgraph==3.2.0 \
taskcluster==44.2.2 \ --hash=sha256:941d96fd36c20b0e84e322d2c25be04fe5f258405793af5e086bbec698cbcb17 \
--hash=sha256:c1b0e82be25b1ed17e07c90b24a382634b2bfce273fdf2682d94568abe10716c \ --hash=sha256:fafcd1bfd8ec8b2f57db4e4d87d5b881da3ad7119d78d407d22b4b71b805d1bf
--hash=sha256:846d73c597f0f47dd8525c85c8d9bc41111d5200b090690d3f16b2f57c56a2e1 \ taskcluster-urls==13.0.1 \
--hash=sha256:0266a6a901e1a2ec838984a7f24e7adb6d58f9f2e221a7f613388f8f23f786fc --hash=sha256:b25e122ecec249c4299ac7b20b08db76e3e2025bdaeb699a9d444556de5fd367 \
tqdm==4.62.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ --hash=sha256:5e25e7e6818e8877178b175ff43d2e6548afad72694aa125f404a7329ece0973 \
--hash=sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c \ --hash=sha256:f66dcbd6572a6216ab65949f0fa0b91f2df647918028436c384e6af5cd12ae2b
--hash=sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d taskcluster==44.2.2 \
typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" or python_version >= "3.6" \ --hash=sha256:c1b0e82be25b1ed17e07c90b24a382634b2bfce273fdf2682d94568abe10716c \
--hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \ --hash=sha256:846d73c597f0f47dd8525c85c8d9bc41111d5200b090690d3f16b2f57c56a2e1 \
--hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \ --hash=sha256:0266a6a901e1a2ec838984a7f24e7adb6d58f9f2e221a7f613388f8f23f786fc
--hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342 tqdm==4.62.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
urllib3==1.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \ --hash=sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c \
--hash=sha256:bad31cb622ceee0ab46c4c884cf61957def0ff2e644de0a7a093678844c9ccac \ --hash=sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d
--hash=sha256:4849f132941d68144df0a3785ccc4fe423430ba5db0108d045c8cadbc90f517a typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" or python_version >= "3.6" \
voluptuous==0.12.1 \ --hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \
--hash=sha256:8ace33fcf9e6b1f59406bfaf6b8ec7bcc44266a9f29080b4deb4fe6ff2492386 \ --hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \
--hash=sha256:663572419281ddfaf4b4197fd4942d181630120fb39b333e3adad70aeb56444b --hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342
wheel==0.37.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ urllib3==1.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \
--hash=sha256:21014b2bd93c6d0034b6ba5d35e4eb284340e09d63c59aef6fc14b0f346146fd \ --hash=sha256:bad31cb622ceee0ab46c4c884cf61957def0ff2e644de0a7a093678844c9ccac \
--hash=sha256:e2ef7239991699e3355d54f8e968a21bb940a1dbf34a4d226741e64462516fad --hash=sha256:4849f132941d68144df0a3785ccc4fe423430ba5db0108d045c8cadbc90f517a
yamllint==1.23.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ voluptuous==0.12.1 \
--hash=sha256:0fa69bf8a86182b7fe14918bdd3a30354c869966bbc7cbfff176af71bda9c806 \ --hash=sha256:8ace33fcf9e6b1f59406bfaf6b8ec7bcc44266a9f29080b4deb4fe6ff2492386 \
--hash=sha256:59f3ff77f44e7f46be6aecdb985830f73a1c51e290b7082a7d38c2ae1940f4a9 --hash=sha256:663572419281ddfaf4b4197fd4942d181630120fb39b333e3adad70aeb56444b
yarl==1.6.3; python_version >= "3.6" \ wheel==0.37.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434 \ --hash=sha256:21014b2bd93c6d0034b6ba5d35e4eb284340e09d63c59aef6fc14b0f346146fd \
--hash=sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478 \ --hash=sha256:e2ef7239991699e3355d54f8e968a21bb940a1dbf34a4d226741e64462516fad
--hash=sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6 \ yamllint==1.23.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e \ --hash=sha256:0fa69bf8a86182b7fe14918bdd3a30354c869966bbc7cbfff176af71bda9c806 \
--hash=sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406 \ --hash=sha256:59f3ff77f44e7f46be6aecdb985830f73a1c51e290b7082a7d38c2ae1940f4a9
--hash=sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76 \ yarl==1.6.3; python_version >= "3.6" \
--hash=sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366 \ --hash=sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434 \
--hash=sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721 \ --hash=sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478 \
--hash=sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643 \ --hash=sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6 \
--hash=sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e \ --hash=sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e \
--hash=sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3 \ --hash=sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406 \
--hash=sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8 \ --hash=sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76 \
--hash=sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a \ --hash=sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366 \
--hash=sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c \ --hash=sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721 \
--hash=sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f \ --hash=sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643 \
--hash=sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970 \ --hash=sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e \
--hash=sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e \ --hash=sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3 \
--hash=sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50 \ --hash=sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8 \
--hash=sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2 \ --hash=sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a \
--hash=sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec \ --hash=sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c \
--hash=sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71 \ --hash=sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f \
--hash=sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc \ --hash=sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970 \
--hash=sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959 \ --hash=sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e \
--hash=sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2 \ --hash=sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50 \
--hash=sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2 \ --hash=sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2 \
--hash=sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896 \ --hash=sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec \
--hash=sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a \ --hash=sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71 \
--hash=sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e \ --hash=sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc \
--hash=sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724 \ --hash=sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959 \
--hash=sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c \ --hash=sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2 \
--hash=sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25 \ --hash=sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2 \
--hash=sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96 \ --hash=sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896 \
--hash=sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0 \ --hash=sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a \
--hash=sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4 \ --hash=sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e \
--hash=sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424 \ --hash=sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724 \
--hash=sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6 \ --hash=sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c \
--hash=sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10 --hash=sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25 \
zipp==3.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.6" \ --hash=sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96 \
--hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 \ --hash=sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0 \
--hash=sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76 --hash=sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4 \
--hash=sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424 \
--hash=sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6 \
--hash=sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10
zipp==3.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.6" \
--hash=sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 \
--hash=sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76

View file

@ -1,6 +1,6 @@
Metadata-Version: 2.1 Metadata-Version: 2.1
Name: taskcluster-taskgraph Name: taskcluster-taskgraph
Version: 3.0.0 Version: 3.2.0
Summary: Build taskcluster taskgraphs Summary: Build taskcluster taskgraphs
Home-page: https://github.com/taskcluster/taskgraph Home-page: https://github.com/taskcluster/taskgraph
License: UNKNOWN License: UNKNOWN
@ -18,6 +18,7 @@ License-File: LICENSE
Requires-Dist: appdirs (>=1.4) Requires-Dist: appdirs (>=1.4)
Requires-Dist: attrs (>=19.1.0) Requires-Dist: attrs (>=19.1.0)
Requires-Dist: json-e (>=2.7) Requires-Dist: json-e (>=2.7)
Requires-Dist: mozilla-repo-urls
Requires-Dist: PyYAML (>=5.4) Requires-Dist: PyYAML (>=5.4)
Requires-Dist: redo (>=2.0) Requires-Dist: redo (>=2.0)
Requires-Dist: requests (>=2.25) Requires-Dist: requests (>=2.25)

View file

@ -9,7 +9,7 @@ taskgraph/generator.py,sha256=ZfSb8dek6tQRxfpHbvQP2KMxXFzmhqwN821tOlNcvzo,15118
taskgraph/graph.py,sha256=9tE3bSSBRHvRLgJzK4dTieGT3RrzQZdR1YbKizEhzlw,4667 taskgraph/graph.py,sha256=9tE3bSSBRHvRLgJzK4dTieGT3RrzQZdR1YbKizEhzlw,4667
taskgraph/main.py,sha256=E7dC1q14L4psrNfUe-PMC8QH4cYjsIs91I-aVmzeBaI,23551 taskgraph/main.py,sha256=E7dC1q14L4psrNfUe-PMC8QH4cYjsIs91I-aVmzeBaI,23551
taskgraph/morph.py,sha256=8qxYdruEQkbHGqv7dh3e1OWhH9Y5i6bFUKzDMs-Ctnw,9625 taskgraph/morph.py,sha256=8qxYdruEQkbHGqv7dh3e1OWhH9Y5i6bFUKzDMs-Ctnw,9625
taskgraph/parameters.py,sha256=rye7dxD3A_Voh9w0Ru28zgZ8rGVv5enUu-k5lE7HvEk,11725 taskgraph/parameters.py,sha256=Ke-07fA2Qcynzbp-wfmO-VnQ4BGF9inr0hsrC5iyL-8,11792
taskgraph/target_tasks.py,sha256=41BIVwiATy8DCQujPduTtnFmgHlKOfw6RPGL4b20WO8,3324 taskgraph/target_tasks.py,sha256=41BIVwiATy8DCQujPduTtnFmgHlKOfw6RPGL4b20WO8,3324
taskgraph/task.py,sha256=QCrOzMaTsy5QHShKUo89XgjJVMl3cSZGZJPLuHCXItE,3132 taskgraph/task.py,sha256=QCrOzMaTsy5QHShKUo89XgjJVMl3cSZGZJPLuHCXItE,3132
taskgraph/taskgraph.py,sha256=tfj0ZMqjuwEQDET0W57EcP-_KBEbqkxJci9Z6DkeOEQ,2397 taskgraph/taskgraph.py,sha256=tfj0ZMqjuwEQDET0W57EcP-_KBEbqkxJci9Z6DkeOEQ,2397
@ -17,7 +17,7 @@ taskgraph/actions/__init__.py,sha256=lVP1e0YyELg7-_42MWWDbT0cKv_p53BApVE6vWOiPww
taskgraph/actions/add_new_jobs.py,sha256=mX_DFDJaQUHetjyMNi5b8zPCCeqfzDrCjDg5DxTaA-I,1831 taskgraph/actions/add_new_jobs.py,sha256=mX_DFDJaQUHetjyMNi5b8zPCCeqfzDrCjDg5DxTaA-I,1831
taskgraph/actions/cancel.py,sha256=UQSt_6y3S6PXNmUo_mNaUOuDvK2bixWjzdjTKXieEEg,1309 taskgraph/actions/cancel.py,sha256=UQSt_6y3S6PXNmUo_mNaUOuDvK2bixWjzdjTKXieEEg,1309
taskgraph/actions/cancel_all.py,sha256=-ETWKl8BHkk5HjGZRIJpUsFOySE6co0pL0dBDupolu8,1947 taskgraph/actions/cancel_all.py,sha256=-ETWKl8BHkk5HjGZRIJpUsFOySE6co0pL0dBDupolu8,1947
taskgraph/actions/registry.py,sha256=p-YTqnhRPSouOqhSoRL5QgUkpO_ab4XIMSFKreu7E_8,13252 taskgraph/actions/registry.py,sha256=xmhoEGMyYj6TTRFwMowZAUp0aqvtLvdVfmRWM7Yh7xo,13122
taskgraph/actions/retrigger.py,sha256=awSC8XRtPJxADz5tbEWTKdNEudG8SpwUOM7z2lXxH1U,9382 taskgraph/actions/retrigger.py,sha256=awSC8XRtPJxADz5tbEWTKdNEudG8SpwUOM7z2lXxH1U,9382
taskgraph/actions/util.py,sha256=jA5xXehV8N2G542LZOEci_gMHEFN-BrIjkA55On0kc0,10673 taskgraph/actions/util.py,sha256=jA5xXehV8N2G542LZOEci_gMHEFN-BrIjkA55On0kc0,10673
taskgraph/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 taskgraph/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@ -28,7 +28,7 @@ taskgraph/optimize/strategies.py,sha256=Y5fS-f_3xsQNfFjCXIwDxrwXBvyp4yZxdPVNh49c
taskgraph/run-task/fetch-content,sha256=uUoyua3OdIgynY5Q9K6EojBwuaM2zo2OiN9bmNS646Q,24291 taskgraph/run-task/fetch-content,sha256=uUoyua3OdIgynY5Q9K6EojBwuaM2zo2OiN9bmNS646Q,24291
taskgraph/run-task/hgrc,sha256=BybWLDR89bWi3pE5T05UqmDHs02CbLypE-omLZWU6Uk,896 taskgraph/run-task/hgrc,sha256=BybWLDR89bWi3pE5T05UqmDHs02CbLypE-omLZWU6Uk,896
taskgraph/run-task/robustcheckout.py,sha256=xc24zaBd6dyuoga1ace0M27jo14K4UXNwhqcbHutJ7U,28977 taskgraph/run-task/robustcheckout.py,sha256=xc24zaBd6dyuoga1ace0M27jo14K4UXNwhqcbHutJ7U,28977
taskgraph/run-task/run-task,sha256=TVjIoZO9kbpaG-GCMJV_wjlR9H2xk8vJi0wB_rFleEg,46953 taskgraph/run-task/run-task,sha256=76p0Zo19a6f4NkwTq8s9y4Emt3YW6Q-VdTInlcqjPjo,46956
taskgraph/transforms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 taskgraph/transforms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
taskgraph/transforms/base.py,sha256=N9ec4kw65V_J2KY4C4QRPlbIREbRDYwTlhClstYmOBU,5285 taskgraph/transforms/base.py,sha256=N9ec4kw65V_J2KY4C4QRPlbIREbRDYwTlhClstYmOBU,5285
taskgraph/transforms/cached_tasks.py,sha256=Z10VD1kEBVXJvj8qSsNTq2mYpklh0V1EN8OT6QK3v_E,2607 taskgraph/transforms/cached_tasks.py,sha256=Z10VD1kEBVXJvj8qSsNTq2mYpklh0V1EN8OT6QK3v_E,2607
@ -36,7 +36,7 @@ taskgraph/transforms/code_review.py,sha256=eE2xrDtdD_n3HT3caQ2HGAkPm6Uutdm4hDCpC
taskgraph/transforms/docker_image.py,sha256=ADiOUB-Ngm9Y6uwzGDpQsDJ_-4w6-ZYwLCxQ-0b16E0,7567 taskgraph/transforms/docker_image.py,sha256=ADiOUB-Ngm9Y6uwzGDpQsDJ_-4w6-ZYwLCxQ-0b16E0,7567
taskgraph/transforms/fetch.py,sha256=jxJw7wlEh_WxAa1Bmy2WIHfpdvL79PDsKwC1DFymbBQ,9584 taskgraph/transforms/fetch.py,sha256=jxJw7wlEh_WxAa1Bmy2WIHfpdvL79PDsKwC1DFymbBQ,9584
taskgraph/transforms/release_notifications.py,sha256=jrb9CCT-z_etDf690T-AeCvdzIoVWBAeM_FGoW7FIzA,3305 taskgraph/transforms/release_notifications.py,sha256=jrb9CCT-z_etDf690T-AeCvdzIoVWBAeM_FGoW7FIzA,3305
taskgraph/transforms/task.py,sha256=n73lD8XtzpJm2BqJpZb_oiGqNHBJzTcT7GWX6jk7Xqc,47839 taskgraph/transforms/task.py,sha256=kWic-qqvK8vEFxQwojRPxc42GAsdkxoV3HVcG1pdBxE,47942
taskgraph/transforms/job/__init__.py,sha256=GKYODycxov7u05owF_ZWgczd7WHi2yHTd8L5Ftvxge0,16929 taskgraph/transforms/job/__init__.py,sha256=GKYODycxov7u05owF_ZWgczd7WHi2yHTd8L5Ftvxge0,16929
taskgraph/transforms/job/common.py,sha256=onHnerPcmmvbSk0oHt8mvJmOo7AnjHQya0ombgMNLG8,7106 taskgraph/transforms/job/common.py,sha256=onHnerPcmmvbSk0oHt8mvJmOo7AnjHQya0ombgMNLG8,7106
taskgraph/transforms/job/index_search.py,sha256=Ngh9FFu1bx2kHVTChW2vcrbnb3SzMneRHopXk18RfB4,1220 taskgraph/transforms/job/index_search.py,sha256=Ngh9FFu1bx2kHVTChW2vcrbnb3SzMneRHopXk18RfB4,1220
@ -60,15 +60,15 @@ taskgraph/util/shell.py,sha256=MB9zHVSvxgOuszgmKr2rWUDahANZkbHHNkjjagZG_3I,1317
taskgraph/util/taskcluster.py,sha256=cGUGvkrefRHngjyZm_iQRYKRlGi4jMIr7ky0fi_YBrg,12445 taskgraph/util/taskcluster.py,sha256=cGUGvkrefRHngjyZm_iQRYKRlGi4jMIr7ky0fi_YBrg,12445
taskgraph/util/taskgraph.py,sha256=ecKEvTfmLVvEKLPO_0g34CqVvc0iCzuNMh3064BZNrE,1969 taskgraph/util/taskgraph.py,sha256=ecKEvTfmLVvEKLPO_0g34CqVvc0iCzuNMh3064BZNrE,1969
taskgraph/util/templates.py,sha256=Dqxfl244u-PX7dnsk3_vYyzDwpDgJtANK6NmZwN3Qow,1417 taskgraph/util/templates.py,sha256=Dqxfl244u-PX7dnsk3_vYyzDwpDgJtANK6NmZwN3Qow,1417
taskgraph/util/time.py,sha256=dmR9Y0IGKuE1eHfFZjDuBUroK63XLBxEMM5ploO4li4,3490 taskgraph/util/time.py,sha256=pNFcTH-iYRfm2-okm1lMATc4B5wO-_FXbOFXEtXD27g,3390
taskgraph/util/treeherder.py,sha256=XrdE-Je0ZvXe6_8f0DvvqNbrHherUk-hUuxirImPEIo,2138 taskgraph/util/treeherder.py,sha256=XrdE-Je0ZvXe6_8f0DvvqNbrHherUk-hUuxirImPEIo,2138
taskgraph/util/vcs.py,sha256=nCmvO_hHJIM4vIJ0vlpbQjdIFRtkpRImCikYde-C_R0,17328 taskgraph/util/vcs.py,sha256=i13idS8y9ooR216mnd1gksdjSgHBNlAZEdq7Xr-ROwE,18536
taskgraph/util/verify.py,sha256=YETuZVkwnfYe57GRPx2x_vedstgqdGiH46HLWAdcks8,8827 taskgraph/util/verify.py,sha256=YETuZVkwnfYe57GRPx2x_vedstgqdGiH46HLWAdcks8,8827
taskgraph/util/workertypes.py,sha256=5g2mgIbEKMzDpZNnmPMoMNyy7Wahi-jmWcV1amDAcPo,2341 taskgraph/util/workertypes.py,sha256=5g2mgIbEKMzDpZNnmPMoMNyy7Wahi-jmWcV1amDAcPo,2341
taskgraph/util/yaml.py,sha256=hfKI_D8Q7dimq4_VvO3WEh8CJsTrsIMwN6set7HIQbY,990 taskgraph/util/yaml.py,sha256=hfKI_D8Q7dimq4_VvO3WEh8CJsTrsIMwN6set7HIQbY,990
taskcluster_taskgraph-3.0.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725 taskcluster_taskgraph-3.2.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
taskcluster_taskgraph-3.0.0.dist-info/METADATA,sha256=rDJwBZW7nHDBPBcMH7n9eTnb2GONIfgG_YHTgsiB7no,1017 taskcluster_taskgraph-3.2.0.dist-info/METADATA,sha256=UDpk6d8wAiYmub9zhzF6H5uMIQ4JA91IeeLiZGQw0ls,1050
taskcluster_taskgraph-3.0.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 taskcluster_taskgraph-3.2.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
taskcluster_taskgraph-3.0.0.dist-info/entry_points.txt,sha256=VoXNtZpN4LvyXYB1wq47AU9CO-DMYMJ0VktKxjugzbY,51 taskcluster_taskgraph-3.2.0.dist-info/entry_points.txt,sha256=VoXNtZpN4LvyXYB1wq47AU9CO-DMYMJ0VktKxjugzbY,51
taskcluster_taskgraph-3.0.0.dist-info/top_level.txt,sha256=3JNeYn_hNiNXC7DrdH_vcv-WYSE7QdgGjdvUYvSjVp0,10 taskcluster_taskgraph-3.2.0.dist-info/top_level.txt,sha256=3JNeYn_hNiNXC7DrdH_vcv-WYSE7QdgGjdvUYvSjVp0,10
taskcluster_taskgraph-3.0.0.dist-info/RECORD,, taskcluster_taskgraph-3.2.0.dist-info/RECORD,,

View file

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.37.1)
Root-Is-Purelib: true
Tag: py3-none-any

View file

@ -7,6 +7,8 @@ import json
from collections import namedtuple from collections import namedtuple
from types import FunctionType from types import FunctionType
from mozilla_repo_urls import parse
from taskgraph import create from taskgraph import create
from taskgraph.config import load_graph_config from taskgraph.config import load_graph_config
from taskgraph.parameters import Parameters from taskgraph.parameters import Parameters
@ -291,24 +293,20 @@ def sanity_check_task_scope(callback, parameters, graph_config):
if action.cb_name == callback: if action.cb_name == callback:
break break
else: else:
raise Exception(f"No action with cb_name {callback}") raise ValueError(f"No action with cb_name {callback}")
actionPerm = "generic" if action.generic else action.cb_name actionPerm = "generic" if action.generic else action.cb_name
repo_param = "head_repository" repo_param = "head_repository"
head_repository = parameters[repo_param] raw_url = parameters[repo_param]
if not head_repository.startswith(("https://hg.mozilla.org", "https://github.com")): parsed_url = parse(raw_url)
raise Exception( expected_scope = f"assume:{parsed_url.taskcluster_role_prefix}:action:{actionPerm}"
"{} is not either https://hg.mozilla.org or https://github.com !"
)
expected_scope = f"assume:repo:{head_repository[8:]}:action:{actionPerm}"
# the scope should appear literally; no need for a satisfaction check. The use of # the scope should appear literally; no need for a satisfaction check. The use of
# get_current_scopes here calls the auth service through the Taskcluster Proxy, giving # get_current_scopes here calls the auth service through the Taskcluster Proxy, giving
# the precise scopes available to this task. # the precise scopes available to this task.
if expected_scope not in taskcluster.get_current_scopes(): if expected_scope not in taskcluster.get_current_scopes():
raise Exception(f"Expected task scope {expected_scope} for this action") raise ValueError(f"Expected task scope {expected_scope} for this action")
def trigger_action_callback( def trigger_action_callback(

View file

@ -160,6 +160,7 @@ def create_tasks(
target_task_graph.for_each_task(update_dependencies) target_task_graph.for_each_task(update_dependencies)
optimized_task_graph, label_to_taskid = optimize_task_graph( optimized_task_graph, label_to_taskid = optimize_task_graph(
target_task_graph, target_task_graph,
to_run,
params, params,
to_run, to_run,
decision_task_id, decision_task_id,

View file

@ -21,7 +21,7 @@ from taskgraph.parameters import Parameters, get_version
from taskgraph.taskgraph import TaskGraph from taskgraph.taskgraph import TaskGraph
from taskgraph.util.python_path import find_object from taskgraph.util.python_path import find_object
from taskgraph.util.schema import Schema, validate_schema from taskgraph.util.schema import Schema, validate_schema
from taskgraph.util.vcs import get_repository from taskgraph.util.vcs import Repository, get_repository
from taskgraph.util.yaml import load_yaml from taskgraph.util.yaml import load_yaml
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -143,6 +143,8 @@ def get_decision_parameters(graph_config, options):
n: options[n] n: options[n]
for n in [ for n in [
"base_repository", "base_repository",
"base_ref",
"base_rev",
"head_repository", "head_repository",
"head_rev", "head_rev",
"head_ref", "head_ref",
@ -166,6 +168,21 @@ def get_decision_parameters(graph_config, options):
except UnicodeDecodeError: except UnicodeDecodeError:
commit_message = "" commit_message = ""
parameters["base_ref"] = _determine_more_accurate_base_ref(
repo,
candidate_base_ref=options.get("base_ref"),
head_ref=options.get("head_ref"),
base_rev=options.get("base_rev"),
)
parameters["base_rev"] = _determine_more_accurate_base_rev(
repo,
base_ref=parameters["base_ref"],
candidate_base_rev=options.get("base_rev"),
head_rev=options.get("head_rev"),
env_prefix=_get_env_prefix(graph_config),
)
# Define default filter list, as most configurations shouldn't need # Define default filter list, as most configurations shouldn't need
# custom filters. # custom filters.
parameters["filters"] = [ parameters["filters"] = [
@ -236,6 +253,68 @@ def get_decision_parameters(graph_config, options):
return result return result
def _determine_more_accurate_base_ref(repo, candidate_base_ref, head_ref, base_rev):
base_ref = candidate_base_ref
if not candidate_base_ref:
base_ref = repo.default_branch
elif candidate_base_ref == head_ref and base_rev == Repository.NULL_REVISION:
logger.info(
"base_ref and head_ref are identical but base_rev equals the null revision. "
"This is a new branch but Github didn't identify its actual base."
)
base_ref = repo.default_branch
if base_ref != candidate_base_ref:
logger.info(
f'base_ref has been reset from "{candidate_base_ref}" to "{base_ref}".'
)
return base_ref
def _determine_more_accurate_base_rev(
repo, base_ref, candidate_base_rev, head_rev, env_prefix
):
if not candidate_base_rev:
logger.info("base_rev is not set.")
base_ref_or_rev = base_ref
elif candidate_base_rev == Repository.NULL_REVISION:
logger.info("base_rev equals the null revision. This branch is a new one.")
base_ref_or_rev = base_ref
elif not repo.does_revision_exist_locally(candidate_base_rev):
logger.warning(
"base_rev does not exist locally. It is likely because the branch was force-pushed. "
"taskgraph is not able to assess how many commits were changed and assumes it is only "
f"the last one. Please set the {env_prefix.upper()}_BASE_REV environment variable "
"in the decision task and provide `--base-rev` to taskgraph."
)
base_ref_or_rev = base_ref
else:
base_ref_or_rev = candidate_base_rev
if base_ref_or_rev == base_ref:
logger.info(
f'Using base_ref "{base_ref}" to determine latest common revision...'
)
base_rev = repo.find_latest_common_revision(base_ref_or_rev, head_rev)
if base_rev != candidate_base_rev:
if base_ref_or_rev == candidate_base_rev:
logger.info("base_rev is not an ancestor of head_rev.")
logger.info(
f'base_rev has been reset from "{candidate_base_rev}" to "{base_rev}".'
)
return base_rev
def _get_env_prefix(graph_config):
repo_keys = list(graph_config["taskgraph"].get("repositories", {}).keys())
return repo_keys[0] if repo_keys else ""
def set_try_config(parameters, task_config_file): def set_try_config(parameters, task_config_file):
if os.path.isfile(task_config_file): if os.path.isfile(task_config_file):
logger.info(f"using try tasks from {task_config_file}") logger.info(f"using try tasks from {task_config_file}")

View file

@ -8,23 +8,43 @@ Support for optimizing tasks based on the set of files that have changed.
import logging import logging
import os
import requests import requests
from redo import retry from redo import retry
from .util.memoize import memoize from .util.memoize import memoize
from .util.path import match as match_path from .util.path import match as match_path
from .util.vcs import get_repository
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@memoize @memoize
def get_changed_files(repository, revision): def get_changed_files(head_repository_url, head_rev, base_rev=None):
""" """
Get the set of files changed in the push headed by the given revision. Get the set of files changed between revisions.
Responses are cached, so multiple calls with the same arguments are OK. Responses are cached, so multiple calls with the same arguments are OK.
""" """
url = "{}/json-automationrelevance/{}".format(repository.rstrip("/"), revision) repo_path = os.getcwd()
repository = get_repository(repo_path)
if repository.tool == "hg":
# TODO Use VCS version once tested enough
return _get_changed_files_json_automationrelevance(
head_repository_url, head_rev
)
return repository.get_changed_files(rev=head_rev, base_rev=base_rev)
def _get_changed_files_json_automationrelevance(head_repository_url, head_rev):
"""
Get the set of files changed in the push headed by the given revision.
"""
url = "{}/json-automationrelevance/{}".format(
head_repository_url.rstrip("/"), head_rev
)
logger.debug("Querying version control for metadata: %s", url) logger.debug("Querying version control for metadata: %s", url)
def get_automationrelevance(): def get_automationrelevance():
@ -48,18 +68,20 @@ def get_changed_files(repository, revision):
def check(params, file_patterns): def check(params, file_patterns):
"""Determine whether any of the files changed in the indicated push to """Determine whether any of the files changed between 2 revisions
https://hg.mozilla.org match any of the given file patterns.""" match any of the given file patterns."""
repository = params.get("head_repository")
revision = params.get("head_rev") head_repository_url = params.get("head_repository")
if not repository or not revision: head_rev = params.get("head_rev")
if not head_repository_url or not head_rev:
logger.warning( logger.warning(
"Missing `head_repository` or `head_rev` parameters; " "Missing `head_repository` or `head_rev` parameters; "
"assuming all files have changed" "assuming all files have changed"
) )
return True return True
changed_files = get_changed_files(repository, revision) base_rev = params.get("base_rev")
changed_files = get_changed_files(head_repository_url, head_rev, base_rev)
for pattern in file_patterns: for pattern in file_patterns:
for path in changed_files: for path in changed_files:

View file

@ -51,9 +51,9 @@ class Kind:
config = copy.deepcopy(self.config) config = copy.deepcopy(self.config)
kind_dependencies = config.get("kind-dependencies", []) kind_dependencies = config.get("kind-dependencies", [])
kind_dependencies_tasks = [ kind_dependencies_tasks = {
task for task in loaded_tasks if task.kind in kind_dependencies task.label: task for task in loaded_tasks if task.kind in kind_dependencies
] }
inputs = loader(self.name, self.path, config, parameters, loaded_tasks) inputs = loader(self.name, self.path, config, parameters, loaded_tasks)

View file

@ -343,7 +343,7 @@ def show_taskgraph(options):
logging.root.setLevel(logging.DEBUG) logging.root.setLevel(logging.DEBUG)
repo = None repo = None
cur_ref = None cur_rev = None
diffdir = None diffdir = None
output_file = options["output_file"] output_file = options["output_file"]
@ -361,16 +361,16 @@ def show_taskgraph(options):
# as best we can after we're done. In all known cases, using # as best we can after we're done. In all known cases, using
# branch or bookmark (which are both available on the VCS object) # branch or bookmark (which are both available on the VCS object)
# as `branch` is preferable to a specific revision. # as `branch` is preferable to a specific revision.
cur_ref = repo.branch or repo.head_ref[:12] cur_rev = repo.branch or repo.head_rev[:12]
diffdir = tempfile.mkdtemp() diffdir = tempfile.mkdtemp()
atexit.register( atexit.register(
shutil.rmtree, diffdir shutil.rmtree, diffdir
) # make sure the directory gets cleaned up ) # make sure the directory gets cleaned up
options["output_file"] = os.path.join( options["output_file"] = os.path.join(
diffdir, f"{options['graph_attr']}_{cur_ref}" diffdir, f"{options['graph_attr']}_{cur_rev}"
) )
print(f"Generating {options['graph_attr']} @ {cur_ref}", file=sys.stderr) print(f"Generating {options['graph_attr']} @ {cur_rev}", file=sys.stderr)
parameters: List[Any[str, Parameters]] = options.pop("parameters") parameters: List[Any[str, Parameters]] = options.pop("parameters")
if not parameters: if not parameters:
@ -418,33 +418,33 @@ def show_taskgraph(options):
del sys.modules[mod] del sys.modules[mod]
if options["diff"] == "default": if options["diff"] == "default":
base_ref = repo.base_ref base_rev = repo.base_rev
else: else:
base_ref = options["diff"] base_rev = options["diff"]
try: try:
repo.update(base_ref) repo.update(base_rev)
base_ref = repo.head_ref[:12] base_rev = repo.head_rev[:12]
options["output_file"] = os.path.join( options["output_file"] = os.path.join(
diffdir, f"{options['graph_attr']}_{base_ref}" diffdir, f"{options['graph_attr']}_{base_rev}"
) )
print(f"Generating {options['graph_attr']} @ {base_ref}", file=sys.stderr) print(f"Generating {options['graph_attr']} @ {base_rev}", file=sys.stderr)
generate_taskgraph(options, parameters, logdir) generate_taskgraph(options, parameters, logdir)
finally: finally:
repo.update(cur_ref) repo.update(cur_rev)
# Generate diff(s) # Generate diff(s)
diffcmd = [ diffcmd = [
"diff", "diff",
"-U20", "-U20",
"--report-identical-files", "--report-identical-files",
f"--label={options['graph_attr']}@{base_ref}", f"--label={options['graph_attr']}@{base_rev}",
f"--label={options['graph_attr']}@{cur_ref}", f"--label={options['graph_attr']}@{cur_rev}",
] ]
for spec in parameters: for spec in parameters:
base_path = os.path.join(diffdir, f"{options['graph_attr']}_{base_ref}") base_path = os.path.join(diffdir, f"{options['graph_attr']}_{base_rev}")
cur_path = os.path.join(diffdir, f"{options['graph_attr']}_{cur_ref}") cur_path = os.path.join(diffdir, f"{options['graph_attr']}_{cur_rev}")
params_name = None params_name = None
if len(parameters) > 1: if len(parameters) > 1:
@ -593,6 +593,15 @@ def image_digest(args):
help='Type of repository, either "hg" or "git"', help='Type of repository, either "hg" or "git"',
) )
@argument("--base-repository", required=True, help='URL for "base" repository to clone') @argument("--base-repository", required=True, help='URL for "base" repository to clone')
@argument(
"--base-ref", default="", help='Reference of the revision in the "base" repository'
)
@argument(
"--base-rev",
default="",
help="Taskgraph decides what to do based on the revision range between "
"`--base-rev` and `--head-rev`. Value is determined automatically if not provided",
)
@argument( @argument(
"--head-repository", "--head-repository",
required=True, required=True,

View file

@ -33,6 +33,12 @@ here = os.path.abspath(os.path.dirname(__file__))
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_ROUTES = 10 MAX_ROUTES = 10
registered_morphs = []
def register_morph(func):
registered_morphs.append(func)
def amend_taskgraph(taskgraph, label_to_taskid, to_add): def amend_taskgraph(taskgraph, label_to_taskid, to_add):
"""Add the given tasks to the taskgraph, returning a new taskgraph""" """Add the given tasks to the taskgraph, returning a new taskgraph"""
@ -156,6 +162,7 @@ def make_index_task(parent_task, taskgraph, label_to_taskid, parameters, graph_c
return task, taskgraph, label_to_taskid return task, taskgraph, label_to_taskid
@register_morph
def add_index_tasks(taskgraph, label_to_taskid, parameters, graph_config): def add_index_tasks(taskgraph, label_to_taskid, parameters, graph_config):
""" """
The TaskCluster queue only allows 10 routes on a task, but we have tasks The TaskCluster queue only allows 10 routes on a task, but we have tasks
@ -196,8 +203,9 @@ def _get_morph_url():
return f"{taskgraph_repo}/raw-file/{taskgraph_rev}/src/taskgraph/morph.py" return f"{taskgraph_repo}/raw-file/{taskgraph_rev}/src/taskgraph/morph.py"
@register_morph
def add_code_review_task(taskgraph, label_to_taskid, parameters, graph_config): def add_code_review_task(taskgraph, label_to_taskid, parameters, graph_config):
logger.debug("Morphing: adding index tasks") logger.debug("Morphing: adding code review task")
review_config = parameters.get("code-review") review_config = parameters.get("code-review")
if not review_config: if not review_config:
@ -256,12 +264,7 @@ def add_code_review_task(taskgraph, label_to_taskid, parameters, graph_config):
def morph(taskgraph, label_to_taskid, parameters, graph_config): def morph(taskgraph, label_to_taskid, parameters, graph_config):
"""Apply all morphs""" """Apply all morphs"""
morphs = [ for m in registered_morphs:
add_index_tasks,
add_code_review_task,
]
for m in morphs:
taskgraph, label_to_taskid = m( taskgraph, label_to_taskid = m(
taskgraph, label_to_taskid, parameters, graph_config taskgraph, label_to_taskid, parameters, graph_config
) )

View file

@ -50,13 +50,8 @@ class IndexSearch(OptimizationStrategy):
@register_strategy("skip-unless-changed") @register_strategy("skip-unless-changed")
class SkipUnlessChanged(OptimizationStrategy): class SkipUnlessChanged(OptimizationStrategy):
def should_remove_task(self, task, params, file_patterns): def should_remove_task(self, task, params, file_patterns):
if params.get("repository_type") != "hg": # pushlog_id == -1 - this is the case when run from a cron.yml job or on a git repository
raise RuntimeError( if params.get("repository_type") == "hg" and params.get("pushlog_id") == -1:
"SkipUnlessChanged optimization only works with mercurial repositories"
)
# pushlog_id == -1 - this is the case when run from a cron.yml job
if params.get("pushlog_id") == -1:
return False return False
changed = files_changed.check(params, file_patterns) changed = files_changed.check(params, file_patterns)

View file

@ -14,6 +14,7 @@ from subprocess import CalledProcessError
from urllib.parse import urlparse from urllib.parse import urlparse
from urllib.request import urlopen from urllib.request import urlopen
from mozilla_repo_urls import parse
from voluptuous import ALLOW_EXTRA, Any, Optional, Required, Schema from voluptuous import ALLOW_EXTRA, Any, Optional, Required, Schema
from taskgraph.util import yaml from taskgraph.util import yaml
@ -31,6 +32,8 @@ class ParameterMismatch(Exception):
base_schema = Schema( base_schema = Schema(
{ {
Required("base_repository"): str, Required("base_repository"): str,
Required("base_ref"): str,
Required("base_rev"): str,
Required("build_date"): int, Required("build_date"): int,
Required("build_number"): int, Required("build_number"): int,
Required("do_not_optimize"): [str], Required("do_not_optimize"): [str],
@ -77,22 +80,26 @@ def _get_defaults(repo_root=None):
repo = get_repository(repo_path) repo = get_repository(repo_path)
try: try:
repo_url = repo.get_url() repo_url = repo.get_url()
project = repo_url.rsplit("/", 1)[1] parsed_url = parse(repo_url)
project = parsed_url.repo_name
except (CalledProcessError, IndexError): except (CalledProcessError, IndexError):
# IndexError is raised if repo url doesn't have any slashes. # IndexError is raised if repo url doesn't have any slashes.
repo_url = "" repo_url = ""
project = "" project = ""
default_base_ref = repo.default_branch
return { return {
"base_repository": repo_url, "base_repository": repo_url,
"base_ref": default_base_ref,
"base_rev": repo.find_latest_common_revision(default_base_ref, repo.head_rev),
"build_date": int(time.time()), "build_date": int(time.time()),
"build_number": 1, "build_number": 1,
"do_not_optimize": [], "do_not_optimize": [],
"existing_tasks": {}, "existing_tasks": {},
"filters": ["target_tasks_method"], "filters": ["target_tasks_method"],
"head_ref": repo.head_ref, "head_ref": repo.branch or repo.head_rev,
"head_repository": repo_url, "head_repository": repo_url,
"head_rev": repo.head_ref, "head_rev": repo.head_rev,
"head_tag": "", "head_tag": "",
"level": "3", "level": "3",
"moz_build_date": datetime.now().strftime("%Y%m%d%H%M%S"), "moz_build_date": datetime.now().strftime("%Y%m%d%H%M%S"),

View file

@ -106,6 +106,13 @@ IS_MACOSX = sys.platform == 'darwin'
IS_POSIX = os.name == 'posix' IS_POSIX = os.name == 'posix'
IS_WINDOWS = os.name == 'nt' IS_WINDOWS = os.name == 'nt'
# Both mercurial and git use sha1 as revision idenfiers. Luckily, both define
# the same value as the null revision.
#
# https://github.com/git/git/blob/dc04167d378fb29d30e1647ff6ff51dd182bc9a3/t/oid-info/hash-info#L7
# https://www.mercurial-scm.org/repo/hg-stable/file/82efc31bd152/mercurial/node.py#l30
NULL_REVISION = "0000000000000000000000000000000000000000"
def print_line(prefix, m): def print_line(prefix, m):
now = datetime.datetime.utcnow().isoformat().encode('utf-8') now = datetime.datetime.utcnow().isoformat().encode('utf-8')
@ -557,6 +564,8 @@ def git_checkout(
destination_path: str, destination_path: str,
head_repo: str, head_repo: str,
base_repo: Optional[str], base_repo: Optional[str],
base_ref: Optional[str],
base_rev: Optional[str],
ref: Optional[str], ref: Optional[str],
commit: Optional[str], commit: Optional[str],
ssh_key_file: Optional[Path], ssh_key_file: Optional[Path],
@ -591,11 +600,47 @@ def git_checkout(
retry_required_command(b'vcs', args, extra_env=env) retry_required_command(b'vcs', args, extra_env=env)
if base_ref:
args = [
'git',
'fetch',
'origin',
base_ref
]
retry_required_command(b'vcs', args, cwd=destination_path, extra_env=env)
# Create local branch so that taskgraph is able to compute differences
# between the head branch and the base one, if needed
args = [
'git',
'checkout',
base_ref
]
retry_required_command(b'vcs', args, cwd=destination_path, extra_env=env)
# When commits are force-pushed (like on a testing branch), base_rev doesn't
# exist on base_ref. Fetching it allows taskgraph to compute differences
# between the previous state before the force-push and the current state.
#
# Unlike base_ref just above, there is no need to checkout the revision:
# it's immediately avaiable after the fetch.
if base_rev and base_rev != NULL_REVISION:
args = [
'git',
'fetch',
'origin',
base_rev
]
retry_required_command(b'vcs', args, cwd=destination_path, extra_env=env)
# If a ref isn't provided, we fetch all refs from head_repo, which may be slow # If a ref isn't provided, we fetch all refs from head_repo, which may be slow
args = [ args = [
'git', 'git',
'fetch', 'fetch',
'--tags', '--no-tags',
head_repo, head_repo,
ref if ref else '+refs/heads/*:refs/remotes/work/*' ref if ref else '+refs/heads/*:refs/remotes/work/*'
] ]
@ -606,11 +651,31 @@ def git_checkout(
'git', 'git',
'checkout', 'checkout',
'-f', '-f',
commit if commit else ref
] ]
if ref:
args.extend(['-B', ref])
args.append(commit if commit else ref)
run_required_command(b'vcs', args, cwd=destination_path) run_required_command(b'vcs', args, cwd=destination_path)
if os.path.exists(os.path.join(destination_path, '.gitmodules')):
args = [
'git',
'submodule',
'init',
]
run_required_command(b'vcs', args, cwd=destination_path)
args = [
'git',
'submodule',
'update',
]
run_required_command(b'vcs', args, cwd=destination_path)
_clean_git_checkout(destination_path) _clean_git_checkout(destination_path)
args = [ args = [
@ -818,6 +883,8 @@ def collect_vcs_options(args, project, name):
repo_type = os.environ.get('%s_REPOSITORY_TYPE' % env_prefix) repo_type = os.environ.get('%s_REPOSITORY_TYPE' % env_prefix)
base_repo = os.environ.get('%s_BASE_REPOSITORY' % env_prefix) base_repo = os.environ.get('%s_BASE_REPOSITORY' % env_prefix)
base_ref = os.environ.get('%s_BASE_REF' % env_prefix)
base_rev = os.environ.get('%s_BASE_REV' % env_prefix)
head_repo = os.environ.get('%s_HEAD_REPOSITORY' % env_prefix) head_repo = os.environ.get('%s_HEAD_REPOSITORY' % env_prefix)
revision = os.environ.get('%s_HEAD_REV' % env_prefix) revision = os.environ.get('%s_HEAD_REV' % env_prefix)
ref = os.environ.get('%s_HEAD_REF' % env_prefix) ref = os.environ.get('%s_HEAD_REF' % env_prefix)
@ -849,6 +916,8 @@ def collect_vcs_options(args, project, name):
'checkout': checkout, 'checkout': checkout,
'sparse-profile': sparse_profile, 'sparse-profile': sparse_profile,
'base-repo': base_repo, 'base-repo': base_repo,
'base-ref': base_ref,
'base-rev': base_rev,
'head-repo': head_repo, 'head-repo': head_repo,
'revision': revision, 'revision': revision,
'ref': ref, 'ref': ref,
@ -896,6 +965,8 @@ def vcs_checkout_from_args(options, *, hgmo_fingerprint):
options['checkout'], options['checkout'],
options['head-repo'], options['head-repo'],
options['base-repo'], options['base-repo'],
options['base-ref'],
options['base-rev'],
ref, ref,
revision, revision,
ssh_key_file, ssh_key_file,

View file

@ -46,9 +46,9 @@ class TransformConfig:
# the parameters for this task-graph generation run # the parameters for this task-graph generation run
params = attr.ib(type=Parameters) params = attr.ib(type=Parameters)
# a list of all the tasks associated with the kind dependencies of the # a dict of all the tasks associated with the kind dependencies of the
# current kind # current kind
kind_dependencies_tasks = attr.ib() kind_dependencies_tasks = attr.ib(type=dict)
# Global configuration of the taskgraph # Global configuration of the taskgraph
graph_config = attr.ib(type=GraphConfig) graph_config = attr.ib(type=GraphConfig)

View file

@ -57,7 +57,7 @@ def cache_task(config, tasks):
return return
digests = {} digests = {}
for task in config.kind_dependencies_tasks: for task in config.kind_dependencies_tasks.values():
if "cached_task" in task.attributes: if "cached_task" in task.attributes:
digests[task.label] = format_task_digest(task.attributes["cached_task"]) digests[task.label] = format_task_digest(task.attributes["cached_task"])

View file

@ -17,7 +17,7 @@ def add_dependencies(config, jobs):
job.setdefault("soft-dependencies", []) job.setdefault("soft-dependencies", [])
job["soft-dependencies"] += [ job["soft-dependencies"] += [
dep_task.label dep_task.label
for dep_task in config.kind_dependencies_tasks for dep_task in config.kind_dependencies_tasks.values()
if dep_task.attributes.get("code-review") is True if dep_task.attributes.get("code-review") is True
] ]
yield job yield job

View file

@ -67,7 +67,7 @@ transforms.add_validate(docker_image_schema)
@transforms.add @transforms.add
def fill_template(config, tasks): def fill_template(config, tasks):
available_packages = set() available_packages = set()
for task in config.kind_dependencies_tasks: for task in config.kind_dependencies_tasks.values():
if task.kind != "packages": if task.kind != "packages":
continue continue
name = task.label.replace("packages-", "") name = task.label.replace("packages-", "")

View file

@ -211,7 +211,7 @@ def use_fetches(config, jobs):
if value: if value:
aliases[f"{config.kind}-{value}"] = label aliases[f"{config.kind}-{value}"] = label
for task in config.kind_dependencies_tasks: for task in config.kind_dependencies_tasks.values():
if task.kind in ("fetch", "toolchain"): if task.kind in ("fetch", "toolchain"):
get_attribute( get_attribute(
artifact_names, artifact_names,
@ -275,8 +275,8 @@ def use_fetches(config, jobs):
else: else:
dep_tasks = [ dep_tasks = [
task task
for task in config.kind_dependencies_tasks for label, task in config.kind_dependencies_tasks.items()
if task.label == dep_label if label == dep_label
] ]
if len(dep_tasks) != 1: if len(dep_tasks) != 1:
raise Exception( raise Exception(

View file

@ -998,6 +998,8 @@ def build_task(config, tasks):
# (and causes scope issues) if it doesn't match the name of the # (and causes scope issues) if it doesn't match the name of the
# base repo # base repo
base_project = config.params["base_repository"].split("/")[-1] base_project = config.params["base_repository"].split("/")[-1]
if base_project.endswith(".git"):
base_project = base_project[:-4]
th_project_suffix = "-pr" th_project_suffix = "-pr"
else: else:
base_project = config.params["project"] base_project = config.params["project"]

View file

@ -99,9 +99,8 @@ def json_time_from_now(input_str, now=None, datetime_format=False):
else: else:
# Sorta a big hack but the json schema validator for date does not like the # Sorta a big hack but the json schema validator for date does not like the
# ISO dates until 'Z' (for timezone) is added... # ISO dates until 'Z' (for timezone) is added...
# the [:23] ensures only whole seconds or milliseconds are included, # Microseconds are excluded (see bug 1381801)
# not microseconds (see bug 1381801) return time.isoformat(timespec="milliseconds") + "Z"
return time.isoformat()[:23] + "Z"
def current_json_time(datetime_format=False): def current_json_time(datetime_format=False):
@ -112,6 +111,5 @@ def current_json_time(datetime_format=False):
if datetime_format is True: if datetime_format is True:
return datetime.datetime.utcnow() return datetime.datetime.utcnow()
else: else:
# the [:23] ensures only whole seconds or milliseconds are included, # Microseconds are excluded (see bug 1381801)
# not microseconds (see bug 1381801) return datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
return datetime.datetime.utcnow().isoformat()[:23] + "Z"

View file

@ -3,7 +3,9 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
import logging
import os import os
import re
import subprocess import subprocess
from abc import ABC, abstractmethod, abstractproperty from abc import ABC, abstractmethod, abstractproperty
from shutil import which from shutil import which
@ -15,38 +17,91 @@ from taskgraph.util.path import ancestors
PUSHLOG_TMPL = "{}/json-pushes?version=2&changeset={}&tipsonly=1&full=1" PUSHLOG_TMPL = "{}/json-pushes?version=2&changeset={}&tipsonly=1&full=1"
logger = logging.getLogger(__name__)
class Repository(ABC): class Repository(ABC):
# Both mercurial and git use sha1 as revision idenfiers. Luckily, both define
# the same value as the null revision.
#
# https://github.com/git/git/blob/dc04167d378fb29d30e1647ff6ff51dd182bc9a3/t/oid-info/hash-info#L7
# https://www.mercurial-scm.org/repo/hg-stable/file/82efc31bd152/mercurial/node.py#l30
NULL_REVISION = "0000000000000000000000000000000000000000"
def __init__(self, path): def __init__(self, path):
self.path = path self.path = path
self.binary = which(self.tool) self.binary = which(self.tool)
if self.binary is None: if self.binary is None:
raise OSError(f"{self.tool} not found!") raise OSError(f"{self.tool} not found!")
self._valid_diff_filter = ("m", "a", "d")
self._env = os.environ.copy() self._env = os.environ.copy()
def run(self, *args: str, **kwargs): def run(self, *args: str, **kwargs):
return_codes = kwargs.pop("return_codes", [])
cmd = (self.binary,) + args cmd = (self.binary,) + args
return subprocess.check_output(
cmd, cwd=self.path, env=self._env, encoding="utf-8", **kwargs try:
) return subprocess.check_output(
cmd, cwd=self.path, env=self._env, encoding="utf-8", **kwargs
)
except subprocess.CalledProcessError as e:
if e.returncode in return_codes:
return ""
raise
@abstractproperty @abstractproperty
def tool(self) -> str: def tool(self) -> str:
"""Version control system being used, either 'hg' or 'git'.""" """Version control system being used, either 'hg' or 'git'."""
@abstractproperty @abstractproperty
def head_ref(self) -> str: def head_rev(self) -> str:
"""Hash of HEAD revision.""" """Hash of HEAD revision."""
@abstractproperty @abstractproperty
def base_ref(self): def base_rev(self):
"""Hash of revision the current topic branch is based on.""" """Hash of revision the current topic branch is based on."""
@abstractproperty @abstractproperty
def branch(self): def branch(self):
"""Current branch or bookmark the checkout has active.""" """Current branch or bookmark the checkout has active."""
@abstractproperty
def all_remote_names(self):
"""Name of all configured remote repositories."""
@abstractproperty
def default_remote_name(self):
"""Name the VCS defines for the remote repository when cloning
it for the first time. This name may not exist anymore if users
changed the default configuration, for instance."""
@abstractproperty
def remote_name(self):
"""Name of the remote repository."""
def _get_most_suitable_remote(self, remote_instructions):
remotes = self.all_remote_names
if len(remotes) == 1:
return remotes[0]
if self.default_remote_name in remotes:
return self.default_remote_name
first_remote = remotes[0]
logger.warning(
f"Unable to determine which remote repository to use between: {remotes}. "
f'Arbitrarily using the first one "{first_remote}". Please set an '
f"`{self.default_remote_name}` remote if the arbitrarily selected one "
f"is not right. To do so: {remote_instructions}"
)
return first_remote
@abstractproperty
def default_branch(self):
"""Name of the default branch."""
@abstractmethod @abstractmethod
def get_url(self, remote=None): def get_url(self, remote=None):
"""Get URL of the upstream repository.""" """Get URL of the upstream repository."""
@ -55,6 +110,43 @@ class Repository(ABC):
def get_commit_message(self, revision=None): def get_commit_message(self, revision=None):
"""Commit message of specified revision or current commit.""" """Commit message of specified revision or current commit."""
@abstractmethod
def get_changed_files(self, diff_filter, mode="unstaged", rev=None, base_rev=None):
"""Return a list of files that are changed in:
* either this repository's working copy,
* or at a given revision (``rev``)
* or between 2 revisions (``base_rev`` and ``rev``)
``diff_filter`` controls which kinds of modifications are returned.
It is a string which may only contain the following characters:
A - Include files that were added
D - Include files that were deleted
M - Include files that were modified
By default, all three will be included.
``mode`` can be one of 'unstaged', 'staged' or 'all'. Only has an
effect on git. Defaults to 'unstaged'.
``rev`` is a specifier for which changesets to consider for
changes. The exact meaning depends on the vcs system being used.
``base_rev`` specifies the range of changesets. This parameter cannot
be used without ``rev``. The range includes ``rev`` but excludes
``base_rev``.
"""
@abstractmethod
def get_outgoing_files(self, diff_filter, upstream):
"""Return a list of changed files compared to upstream.
``diff_filter`` works the same as `get_changed_files`.
``upstream`` is a remote ref to compare against. If unspecified,
this will be determined automatically. If there is no remote ref,
a MissingUpstreamRepo exception will be raised.
"""
@abstractmethod @abstractmethod
def working_directory_clean(self, untracked=False, ignored=False): def working_directory_clean(self, untracked=False, ignored=False):
"""Determine if the working directory is free of modifications. """Determine if the working directory is free of modifications.
@ -71,20 +163,33 @@ class Repository(ABC):
def update(self, ref): def update(self, ref):
"""Update the working directory to the specified reference.""" """Update the working directory to the specified reference."""
@abstractmethod
def find_latest_common_revision(self, base_ref_or_rev, head_rev):
"""Find the latest revision that is common to both the given
``head_rev`` and ``base_ref_or_rev``"""
@abstractmethod
def does_revision_exist_locally(self, revision):
"""Check whether this revision exists in the local repository.
If this function returns an unexpected value, then make sure
the revision was fetched from the remote repository."""
class HgRepository(Repository): class HgRepository(Repository):
tool = "hg" tool = "hg"
default_remote_name = "default"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._env["HGPLAIN"] = "1" self._env["HGPLAIN"] = "1"
@property @property
def head_ref(self): def head_rev(self):
return self.run("log", "-r", ".", "-T", "{node}").strip() return self.run("log", "-r", ".", "-T", "{node}").strip()
@property @property
def base_ref(self): def base_rev(self):
return self.run("log", "-r", "last(ancestors(.) and public())", "-T", "{node}") return self.run("log", "-r", "last(ancestors(.) and public())", "-T", "{node}")
@property @property
@ -97,13 +202,85 @@ class HgRepository(Repository):
return None return None
@property
def all_remote_names(self):
remotes = self.run("paths", "--quiet").splitlines()
if not remotes:
raise RuntimeError("No remotes defined")
return remotes
@property
def remote_name(self):
return self._get_most_suitable_remote(
"Edit .hg/hgrc and add:\n\n[paths]\ndefault = $URL",
)
@property
def default_branch(self):
# Mercurial recommends keeping "default"
# https://www.mercurial-scm.org/wiki/StandardBranching#Don.27t_use_a_name_other_than_default_for_your_main_development_branch
return "default"
def get_url(self, remote="default"): def get_url(self, remote="default"):
return self.run("path", "-T", "{url}", remote).strip() return self.run("path", "-T", "{url}", remote).strip()
def get_commit_message(self, revision=None): def get_commit_message(self, revision=None):
revision = revision or self.head_ref revision = revision or self.head_rev
return self.run("log", "-r", ".", "-T", "{desc}") return self.run("log", "-r", ".", "-T", "{desc}")
def _format_diff_filter(self, diff_filter, for_status=False):
df = diff_filter.lower()
assert all(f in self._valid_diff_filter for f in df)
# When looking at the changes in the working directory, the hg status
# command uses 'd' for files that have been deleted with a non-hg
# command, and 'r' for files that have been `hg rm`ed. Use both.
return df.replace("d", "dr") if for_status else df
def _files_template(self, diff_filter):
template = ""
df = self._format_diff_filter(diff_filter)
if "a" in df:
template += "{file_adds % '{file}\\n'}"
if "d" in df:
template += "{file_dels % '{file}\\n'}"
if "m" in df:
template += "{file_mods % '{file}\\n'}"
return template
def get_changed_files(
self, diff_filter="ADM", mode="unstaged", rev=None, base_rev=None
):
if rev is None:
if base_rev is not None:
raise ValueError("Cannot specify `base_rev` without `rev`")
# Use --no-status to print just the filename.
df = self._format_diff_filter(diff_filter, for_status=True)
return self.run("status", "--no-status", f"-{df}").splitlines()
else:
template = self._files_template(diff_filter)
revision_argument = rev if base_rev is None else f"{base_rev}~-1::{rev}"
return self.run("log", "-r", revision_argument, "-T", template).splitlines()
def get_outgoing_files(self, diff_filter="ADM", upstream=None):
template = self._files_template(diff_filter)
if not upstream:
return self.run(
"log", "-r", "draft() and ancestors(.)", "--template", template
).split()
return self.run(
"outgoing",
"-r",
".",
"--quiet",
"--template",
template,
upstream,
return_codes=(1,),
).split()
def working_directory_clean(self, untracked=False, ignored=False): def working_directory_clean(self, untracked=False, ignored=False):
args = ["status", "--modified", "--added", "--removed", "--deleted"] args = ["status", "--modified", "--added", "--removed", "--deleted"]
if untracked: if untracked:
@ -118,34 +295,171 @@ class HgRepository(Repository):
def update(self, ref): def update(self, ref):
return self.run("update", "--check", ref) return self.run("update", "--check", ref)
def find_latest_common_revision(self, base_ref_or_rev, head_rev):
return self.run(
"log",
"-r",
f"last(ancestors('{base_ref_or_rev}') and ancestors('{head_rev}'))",
"--template",
"{node}",
).strip()
def does_revision_exist_locally(self, revision):
try:
return self.run("log", "-r", revision).strip() != ""
except subprocess.CalledProcessError as e:
# Error code 255 comes with the message:
# "abort: unknown revision $REVISION"
if e.returncode == 255:
return False
raise
class GitRepository(Repository): class GitRepository(Repository):
tool = "git" tool = "git"
default_remote_name = "origin"
_LS_REMOTE_PATTERN = re.compile(r"ref:\s+refs/heads/(?P<branch_name>\S+)\s+HEAD")
@property @property
def head_ref(self): def head_rev(self):
return self.run("rev-parse", "--verify", "HEAD").strip() return self.run("rev-parse", "--verify", "HEAD").strip()
@property @property
def base_ref(self): def base_rev(self):
refs = self.run( refs = self.run(
"rev-list", "HEAD", "--topo-order", "--boundary", "--not", "--remotes" "rev-list", "HEAD", "--topo-order", "--boundary", "--not", "--remotes"
).splitlines() ).splitlines()
if refs: if refs:
return refs[-1][1:] # boundary starts with a prefix `-` return refs[-1][1:] # boundary starts with a prefix `-`
return self.head_ref return self.head_rev
@property @property
def branch(self): def branch(self):
return self.run("branch", "--show-current").strip() or None return self.run("branch", "--show-current").strip() or None
@property
def all_remote_names(self):
remotes = self.run("remote").splitlines()
if not remotes:
raise RuntimeError("No remotes defined")
return remotes
@property
def remote_name(self):
try:
remote_branch_name = self.run(
"rev-parse", "--verify", "--abbrev-ref", "--symbolic-full-name", "@{u}"
).strip()
return remote_branch_name.split("/")[0]
except subprocess.CalledProcessError as e:
# Error code 128 comes with the message:
# "fatal: no upstream configured for branch $BRANCH"
if e.returncode != 128:
raise
return self._get_most_suitable_remote("`git remote add origin $URL`")
@property
def default_branch(self):
try:
# this one works if the current repo was cloned from an existing
# repo elsewhere
return self._get_default_branch_from_cloned_metadata()
except (subprocess.CalledProcessError, RuntimeError):
pass
try:
# This call works if you have (network) access to the repo
return self._get_default_branch_from_remote_query()
except (subprocess.CalledProcessError, RuntimeError):
pass
# this one is the last resort in case the remote is not accessible and
# the local repo is where `git init` was made
return self._guess_default_branch()
def _get_default_branch_from_remote_query(self):
# This function requires network access to the repo
remote_name = self.remote_name
output = self.run("ls-remote", "--symref", remote_name, "HEAD")
matches = self._LS_REMOTE_PATTERN.search(output)
if not matches:
raise RuntimeError(
f'Could not find the default branch of remote repository "{remote_name}". '
"Got: {output}"
)
branch_name = matches.group("branch_name")
return f"{remote_name}/{branch_name}"
def _get_default_branch_from_cloned_metadata(self):
return self.run("rev-parse", "--abbrev-ref", f"{self.remote_name}/HEAD").strip()
def _guess_default_branch(self):
branches = [
line.strip()
for line in self.run(
"branch", "--all", "--no-color", "--format=%(refname)"
).splitlines()
for candidate_branch in ("main", "master", "branches/default/tip")
if line.strip().endswith(candidate_branch)
]
if len(branches) == 1:
return branches[0]
raise RuntimeError(f"Unable to find default branch. Got: {branches}")
def get_url(self, remote="origin"): def get_url(self, remote="origin"):
return self.run("remote", "get-url", remote).strip() return self.run("remote", "get-url", remote).strip()
def get_commit_message(self, revision=None): def get_commit_message(self, revision=None):
revision = revision or self.head_ref revision = revision or self.head_rev
return self.run("log", "-n1", "--format=%B") return self.run("log", "-n1", "--format=%B")
def get_changed_files(
self, diff_filter="ADM", mode="unstaged", rev=None, base_rev=None
):
assert all(f.lower() in self._valid_diff_filter for f in diff_filter)
if rev is None:
if base_rev is not None:
raise ValueError("Cannot specify `base_rev` without `rev`")
cmd = ["diff"]
if mode == "staged":
cmd.append("--cached")
elif mode == "all":
cmd.append("HEAD")
else:
revision_argument = (
f"{rev}~1..{rev}" if base_rev is None else f"{base_rev}..{rev}"
)
cmd = ["log", "--format=format:", revision_argument]
cmd.append("--name-only")
cmd.append("--diff-filter=" + diff_filter.upper())
files = self.run(*cmd).splitlines()
return [f for f in files if f]
def get_outgoing_files(self, diff_filter="ADM", upstream=None):
assert all(f.lower() in self._valid_diff_filter for f in diff_filter)
not_condition = upstream if upstream else "--remotes"
files = self.run(
"log",
"--name-only",
f"--diff-filter={diff_filter.upper()}",
"--oneline",
"--pretty=format:",
"HEAD",
"--not",
not_condition,
).splitlines()
return [f for f in files if f]
def working_directory_clean(self, untracked=False, ignored=False): def working_directory_clean(self, untracked=False, ignored=False):
args = ["status", "--porcelain"] args = ["status", "--porcelain"]
@ -167,6 +481,19 @@ class GitRepository(Repository):
def update(self, ref): def update(self, ref):
self.run("checkout", ref) self.run("checkout", ref)
def find_latest_common_revision(self, base_ref_or_rev, head_rev):
return self.run("merge-base", base_ref_or_rev, head_rev).strip()
def does_revision_exist_locally(self, revision):
try:
return self.run("cat-file", "-t", revision).strip() == "commit"
except subprocess.CalledProcessError as e:
# Error code 128 comes with the message:
# "git cat-file: could not get object info"
if e.returncode == 128:
return False
raise
def get_repository(path): def get_repository(path):
"""Get a repository object for the repository at `path`. """Get a repository object for the repository at `path`.

View file

@ -13,6 +13,7 @@ from taskgraph.config import GraphConfig
from taskgraph.parameters import Parameters from taskgraph.parameters import Parameters
from taskgraph.taskgraph import TaskGraph from taskgraph.taskgraph import TaskGraph
from taskgraph.util.attributes import match_run_on_projects from taskgraph.util.attributes import match_run_on_projects
from taskgraph.util.treeherder import join_symbol
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -131,15 +132,26 @@ def verify_task_graph_symbol(task, taskgraph, scratch_pad, graph_config, paramet
treeherder = extra["treeherder"] treeherder = extra["treeherder"]
collection_keys = tuple(sorted(treeherder.get("collection", {}).keys())) collection_keys = tuple(sorted(treeherder.get("collection", {}).keys()))
if len(collection_keys) != 1:
raise Exception(
"Task {} can't be in multiple treeherder collections "
"(the part of the platform after `/`): {}".format(
task.label, collection_keys
)
)
platform = treeherder.get("machine", {}).get("platform") platform = treeherder.get("machine", {}).get("platform")
group_symbol = treeherder.get("groupSymbol") group_symbol = treeherder.get("groupSymbol")
symbol = treeherder.get("symbol") symbol = treeherder.get("symbol")
key = (collection_keys, platform, group_symbol, symbol) key = (platform, collection_keys[0], group_symbol, symbol)
if key in scratch_pad: if key in scratch_pad:
raise Exception( raise Exception(
"conflict between `{}`:`{}` for values `{}`".format( "Duplicate treeherder platform and symbol in tasks "
task.label, scratch_pad[key], key "`{}`and `{}`: {} {}".format(
task.label,
scratch_pad[key],
f"{platform}/{collection_keys[0]}",
join_symbol(group_symbol, symbol),
) )
) )
else: else: