Compare commits

..

2 Commits

Author SHA1 Message Date
fd63893bbe Update TOX_CONSTRAINTS_FILE for stable/yoga
Update the URL to the upper-constraints file to point to the redirect
rule on releases.openstack.org so that anyone working on this branch
will switch to the correct upper-constraints list automatically when
the requirements repository branches.

Until the requirements repository has as stable/yoga branch, tests will
continue to use the upper-constraints list on master.

Change-Id: I4181146b4c93545a2eb671c74ceacd6cdf1d9c96
2022-03-03 10:51:02 +00:00
8057e472b8 Update .gitreview for stable/yoga
Change-Id: I17ae492b2aa4b6bcfd05393bbb826ffbf29de105
2022-03-03 10:51:00 +00:00
28 changed files with 61 additions and 415 deletions

1
.gitignore vendored
View File

@@ -16,4 +16,3 @@ dist
AUTHORS
ChangeLog
releasenotes/build
.idea/

View File

@@ -2,3 +2,4 @@
host=review.opendev.org
port=29418
project=openstack/python-cloudkittyclient.git
defaultbranch=stable/yoga

View File

@@ -43,9 +43,10 @@
- project:
templates:
- openstack-lower-constraints-jobs
- check-requirements
- openstack-cover-jobs
- openstack-python3-jobs
- openstack-python3-yoga-jobs
- openstackclient-plugin-jobs
- publish-openstack-docs-pti
- release-notes-jobs-python3

View File

@@ -15,57 +15,26 @@
#
import json
import os
import shlex
import subprocess
from cloudkittyclient.tests import utils
from oslo_log import log
LOG = log.getLogger(__name__)
class BaseFunctionalTest(utils.BaseTestCase):
# DevStack is using VENV by default. Therefore, to execute the commands,
# we need to activate the VENV. And, to do that, we need the VENV path.
# This path is hardcoded here because we could not find a variable in this
# Python code to retrieve the VENV variable from the test machine.
# It seems that because of the stack TOX -> stestr -> this python code, and
# so on, we are not able to access the DevStack variables here.
#
# If somebody finds a solution, we can remove the hardcoded path here.
DEV_STACK_VENV_BASE_PATH = "/opt/stack/data/venv"
BASE_COMMAND_WITH_VENV = "source %s/bin/activate && %s "
def _run(self, executable, action,
flags='', params='', fmt='-f json', stdin=None, has_output=True):
if not has_output:
fmt = ''
does_venv_exist = not os.system("ls -lah /opt/stack/data/venv")
LOG.info("Test to check if the VENV file exist returned: [%s].",
does_venv_exist)
system_variables = os.environ.copy()
LOG.info("System variables [%s] found when executing the tests.",
system_variables)
cmd = ' '.join([executable, flags, action, params, fmt])
actual_command_with_venv = self.BASE_COMMAND_WITH_VENV % (
self.DEV_STACK_VENV_BASE_PATH, cmd)
LOG.info("Command being executed: [%s].", actual_command_with_venv)
cmd = shlex.split(cmd)
p = subprocess.Popen(
["bash", "-c", actual_command_with_venv],
env=os.environ.copy(), shell=False, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stdin=subprocess.PIPE if stdin else None
cmd, env=os.environ.copy(), shell=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE if stdin else None,
)
stdout, stderr = p.communicate(input=stdin)
LOG.info("Standard output [%s] and error output [%s] for command "
"[%s]. ", stdout, stderr, actual_command_with_venv)
if p.returncode != 0:
raise RuntimeError('"{cmd}" returned {val}: {msg}'.format(
cmd=' '.join(cmd), val=p.returncode, msg=stderr))

View File

@@ -16,7 +16,6 @@
from cloudkittyclient.v1 import client
from cloudkittyclient.v2 import dataframes
from cloudkittyclient.v2.rating import modules
from cloudkittyclient.v2 import reprocessing
from cloudkittyclient.v2 import scope
from cloudkittyclient.v2 import summary
@@ -43,4 +42,3 @@ class Client(client.Client):
self.scope = scope.ScopeManager(self.api_client)
self.summary = summary.SummaryManager(self.api_client)
self.rating = modules.RatingManager(self.api_client)
self.reprocessing = reprocessing.ReprocessingManager(self.api_client)

View File

