Compare commits

...

20 Commits

Author SHA1 Message Date
OpenStack Proposal Bot
8c10c6fd8e Updated from global requirements
Change-Id: If8d974e12b3a85a37767ed1bdb81fc53607be108
2016-10-17 18:55:59 +00:00
Yuval Brik
6d11495dee Update defaultbranch for newton
Change-Id: I1a4033cb6c9a384ac6630ec071e4bbe2cfb4c63b
2016-10-06 17:53:12 +03:00
Jenkins
e689b3233e Merge "Add a description field for resource plan" 2016-10-06 13:07:28 +00:00
Jenkins
1f422f4c5c Merge "Add trigger update client" 2016-10-06 13:07:19 +00:00
Jenkins
fda6cdfb7b Merge "Remove copy of incubated Oslo code" 2016-10-06 12:57:15 +00:00
OpenStack Proposal Bot
448bb4468f Updated from global requirements
Change-Id: Ic3a9ecca756ec01c986f0028b5bf49dfbb11ca48
2016-09-30 20:05:43 +00:00
ChangBo Guo(gcb)
ce263ecc9e Remove copy of incubated Oslo code
The Oslo team has moved all previously incubated code from the
openstack/oslo-incubator repository into separate library repositories
and released those libraries to the Python Package Index. Many of our
big tent project teams are still using the old, unsupported, incubated
versions of the code. The Oslo team has been working to remove that
incubated code from projects, and the time has come to finish that work.

As one of community-wide goals in Ocata, please see:
https://github.com/openstack/governance/blob/master/goals/ocata/remove-incubated-oslo-code.rst

Note: This commit also fix pep8 violations.

Change-Id: Ic2d8079b85ebd302a27785772462378f13d593d0
2016-09-29 15:33:58 +00:00
Jenkins
d2d3a475e2 Merge "Update homepage with developer documentation page" 2016-09-29 11:05:51 +00:00
yizhihui
e0a2b0edcd Fix restore-create failed with "no attribute 'username'"
Change-Id: I5fa824a82d199fef6620318ccb93f156c69652bd
2016-09-29 12:29:05 +08:00
Tony Xu
6113f97d2f Update homepage with developer documentation page
Change-Id: I4d833e796da4ee02485e197c7ec5603f229e188f
2016-09-27 00:37:45 +08:00
Jenkins
b7ceed787e Merge "Add restore user and password to restore-create" 2016-09-25 08:53:32 +00:00
chenying
f18cf64f9d Add a description field for resource plan
Change-Id: I2191a557b2777a9c7e92b1c9bc0fb153cb7ec2af
2016-09-22 20:19:46 +08:00
Jenkins
c0bddb93d5 Merge "Shell: restore & plan CLI parameters" 2016-09-22 10:53:10 +00:00
OpenStack Proposal Bot
25beeb0819 Updated from global requirements
Change-Id: Ie1ac3466dfc6d820fdae4b0b596281a42aac6a47
2016-09-21 06:48:20 +00:00
zhangshuai
4c6a252b76 Add trigger update client
To keep consistency with api doc, add trigger update client,
and fix plan update client.

Change-Id: Ie2e6e01bde0d3c8a947685874dcb2d82a6a49e12
2016-09-21 11:21:00 +08:00
Yuval Brik
7c3ab24bf1 Add restore user and password to restore-create
Add restore_user and restore_password to restore-create command.
Will be sent in restore body as 'restore_auth'

Change-Id: Iac54ad345c4a43427b0353fde6ca9e159300d522
2016-09-15 16:40:39 +03:00
Yuval Brik
b304d5978d Shell: restore & plan CLI parameters
Current restore and plan parameter are passed in the key=value form,
which doesn't fix the requirement of key=value pairs for each
resource (i.e dictionary).
Change the restore and plan parameters to be passed in one of the
following formats:
1. JSON using the --parameter-json '{"OS::Cinder::Volume": { ... } }'
2. Multiple --parameter option for each resource:
   --parameter resource_type=OS::Cinder::Volume,resource_id=<uuid>,k=v

Change-Id: I416dc1f00060a5c994984ddfc04c30d1a04c803c
2016-09-15 16:35:41 +03:00
Jenkins
bfc9c63952 Merge "Updated from global requirements" 2016-09-05 09:30:20 +00:00
Yuval Brik
0fc2893234 Rename package python-karborclient
Rename package name to be python-karborclient following the project
and repository rename.

