Add envs for different sqlalchemy versions
Adjust tests to skip the sqlalchemy test if sqlalchemy is not installed. Adjust examples to fallback to a directory based backend if the sqlalchemy does not load or is not available. Include a updated tox.ini (generated from the toxgen.py script) that includes the new venv variations. Change-Id: I7686f09901a9b65d7c81b4e037b5bffc24aa7ef7
This commit is contained in:
16
README.md
16
README.md
@@ -1,7 +1,8 @@
|
|||||||
TaskFlow
|
TaskFlow
|
||||||
========
|
========
|
||||||
|
|
||||||
A library to do [jobs, tasks, flows] in a HA manner using different backends to be used with OpenStack projects.
|
A library to do [jobs, tasks, flows] in a HA manner using different backends to
|
||||||
|
be used with OpenStack projects.
|
||||||
|
|
||||||
* More information at http://wiki.openstack.org/wiki/TaskFlow
|
* More information at http://wiki.openstack.org/wiki/TaskFlow
|
||||||
|
|
||||||
@@ -9,3 +10,16 @@ Join us
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
- http://launchpad.net/taskflow
|
- http://launchpad.net/taskflow
|
||||||
|
|
||||||
|
Help
|
||||||
|
----
|
||||||
|
|
||||||
|
### Tox.ini
|
||||||
|
|
||||||
|
To generate tox.ini, use the `toxgen.py` tool located in `tools/` and provide
|
||||||
|
that script as input the `tox-tmpl.ini` file to generate the final `tox.ini`
|
||||||
|
file.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
$ ./tools/toxgen.py -i tox-tmpl.ini -o tox.ini
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ anyjson>=0.3.3
|
|||||||
iso8601>=0.1.8
|
iso8601>=0.1.8
|
||||||
# Python 2->3 compatibility library.
|
# Python 2->3 compatibility library.
|
||||||
six>=1.4.1
|
six>=1.4.1
|
||||||
# Only needed if database backend used.
|
|
||||||
SQLAlchemy>=0.7.8,<=0.7.99
|
|
||||||
alembic>=0.4.1
|
|
||||||
# Very nice graph library
|
# Very nice graph library
|
||||||
networkx>=1.8
|
networkx>=1.8
|
||||||
Babel>=1.3
|
Babel>=1.3
|
||||||
@@ -14,8 +11,3 @@ Babel>=1.3
|
|||||||
stevedore>=0.12
|
stevedore>=0.12
|
||||||
# Backport for concurrent.futures which exists in 3.2+
|
# Backport for concurrent.futures which exists in 3.2+
|
||||||
futures>=2.1.3
|
futures>=2.1.3
|
||||||
# Only needed if the eventlet executor is used.
|
|
||||||
# eventlet>=0.13.0
|
|
||||||
# NOTE(harlowja): if you want to be able to use the graph_utils
|
|
||||||
# export_graph_to_dot function you will need to uncomment the following.
|
|
||||||
# pydot>=1.0
|
|
||||||
|
|||||||
100
taskflow/examples/example_utils.py
Normal file
100
taskflow/examples/example_utils.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (C) 2013 Yahoo! Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 contextlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from taskflow import exceptions
|
||||||
|
from taskflow.openstack.common.py3kcompat import urlutils
|
||||||
|
from taskflow.persistence import backends
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sqlalchemy as _sa # noqa
|
||||||
|
SQLALCHEMY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
SQLALCHEMY_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def rm_path(persist_path):
|
||||||
|
if not os.path.exists(persist_path):
|
||||||
|
return
|
||||||
|
if os.path.isdir(persist_path):
|
||||||
|
rm_func = shutil.rmtree
|
||||||
|
elif os.path.isfile(persist_path):
|
||||||
|
rm_func = os.unlink
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown how to `rm` path: %s" % (persist_path))
|
||||||
|
try:
|
||||||
|
rm_func(persist_path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _make_conf(backend_uri):
|
||||||
|
parsed_url = urlutils.urlparse(backend_uri)
|
||||||
|
backend_type = parsed_url.scheme.lower()
|
||||||
|
if not backend_type:
|
||||||
|
raise ValueError("Unknown backend type for uri: %s" % (backend_type))
|
||||||
|
if backend_type in ('file', 'dir'):
|
||||||
|
conf = {
|
||||||
|
'path': parsed_url.path,
|
||||||
|
'connection': backend_uri,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
conf = {
|
||||||
|
'connection': backend_uri,
|
||||||
|
}
|
||||||
|
return conf
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def get_backend(backend_uri=None):
|
||||||
|
tmp_dir = None
|
||||||
|
if not backend_uri:
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
backend_uri = str(sys.argv[1])
|
||||||
|
if not backend_uri:
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
backend_uri = "file:///%s" % tmp_dir
|
||||||
|
try:
|
||||||
|
backend = backends.fetch(_make_conf(backend_uri))
|
||||||
|
except exceptions.NotFound as e:
|
||||||
|
# Fallback to one that will work if the provided backend is not found.
|
||||||
|
if not tmp_dir:
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
backend_uri = "file:///%s" % tmp_dir
|
||||||
|
LOG.exception("Falling back to file backend using temporary"
|
||||||
|
" directory located at: %s", tmp_dir)
|
||||||
|
backend = backends.fetch(_make_conf(backend_uri))
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
try:
|
||||||
|
# Ensure schema upgraded before we continue working.
|
||||||
|
with contextlib.closing(backend.get_connection()) as conn:
|
||||||
|
conn.upgrade()
|
||||||
|
yield backend
|
||||||
|
finally:
|
||||||
|
# Make sure to cleanup the temporary path if one was created for us.
|
||||||
|
if tmp_dir:
|
||||||
|
rm_path(tmp_dir)
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -25,18 +24,20 @@ import traceback
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.ERROR)
|
logging.basicConfig(level=logging.ERROR)
|
||||||
|
|
||||||
|
self_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
os.pardir,
|
os.pardir,
|
||||||
os.pardir))
|
os.pardir))
|
||||||
sys.path.insert(0, top_dir)
|
sys.path.insert(0, top_dir)
|
||||||
|
sys.path.insert(0, self_dir)
|
||||||
|
|
||||||
from taskflow import engines
|
from taskflow import engines
|
||||||
from taskflow.patterns import linear_flow as lf
|
from taskflow.patterns import linear_flow as lf
|
||||||
from taskflow.persistence import backends
|
|
||||||
from taskflow.persistence import logbook
|
from taskflow.persistence import logbook
|
||||||
from taskflow import task
|
from taskflow import task
|
||||||
from taskflow.utils import persistence_utils as p_utils
|
from taskflow.utils import persistence_utils as p_utils
|
||||||
|
|
||||||
|
import example_utils # noqa
|
||||||
|
|
||||||
# INTRO: In this example we create two tasks, one that will say hi and one
|
# INTRO: In this example we create two tasks, one that will say hi and one
|
||||||
# that will say bye with optional capability to raise an error while
|
# that will say bye with optional capability to raise an error while
|
||||||
@@ -49,6 +50,7 @@ from taskflow.utils import persistence_utils as p_utils
|
|||||||
# as well as shows you what happens during reversion and what happens to
|
# as well as shows you what happens during reversion and what happens to
|
||||||
# the database during both of these modes (failing or not failing).
|
# the database during both of these modes (failing or not failing).
|
||||||
|
|
||||||
|
|
||||||
def print_wrapped(text):
|
def print_wrapped(text):
|
||||||
print("-" * (len(text)))
|
print("-" * (len(text)))
|
||||||
print(text)
|
print(text)
|
||||||
@@ -81,48 +83,44 @@ def make_flow(blowup=False):
|
|||||||
return flow
|
return flow
|
||||||
|
|
||||||
|
|
||||||
# Persist the flow and task state here, if the file exists already blowup
|
# Persist the flow and task state here, if the file/dir exists already blowup
|
||||||
# if not don't blowup, this allows a user to see both the modes and to
|
# if not don't blowup, this allows a user to see both the modes and to
|
||||||
# see what is stored in each case.
|
# see what is stored in each case.
|
||||||
persist_filename = os.path.join(tempfile.gettempdir(), "persisting.db")
|
if example_utils.SQLALCHEMY_AVAILABLE:
|
||||||
if os.path.isfile(persist_filename):
|
persist_path = os.path.join(tempfile.gettempdir(), "persisting.db")
|
||||||
|
backend_uri = "sqlite:///%s" % (persist_path)
|
||||||
|
else:
|
||||||
|
persist_path = os.path.join(tempfile.gettempdir(), "persisting")
|
||||||
|
backend_uri = "file:///%s" % (persist_path)
|
||||||
|
|
||||||
|
if os.path.exists(persist_path):
|
||||||
blowup = False
|
blowup = False
|
||||||
else:
|
else:
|
||||||
blowup = True
|
blowup = True
|
||||||
|
|
||||||
# Ensure schema upgraded before we continue working.
|
with example_utils.get_backend(backend_uri) as backend:
|
||||||
backend_config = {
|
# Now we can run.
|
||||||
'connection': "sqlite:///%s" % (persist_filename),
|
engine_config = {
|
||||||
}
|
'backend': backend,
|
||||||
with contextlib.closing(backends.fetch(backend_config)) as be:
|
'engine_conf': 'serial',
|
||||||
with contextlib.closing(be.get_connection()) as conn:
|
'book': logbook.LogBook("my-test"),
|
||||||
conn.upgrade()
|
}
|
||||||
|
|
||||||
# Now we can run.
|
# Make a flow that will blowup if the file doesn't exist previously, if it
|
||||||
engine_config = {
|
# did exist, assume we won't blowup (and therefore this shows the undo
|
||||||
'backend': backend_config,
|
# and redo that a flow will go through).
|
||||||
'engine_conf': 'serial',
|
flow = make_flow(blowup=blowup)
|
||||||
'book': logbook.LogBook("my-test"),
|
print_wrapped("Running")
|
||||||
}
|
|
||||||
|
|
||||||
# Make a flow that will blowup if the file doesn't exist previously, if it
|
|
||||||
# did exist, assume we won't blowup (and therefore this shows the undo
|
|
||||||
# and redo that a flow will go through).
|
|
||||||
flow = make_flow(blowup=blowup)
|
|
||||||
print_wrapped("Running")
|
|
||||||
|
|
||||||
try:
|
|
||||||
eng = engines.load(flow, **engine_config)
|
|
||||||
eng.run()
|
|
||||||
try:
|
try:
|
||||||
os.unlink(persist_filename)
|
eng = engines.load(flow, **engine_config)
|
||||||
except (OSError, IOError):
|
eng.run()
|
||||||
pass
|
if not blowup:
|
||||||
except Exception:
|
example_utils.rm_path(persist_path)
|
||||||
# NOTE(harlowja): don't exit with non-zero status code, so that we can
|
except Exception:
|
||||||
# print the book contents, as well as avoiding exiting also makes the
|
# NOTE(harlowja): don't exit with non-zero status code, so that we can
|
||||||
# unit tests (which also runs these examples) pass.
|
# print the book contents, as well as avoiding exiting also makes the
|
||||||
traceback.print_exc(file=sys.stdout)
|
# unit tests (which also runs these examples) pass.
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
|
||||||
print_wrapped("Book contents")
|
print_wrapped("Book contents")
|
||||||
print(p_utils.pformat(engine_config['book']))
|
print(p_utils.pformat(engine_config['book']))
|
||||||
|
|||||||
@@ -22,17 +22,21 @@ import sys
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.ERROR)
|
logging.basicConfig(level=logging.ERROR)
|
||||||
|
|
||||||
|
self_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
os.pardir,
|
os.pardir,
|
||||||
os.pardir))
|
os.pardir))
|
||||||
sys.path.insert(0, top_dir)
|
sys.path.insert(0, top_dir)
|
||||||
|
sys.path.insert(0, self_dir)
|
||||||
|
|
||||||
import taskflow.engines
|
import taskflow.engines
|
||||||
|
|
||||||
from taskflow.patterns import linear_flow as lf
|
from taskflow.patterns import linear_flow as lf
|
||||||
from taskflow.persistence import backends
|
|
||||||
from taskflow import task
|
from taskflow import task
|
||||||
from taskflow.utils import persistence_utils as p_utils
|
from taskflow.utils import persistence_utils as p_utils
|
||||||
|
|
||||||
|
import example_utils # noqa
|
||||||
|
|
||||||
# INTRO: In this example linear_flow is used to group three tasks, one which
|
# INTRO: In this example linear_flow is used to group three tasks, one which
|
||||||
# will suspend the future work the engine may do. This suspend engine is then
|
# will suspend the future work the engine may do. This suspend engine is then
|
||||||
# discarded and the workflow is reloaded from the persisted data and then the
|
# discarded and the workflow is reloaded from the persisted data and then the
|
||||||
@@ -61,17 +65,6 @@ def print_task_states(flowdetail, msg):
|
|||||||
print(" %s==%s: %s, result=%s" % item)
|
print(" %s==%s: %s, result=%s" % item)
|
||||||
|
|
||||||
|
|
||||||
def get_backend():
|
|
||||||
try:
|
|
||||||
backend_uri = sys.argv[1]
|
|
||||||
except Exception:
|
|
||||||
backend_uri = 'sqlite://'
|
|
||||||
|
|
||||||
backend = backends.fetch({'connection': backend_uri})
|
|
||||||
backend.get_connection().upgrade()
|
|
||||||
return backend
|
|
||||||
|
|
||||||
|
|
||||||
def find_flow_detail(backend, lb_id, fd_id):
|
def find_flow_detail(backend, lb_id, fd_id):
|
||||||
conn = backend.get_connection()
|
conn = backend.get_connection()
|
||||||
lb = conn.get_logbook(lb_id)
|
lb = conn.get_logbook(lb_id)
|
||||||
@@ -102,40 +95,38 @@ def flow_factory():
|
|||||||
|
|
||||||
### INITIALIZE PERSISTENCE ####################################
|
### INITIALIZE PERSISTENCE ####################################
|
||||||
|
|
||||||
backend = get_backend()
|
with example_utils.get_backend() as backend:
|
||||||
logbook = p_utils.temporary_log_book(backend)
|
logbook = p_utils.temporary_log_book(backend)
|
||||||
|
|
||||||
|
### CREATE AND RUN THE FLOW: FIRST ATTEMPT ####################
|
||||||
|
|
||||||
### CREATE AND RUN THE FLOW: FIRST ATTEMPT ####################
|
flow = flow_factory()
|
||||||
|
flowdetail = p_utils.create_flow_detail(flow, logbook, backend)
|
||||||
|
engine = taskflow.engines.load(flow, flow_detail=flowdetail,
|
||||||
|
backend=backend)
|
||||||
|
|
||||||
flow = flow_factory()
|
print_task_states(flowdetail, "At the beginning, there is no state")
|
||||||
flowdetail = p_utils.create_flow_detail(flow, logbook, backend)
|
print_wrapped("Running")
|
||||||
engine = taskflow.engines.load(flow, flow_detail=flowdetail,
|
engine.run()
|
||||||
backend=backend)
|
print_task_states(flowdetail, "After running")
|
||||||
|
|
||||||
print_task_states(flowdetail, "At the beginning, there is no state")
|
### RE-CREATE, RESUME, RUN ####################################
|
||||||
print_wrapped("Running")
|
|
||||||
engine.run()
|
|
||||||
print_task_states(flowdetail, "After running")
|
|
||||||
|
|
||||||
|
print_wrapped("Resuming and running again")
|
||||||
|
|
||||||
### RE-CREATE, RESUME, RUN ####################################
|
# NOTE(harlowja): reload the flow detail from backend, this will allow us
|
||||||
|
# to resume the flow from its suspended state, but first we need to search
|
||||||
print_wrapped("Resuming and running again")
|
# for the right flow details in the correct logbook where things are
|
||||||
|
# stored.
|
||||||
# NOTE(harlowja): reload the flow detail from backend, this will allow us to
|
#
|
||||||
# resume the flow from its suspended state, but first we need to search for
|
# We could avoid re-loading the engine and just do engine.run() again, but
|
||||||
# the right flow details in the correct logbook where things are stored.
|
# this example shows how another process may unsuspend a given flow and
|
||||||
#
|
# start it again for situations where this is useful to-do (say the process
|
||||||
# We could avoid re-loading the engine and just do engine.run() again, but this
|
# running the above flow crashes).
|
||||||
# example shows how another process may unsuspend a given flow and start it
|
flow2 = flow_factory()
|
||||||
# again for situations where this is useful to-do (say the process running
|
flowdetail2 = find_flow_detail(backend, logbook.uuid, flowdetail.uuid)
|
||||||
# the above flow crashes).
|
engine2 = taskflow.engines.load(flow2,
|
||||||
flow2 = flow_factory()
|
flow_detail=flowdetail2,
|
||||||
flowdetail2 = find_flow_detail(backend, logbook.uuid,
|
backend=backend)
|
||||||
flowdetail.uuid)
|
engine2.run()
|
||||||
engine2 = taskflow.engines.load(flow2,
|
print_task_states(flowdetail2, "At the end")
|
||||||
flow_detail=flowdetail2,
|
|
||||||
backend=backend)
|
|
||||||
engine2.run()
|
|
||||||
print_task_states(flowdetail2, "At the end")
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
self_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
sys.path.insert(0, self_dir)
|
||||||
|
|
||||||
|
import example_utils # noqa
|
||||||
|
|
||||||
# INTRO: In this example we create a common persistence database (sqlite based)
|
# INTRO: In this example we create a common persistence database (sqlite based)
|
||||||
# and then we run a few set of processes which themselves use this persistence
|
# and then we run a few set of processes which themselves use this persistence
|
||||||
# database, those processes 'crash' (in a simulated way) by exiting with a
|
# database, those processes 'crash' (in a simulated way) by exiting with a
|
||||||
@@ -58,10 +63,15 @@ def _path_to(name):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
backend_uri = None
|
||||||
|
tmp_path = None
|
||||||
try:
|
try:
|
||||||
fd, db_path = tempfile.mkstemp(prefix='tf-resume-example')
|
if example_utils.SQLALCHEMY_AVAILABLE:
|
||||||
os.close(fd)
|
tmp_path = tempfile.mktemp(prefix='tf-resume-example')
|
||||||
backend_uri = 'sqlite:///%s' % db_path
|
backend_uri = "sqlite:///%s" % (tmp_path)
|
||||||
|
else:
|
||||||
|
tmp_path = tempfile.mkdtemp(prefix='tf-resume-example')
|
||||||
|
backend_uri = 'file:///%s' % (tmp_path)
|
||||||
|
|
||||||
def run_example(name, add_env=None):
|
def run_example(name, add_env=None):
|
||||||
_exec([sys.executable, _path_to(name), backend_uri], add_env)
|
_exec([sys.executable, _path_to(name), backend_uri], add_env)
|
||||||
@@ -78,7 +88,8 @@ def main():
|
|||||||
print('\nResuming all failed flows')
|
print('\nResuming all failed flows')
|
||||||
run_example('resume_all.py')
|
run_example('resume_all.py')
|
||||||
finally:
|
finally:
|
||||||
os.unlink(db_path)
|
if tmp_path:
|
||||||
|
example_utils.rm_path(tmp_path)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright (C) 2013 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 sys
|
|
||||||
|
|
||||||
from taskflow.persistence import backends
|
|
||||||
|
|
||||||
|
|
||||||
def get_backend():
|
|
||||||
try:
|
|
||||||
backend_uri = sys.argv[1]
|
|
||||||
except Exception:
|
|
||||||
backend_uri = 'sqlite://'
|
|
||||||
backend = backends.fetch({'connection': backend_uri})
|
|
||||||
backend.get_connection().upgrade()
|
|
||||||
return backend
|
|
||||||
@@ -25,16 +25,17 @@ logging.basicConfig(level=logging.ERROR)
|
|||||||
self_dir = os.path.abspath(os.path.dirname(__file__))
|
self_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
top_dir = os.path.abspath(
|
top_dir = os.path.abspath(
|
||||||
os.path.join(self_dir, os.pardir, os.pardir, os.pardir))
|
os.path.join(self_dir, os.pardir, os.pardir, os.pardir))
|
||||||
|
example_dir = os.path.abspath(os.path.join(self_dir, os.pardir))
|
||||||
|
|
||||||
sys.path.insert(0, top_dir)
|
sys.path.insert(0, top_dir)
|
||||||
sys.path.insert(0, self_dir)
|
sys.path.insert(0, example_dir)
|
||||||
|
|
||||||
|
|
||||||
import taskflow.engines
|
import taskflow.engines
|
||||||
|
|
||||||
from taskflow import states
|
from taskflow import states
|
||||||
|
|
||||||
import my_utils # noqa
|
import example_utils # noqa
|
||||||
|
|
||||||
|
|
||||||
FINISHED_STATES = (states.SUCCESS, states.FAILURE, states.REVERTED)
|
FINISHED_STATES = (states.SUCCESS, states.FAILURE, states.REVERTED)
|
||||||
@@ -48,12 +49,12 @@ def resume(flowdetail, backend):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
backend = my_utils.get_backend()
|
with example_utils.get_backend() as backend:
|
||||||
logbooks = list(backend.get_connection().get_logbooks())
|
logbooks = list(backend.get_connection().get_logbooks())
|
||||||
for lb in logbooks:
|
for lb in logbooks:
|
||||||
for fd in lb:
|
for fd in lb:
|
||||||
if fd.state not in FINISHED_STATES:
|
if fd.state not in FINISHED_STATES:
|
||||||
resume(fd, backend)
|
resume(fd, backend)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -25,19 +25,21 @@ logging.basicConfig(level=logging.ERROR)
|
|||||||
self_dir = os.path.abspath(os.path.dirname(__file__))
|
self_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
top_dir = os.path.abspath(
|
top_dir = os.path.abspath(
|
||||||
os.path.join(self_dir, os.pardir, os.pardir, os.pardir))
|
os.path.join(self_dir, os.pardir, os.pardir, os.pardir))
|
||||||
|
example_dir = os.path.abspath(os.path.join(self_dir, os.pardir))
|
||||||
|
|
||||||
sys.path.insert(0, top_dir)
|
sys.path.insert(0, top_dir)
|
||||||
sys.path.insert(0, self_dir)
|
sys.path.insert(0, self_dir)
|
||||||
|
sys.path.insert(0, example_dir)
|
||||||
|
|
||||||
import taskflow.engines
|
import taskflow.engines
|
||||||
|
|
||||||
|
import example_utils # noqa
|
||||||
import my_flows # noqa
|
import my_flows # noqa
|
||||||
import my_utils # noqa
|
|
||||||
|
|
||||||
|
|
||||||
backend = my_utils.get_backend()
|
with example_utils.get_backend() as backend:
|
||||||
engine = taskflow.engines.load_from_factory(my_flows.flow_factory,
|
engine = taskflow.engines.load_from_factory(my_flows.flow_factory,
|
||||||
backend=backend)
|
backend=backend)
|
||||||
print('Running flow %s %s' % (engine.storage.flow_name,
|
print('Running flow %s %s' % (engine.storage.flow_name,
|
||||||
engine.storage.flow_uuid))
|
engine.storage.flow_uuid))
|
||||||
engine.run()
|
engine.run()
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ import time
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.ERROR)
|
logging.basicConfig(level=logging.ERROR)
|
||||||
|
|
||||||
|
self_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
os.pardir,
|
os.pardir,
|
||||||
os.pardir))
|
os.pardir))
|
||||||
sys.path.insert(0, top_dir)
|
sys.path.insert(0, top_dir)
|
||||||
|
sys.path.insert(0, self_dir)
|
||||||
|
|
||||||
from taskflow.patterns import graph_flow as gf
|
from taskflow.patterns import graph_flow as gf
|
||||||
from taskflow.patterns import linear_flow as lf
|
from taskflow.patterns import linear_flow as lf
|
||||||
@@ -40,10 +42,10 @@ from taskflow import engines
|
|||||||
from taskflow import exceptions as exc
|
from taskflow import exceptions as exc
|
||||||
from taskflow import task
|
from taskflow import task
|
||||||
|
|
||||||
from taskflow.persistence import backends
|
|
||||||
from taskflow.utils import eventlet_utils as e_utils
|
from taskflow.utils import eventlet_utils as e_utils
|
||||||
from taskflow.utils import persistence_utils as p_utils
|
from taskflow.utils import persistence_utils as p_utils
|
||||||
|
|
||||||
|
import example_utils # noqa
|
||||||
|
|
||||||
# INTRO: This examples shows how a hierarchy of flows can be used to create a
|
# INTRO: This examples shows how a hierarchy of flows can be used to create a
|
||||||
# vm in a reliable & resumable manner using taskflow + a miniature version of
|
# vm in a reliable & resumable manner using taskflow + a miniature version of
|
||||||
@@ -67,17 +69,6 @@ def print_wrapped(text):
|
|||||||
print("-" * (len(text)))
|
print("-" * (len(text)))
|
||||||
|
|
||||||
|
|
||||||
def get_backend():
|
|
||||||
try:
|
|
||||||
backend_uri = sys.argv[1]
|
|
||||||
except Exception:
|
|
||||||
backend_uri = 'sqlite://'
|
|
||||||
|
|
||||||
backend = backends.fetch({'connection': backend_uri})
|
|
||||||
backend.get_connection().upgrade()
|
|
||||||
return backend
|
|
||||||
|
|
||||||
|
|
||||||
class PrintText(task.Task):
|
class PrintText(task.Task):
|
||||||
"""Just inserts some text print outs in a workflow."""
|
"""Just inserts some text print outs in a workflow."""
|
||||||
def __init__(self, print_what, no_slow=False):
|
def __init__(self, print_what, no_slow=False):
|
||||||
@@ -243,50 +234,51 @@ def create_flow():
|
|||||||
print_wrapped("Initializing")
|
print_wrapped("Initializing")
|
||||||
|
|
||||||
# Setup the persistence & resumption layer.
|
# Setup the persistence & resumption layer.
|
||||||
backend = get_backend()
|
with example_utils.get_backend() as backend:
|
||||||
try:
|
try:
|
||||||
book_id, flow_id = sys.argv[2].split("+", 1)
|
book_id, flow_id = sys.argv[2].split("+", 1)
|
||||||
if not uuidutils.is_uuid_like(book_id):
|
if not uuidutils.is_uuid_like(book_id):
|
||||||
|
book_id = None
|
||||||
|
if not uuidutils.is_uuid_like(flow_id):
|
||||||
|
flow_id = None
|
||||||
|
except (IndexError, ValueError):
|
||||||
book_id = None
|
book_id = None
|
||||||
if not uuidutils.is_uuid_like(flow_id):
|
|
||||||
flow_id = None
|
flow_id = None
|
||||||
except (IndexError, ValueError):
|
|
||||||
book_id = None
|
|
||||||
flow_id = None
|
|
||||||
|
|
||||||
# Set up how we want our engine to run, serial, parallel...
|
# Set up how we want our engine to run, serial, parallel...
|
||||||
engine_conf = {
|
engine_conf = {
|
||||||
'engine': 'parallel',
|
'engine': 'parallel',
|
||||||
}
|
}
|
||||||
if e_utils.EVENTLET_AVAILABLE:
|
if e_utils.EVENTLET_AVAILABLE:
|
||||||
engine_conf['executor'] = e_utils.GreenExecutor(5)
|
engine_conf['executor'] = e_utils.GreenExecutor(5)
|
||||||
|
|
||||||
# Create/fetch a logbook that will track the workflows work.
|
# Create/fetch a logbook that will track the workflows work.
|
||||||
book = None
|
book = None
|
||||||
flow_detail = None
|
flow_detail = None
|
||||||
if all([book_id, flow_id]):
|
if all([book_id, flow_id]):
|
||||||
with contextlib.closing(backend.get_connection()) as conn:
|
with contextlib.closing(backend.get_connection()) as conn:
|
||||||
try:
|
try:
|
||||||
book = conn.get_logbook(book_id)
|
book = conn.get_logbook(book_id)
|
||||||
flow_detail = book.find(flow_id)
|
flow_detail = book.find(flow_id)
|
||||||
except exc.NotFound:
|
except exc.NotFound:
|
||||||
pass
|
pass
|
||||||
if book is None and flow_detail is None:
|
if book is None and flow_detail is None:
|
||||||
book = p_utils.temporary_log_book(backend)
|
book = p_utils.temporary_log_book(backend)
|
||||||
engine = engines.load_from_factory(create_flow,
|
engine = engines.load_from_factory(create_flow,
|
||||||
backend=backend, book=book,
|
backend=backend, book=book,
|
||||||
engine_conf=engine_conf)
|
engine_conf=engine_conf)
|
||||||
print("!! Your tracking id is: '%s+%s'" % (book.uuid,
|
print("!! Your tracking id is: '%s+%s'" % (book.uuid,
|
||||||
engine.storage.flow_uuid))
|
engine.storage.flow_uuid))
|
||||||
print("!! Please submit this on later runs for tracking purposes")
|
print("!! Please submit this on later runs for tracking purposes")
|
||||||
else:
|
else:
|
||||||
# Attempt to load from a previously potentially partially completed flow.
|
# Attempt to load from a previously partially completed flow.
|
||||||
engine = engines.load_from_detail(flow_detail,
|
engine = engines.load_from_detail(flow_detail,
|
||||||
backend=backend, engine_conf=engine_conf)
|
backend=backend,
|
||||||
|
engine_conf=engine_conf)
|
||||||
|
|
||||||
# Make me my vm please!
|
# Make me my vm please!
|
||||||
print_wrapped('Running')
|
print_wrapped('Running')
|
||||||
engine.run()
|
engine.run()
|
||||||
|
|
||||||
# How to use.
|
# How to use.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ import time
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.ERROR)
|
logging.basicConfig(level=logging.ERROR)
|
||||||
|
|
||||||
|
self_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
os.pardir,
|
os.pardir,
|
||||||
os.pardir))
|
os.pardir))
|
||||||
sys.path.insert(0, top_dir)
|
sys.path.insert(0, top_dir)
|
||||||
|
sys.path.insert(0, self_dir)
|
||||||
|
|
||||||
from taskflow.patterns import graph_flow as gf
|
from taskflow.patterns import graph_flow as gf
|
||||||
from taskflow.patterns import linear_flow as lf
|
from taskflow.patterns import linear_flow as lf
|
||||||
@@ -37,9 +39,9 @@ from taskflow.patterns import linear_flow as lf
|
|||||||
from taskflow import engines
|
from taskflow import engines
|
||||||
from taskflow import task
|
from taskflow import task
|
||||||
|
|
||||||
from taskflow.persistence import backends
|
|
||||||
from taskflow.utils import persistence_utils as p_utils
|
from taskflow.utils import persistence_utils as p_utils
|
||||||
|
|
||||||
|
import example_utils # noqa
|
||||||
|
|
||||||
# INTRO: This examples shows how a hierarchy of flows can be used to create a
|
# INTRO: This examples shows how a hierarchy of flows can be used to create a
|
||||||
# pseudo-volume in a reliable & resumable manner using taskflow + a miniature
|
# pseudo-volume in a reliable & resumable manner using taskflow + a miniature
|
||||||
@@ -69,17 +71,6 @@ def find_flow_detail(backend, book_id, flow_id):
|
|||||||
return lb.find(flow_id)
|
return lb.find(flow_id)
|
||||||
|
|
||||||
|
|
||||||
def get_backend():
|
|
||||||
try:
|
|
||||||
backend_uri = sys.argv[1]
|
|
||||||
except Exception:
|
|
||||||
backend_uri = 'sqlite://'
|
|
||||||
|
|
||||||
backend = backends.fetch({'connection': backend_uri})
|
|
||||||
backend.get_connection().upgrade()
|
|
||||||
return backend
|
|
||||||
|
|
||||||
|
|
||||||
class PrintText(task.Task):
|
class PrintText(task.Task):
|
||||||
def __init__(self, print_what, no_slow=False):
|
def __init__(self, print_what, no_slow=False):
|
||||||
content_hash = hashlib.md5(print_what.encode('utf-8')).hexdigest()[0:8]
|
content_hash = hashlib.md5(print_what.encode('utf-8')).hexdigest()[0:8]
|
||||||
@@ -131,38 +122,39 @@ flow = lf.Flow("root").add(
|
|||||||
PrintText("Finished volume create", no_slow=True))
|
PrintText("Finished volume create", no_slow=True))
|
||||||
|
|
||||||
# Setup the persistence & resumption layer.
|
# Setup the persistence & resumption layer.
|
||||||
backend = get_backend()
|
with example_utils.get_backend() as backend:
|
||||||
try:
|
try:
|
||||||
book_id, flow_id = sys.argv[2].split("+", 1)
|
book_id, flow_id = sys.argv[2].split("+", 1)
|
||||||
except (IndexError, ValueError):
|
except (IndexError, ValueError):
|
||||||
book_id = None
|
book_id = None
|
||||||
flow_id = None
|
flow_id = None
|
||||||
|
|
||||||
if not all([book_id, flow_id]):
|
if not all([book_id, flow_id]):
|
||||||
# If no 'tracking id' (think a fedex or ups tracking id) is provided then
|
# If no 'tracking id' (think a fedex or ups tracking id) is provided
|
||||||
# we create one by creating a logbook (where flow details are stored) and
|
# then we create one by creating a logbook (where flow details are
|
||||||
# creating a flow detail (where flow and task state is stored). The
|
# stored) and creating a flow detail (where flow and task state is
|
||||||
# combination of these 2 objects unique ids (uuids) allows the users of
|
# stored). The combination of these 2 objects unique ids (uuids) allows
|
||||||
# taskflow to reassociate the workflows that were potentially running (and
|
# the users of taskflow to reassociate the workflows that were
|
||||||
# which may have partially completed) back with taskflow so that those
|
# potentially running (and which may have partially completed) back
|
||||||
# workflows can be resumed (or reverted) after a process/thread/engine
|
# with taskflow so that those workflows can be resumed (or reverted)
|
||||||
# has failed in someway.
|
# after a process/thread/engine has failed in someway.
|
||||||
logbook = p_utils.temporary_log_book(backend)
|
logbook = p_utils.temporary_log_book(backend)
|
||||||
flow_detail = p_utils.create_flow_detail(flow, logbook, backend)
|
flow_detail = p_utils.create_flow_detail(flow, logbook, backend)
|
||||||
print("!! Your tracking id is: '%s+%s'" % (logbook.uuid, flow_detail.uuid))
|
print("!! Your tracking id is: '%s+%s'" % (logbook.uuid,
|
||||||
print("!! Please submit this on later runs for tracking purposes")
|
flow_detail.uuid))
|
||||||
else:
|
print("!! Please submit this on later runs for tracking purposes")
|
||||||
flow_detail = find_flow_detail(backend, book_id, flow_id)
|
else:
|
||||||
|
flow_detail = find_flow_detail(backend, book_id, flow_id)
|
||||||
|
|
||||||
# Annnnd load and run.
|
# Annnnd load and run.
|
||||||
engine_conf = {
|
engine_conf = {
|
||||||
'engine': 'serial',
|
'engine': 'serial',
|
||||||
}
|
}
|
||||||
engine = engines.load(flow,
|
engine = engines.load(flow,
|
||||||
flow_detail=flow_detail,
|
flow_detail=flow_detail,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
engine_conf=engine_conf)
|
engine_conf=engine_conf)
|
||||||
engine.run()
|
engine.run()
|
||||||
|
|
||||||
# How to use.
|
# How to use.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import taskflow.test
|
import taskflow.test
|
||||||
|
|
||||||
ROOT_DIR = os.path.abspath(
|
ROOT_DIR = os.path.abspath(
|
||||||
@@ -48,9 +49,8 @@ def root_path(*args):
|
|||||||
|
|
||||||
def run_example(name):
|
def run_example(name):
|
||||||
path = root_path('taskflow', 'examples', '%s.py' % name)
|
path = root_path('taskflow', 'examples', '%s.py' % name)
|
||||||
obj = subprocess.Popen(
|
obj = subprocess.Popen([sys.executable, path],
|
||||||
[sys.executable, path],
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
output = obj.communicate()
|
output = obj.communicate()
|
||||||
if output[1]:
|
if output[1]:
|
||||||
raise RuntimeError('Example wrote to stderr:\n%s'
|
raise RuntimeError('Example wrote to stderr:\n%s'
|
||||||
@@ -59,15 +59,14 @@ def run_example(name):
|
|||||||
|
|
||||||
|
|
||||||
def expected_output_path(name):
|
def expected_output_path(name):
|
||||||
return root_path('taskflow', 'examples',
|
return root_path('taskflow', 'examples', '%s.out.txt' % name)
|
||||||
'%s.out.txt' % name)
|
|
||||||
|
|
||||||
|
|
||||||
def list_examples():
|
def list_examples():
|
||||||
ext = '.py'
|
|
||||||
examples_dir = root_path('taskflow', 'examples')
|
examples_dir = root_path('taskflow', 'examples')
|
||||||
for filename in os.listdir(examples_dir):
|
for filename in os.listdir(examples_dir):
|
||||||
if filename.endswith(ext):
|
name, ext = os.path.splitext(filename)
|
||||||
|
if ext == ".py" and 'utils' not in name.lower():
|
||||||
yield filename[:-len(ext)]
|
yield filename[:-len(ext)]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,19 @@
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from taskflow.persistence.backends import impl_sqlalchemy
|
import testtools
|
||||||
|
|
||||||
|
try:
|
||||||
|
from taskflow.persistence.backends import impl_sqlalchemy
|
||||||
|
SQLALCHEMY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
SQLALCHEMY_AVAILABLE = False
|
||||||
|
|
||||||
from taskflow import test
|
from taskflow import test
|
||||||
from taskflow.tests.unit.persistence import base
|
from taskflow.tests.unit.persistence import base
|
||||||
|
|
||||||
|
|
||||||
|
@testtools.skipIf(not SQLALCHEMY_AVAILABLE, 'sqlalchemy is not available')
|
||||||
class SqlPersistenceTest(test.TestCase, base.PersistenceTestMixin):
|
class SqlPersistenceTest(test.TestCase, base.PersistenceTestMixin):
|
||||||
"""Inherits from the base test and sets up a sqlite temporary db."""
|
"""Inherits from the base test and sets up a sqlite temporary db."""
|
||||||
def _get_connection(self):
|
def _get_connection(self):
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
eventlet>=0.13.0
|
|
||||||
211
tools/toxgen.py
Executable file
211
tools/toxgen.py
Executable file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# From: https://bitbucket.org/cdevienne/toxgen (pypi soon hopefully) and
|
||||||
|
# modified slightly to work in python 2.6 and set some values that are not
|
||||||
|
# being set.
|
||||||
|
#
|
||||||
|
# TODO(harlowja): remove me when toxgen is a pypi package.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Produce a tox.ini file from a template config file.
|
||||||
|
|
||||||
|
The template config file is a standard tox.ini file with additional sections.
|
||||||
|
Theses sections will be combined to create new testenv: sections if they do
|
||||||
|
not exists yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import itertools
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from six.moves import configparser
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections import OrderedDict
|
||||||
|
except ImportError:
|
||||||
|
from ordereddict import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
HEADER = '# DO NOT EDIT THIS FILE - it is machine generated from %(filename)s'
|
||||||
|
SKIP_VENVS = frozenset(['venv'])
|
||||||
|
|
||||||
|
parser = optparse.OptionParser(epilog=__doc__)
|
||||||
|
parser.add_option('-i', '--input', dest='input',
|
||||||
|
default='tox-tmpl.ini', metavar='FILE')
|
||||||
|
parser.add_option('-o', '--output', dest='output',
|
||||||
|
default='tox.ini', metavar='FILE')
|
||||||
|
|
||||||
|
|
||||||
|
class AxisItem(object):
|
||||||
|
def __init__(self, axis, name, config):
|
||||||
|
self.axis = axis
|
||||||
|
self.isdefault = name[-1] == '*'
|
||||||
|
self.name = name[:-1] if self.isdefault else name
|
||||||
|
self.load(config)
|
||||||
|
|
||||||
|
def load(self, config):
|
||||||
|
sectionname = 'axis:%s:%s' % (self.axis.name, self.name)
|
||||||
|
if config.has_section(sectionname):
|
||||||
|
self.options = dict(config.items(sectionname))
|
||||||
|
else:
|
||||||
|
self.options = dict()
|
||||||
|
|
||||||
|
for name, value in self.axis.defaults.items():
|
||||||
|
if name not in self.options:
|
||||||
|
self.options[name] = value
|
||||||
|
|
||||||
|
|
||||||
|
class Axis(object):
|
||||||
|
def __init__(self, name, config):
|
||||||
|
self.name = name
|
||||||
|
self.load(config)
|
||||||
|
|
||||||
|
def load(self, config):
|
||||||
|
self.items = dict()
|
||||||
|
values = config.get('axes', self.name).split(',')
|
||||||
|
if config.has_section('axis:%s' % self.name):
|
||||||
|
self.defaults = dict(
|
||||||
|
config.items('axis:%s' % self.name)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.defaults = {}
|
||||||
|
for value in values:
|
||||||
|
self.items[value.strip('*')] = AxisItem(self, value, config)
|
||||||
|
|
||||||
|
|
||||||
|
def format_list(contents, max_len=80, sep=","):
|
||||||
|
lines = []
|
||||||
|
for line in contents:
|
||||||
|
if not lines:
|
||||||
|
lines.append(line + ",")
|
||||||
|
else:
|
||||||
|
last_len = len(lines[-1])
|
||||||
|
if last_len + len(line) >= max_len:
|
||||||
|
lines.append(str(line) + sep)
|
||||||
|
else:
|
||||||
|
lines[-1] = lines[-1] + str(line) + sep
|
||||||
|
return "\n".join(lines).rstrip(",")
|
||||||
|
|
||||||
|
|
||||||
|
def render(incfg, filename, adjust_envlist=True):
|
||||||
|
test_envs = set()
|
||||||
|
for s in incfg.sections():
|
||||||
|
if s.startswith("testenv:"):
|
||||||
|
env = s[len("testenv:"):].strip()
|
||||||
|
if env in SKIP_VENVS or not env:
|
||||||
|
continue
|
||||||
|
test_envs.add(env)
|
||||||
|
test_envs = [s for s in test_envs if s]
|
||||||
|
|
||||||
|
try:
|
||||||
|
envlist = incfg.get("tox", 'envlist')
|
||||||
|
envlist = [e.strip() for e in envlist.split(",")]
|
||||||
|
envlist = set([e for e in envlist if e])
|
||||||
|
except (configparser.NoOptionError, configparser.NoSectionError):
|
||||||
|
envlist = set()
|
||||||
|
for e in test_envs:
|
||||||
|
if e not in envlist:
|
||||||
|
envlist.add(e)
|
||||||
|
|
||||||
|
if not incfg.has_section("tox"):
|
||||||
|
incfg.add_section("tox")
|
||||||
|
incfg.set("tox", "envlist",
|
||||||
|
format_list(list(sorted(envlist)), max_len=-1))
|
||||||
|
|
||||||
|
text = six.StringIO()
|
||||||
|
incfg.write(text)
|
||||||
|
contents = [
|
||||||
|
HEADER % {'filename': os.path.basename(filename)},
|
||||||
|
'',
|
||||||
|
# Remove how configparser uses tabs instead of spaces, madness...
|
||||||
|
text.getvalue().replace("\t", " " * 4),
|
||||||
|
]
|
||||||
|
return "\n".join(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def compile_template(incfg):
|
||||||
|
axes = dict()
|
||||||
|
|
||||||
|
if incfg.has_section('axes'):
|
||||||
|
for axis in incfg.options('axes'):
|
||||||
|
axes[axis] = Axis(axis, incfg)
|
||||||
|
|
||||||
|
out = configparser.ConfigParser(dict_type=OrderedDict)
|
||||||
|
for section in incfg.sections():
|
||||||
|
if section == 'axes' or section.startswith('axis:'):
|
||||||
|
continue
|
||||||
|
out.add_section(section)
|
||||||
|
for name, value in incfg.items(section):
|
||||||
|
out.set(section, name, value)
|
||||||
|
|
||||||
|
items = [axis.items.keys() for axis in axes.values()]
|
||||||
|
for combination in itertools.product(*items):
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
section_name = (
|
||||||
|
'testenv:' + '-'.join([item for item in combination if item])
|
||||||
|
)
|
||||||
|
section_alt_name = (
|
||||||
|
'testenv:' + '-'.join([
|
||||||
|
itemname
|
||||||
|
for axis, itemname in zip(axes.values(), combination)
|
||||||
|
if itemname and not axis.items[itemname].isdefault
|
||||||
|
])
|
||||||
|
)
|
||||||
|
if section_alt_name == section_name:
|
||||||
|
section_alt_name = None
|
||||||
|
|
||||||
|
axes_items = [
|
||||||
|
'%s:%s' % (axis, itemname)
|
||||||
|
for axis, itemname in zip(axes, combination)
|
||||||
|
]
|
||||||
|
|
||||||
|
for axis, itemname in zip(axes.values(), combination):
|
||||||
|
axis_options = axis.items[itemname].options
|
||||||
|
if 'constraints' in axis_options:
|
||||||
|
constraints = axis_options['constraints'].split('\n')
|
||||||
|
for c in constraints:
|
||||||
|
if c.startswith('!') and c[1:] in axes_items:
|
||||||
|
continue
|
||||||
|
for name, value in axis_options.items():
|
||||||
|
if name in options:
|
||||||
|
options[name] += value
|
||||||
|
else:
|
||||||
|
options[name] = value
|
||||||
|
|
||||||
|
constraints = options.pop('constraints', '').split('\n')
|
||||||
|
neg_constraints = [c[1:] for c in constraints if c and c[0] == '!']
|
||||||
|
if not set(neg_constraints).isdisjoint(axes_items):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not out.has_section(section_name):
|
||||||
|
out.add_section(section_name)
|
||||||
|
|
||||||
|
if (section_alt_name and not out.has_section(section_alt_name)):
|
||||||
|
out.add_section(section_alt_name)
|
||||||
|
|
||||||
|
for name, value in reversed(options.items()):
|
||||||
|
if not out.has_option(section_name, name):
|
||||||
|
out.set(section_name, name, value)
|
||||||
|
if section_alt_name and not out.has_option(section_alt_name, name):
|
||||||
|
out.set(section_alt_name, name, value)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
tmpl = configparser.ConfigParser()
|
||||||
|
with open(options.input, 'rb') as fh:
|
||||||
|
tmpl.readfp(fh, filename=options.input)
|
||||||
|
with open(options.output, 'wb') as outfile:
|
||||||
|
text = render(compile_template(tmpl), options.input)
|
||||||
|
outfile.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
84
tox-tmpl.ini
Normal file
84
tox-tmpl.ini
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# NOTE(harlowja): this is a template, not a fully-generated tox.ini, use toxgen
|
||||||
|
# to translate this into a fully specified tox.ini file before using. Changes
|
||||||
|
# made to tox.ini will only be reflected if ran through the toxgen generator.
|
||||||
|
|
||||||
|
[tox]
|
||||||
|
minversion = 1.6
|
||||||
|
skipsdist = True
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
usedevelop = True
|
||||||
|
install_command = pip install {opts} {packages}
|
||||||
|
setenv = VIRTUAL_ENV={envdir}
|
||||||
|
LANG=en_US.UTF-8
|
||||||
|
LANGUAGE=en_US:en
|
||||||
|
LC_ALL=C
|
||||||
|
deps = -r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/test-requirements.txt
|
||||||
|
commands = python setup.py testr --slowest --testr-args='{posargs}'
|
||||||
|
|
||||||
|
[tox:jenkins]
|
||||||
|
downloadcache = ~/cache/pip
|
||||||
|
|
||||||
|
[testenv:pep8]
|
||||||
|
commands =
|
||||||
|
flake8 {posargs}
|
||||||
|
|
||||||
|
[testenv:pylint]
|
||||||
|
setenv = VIRTUAL_ENV={envdir}
|
||||||
|
deps = -r{toxinidir}/requirements.txt
|
||||||
|
pylint==0.26.0
|
||||||
|
commands = pylint
|
||||||
|
|
||||||
|
[testenv:cover]
|
||||||
|
deps = -r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/test-requirements.txt
|
||||||
|
commands = python setup.py testr --coverage --testr-args='{posargs}'
|
||||||
|
|
||||||
|
[testenv:venv]
|
||||||
|
commands = {posargs}
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
ignore = H402
|
||||||
|
builtins = _
|
||||||
|
exclude = .venv,.tox,dist,doc,./taskflow/openstack/common,*egg,.git,build,tools
|
||||||
|
|
||||||
|
[axes]
|
||||||
|
python = py26,py27,py33
|
||||||
|
sqlalchemy = sa7,sa8,sa9,*
|
||||||
|
eventlet = ev,*
|
||||||
|
|
||||||
|
[axis:python:py26]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
|
||||||
|
[axis:python:py27]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
|
||||||
|
[axis:python:py33]
|
||||||
|
basepython = python3.3
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
|
||||||
|
[axis:eventlet:ev]
|
||||||
|
deps =
|
||||||
|
eventlet>=0.13.0
|
||||||
|
constraints=
|
||||||
|
!python:py33
|
||||||
|
|
||||||
|
[axis:sqlalchemy:sa7]
|
||||||
|
deps =
|
||||||
|
SQLAlchemy<=0.7.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[axis:sqlalchemy:sa8]
|
||||||
|
deps =
|
||||||
|
SQLAlchemy>0.7.99
|
||||||
|
SQLAlchemy<=0.8.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[axis:sqlalchemy:sa9]
|
||||||
|
deps =
|
||||||
|
SQLAlchemy>0.8.99
|
||||||
|
SQLAlchemy<=0.9.99
|
||||||
|
alembic>=0.4.1
|
||||||
198
tox.ini
198
tox.ini
@@ -1,47 +1,60 @@
|
|||||||
|
# DO NOT EDIT THIS FILE - it is machine generated from tox-tmpl.ini
|
||||||
|
|
||||||
[tox]
|
[tox]
|
||||||
minversion = 1.6
|
minversion = 1.6
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
envlist = py26,py27,py33,pep8
|
envlist = cover,
|
||||||
|
pep8,
|
||||||
|
py26,
|
||||||
|
py26-ev,
|
||||||
|
py26-sa7,
|
||||||
|
py26-sa7-ev,
|
||||||
|
py26-sa8,
|
||||||
|
py26-sa8-ev,
|
||||||
|
py26-sa9,
|
||||||
|
py26-sa9-ev,
|
||||||
|
py27,
|
||||||
|
py27-ev,
|
||||||
|
py27-sa7,
|
||||||
|
py27-sa7-ev,
|
||||||
|
py27-sa8,
|
||||||
|
py27-sa8-ev,
|
||||||
|
py27-sa9,
|
||||||
|
py27-sa9-ev,
|
||||||
|
py33,
|
||||||
|
py33-sa7,
|
||||||
|
py33-sa8,
|
||||||
|
py33-sa9,
|
||||||
|
pylint
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop = True
|
|
||||||
install_command = pip install {opts} {packages}
|
install_command = pip install {opts} {packages}
|
||||||
setenv = VIRTUAL_ENV={envdir}
|
|
||||||
LANG=en_US.UTF-8
|
|
||||||
LANGUAGE=en_US:en
|
|
||||||
LC_ALL=C
|
|
||||||
deps = -r{toxinidir}/requirements.txt
|
|
||||||
-r{toxinidir}/test-requirements.txt
|
|
||||||
commands = python setup.py testr --slowest --testr-args='{posargs}'
|
commands = python setup.py testr --slowest --testr-args='{posargs}'
|
||||||
|
setenv = VIRTUAL_ENV={envdir}
|
||||||
[testenv:py26]
|
LANG=en_US.UTF-8
|
||||||
|
LANGUAGE=en_US:en
|
||||||
|
LC_ALL=C
|
||||||
deps = -r{toxinidir}/requirements.txt
|
deps = -r{toxinidir}/requirements.txt
|
||||||
-r{toxinidir}/test-requirements.txt
|
-r{toxinidir}/test-requirements.txt
|
||||||
-r{toxinidir}/test-2.x-requirements.txt
|
usedevelop = True
|
||||||
|
|
||||||
[testenv:py27]
|
[testenv:pylint]
|
||||||
|
commands = pylint
|
||||||
|
setenv = VIRTUAL_ENV={envdir}
|
||||||
deps = -r{toxinidir}/requirements.txt
|
deps = -r{toxinidir}/requirements.txt
|
||||||
-r{toxinidir}/test-requirements.txt
|
pylint==0.26.0
|
||||||
-r{toxinidir}/test-2.x-requirements.txt
|
|
||||||
|
[testenv:cover]
|
||||||
|
commands = python setup.py testr --coverage --testr-args='{posargs}'
|
||||||
|
deps = -r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/test-requirements.txt
|
||||||
|
|
||||||
[tox:jenkins]
|
[tox:jenkins]
|
||||||
downloadcache = ~/cache/pip
|
downloadcache = ~/cache/pip
|
||||||
|
|
||||||
[testenv:pep8]
|
[testenv:pep8]
|
||||||
commands =
|
commands =
|
||||||
flake8 {posargs}
|
flake8 {posargs}
|
||||||
|
|
||||||
[testenv:pylint]
|
|
||||||
setenv = VIRTUAL_ENV={envdir}
|
|
||||||
deps = -r{toxinidir}/requirements.txt
|
|
||||||
pylint==0.26.0
|
|
||||||
commands = pylint
|
|
||||||
|
|
||||||
[testenv:cover]
|
|
||||||
deps = -r{toxinidir}/requirements.txt
|
|
||||||
-r{toxinidir}/test-requirements.txt
|
|
||||||
-r{toxinidir}/test-2.x-requirements.txt
|
|
||||||
commands = python setup.py testr --coverage --testr-args='{posargs}'
|
|
||||||
|
|
||||||
[testenv:venv]
|
[testenv:venv]
|
||||||
commands = {posargs}
|
commands = {posargs}
|
||||||
@@ -50,3 +63,132 @@ commands = {posargs}
|
|||||||
ignore = H402
|
ignore = H402
|
||||||
builtins = _
|
builtins = _
|
||||||
exclude = .venv,.tox,dist,doc,./taskflow/openstack/common,*egg,.git,build,tools
|
exclude = .venv,.tox,dist,doc,./taskflow/openstack/common,*egg,.git,build,tools
|
||||||
|
|
||||||
|
[testenv:py27]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
|
||||||
|
[testenv:py27-ev]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py27-sa9]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.8.99
|
||||||
|
SQLAlchemy<=0.9.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py27-sa9-ev]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.8.99
|
||||||
|
SQLAlchemy<=0.9.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py27-sa8]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.7.99
|
||||||
|
SQLAlchemy<=0.8.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py27-sa8-ev]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.7.99
|
||||||
|
SQLAlchemy<=0.8.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py27-sa7]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy<=0.7.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py27-sa7-ev]
|
||||||
|
basepython = python2.7
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy<=0.7.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py26]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
|
||||||
|
[testenv:py26-ev]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py26-sa9]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.8.99
|
||||||
|
SQLAlchemy<=0.9.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py26-sa9-ev]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.8.99
|
||||||
|
SQLAlchemy<=0.9.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py26-sa8]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.7.99
|
||||||
|
SQLAlchemy<=0.8.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py26-sa8-ev]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.7.99
|
||||||
|
SQLAlchemy<=0.8.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py26-sa7]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy<=0.7.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py26-sa7-ev]
|
||||||
|
basepython = python2.6
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy<=0.7.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
eventlet>=0.13.0
|
||||||
|
|
||||||
|
[testenv:py33]
|
||||||
|
basepython = python3.3
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
|
||||||
|
[testenv:py33-sa9]
|
||||||
|
basepython = python3.3
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.8.99
|
||||||
|
SQLAlchemy<=0.9.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py33-sa8]
|
||||||
|
basepython = python3.3
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy>0.7.99
|
||||||
|
SQLAlchemy<=0.8.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
[testenv:py33-sa7]
|
||||||
|
basepython = python3.3
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
SQLAlchemy<=0.7.99
|
||||||
|
alembic>=0.4.1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user