@@ -1,76 +0,0 @@
# 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.
#
from cloudkittyclient.common import base
from cloudkittyclient import exc
class ReprocessingManager(base.BaseManager):
"""Class used to handle /v2/task/reprocesses endpoint"""
url = '/v2/task/reprocesses'
def get_reprocessing_tasks(self, offset=0, limit=100, scope_ids=[],
order="DESC", **kwargs):
"""Returns a paginated list of reprocessing tasks.
Some optional filters can be provided.
:param offset: Index of the first reprocessing task
that should be returned.
:type offset: int
:param limit: Maximal number of reprocessing task to return.
:type limit: int
:param scope_ids: Optional scope_ids to filter on.
:type scope_ids: list of str
:param order: Optional order (asc/desc) to sort tasks.
:type order: str
"""
kwargs = kwargs or {}
kwargs['order'] = order
kwargs['offset'] = offset
kwargs['limit'] = limit
authorized_args = ['offset', 'limit', 'order']
url = self.get_url(None, kwargs, authorized_args=authorized_args)
if scope_ids:
url += "&scope_ids=%s" % (",".join(scope_ids))
return self.api_client.get(url).json()
def post_reprocessing_task(self, scope_ids=[], start=None, end=None,
reason=None, **kwargs):
"""Creates a reprocessing task
:param start: The start date of the reprocessing task
:type start: timeutils.parse_isotime
:param end: The end date of the reprocessing task
:type end: timeutils.parse_isotime
:param scope_ids: The scope IDs to create the reprocessing task to
:type scope_ids: list of str
:param reason: The reason for the reprocessing task
:type reason: str
"""
if not scope_ids:
raise exc.ArgumentRequired("'scope-id' argument is required")
body = dict(
scope_ids=scope_ids,
start_reprocess_time=start,
end_reprocess_time=end,
reason=reason
)
body = dict(filter(lambda elem: bool(elem[1]), body.items()))
return self.api_client.post(self.url, json=body).json()

View File

@@ -1,95 +0,0 @@
# 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.
#
from cliff import lister
from oslo_utils import timeutils
from cloudkittyclient import utils
class CliReprocessingTasksGet(lister.Lister):
"""Get reprocessing tasks."""
result_columns = [
('scope_id', 'Scope ID'),
('reason', 'Reason'),
('start_reprocess_time', 'Start reprocessing time'),
('end_reprocess_time', 'End reprocessing time'),
('current_reprocess_time', 'Current reprocessing time'),
]
def get_parser(self, prog_name):
parser = super(CliReprocessingTasksGet, self).get_parser(prog_name)
parser.add_argument('--scope-id', type=str, default=[],
action='append', help='Optional filter on scope '
'IDs. This filter can be '
'used multiple times.')
parser.add_argument('--offset', type=int, default=0,
help='Index of the first scope. '
'The default value is 0.')
parser.add_argument('--limit', type=int, default=100,
help='Maximal number of scopes. '
'The default value is 100.')
parser.add_argument('--order', type=str, default="DESC",
help='The order to sort the reprocessing tasks '
'(ASC or DESC).')
return parser
def take_action(self, parsed_args):
resp = utils.get_client_from_osc(
self).reprocessing.get_reprocessing_tasks(
scope_ids=parsed_args.scope_id, offset=parsed_args.offset,
limit=parsed_args.limit, order=parsed_args.order
)
values = utils.list_to_cols(resp['results'], self.result_columns)
return [col[1] for col in self.result_columns], values
class CliReprocessingTasksPost(lister.Lister):
"""Create a reprocessing task."""
def get_parser(self, prog_name):
parser = super(CliReprocessingTasksPost, self).get_parser(prog_name)
parser.add_argument('--scope-id', type=str, default=[],
action='append',
help='The scope IDs to reprocess. This option can '
'be used multiple times to execute the same '
'reprocessing task for different scope IDs.')
parser.add_argument('--start-reprocess-time',
type=timeutils.parse_isotime,
help="Start of the period to reprocess in ISO8601 "
"format. Example: '2022-04-22T00:00:00Z.'")
parser.add_argument('--end-reprocess-time',
type=timeutils.parse_isotime,
help="End of the period to reprocess in ISO8601 "
"format. Example: '2022-04-22T00:00:00Z.'")
parser.add_argument('--reason', type=str,
help="The reason to create the reprocessing task.")
return parser
def take_action(self, parsed_args):
return ["Result"], utils.get_client_from_osc(
self).reprocessing.post_reprocessing_task(
scope_ids=parsed_args.scope_id,
start=parsed_args.start_reprocess_time,
end=parsed_args.end_reprocess_time,
reason=parsed_args.reason
)

View File

@@ -15,8 +15,6 @@
from cloudkittyclient.common import base
from cloudkittyclient import exc
from distutils.util import strtobool
class ScopeManager(base.BaseManager):
"""Class used to handle /v2/scope endpoint"""
@@ -102,41 +100,3 @@ class ScopeManager(base.BaseManager):
url = self.get_url(None, kwargs)
return self.api_client.put(url, json=body)
def update_scope(self, **kwargs):
"""Update storage scope
The `scope_id field` is mandatory, and all other are optional. Only the
attributes sent will be updated. The attributes that are not sent will
not be changed in the backend.
:param collector: collector to be used by the scope.
:type collector: str
:param fetcher: fetcher to be used by the scope.
:type fetcher: str
:param scope_id: Mandatory scope_id to update.
:type scope_id: str
:param scope_key: scope_key to be used by the scope.
:type scope_key: str
:param active: Indicates if the scope is active or not
:type active: str
"""
if not kwargs.get('scope_id'):
raise exc.ArgumentRequired("'scope_id' argument is required")
body = dict(
scope_id=kwargs.get('scope_id'),
scope_key=kwargs.get('scope_key'),
collector=kwargs.get('collector'),
fetcher=kwargs.get('fetcher')
)
if kwargs.get('active'):
body['active'] = strtobool(kwargs.get('active'))
# Stripping None
body = dict(filter(lambda elem: elem[1] is not None, body.items()))
url = self.get_url(None, kwargs)
return self.api_client.patch(url, json=body).json()

View File

@@ -96,36 +96,3 @@ class CliScopeStateReset(command.Command):
all_scopes=parsed_args.all_scopes,
state=parsed_args.state,
)
class CliPatchScope(command.Command):
"""Update scope attributes."""
info_columns = [
('scope_key', 'Scope Key'),
('collector', 'Collector'),
('fetcher', 'Fetcher'),
('active', 'Active'),
]
def get_parser(self, prog_name):
parser = super(CliPatchScope, self).get_parser(prog_name)
for col in self.info_columns:
parser.add_argument(
'--' + col[0].replace('_', '-'), type=str,
help='Optional filter on ' + col[1])
parser.add_argument(
'-id', '--scope-id', required=True, type=str,
help="The scope ID to be updated")
return parser
def take_action(self, parsed_args):
return utils.get_client_from_osc(self).scope.update_scope(
collector=parsed_args.collector,
fetcher=parsed_args.fetcher,
scope_id=parsed_args.scope_id,
scope_key=parsed_args.scope_key,
active=parsed_args.active)

View File

@@ -35,20 +35,10 @@ class CliSummaryGet(lister.Lister):
help='Maximal number of elements')
parser.add_argument('-g', '--groupby', type=str, action='append',
help='Attribute to group the summary by. Can be '
'specified several times. One can also group '
'by different time options such as: "time-d" '
'to group by day of the year, "time-w" to '
'group by week of the year, "time-m" to '
'group by month, and "time-y" to group data '
'by year.')
'specified several times')
parser.add_argument('--filter', type=filter_, action='append',
help="Optional filter, in 'key:value' format. Can "
"be specified several times. It is also "
"possible to filter data using the group by "
"values. However, one needs to group by as "
"well; for instance, if one wants to filter "
"by resource id (id), then we need to group "
"by id via the option '-g id'.")
"be specified several times.")
parser.add_argument('-b', '--begin', type=timeutils.parse_isotime,
help="Start of the period to query, in iso8601 "
"format. Example: 2019-05-01T00:00:00Z.")

View File

@@ -44,8 +44,8 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = 'python-cloudkittyclient'
copyright = '2017, OpenStack Foundation'
project = u'python-cloudkittyclient'
copyright = u'2017, OpenStack Foundation'
# openstackdocstheme options
repository_name = 'openstack/python-cloudkittyclient'
@@ -99,8 +99,8 @@ latex_elements = {
latex_documents = [
('index',
'doc-%s.tex' % project,
'%s Documentation' % project,
'OpenStack Foundation', 'howto', True),
u'%s Documentation' % project,
u'OpenStack Foundation', 'howto', True),
]
# Example configuration for intersphinx: refer to the Python standard library.

23
lower-constraints.txt Normal file
View File

@@ -0,0 +1,23 @@
# requirements
pbr==5.5.1 # Apache-2.0
cliff==3.5.0 # Apache-2.0
keystoneauth1==4.3.0 # Apache-2.0
oslo.utils==4.7.0 # Apache-2.0
oslo.log==4.4.0 # Apache-2.0
PyYAML==5.3.1 # MIT
jsonpath-rw-ext==1.2.0 # Apache-2.0
os-client-config==2.1.0 # Apache-2.0
osc-lib==2.3.0 # Apache-2.0
# test-requirements.txt
pyflakes==2.1.1
coverage==4.0 # Apache-2.0
python-subunit==1.4.0 # Apache-2.0/BSD
oslotest==1.10.0 # Apache-2.0
stestr==2.0 # Apache-2.0
python-openstackclient==3.14 # Apache-2.0
# doc/requirements.txt
openstackdocstheme==1.30.0 # Apache-2.0
sphinx==1.6.2 # BSD
reno==2.5.0 # Apache2

View File

@@ -1,6 +0,0 @@
---
features:
- |
Introduce the patch scope API in the CLI. The command "rating scope
patch" is added to the OpenStack CLI with this patch, and the command
"scope patch" is added to the CloudKitty python client.

View File

@@ -1,10 +0,0 @@
---
features:
- |
Introduce reprocessing task API in the CLI. The following new commands
are added to the OpenStack CLI "rating tasks reprocessing get" and
"rating tasks reprocessing create". For CloudKitty CLI, we added the
following new commands "tasks reprocessing get" and "tasks reprocessing
create". Both command sets work in a similar fashion, but one is
targetting the OpenStack CLI integration, whereas the other is
targetting CloudKitty client only.

View File

@@ -1,5 +0,0 @@
---
fixes:
- |
Fixed a bug where creating a reprocessing task would fail due to sending a
POST request to the wrong endpoint.

View File

@@ -1,6 +0,0 @@
===========================
2023.1 Series Release Notes
===========================
.. release-notes::
:branch: stable/2023.1

View File

@@ -1,6 +0,0 @@
===========================
2023.2 Series Release Notes
===========================
.. release-notes::
:branch: stable/2023.2

View File

@@ -1,6 +0,0 @@
===========================
2024.1 Series Release Notes
===========================
.. release-notes::
:branch: stable/2024.1

View File