Change-Id: I7171e6ef12bc8de7fc6038534dac86a44094ad51
2016-09-04 12:17:33 +03:00
OpenStack Proposal Bot
51e14a5231 Updated from global requirements
Change-Id: I66bcdb9509ff266baf01129f56814836ab975eb6
2016-09-03 02:01:34 +00:00
27 changed files with 185 additions and 112 deletions

View File

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

View File

@@ -16,4 +16,4 @@ import pbr.version
__version__ = pbr.version.VersionInfo(
'python-smaugclient').version_string()
'python-karborclient').version_string()

View File

@@ -24,7 +24,7 @@ import os
import six
from stevedore import extension
from karborclient.openstack.common.apiclient import exceptions
from karborclient.common.apiclient import exceptions
_discovered_plugins = {}
@@ -41,7 +41,7 @@ def discover_auth_systems():
def add_plugin(ext):
_discovered_plugins[ext.name] = ext.plugin
ep_namespace = "karborclient.openstack.common.apiclient.auth"
ep_namespace = "karborclient.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
mgr.map(add_plugin)
@@ -143,8 +143,7 @@ class BaseAuthPlugin(object):
@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin.
"""
"""Populate the parser with the options for this plugin."""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
@@ -153,8 +152,7 @@ class BaseAuthPlugin(object):
@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins.
"""
"""Add options that are common for several plugins."""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)
@@ -191,8 +189,7 @@ class BaseAuthPlugin(object):
@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication.
"""
"""Protected method for authentication."""
def sufficient_options(self):
"""Check if all required options are present.

View File

@@ -31,8 +31,8 @@ from oslo_utils import uuidutils
import six
from six.moves.urllib import parse
from karborclient.common.apiclient import exceptions
from karborclient.i18n import _
from karborclient.openstack.common.apiclient import exceptions
def getid(obj):
@@ -462,8 +462,7 @@ class Resource(object):
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion.
"""
"""Human-readable ID which can be used for bash completion."""
if self.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
@@ -481,7 +480,7 @@ class Resource(object):
def __getattr__(self, k):
if k not in self.__dict__:
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)

View File

@@ -40,7 +40,6 @@ from karborclient.i18n import _
from karborclient.openstack.common.apiclient import exceptions
_logger = logging.getLogger(__name__)
@@ -63,7 +62,7 @@ class HTTPClient(object):
into terminal and send the same request with curl.
"""
user_agent = "karborclient.openstack.common.apiclient"
user_agent = "karborclient.common.apiclient"
def __init__(self,
auth_plugin,
@@ -272,7 +271,7 @@ class HTTPClient(object):
>>> def test_clients():
... from keystoneclient.auth import keystone
... from openstack.common.apiclient import client
... from karborclient.common.apiclient import client
... auth = keystone.KeystoneAuthPlugin(
... username="user", password="pass", tenant_name="tenant",
... auth_url="http://auth:5000/v2.0")
@@ -358,8 +357,7 @@ class BaseClient(object):
"Must be one of: %(version_map)s") % {
'api_name': api_name,
'version': version,
'version_map': ', '.join(version_map.keys())
}
'version_map': ', '.join(version_map.keys())}
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)

View File

@@ -28,8 +28,7 @@ from karborclient.i18n import _
class ClientException(Exception):
"""The base exception class for all exceptions this library raises.
"""
"""The base exception class for all exceptions this library raises."""
pass
@@ -107,8 +106,7 @@ class AmbiguousEndpoints(EndpointException):
class HttpError(ClientException):
"""The base exception class for all HTTP exceptions.
"""
"""The base exception class for all HTTP exceptions."""
http_status = 0
message = _("HTTP Error")
@@ -426,7 +424,7 @@ def from_response(response, method, url):
"""
req_id = response.headers.get("x-openstack-request-id")
#NOTE(hdd) true for older versions of nova and cinder
# NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
kwargs = {

View File

@@ -30,7 +30,7 @@ import requests
import six
from six.moves.urllib import parse
from karborclient.openstack.common.apiclient import client
from karborclient.common.apiclient import client
def assert_has_keys(dct, required=None, optional=None):
@@ -48,8 +48,7 @@ def assert_has_keys(dct, required=None, optional=None):
class TestResponse(requests.Response):
"""Wrap requests.Response and provide a convenient initialization.
"""
"""Wrap requests.Response and provide a convenient initialization."""
def __init__(self, data):
super(TestResponse, self).__init__()
@@ -86,13 +85,12 @@ class FakeHTTPClient(client.HTTPClient):
def __init__(self, *args, **kwargs):
self.callstack = []
self.fixtures = kwargs.pop("fixtures", None) or {}
if not args and not "auth_plugin" in kwargs:
if not args and "auth_plugin" not in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called.
"""
"""Assert than an API method was just called."""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
@@ -107,8 +105,7 @@ class FakeHTTPClient(client.HTTPClient):
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test.
"""
"""Assert than an API method was called anytime in the test."""
expected = (method, url)
assert self.callstack, \

View File

@@ -20,8 +20,9 @@ import copy
import six
from six.moves.urllib import parse
from karborclient.common.apiclient import exceptions
from karborclient.common import http
from karborclient.openstack.common.apiclient import exceptions
SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('id', 'status', 'name', 'created_at')

View File

@@ -26,7 +26,7 @@ import requests
import six
from six.moves import urllib
from karborclient.openstack.common.apiclient import exceptions as exc
from karborclient.common.apiclient import exceptions as exc
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-karborclient'

View File

@@ -23,7 +23,7 @@ from oslo_utils import importutils
import prettytable
from karborclient.openstack.common.apiclient import exceptions
from karborclient.common.apiclient import exceptions
# Decorator for cli-args

View File

@@ -1,17 +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.
import six
six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))

View File

@@ -33,8 +33,9 @@ import six.moves.urllib.parse as urlparse
import karborclient
from karborclient import client as karbor_client
from karborclient.common.apiclient import exceptions as exc
from karborclient.common import utils
from karborclient.openstack.common.apiclient import exceptions as exc
logger = logging.getLogger(__name__)

View File

@@ -16,8 +16,8 @@ import socket
import mock
import testtools
from karborclient.common.apiclient import exceptions as exc
from karborclient.common import http
from karborclient.openstack.common.apiclient import exceptions as exc
from karborclient.tests.unit import fakes

View File

@@ -23,7 +23,7 @@ from oslo_log import log
import six
from testtools import matchers
from karborclient.openstack.common.apiclient import exceptions
from karborclient.common.apiclient import exceptions
import karborclient.shell
from karborclient.tests.unit import base

View File

@@ -48,7 +48,7 @@ class PlansTest(base.TestCaseShell):
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_create_plan(self, mock_request):
mock_request.return_value = mock_request_return
cs.plans.create('Plan name', 'provider_id', '', "")
cs.plans.create('Plan name', 'provider_id', '', "", '')
mock_request.assert_called_with(
'POST',
'/plans',
@@ -56,7 +56,8 @@ class PlansTest(base.TestCaseShell):
'plan': {'provider_id': 'provider_id',
'name': 'Plan name',
'resources': '',
'parameters': ''}},
'parameters': '',
'description': ''}},
headers={})
@mock.patch('karborclient.common.http.HTTPClient.raw_request')

View File

@@ -49,7 +49,9 @@ class RestoresTest(base.TestCaseShell):
cs.restores.create('586cc6ce-e286-40bd-b2b5-dd32694d9944',
'2220f8b1-975d-4621-a872-fa9afb43cb6c',
'192.168.1.2:35357/v2.0',
'{"username": "admin"}')
'{}',
'{"type": "password", "username": "admin", '
'"password": "test"}')
mock_request.assert_called_with(
'POST',
'/restores',
@@ -57,9 +59,11 @@ class RestoresTest(base.TestCaseShell):
'restore':
{
'checkpoint_id': '2220f8b1-975d-4621-a872-fa9afb43cb6c',
'parameters': '{"username": "admin"}',
'parameters': '{}',
'provider_id': '586cc6ce-e286-40bd-b2b5-dd32694d9944',
'restore_target': '192.168.1.2:35357/v2.0'
'restore_target': '192.168.1.2:35357/v2.0',
'restore_auth': '{"type": "password", "username": '
'"admin", "password": "test"}'
}}, headers={})
@mock.patch('karborclient.common.http.HTTPClient.json_request')

View File

@@ -66,7 +66,7 @@ class TriggersTest(base.TestCaseShell):
headers={})
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_show_plan(self, mock_request):
def test_show_trigger(self, mock_request):
mock_request.return_value = mock_request_return
cs.triggers.get('1')
mock_request.assert_called_with(
@@ -82,3 +82,18 @@ class TriggersTest(base.TestCaseShell):
'GET',
'/triggers/1',
headers={'X-Configuration-Session': 'fake_session_id'})
@mock.patch('karborclient.common.http.HTTPClient.json_request')
def test_update_trigger(self, mock_request):
mock_request.return_value = mock_request_return
trigger_id = '123'
data = {"name": "My Trigger",
"properties": {"pattern": "0 10 * * *", "format": "crontab"}}
body = {"trigger_info": data}
cs.triggers.update(trigger_id, data)
mock_request.assert_called_with(
'PUT',
'/triggers/123',
data=body,
headers={}
)

View File

@@ -24,8 +24,10 @@ class Plan(base.Resource):
class PlanManager(base.ManagerWithFind):
resource_class = Plan
def create(self, name, provider_id, resources, parameters):
def create(self, name, provider_id, resources, parameters,
description=None):
body = {'plan': {'name': name,
'description': description,
'provider_id': provider_id,
'resources': resources,
'parameters': parameters

View File

@@ -24,13 +24,17 @@ class Restore(base.Resource):
class RestoreManager(base.ManagerWithFind):
resource_class = Restore
def create(self, provider_id, checkpoint_id, restore_target, parameters):
body = {'restore': {'provider_id': provider_id,
'checkpoint_id': checkpoint_id,
'restore_target': restore_target,
'parameters': parameters,
}
}
def create(self, provider_id, checkpoint_id, restore_target, parameters,
restore_auth):
body = {
'restore': {
'provider_id': provider_id,
'checkpoint_id': checkpoint_id,
'restore_target': restore_target,
'restore_auth': restore_auth,
'parameters': parameters,
}
}
url = "/restores"
return self._create(url, body, 'restore')

View File

@@ -11,15 +11,15 @@
# under the License.
import argparse
import os
from karborclient.common import base
from karborclient.common import utils
from karborclient.openstack.common.apiclient import exceptions
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from karborclient.common.apiclient import exceptions
from karborclient.common import base
from karborclient.common import utils
@utils.arg('--all-tenants',
dest='all_tenants',
@@ -94,7 +94,7 @@ def do_plan_list(cs, args):
limit=args.limit, sort_key=args.sort_key,
sort_dir=args.sort_dir, sort=args.sort)
key_list = ['Id', 'Name', 'Provider id', 'Status']
key_list = ['Id', 'Name', 'Description', 'Provider id', 'Status']
if args.sort_key or args.sort_dir or args.sort:
sortby_index = None
@@ -114,20 +114,30 @@ def do_plan_list(cs, args):
metavar='<id=type=name,id=type=name>',
help='Resource in list must be a dict when creating'
' a plan.The keys of resource are id and type.')
@utils.arg('--parameters',
@utils.arg('--parameters-json',
type=str,
dest='parameters_json',
metavar='<parameters>',
default=None,
help='The parameters of a plan.')
help='Plan parameters in json format.')
@utils.arg('--parameters',
action='append',
metavar='resource_type=<type>[,resource_id=<id>,key=val,...]',
default=[],
help='Plan parameters, may be specified multiple times. '
'resource_type: type of resource to apply parameters. '
'resource_id: limit the parameters to a specific resource. '
'Other keys and values: according to provider\'s protect schema.'
)
@utils.arg('--description',
metavar='<description>',
help='The description of a plan.')
def do_plan_create(cs, args):
"""Create a plan."""
plan_resources = _extract_resources(args)
if args.parameters is not None:
plan_parameters = jsonutils.loads(args.parameters)
else:
plan_parameters = {}
plan_parameters = _extract_parameters(args)
plan = cs.plans.create(args.name, args.provider_id, plan_resources,
plan_parameters)
plan_parameters, description=args.description)
utils.print_dict(plan.to_dict())
@@ -164,7 +174,7 @@ def do_plan_delete(cs, args):
help="Id of plan to update.")
@utils.arg("--name", metavar="<name>",
help="A name to which the plan will be renamed.")
@utils.arg("--resources", metavar="<id=type,id=type>",
@utils.arg("--resources", metavar="<id=type=name,id=type=name>",
help="Resources to which the plan will be updated.")
@utils.arg("--status", metavar="<suspended|started>",
help="status to which the plan will be updated.")
@@ -214,12 +224,29 @@ def _extract_resources(args):
@utils.arg('restore_target',
metavar='<restore_target>',
help='Restore target.')
@utils.arg('--parameters',
@utils.arg('restore_username',
metavar='<restore_username>',
default="",
help='Username to restore target.')
@utils.arg('restore_password',
metavar='<restore_password>',
default="",
help='Password to restore target.')
@utils.arg('--parameters-json',
type=str,
nargs='*',
metavar='<key=value>',
dest='parameters_json',
metavar='<parameters>',
default=None,
help='The parameters of a restore target.')
help='Restore parameters in json format.')
@utils.arg('--parameters',
action='append',
metavar='resource_type=<type>[,resource_id=<id>,key=val,...]',
default=[],
help='Restore parameters, may be specified multiple times. '
'resource_type: type of resource to apply parameters. '
'resource_id: limit the parameters to a specific resource. '
'Other keys and values: according to provider\'s restore schema.'
)
def do_restore_create(cs, args):
"""Create a restore."""
if not uuidutils.is_uuid_like(args.provider_id):
@@ -230,27 +257,56 @@ def do_restore_create(cs, args):
raise exceptions.CommandError(
"Invalid checkpoint id provided.")
if args.parameters is not None:
restore_parameters = _extract_parameters(args)
else:
raise exceptions.CommandError(
"parameters must be provided.")
restore_parameters = _extract_parameters(args)
restore_auth = {
'type': 'password',
'username': args.restore_username,
'password': args.restore_password,
}
restore = cs.restores.create(args.provider_id, args.checkpoint_id,
args.restore_target, restore_parameters)
args.restore_target, restore_parameters,
restore_auth)
utils.print_dict(restore.to_dict())
def _extract_parameters(args):
parameters = {}
for data in args.parameters:
# unset doesn't require a val, so we have the if/else
if '=' in data:
(key, value) = data.split('=', 1)
else:
key = data
value = None
if all((args.parameters, args.parameters_json)):
raise exceptions.CommandError(
"Must provide parameters or parameters-json, not both")
if not any((args.parameters, args.parameters_json)):
return {}
if args.parameters_json:
return jsonutils.loads(args.parameters_json)
parameters = {}
for resource_params in args.parameters:
resource_type = None
resource_id = None
parameter = {}
for param_kv in resource_params.split(','):
try:
key, value = param_kv.split('=')
except Exception:
raise exceptions.CommandError(
'parameters must be in the form: key1=val1,key2=val2,...'
)
if key == "resource_type":
resource_type = value
if key == "resource_id":
if not uuidutils.is_uuid_like(value):
raise exceptions.CommandError('resource_id must be a uuid')
resource_id = value
parameters[key] = value
if resource_type is None:
raise exceptions.CommandError(
'Must specify resource_type for parameters'
)
if resource_id is None:
resource_key = resource_type
else:
resource_key = "%s#%s" % (resource_type, resource_id)
parameters[resource_key] = parameter
parameters[key] = value
return parameters
@@ -732,6 +788,22 @@ def _extract_properties(args):
return properties
@utils.arg("trigger_id", metavar="<TRIGGER ID>",
help="Id of trigger to update.")
@utils.arg("--name", metavar="<name>",
help="A new name to which the trigger will be renamed.")
@utils.arg("--properties", metavar="<key=value:key=value>",
help="Properties of trigger which will be updated.")
def do_trigger_update(cs, args):
"""Update a trigger."""
trigger_info = {}
trigger_properties = _extract_properties(args)
trigger_info['name'] = args.name
trigger_info['properties'] = trigger_properties
trigger = cs.triggers.update(args.trigger_id, trigger_info)
utils.print_dict(trigger.to_dict())
@utils.arg('trigger',
metavar='<trigger>',
help='ID of trigger.')

View File

@@ -46,6 +46,14 @@ class TriggerManager(base.ManagerWithFind):
trigger_id=trigger_id)
return self._get(url, response_key="trigger_info", headers=headers)
def update(self, trigger_id, data):
body = {"trigger_info": data}
return self._update('/triggers/{trigger_id}'
.format(trigger_id=trigger_id),
body, "trigger_info")
def list(self, detailed=False, search_opts=None, marker=None, limit=None,
sort_key=None, sort_dir=None, sort=None):
"""Lists all triggers."""

View File

@@ -1,8 +0,0 @@
[DEFAULT]
# The list of modules to copy from openstack-common
module=apiclient.exceptions
module=apiclient
# The base module to hold the copy of openstack.common
base=karborclient

View File

@@ -3,7 +3,7 @@
# process, which may cause wedges in the gate later.
pbr>=1.6 # Apache-2.0
PrettyTable<0.8,>=0.7 # BSD
python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0
requests>=2.10.0 # Apache-2.0
simplejson>=2.2.0 # MIT
Babel>=2.3.4 # BSD

View File

@@ -1,11 +1,11 @@
[metadata]
name = python-smaugclient
name = python-karborclient
summary = Python client library for Karbor API
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
home-page = http://docs.openstack.org/developer/karbor/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology

View File

@@ -37,4 +37,4 @@ commands = oslo_debug_helper {posargs}
show-source = True
ignore = E123,E125
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools