Compare commits

..

29 Commits

Author SHA1 Message Date
Zuul
1e8b06243e Merge "Fix create_threshold method when using cost as 0" 2020-12-22 16:20:55 +00:00
Rafael Weingärtner
a15f11a7d2 Fix create_threshold method when using cost as 0
When using 0 as the cost, the `create_threshold` method
throws an exception. That happens because 0 (zero) is evaluated
to False. Therefore, we need to change the validation method to
check if the values are None.

Change-Id: Iedd541c0ad16db0d11d6e6de332eddf880af1698
2020-12-11 10:25:10 -03:00
wuchunyang
c2b5ed9535 Replace deprecated UPPER_CONSTRAINTS_FILE variable
UPPER_CONSTRAINTS_FILE is old name and deprecated
-https://zuul-ci.org/docs/zuul-jobs/python-roles.html#rolevar-tox.tox_constraints_file
This allows to use lower-constraints file as more
readable way instead of UPPER_CONSTRAINTS_FILE=<lower-constraints file>.

Change-Id: I25d4154797da0453f59e310d2392d30ca35e5258
2020-11-27 12:41:36 +00:00
Zuul
bddf634143 Merge "Drop mock from lower-constraints.txt" 2020-09-25 11:55:49 +00:00
Zuul
5442134a5f Merge "bump py37 to py38 in tox.ini" 2020-09-25 11:55:48 +00:00
wangzihao
3ec8f86e5a bump py37 to py38 in tox.ini
in 'victoria' cycle, we should test py38 by default.

Change-Id: Ib01cf2aad3edf31a067a6ac4459529851f8d8ecb
2020-09-22 00:50:20 +00:00
wangzihao
5b35e817b2 Drop mock from lower-constraints.txt
The mock is not needed for py36 and later.

Change-Id: I24afe1f1255ac47dfba300946149732f70f5f399
2020-09-21 11:46:13 +08:00
wangzihao
953c6c9443 Bump hacking min version to 3.0.1
hacking 3.0.1 fix the pinning of flake8 to avoid bringing in a new
version with new checks.

bumping the min version for hacking so that any older hacking versions
which auto adopt the new checks are not used.

Change-Id: I5875f1c0261ff6039773d7daf412102f2c02651f
2020-09-18 10:36:40 +08:00
30e21ddf7b Add Python3 wallaby unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for wallaby.

See also the PTI in governance [1].

[1]: https://governance.openstack.org/tc/reference/project-testing-interface.html

Change-Id: I4c238eefa02cece34ee8f77a0dee91a5bf698a65
2020-09-08 22:47:17 +00:00
c24de5fe54 Update master for stable/victoria
Add file to the reno documentation build to show release notes for
stable/victoria.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/victoria.

Change-Id: Ifdc6cb5adbb2139a2437d220b231c1c61526e648
Sem-Ver: feature
2020-09-08 22:47:15 +00:00
Justin Ferrieu
2a3dd279dc Add support for GET /v2/dataframes API endpoint to the client
Support for the ``GET /v2/dataframes`` endpoint has been added
to the client. A new ``dataframes get`` CLI command is also available.

Story: 2005890
Task: 36384
Depends-On: https://review.opendev.org/#/c/679636
Change-Id: Idfe93025e0f740906d0f53f33547c7746fc15169
2020-09-08 12:49:29 +00:00
Andreas Jaeger
def535752f Update hacking for Python3
The repo is Python 3 now, so update hacking to version 3.0 which
supports Python 3.

Fix problems found.

Change-Id: I8fed95ecb736e0b6d7c4b63a55553de1539139be
2020-09-07 11:29:21 -03:00
Zuul
d3408517d6 Merge "Remove translation sections from setup.cfg" 2020-09-01 13:43:38 +00:00
Zuul
37df7e1258 Merge "migrate testing to ubuntu focal" 2020-09-01 13:34:30 +00:00
Zuul
e9a6c22cef Merge "Remove six" 2020-08-31 17:58:31 +00:00
melissaml
db10c24e25 Remove translation sections from setup.cfg
These translation sections are not needed anymore, Babel can
generate translation files without them.

Change-Id: I6559348831b9277b70834556adbd9445c5982c79
2020-08-27 08:25:29 -05:00
Zuul
2bcd29dcbd Merge "Cleanup py27 support" 2020-08-24 15:57:36 +00:00
Zuul
d5a99b511f Merge "Fix pygments style" 2020-08-24 14:55:03 +00:00
Zuul
2a475bbb4a Merge "add py38 package metedata" 2020-08-21 16:41:25 +00:00
Zuul
8f9e104f84 Merge "Add Python3 victoria unit tests" 2020-08-21 16:07:40 +00:00
Zuul
a2f3de89b4 Merge "Update master for stable/ussuri" 2020-08-21 15:34:40 +00:00
jiasirui
608cd0262f add py38 package metedata
Change-Id: I08a5505237eabf5c440ed4e3f068b5a834d5917d
2020-08-18 15:43:38 +00:00
Ghanshyam Mann
de3c4924e9 migrate testing to ubuntu focal
As per victoria cycle testing runtime and community goal[1]
we need to migrate upstream CI/CD to Ubuntu Focal(20.04).

Fixing:
- bug#1886296
Bump the pyflakes to 2.1.1 as min version to run pep8 jobs
on py3.8 which is default python vesion in ubuntu focal.

Story: #2007865
Task: #40180

Closes-Bug: #1886296
[1] https://governance.openstack.org/tc/goals/selected/victoria/migrate-ci-cd-jobs-to-ubuntu-focal.html

Change-Id: I482ac98bc56f0e3cfb8b767f47649da11ed1afab
2020-08-13 17:24:09 +00:00
fuzihao
e88a3fa033 Fix pygments style
New theme of docs (Victoria+) respects pygments_style.
Since we starts using Victoria reqs while being on Ussuri,
this patch ensures proper rendering both in Ussuri and Victoria.

Change-Id: If42b2154a5a28f92d89dde9882afa31f01bc5ac3
2020-05-20 14:51:45 +08:00
jacky06
61dc82cb54 Remove six
We don't need this in a Python 3-only world.

Change-Id: Ibeb506281e88b44d454497d06f9187308859ac9c
2020-05-10 23:08:12 +08:00
Sean McGinnis
e69f9d5452 Use unittest.mock instead of third party mock
Now that we no longer support py27, we can use the standard library
unittest.mock module instead of the third party mock lib.

Change-Id: I9bf0a8fbb7b4f22aa2f5b5ed0836d11cac27552b
Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
2020-04-18 11:57:46 -05:00
73cb650ee4 Add Python3 victoria unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for victoria.

See also the PTI in governance [1].

[1]: https://governance.openstack.org/tc/reference/project-testing-interface.html

Change-Id: I603335a1099880a00de6e4cfef6393a436ff990b
2020-04-11 18:49:23 +00:00
8bc96e21a9 Update master for stable/ussuri
Add file to the reno documentation build to show release notes for
stable/ussuri.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/ussuri.

Change-Id: I26f6e1f91faec1ce661ecfe2570c524b64687da9
Sem-Ver: feature
2020-04-11 18:49:18 +00:00
Andreas Jaeger
3f97e9844a Cleanup py27 support
Make a few cleanups:
- Remove python 2.7 stanza from setup.py
- Remove obsolete sections from setup.cfg
- Update classifiers

Change-Id: I79e3c540b56b024c7d01e4c916cdd79da9000331
2020-04-04 12:32:35 +02:00
25 changed files with 201 additions and 74 deletions

View File

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

View File

@@ -46,7 +46,7 @@
- openstack-lower-constraints-jobs
- check-requirements
- openstack-cover-jobs
- openstack-python3-ussuri-jobs
- openstack-python3-wallaby-jobs
- openstackclient-plugin-jobs
- publish-openstack-docs-pti
check:

View File

@@ -1,2 +0,0 @@
[python: **.py]

View File

@@ -12,11 +12,9 @@
# 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 string import Formatter as StringFormatter
from six import add_metaclass
from six.moves.urllib.parse import urlencode
from string import Formatter as StringFormatter
from urllib.parse import urlencode
from cloudkittyclient import utils
@@ -31,8 +29,7 @@ class HttpDecoratorMeta(type):
)
@add_metaclass(HttpDecoratorMeta)
class BaseManager(object):
class BaseManager(object, metaclass=HttpDecoratorMeta):
"""Base class for Endpoint Manager objects."""
url = ''

View File

@@ -162,6 +162,11 @@ class CkDataframesTest(base.BaseFunctionalTest):
has_output=False,
)
def test_dataframes_get(self):
# TODO(jferrieu): functional tests will be added in another
# patch for `dataframes get`
pass
class OSCDataframesTest(CkDataframesTest):
def __init__(self, *args, **kwargs):

View File

@@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
import mock
from unittest import mock
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v1 import base

View File

@@ -13,7 +13,7 @@
# under the License.
#
import collections
import mock
from unittest import mock
from cloudkittyclient.tests.unit.v1 import base
from cloudkittyclient.v1 import report_cli

View File

@@ -14,6 +14,8 @@
#
import json
from collections import OrderedDict
from cloudkittyclient import exc
from cloudkittyclient.tests.unit.v2 import base
@@ -149,3 +151,22 @@ class TestDataframes(base.BaseAPIEndpointTestCase):
self.assertRaises(
exc.ArgumentRequired,
self.dataframes.add_dataframes)
def test_get_dataframes(self):
self.dataframes.get_dataframes()
self.api_client.get.assert_called_once_with('/v2/dataframes')
def test_get_dataframes_with_pagination_args(self):
self.dataframes.get_dataframes(offset=10, limit=10)
try:
self.api_client.get.assert_called_once_with(
'/v2/dataframes?limit=10&offset=10')
except AssertionError:
self.api_client.get.assert_called_once_with(
'/v2/dataframes?offset=10&limit=10')
def test_get_dataframes_filters(self):
self.dataframes.get_dataframes(
filters=OrderedDict([('one', 'two'), ('three', 'four')]))
self.api_client.get.assert_called_once_with(
'/v2/dataframes?filters=one%3Atwo%2Cthree%3Afour')

View File

@@ -12,12 +12,13 @@
# 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 unittest import mock
import fixtures
import testtools
from keystoneauth1 import adapter
from keystoneauth1 import session
import mock
class BaseTestCase(testtools.TestCase):

View File

@@ -373,7 +373,7 @@ class HashmapManager(base.BaseManager):
:type level: str
"""
for arg in ['cost', 'level']:
if not kwargs.get(arg):
if kwargs.get(arg) is None:
raise exc.ArgumentRequired(
"'{}' argument is required".format(arg))
if not kwargs.get('service_id') and not kwargs.get('field_id'):

View File

@@ -13,7 +13,6 @@
# under the License.
#
import json
import six
from cloudkittyclient.common import base
from cloudkittyclient import exc
@@ -36,7 +35,7 @@ class DataframesManager(base.BaseManager):
if not dataframes:
raise exc.ArgumentRequired("'dataframes' argument is required")
if not isinstance(dataframes, six.string_types):
if not isinstance(dataframes, str):
try:
dataframes = json.dumps(dataframes)
except TypeError:
@@ -49,3 +48,30 @@ class DataframesManager(base.BaseManager):
url,
data=dataframes,
)
def get_dataframes(self, **kwargs):
"""Returns a paginated list of DataFrames.
This support filters and datetime framing.
:param offset: Index of the first dataframe that should be returned.
:type offset: int
:param limit: Maximal number of dataframes to return.
:type limit: int
:param filters: Optional dict of filters to select data on.
:type filters: dict
:param begin: Start of the period to gather data from
:type begin: datetime.datetime
:param end: End of the period to gather data from
:type end: datetime.datetime
"""
kwargs['filters'] = ','.join(
'{}:{}'.format(k, v) for k, v in
(kwargs.get('filters', None) or {}).items()
)
authorized_args = [
'offset', 'limit', 'filters', 'begin', 'end']
url = self.get_url(None, kwargs, authorized_args=authorized_args)
return self.api_client.get(url).json()

View File

@@ -15,6 +15,8 @@
import argparse
from cliff import command
from cliff import lister
from oslo_utils import timeutils
from cloudkittyclient import utils
@@ -40,3 +42,75 @@ class CliDataframesAdd(command.Command):
utils.get_client_from_osc(self).dataframes.add_dataframes(
dataframes=dataframes,
)
class CliDataframesGet(lister.Lister):
"""Get dataframes from the storage backend."""
columns = [
('begin', 'Begin'),
('end', 'End'),
('metric', 'Metric Type'),
('unit', 'Unit'),
('qty', 'Quantity'),
('price', 'Price'),
('groupby', 'Group By'),
('metadata', 'Metadata'),
]
def get_parser(self, prog_name):
parser = super(CliDataframesGet, self).get_parser(prog_name)
def filter_(elem):
if len(elem.split(':')) != 2:
raise TypeError
return str(elem)
parser.add_argument('--offset', type=int, default=0,
help='Index of the first dataframe')
parser.add_argument('--limit', type=int, default=100,
help='Maximal number of dataframes')
parser.add_argument('--filter', type=filter_, action='append',
help="Optional filter, in 'key:value' format. Can "
"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.")
parser.add_argument('-e', '--end', type=timeutils.parse_isotime,
help="End of the period to query, in iso8601 "
"format. Example: 2019-06-01T00:00:00Z.")
return parser
def take_action(self, parsed_args):
filters = dict(elem.split(':') for elem in (parsed_args.filter or []))
dataframes = utils.get_client_from_osc(self).dataframes.get_dataframes(
offset=parsed_args.offset,
limit=parsed_args.limit,
begin=parsed_args.begin,
end=parsed_args.end,
filters=filters,
).get('dataframes', [])
def format_(d):
return ' '.join([
'{}="{}"'.format(k, v) for k, v in (d or {}).items()])
values = []
for df in dataframes:
period = df['period']
usage = df['usage']
for metric_type, points in usage.items():
for point in points:
values.append([
period['begin'],
period['end'],
metric_type,
point['vol']['unit'],
point['vol']['qty'],
point['rating']['price'],
format_(point.get('groupby', {})),
format_(point.get('metadata', {})),
])
return [col[1] for col in self.columns], values

View File

@@ -60,7 +60,7 @@ add_function_parentheses = True
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = 'native'
# -- Options for HTML output --------------------------------------------------

View File

@@ -1,21 +1,20 @@
# requirements
pbr==2.0.0 # Apache-2.0
cliff==2.11.0 # Apache-2.0
keystoneauth1==3.4.0 # Apache-2.0
oslo.utils==3.35 # Apache-2.0
oslo.log==3.36 # Apache-2.0
PyYAML==3.12 # MIT
jsonpath-rw-ext==1.0 # Apache-2.0
six==1.11 # MIT
os-client-config==1.29.0 # Apache-2.0
osc-lib==1.12.1 # Apache-2.0
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==0.0.18 # Apache-2.0/BSD
python-subunit==1.4.0 # Apache-2.0/BSD
oslotest==1.10.0 # Apache-2.0
stestr==2.0 # Apache-2.0
mock==2.0 # BSD
python-openstackclient==3.14 # Apache-2.0
# doc/requirements.txt

View File

@@ -0,0 +1,5 @@
---
features:
- |
Support for the ``GET /v2/dataframes`` endpoint has been added
to the client. A new ``dataframes get`` CLI command is also available.

View File

@@ -0,0 +1,8 @@
---
fixes:
- |
Fix `create_threshold` method when using cost as 0.
When using 0 as the cost, the `create_threshold` method
throws an exception. That happens because 0 (zero) is evaluated
to False. Therefore, we need to change the validation method to
check if the values are None.

View File

@@ -82,7 +82,7 @@ exclude_patterns = []
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = 'native'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []

View File

@@ -8,6 +8,8 @@ Contents
:maxdepth: 2
unreleased
victoria
ussuri
train
stein
rocky

View File

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

View File

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

View File

@@ -2,13 +2,12 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=2.0.0,!=2.1.0 # Apache-2.0
cliff>=2.11.0 # Apache-2.0
keystoneauth1>=3.4.0 # Apache-2.0
oslo.utils>=3.35 # Apache-2.0
oslo.log>=3.36 # Apache-2.0
PyYAML>=3.12 # MIT
jsonpath-rw-ext>=1.0 # Apache-2.0
six>=1.11 # MIT
os-client-config>=1.29.0 # Apache-2.0
osc-lib>=1.12.1 # Apache-2.0
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

View File

@@ -14,9 +14,12 @@ classifier =
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
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
[files]
packages =
@@ -85,6 +88,7 @@ openstack.rating.v1 =
rating_pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript
openstack.rating.v2 =
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
@@ -136,7 +140,6 @@ openstack.rating.v2 =
rating_collector_state_get = cloudkittyclient.v1.collector_cli:CliCollectorGetState
rating_collector_enable = cloudkittyclient.v1.collector_cli:CliCollectorEnable
rating_collector_disable = cloudkittyclient.v1.collector_cli:CliCollectorDisable
rating_dataframes_get = cloudkittyclient.v1.storage_cli:CliGetDataframes
rating_pyscript_create = cloudkittyclient.v1.rating.pyscripts_cli:CliCreateScript
rating_pyscript_list = cloudkittyclient.v1.rating.pyscripts_cli:CliListScripts
@@ -202,6 +205,7 @@ cloudkittyclient_v1 =
cloudkittyclient_v2 =
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
@@ -253,7 +257,6 @@ cloudkittyclient_v2 =
collector_state_get = cloudkittyclient.v1.collector_cli:CliCollectorGetState
collector_enable = cloudkittyclient.v1.collector_cli:CliCollectorEnable
collector_disable = cloudkittyclient.v1.collector_cli:CliCollectorDisable
dataframes_get = cloudkittyclient.v1.storage_cli:CliGetDataframes
pyscript_create = cloudkittyclient.v1.rating.pyscripts_cli:CliCreateScript
pyscript_list = cloudkittyclient.v1.rating.pyscripts_cli:CliListScripts
@@ -266,20 +269,3 @@ keystoneauth1.plugin =
cliff.formatter.list =
df-to-csv = cloudkittyclient.format:DataframeToCsvFormatter
[upload_sphinx]
upload-dir = doc/build/html
[compile_catalog]
directory = cloudkittyclient/locale
domain = python-cloudkittyclient
[update_catalog]
domain = python-cloudkittyclient
output_dir = cloudkittyclient/locale
input_file = cloudkittyclient/locale/python-cloudkittyclient.pot
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext
mapping_file = babel.cfg
output_file = cloudkittyclient/locale/python-cloudkittyclient.pot

View File

@@ -13,17 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)

View File

@@ -2,11 +2,15 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
hacking>=3.0.1,<3.1.0 # Apache-2.0
# remove this pyflakes from here once you bump the
# hacking to 3.2.0 or above. hacking 3.2.0 takes
# care of pyflakes version compatibilty.
pyflakes>=2.1.1
coverage>=4.0,!=4.4 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
python-subunit>=1.4.0 # Apache-2.0/BSD
oslotest>=1.10.0 # Apache-2.0
stestr>=2.0 # Apache-2.0
mock>=2.0 # BSD
python-openstackclient>=3.14 # Apache-2.0

View File

@@ -1,6 +1,6 @@
[tox]
minversion = 3.1.1
envlist = py36,py37,pep8
envlist = py36,py38,pep8
skipsdist = True
ignore_basepython_conflict = True
@@ -61,7 +61,7 @@ commands =
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
ignore = E123,E125,W503,W504
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,releasenotes
@@ -70,7 +70,7 @@ import_exceptions = cloudkittyclient.i18n
[testenv:releasenotes]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri}
-c{env:TOX_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt}
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html