@@ -42,8 +42,8 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = 'CloudKitty Client Release Notes'
copyright = '2016, CloudKitty developers'
project = u'CloudKitty Client Release Notes'
copyright = u'2016, CloudKitty developers'
# Release notes are version independent.
# The short X.Y version.
@@ -194,8 +194,8 @@ latex_elements = {
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'PythonCloudKittyClient.tex',
'CloudKitty Client Release Notes Documentation',
'CloudKitty developers', 'manual'),
u'CloudKitty Client Release Notes Documentation',
u'CloudKitty developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -225,8 +225,8 @@ latex_documents = [
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'cloudkittyclient',
'CloudKitty Client Release Notes Documentation',
['CloudKitty developers'], 1)
u'CloudKitty Client Release Notes Documentation',
[u'CloudKitty developers'], 1)
]
# If true, show URL addresses after external links.
@@ -240,8 +240,8 @@ man_pages = [
# dir menu entry, description, category)
texinfo_documents = [
('index', 'cloudkittyclient',
'CloudKitty Client Release Notes Documentation',
'CloudKitty Client developers', 'CloudKittyClient',
u'CloudKitty Client Release Notes Documentation',
u'CloudKitty Client developers', 'CloudKittyClient',
'One line description of project.', 'Miscellaneous'),
]

View File

@@ -8,11 +8,6 @@ Contents
:maxdepth: 2
unreleased
2024.1
2023.2
2023.1
zed
yoga
xena
wallaby
victoria

View File

@@ -3,4 +3,4 @@ Victoria Series Release Notes
=============================
.. release-notes::
:branch: unmaintained/victoria
:branch: stable/victoria

View File

@@ -3,4 +3,4 @@ Wallaby Series Release Notes
============================
.. release-notes::
:branch: unmaintained/wallaby
:branch: stable/wallaby

View File

@@ -3,4 +3,4 @@ Xena Series Release Notes
=========================
.. release-notes::
:branch: unmaintained/xena
:branch: stable/xena

View File

@@ -1,6 +0,0 @@
=========================
Yoga Series Release Notes
=========================
.. release-notes::
:branch: unmaintained/yoga

View File

@@ -1,6 +0,0 @@
========================
Zed Series Release Notes
========================
.. release-notes::
:branch: unmaintained/zed

View File

@@ -1,7 +1,3 @@
# Requirements lower bounds listed here are our best effort to keep them up to
# date but we do not test them so no guarantee of having them all correct. If
# you find any incorrect lower bounds, let us know or propose a fix.
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.

View File

@@ -6,7 +6,7 @@ description_file =
author = OpenStack
author_email = openstack-discuss@lists.openstack.org
home_page = https://docs.openstack.org/python-cloudkittyclient/latest/
python_requires = >=3.8
python_requires = >=3.6
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -17,10 +17,10 @@ classifier =
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[files]
packages =
@@ -89,15 +89,11 @@ openstack.rating.v1 =
rating_pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript
openstack.rating.v2 =
rating_tasks_reprocessing_get = cloudkittyclient.v2.reprocessing_cli:CliReprocessingTasksGet
rating_tasks_reprocessing_create = cloudkittyclient.v2.reprocessing_cli:CliReprocessingTasksPost
rating_dataframes_get = cloudkittyclient.v2.dataframes_cli:CliDataframesGet
rating_dataframes_add = cloudkittyclient.v2.dataframes_cli:CliDataframesAdd
rating_scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet
rating_scope_state_reset = cloudkittyclient.v2.scope_cli:CliScopeStateReset
rating_scope_patch = cloudkittyclient.v2.scope_cli:CliPatchScope
rating_summary_get = cloudkittyclient.v2.summary_cli:CliSummaryGet
@@ -209,15 +205,11 @@ cloudkittyclient_v1 =
pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript
cloudkittyclient_v2 =
tasks_reprocessing_get = cloudkittyclient.v2.reprocessing_cli:CliReprocessingTasksGet
tasks_reprocessing_create = cloudkittyclient.v2.reprocessing_cli:CliReprocessingTasksPost
dataframes_add = cloudkittyclient.v2.dataframes_cli:CliDataframesAdd
dataframes_get = cloudkittyclient.v2.dataframes_cli:CliDataframesGet
scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet
scope_state_reset = cloudkittyclient.v2.scope_cli:CliScopeStateReset
scope_patch = cloudkittyclient.v2.scope_cli:CliPatchScope
summary_get = cloudkittyclient.v2.summary_cli:CliSummaryGet

43
tox.ini
View File

@@ -9,7 +9,6 @@ basepython = python3
usedevelop = True
install_command = pip install -U {opts} {packages}
setenv =
DEVSTACK_VENV={env:DEVSTACK_VENV}
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
@@ -30,40 +29,12 @@ commands =
commands = oslo_debug_helper -t cloudkittyclient/tests {posargs}
[testenv:functional-v1]
passenv =
OS_CLOUD
OS_PROJECT_DOMAIN_ID
OS_USER_DOMAIN_ID
OS_PROJECT_DOMAIN_NAME
OS_USER_DOMAIN_NAME
OS_PROJECT_NAME
OS_IDENTITY_API_VERSION
OS_PASSWORD
OS_AUTH_TYPE
OS_AUTH_URL
OS_USERNAME
OS_ENDPOINT
DEVSTACK_VENV
VIRTUAL_ENV
passenv = OS_CLOUD OS_PROJECT_DOMAIN_ID OS_USER_DOMAIN_ID OS_PROJECT_DOMAIN_NAME OS_USER_DOMAIN_NAME OS_PROJECT_NAME OS_IDENTITY_API_VERSION OS_PASSWORD OS_AUTH_TYPE OS_AUTH_URL OS_USERNAME OS_ENDPOINT
setenv = OS_RATING_API_VERSION=1
commands = stestr run --concurrency=1 --test-path ./cloudkittyclient/tests/functional/v1
[testenv:functional-v2]
passenv =
OS_CLOUD
OS_PROJECT_DOMAIN_ID
OS_USER_DOMAIN_ID
OS_PROJECT_DOMAIN_NAME
OS_USER_DOMAIN_NAME
OS_PROJECT_NAME
OS_IDENTITY_API_VERSION
OS_PASSWORD
OS_AUTH_TYPE
OS_AUTH_URL
OS_USERNAME
OS_ENDPOINT
DEVSTACK_VENV
VIRTUAL_ENV
passenv = OS_CLOUD OS_PROJECT_DOMAIN_ID OS_USER_DOMAIN_ID OS_PROJECT_DOMAIN_NAME OS_USER_DOMAIN_NAME OS_PROJECT_NAME OS_IDENTITY_API_VERSION OS_PASSWORD OS_AUTH_TYPE OS_AUTH_URL OS_USERNAME OS_ENDPOINT
setenv = OS_RATING_API_VERSION=2
commands = stestr run --concurrency=1 --test-path ./cloudkittyclient/tests/functional/v2
@@ -75,7 +46,7 @@ commands = {posargs}
[testenv:docs]
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/yoga}
-r{toxinidir}/doc/requirements.txt
commands = sphinx-build --keep-going -b html doc/source doc/build/html
@@ -101,7 +72,13 @@ import_exceptions = cloudkittyclient.i18n
[testenv:releasenotes]
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt}
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/yoga}
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html
[testenv:lower-constraints]
deps =
-c{toxinidir}/lower-constraints.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt