7 | {% endif %}
--------------------------------------------------------------------------------
/rostful/templates/security/email/change_notice.txt:
--------------------------------------------------------------------------------
1 | Your password has been changed
2 | {% if security.recoverable %}
3 | If you did not change your password, click the link below to reset it.
4 | {{ url_for_security('forgot_password', _external=True) }}
5 | {% endif %}
6 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # This should gather all unit tests for the flask server.
2 | # - using the mock in pyros
3 | # - using the ros interface
4 | # - using other mp systems interface
5 |
6 | # Integration REST API tests will be in each multiprocess system folder
7 |
8 |
9 |
--------------------------------------------------------------------------------
/rostful/templates/security/email/change_notice.html:
--------------------------------------------------------------------------------
1 |
4 | {% endif %}
5 |
--------------------------------------------------------------------------------
/rostful/_version.py:
--------------------------------------------------------------------------------
1 | # Store the version here so:
2 | # 1) we don't load dependencies by storing it in __init__.py
3 | # 2) we can import it in setup.py for the same reason
4 | # 3) we can import it into your module module
5 | __version_info__ = ('0', '2', '1')
6 | __version__ = '.'.join(__version_info__)
7 |
--------------------------------------------------------------------------------
/rostful/templates/security/send_login.html:
--------------------------------------------------------------------------------
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %}
2 | {% include "security/_messages.html" %}
3 |
Login
4 |
9 | {% include "security/_menu.html" %}
--------------------------------------------------------------------------------
/doc/weblinks.rst:
--------------------------------------------------------------------------------
1 | ..
2 | This file contains a common collection of web links. Include it
3 | wherever these links are needed.
4 |
5 | It is explicitly excluded in ``conf.py``, because it does not
6 | appear anywhere in the TOC tree.
7 |
8 | .. _ROS: http://wiki.ros.org
9 | .. _Pyros: http://github.com/asmodehn/pyros
10 | .. _REST: https://en.wikipedia.org/wiki/Representational_state_transfer
11 | .. _ROS name: http://wiki.ros.org/Names
12 |
13 |
14 |
--------------------------------------------------------------------------------
/rostful/templates/security/forgot_password.html:
--------------------------------------------------------------------------------
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %}
2 | {% include "security/_messages.html" %}
3 |
Send password reset instructions
4 |
9 | {% include "security/_menu.html" %}
--------------------------------------------------------------------------------
/rostful/templates/security/send_confirmation.html:
--------------------------------------------------------------------------------
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %}
2 | {% include "security/_messages.html" %}
3 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/rostful/context.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from flask import current_app, g
3 |
4 | from .exceptions import NoPyrosClient
5 |
6 | # TODO : improve that into using app context.
7 | # Creating pyros client should be simple and fast to be created everytime a request arrives.
8 | # Pyros should also be able to support multiple client at the same time...
9 |
10 | def set_pyros_client(app, pyros_client):
11 | app.pyros_client = pyros_client
12 |
13 |
14 | def get_pyros_client():
15 | """
16 | Give access to pyros client from app context
17 | :return:
18 | """
19 | pyros_client = getattr(g, '_pyros_client', None)
20 | if pyros_client is None:
21 | try:
22 | pyros_client = g._pyros_client = current_app.pyros_client # copying the app pyros client from app object to context
23 | except AttributeError as exc:
24 | if not hasattr(current_app, 'pyros_client'):
25 | raise NoPyrosClient("Warning : Pyros Client not found in current app : {0}".format(exc))
26 | else:
27 | raise
28 | return pyros_client
29 |
30 |
31 | def register_teardown(app):
32 | @app.teardown_appcontext
33 | def teardown_pyros_client(exception):
34 | pyros_client = getattr(g, '_pyros_client', None)
35 | # TODO : cleanup if necessary.
36 | #if pyros_client is not None:
37 | # pyros_client.close()
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | Rostful
2 | =======
3 |
4 | This `Python package`_ and `ROS`_ package allows sending `REST`_ request to a multiprocess system supported by `Pyros`_.
5 |
6 | ROS usage
7 | ---------
8 |
9 | Rostful interfaces a `ROS`_ system and the web world through a `REST`_ API.
10 | A `ROS name` ::
11 |
12 | /namespace/node_name/service_name
13 |
14 | is made available via the URL (by default) ::
15 |
16 | http://localhost:8000/ros/namespace/node_name/service_name
17 |
18 | - A service accept a POST request and returns a json message containing the original ROS service response
19 | - A Topic accept a POST on a Subscriber ( and returns nothing ) and a GET on a Publisher (and returns the last message received from that publisher)
20 | - A Param accept GET and POST request to get/set the value.
21 |
22 | Errors:
23 |
24 | - A request that is successful but doesnt return anything ( publisher didn't send any message ) return 204
25 | - A request to an non existent topic or service returns 404
26 | - A request with wrong message format returns 400
27 | - A request that triggers an error in the ROS system returns 500, as well as a traceback, usually very handy for debugging the issue.
28 | - A request that is not replied in 10 seconds returns 504
29 |
30 |
31 |
32 | .. include:: weblinks.rst
33 |
34 | Contents:
35 |
36 | .. toctree::
37 | :maxdepth: 2
38 |
39 | readme_link
40 | internals
41 | changelog_link
42 |
43 | Indices and tables
44 | ==================
45 |
46 | * :ref:`genindex`
47 | * :ref:`modindex`
48 |
--------------------------------------------------------------------------------
/rostful/static/js/jquery-mobile/images/icons-svg/grid-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
45 |
--------------------------------------------------------------------------------
/rostful/static/js/jquery-mobile/images/icons-svg/grid-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
45 |
--------------------------------------------------------------------------------
/rostful/templates/param.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Param {{ param.fullname }}{% endblock %}
3 | {% block header %}Param {{ param.fullname }}{% endblock %}
4 | {% block GET %}
5 |
6 |
Receive message
7 |
10 |
11 | {% endblock %}
12 |
13 | {% block GET_ajax %}
14 |
40 | {% endblock %}
41 |
42 |
43 | {% from 'macros.html' import content_navbar %}
44 | {% block footer %}{{ content_navbar() }}{% endblock %}
45 |
46 |
47 |
--------------------------------------------------------------------------------
/tests/test_rostful/test_services.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import sys
4 | import os
5 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
6 |
7 | import mock
8 | import unittest
9 |
10 | import pyros
11 | from rostful import create_app, set_pyros_client, ServiceNotFound
12 |
13 |
14 | class TestServicesNoPyros(unittest.TestCase):
15 |
16 | @classmethod
17 | def setUpClass(cls):
18 | pass
19 |
20 | @classmethod
21 | def tearDownClass(cls):
22 | pass
23 |
24 | def setUp(self):
25 | app = create_app()
26 | app.config['TESTING'] = True
27 | app.testing = True # required to check for exceptions
28 | self.client = app.test_client()
29 |
30 | def tearDown(self):
31 | pass
32 |
33 |
34 | class TestServicesPyros(TestServicesNoPyros):
35 |
36 | @classmethod
37 | def setUpClass(cls):
38 | pass
39 |
40 | @classmethod
41 | def tearDownClass(cls):
42 | pass
43 |
44 | # overloading run to instantiate the pyros context manager
45 | # this will call setup and teardown
46 | def run(self, result=None):
47 | # argv is rosargs but these have no effect on client, so no need to pass anything here
48 | with pyros.pyros_ctx(name='rostful', argv=[], mock_client=True, base_path=os.path.join(os.path.dirname(__file__), '..', '..', '..')) as node_ctx:
49 | self.node_ctx = node_ctx
50 | set_pyros_client(self.node_ctx.client)
51 | super(TestServicesPyros, self).run(result)
52 |
53 | def setUp(self):
54 | super(TestServicesPyros, self).setUp()
55 |
56 | def tearDown(self):
57 | super(TestServicesPyros, self).tearDown()
58 |
59 | if __name__ == '__main__':
60 |
61 | import nose
62 | nose.runmodule()
63 |
--------------------------------------------------------------------------------
/tests/test_rostful/test_topics.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import sys
4 | import os
5 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src')))
6 |
7 | import flask
8 |
9 | import unittest
10 | import nose
11 |
12 | import pyros
13 | from rostful import create_app, set_pyros_client, ServiceNotFound
14 |
15 |
16 | class TestTopicsNoPyros(unittest.TestCase):
17 |
18 | @classmethod
19 | def setUpClass(cls):
20 | pass
21 |
22 | @classmethod
23 | def tearDownClass(cls):
24 | pass
25 |
26 | def setUp(self):
27 | app = create_app
28 | app.config['TESTING'] = True
29 | app.testing = True # required to check for exceptions
30 | self.client = app.test_client()
31 |
32 | def tearDown(self):
33 | pass
34 |
35 |
36 | class TestTopicsPyros(TestTopicsNoPyros):
37 |
38 | @classmethod
39 | def setUpClass(cls):
40 | pass
41 |
42 | @classmethod
43 | def tearDownClass(cls):
44 | pass
45 |
46 | # overloading run to instantiate the pyros context manager
47 | # this will call setup and teardown
48 | def run(self, result=None):
49 | # argv is rosargs but these have no effect on client, so no need to pass anything here
50 | with pyros.pyros_ctx(name='rostful', argv=[], mock_client=True, base_path=os.path.join(os.path.dirname(__file__), '..', '..', '..')) as node_ctx:
51 | self.node_ctx = node_ctx
52 | set_pyros_client(self.node_ctx.client)
53 | super(TestTopicsPyros, self).run(result)
54 |
55 | def setUp(self):
56 | super(TestTopicsPyros, self).setUp()
57 |
58 | def tearDown(self):
59 | super(TestTopicsPyros, self).tearDown()
60 |
61 |
62 | if __name__ == '__main__':
63 |
64 | import nose
65 | nose.runmodule()
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # tox.ini , put in same dir as setup.py
2 | [tox]
3 |
4 | skip_missing_interpreters=True
5 |
6 | envlist =
7 | # based on ros distro with python2 base
8 | py27-{indigo,kinetic,latest},
9 |
10 | # Proper py3 version for LTS releases
11 | #py34-indigo,
12 | #py35-kinetic,
13 |
14 | # based on ros distro with python3 base
15 | #py36-{latest}
16 |
17 | #, pypy
18 | #, pypy3
19 |
20 | [travis]
21 | python =
22 | # we test every current ROS1 distro on python 2.7 (official python support for ROS1)
23 | 2.7 : py27
24 | # specific old python supported natively on ubuntu/ROS LTS distro
25 | #3.4 : py34
26 | #3.5 : py35
27 | # we test every current ROS1 distro on latest python (to ensure support from latest python)
28 | #3.6 : py36
29 |
30 | # not tested yet
31 | #pypy = pypy
32 | #pypy3 = pypy3
33 |
34 | # We depend on travis matrix
35 | [travis:env]
36 | ROS_DISTRO =
37 | kinetic: kinetic
38 | indigo: indigo
39 | latest: latest
40 |
41 | [testenv]
42 |
43 | setenv =
44 | # prevent tox to create a bunch of useless bytecode files in tests/
45 | PYTHONDONTWRITEBYTECODE=1
46 |
47 | # Dependencies matching the version in each ROS distro
48 | deps =
49 |
50 | indigo: -rrequirements/ROS/indigo.txt
51 | indigo: -rrequirements/tests.txt
52 | indigo: -rrequirements/dev.txt
53 |
54 | kinetic: -rrequirements/ROS/kinetic.txt
55 | kinetic: -rrequirements/tests.txt
56 | kinetic: -rrequirements/dev.txt
57 |
58 | latest: -rrequirements/tests.txt
59 | latest: -rrequirements/dev.txt
60 |
61 | # to always force recreation and avoid unexpected side effects
62 | recreate=True
63 |
64 | changedir = tests
65 |
66 | commands=
67 | # we want to make sure python finds the installed package in tox env
68 | # and doesn't confuse with pyc generated during dev (which happens if we use self test feature here)
69 | python -m pytest --basetemp={envtmpdir} test_rostful {posargs}
70 | # Note : -s here might break your terminal...
71 |
--------------------------------------------------------------------------------
/tests/test_rostful/test_frontend.py:
--------------------------------------------------------------------------------
1 | # This should test that a frontend is provided
2 | # Testing the UI is probably out of scope for this test.
3 | # Might also be unsuitable if the UI changes often...
4 |
5 | from __future__ import absolute_import
6 |
7 | import sys
8 | import os
9 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
10 |
11 | import mock
12 | import unittest
13 |
14 | import pyros
15 | from rostful import create_app, set_pyros_client, ServiceNotFound
16 |
17 |
18 | class TestFrontendNoPyros(unittest.TestCase):
19 |
20 | @classmethod
21 | def setUpClass(cls):
22 | pass
23 |
24 | @classmethod
25 | def tearDownClass(cls):
26 | pass
27 |
28 | def setUp(self):
29 | app = create_app()
30 | app.config['TESTING'] = True
31 | app.testing = True # required to check for exceptions
32 | self.client = app.test_client()
33 |
34 | def tearDown(self):
35 | pass
36 |
37 |
38 | class TestFrontendPyros(TestFrontendNoPyros):
39 |
40 | @classmethod
41 | def setUpClass(cls):
42 | pass
43 |
44 | @classmethod
45 | def tearDownClass(cls):
46 | pass
47 |
48 | # overloading run to instantiate the pyros context manager
49 | # this will call setup and teardown
50 | def run(self, result=None):
51 | # argv is rosargs but these have no effect on client, so no need to pass anything here
52 | with pyros.pyros_ctx(name='rostful', argv=[], mock_client=True, base_path=os.path.join(os.path.dirname(__file__), '..', '..', '..')) as node_ctx:
53 | self.node_ctx = node_ctx
54 | set_pyros_client(self.node_ctx.client)
55 | super(TestFrontendPyros, self).run(result)
56 |
57 | def setUp(self):
58 | super(TestFrontendPyros, self).setUp()
59 |
60 | def tearDown(self):
61 | super(TestFrontendPyros, self).tearDown()
62 |
63 | if __name__ == '__main__':
64 |
65 | import nose
66 | nose.runmodule()
67 |
--------------------------------------------------------------------------------
/rostful/api_0_1/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import importlib
5 | import json
6 | import base64
7 | from flask import jsonify, url_for
8 |
9 | import flask_restful as restful
10 |
11 |
12 | from webargs.flaskparser import FlaskParser, use_kwargs
13 | from webargs import fields
14 |
15 | from . import api, current_app
16 |
17 | parser = FlaskParser()
18 |
19 | """
20 | Flask View Dealing with URLs
21 | """
22 |
23 |
24 | @api.route('/', strict_slashes=False)
25 | class Index(restful.Resource): # TODO : unit test that stuff !!! http://flask.pocoo.org/docs/0.10/testing/
26 |
27 | """
28 | This is used especially to handle errors in this blueprint (REST style : JSON response)
29 | """
30 | def get(self):
31 |
32 | """Print available url and matching endpoints."""
33 | routes = {}
34 | for rule in current_app.url_map.iter_rules():
35 | try:
36 | if rule.endpoint != 'static':
37 | if hasattr(current_app.view_functions[rule.endpoint], 'import_name'):
38 | import_name = current_app.view_functions[rule.endpoint].import_name
39 | obj = importlib.import_module(import_name)
40 | else:
41 | obj = current_app.view_functions[rule.endpoint]
42 |
43 | routes[rule.rule] = {
44 | "methods": ','.join(rule.methods),
45 | "endpoint": rule.endpoint,
46 | "description": obj.__doc__
47 | }
48 |
49 | except Exception as exc:
50 | routes[rule.rule] = {
51 | "methods": ','.join(rule.methods),
52 | "endpoint": rule.endpoint,
53 | "description": "INVALID ROUTE DEFINITION!!!"
54 | }
55 | route_info = "%s => %s" % (rule.rule, rule.endpoint)
56 | current_app.logger.error("Invalid route: %s" % route_info, exc_info=True)
57 |
58 | return jsonify(routes)
59 |
60 |
--------------------------------------------------------------------------------
/.pyup.yml:
--------------------------------------------------------------------------------
1 | # configure updates globally
2 | # default: all
3 | # allowed: all, insecure, False
4 | update: all
5 |
6 | # configure dependency pinning globally
7 | # default: True
8 | # allowed: True, False
9 | pin: True
10 |
11 | # set the default branch
12 | # default: empty, the default branch on GitHub
13 | branch: master
14 |
15 | # update schedule
16 | # default: empty
17 | # allowed: "every day", "every week", ..
18 | schedule: "every week"
19 |
20 | # search for requirement files
21 | # default: True
22 | # allowed: True, False
23 | search: False
24 |
25 | # Specify requirement files by hand, default is empty
26 | # default: empty
27 | # allowed: list
28 | requirements:
29 | # These need to be manually pinned to match ROS distros versions
30 | - requirements/ROS/indigo.txt:
31 | # don't update dependencies, don't try to auto pin
32 | update: False
33 | pin: False
34 | - requirements/ROS/kinetic.txt:
35 | # don't update dependencies, don't try to auto pin
36 | update: False
37 | pin: False
38 |
39 | - requirements/dev.txt:
40 | # update all dependencies, use global 'pin' default
41 | update: all
42 |
43 | - requirements/tests.txt:
44 | # update all dependencies, use global 'pin' default
45 | update: all
46 |
47 | - requirements/tools.txt:
48 | # update all dependencies, never pin (dev will use whatever is latest for them)
49 | update: all
50 | pin: False
51 |
52 |
53 | # TODO : review tests nd default values after ROS things are out of the python repo
54 |
55 | # add a label to pull requests, default is not set
56 | # requires private repo permissions, even on public repos
57 | # default: empty
58 | label_prs: update
59 |
60 | # assign users to pull requests, default is not set
61 | # requires private repo permissions, even on public repos
62 | # default: empty
63 | assignees:
64 | - asmodehn
65 |
66 | # configure the branch prefix the bot is using
67 | # default: pyup-
68 | branch_prefix: pyup/
69 |
70 | # set a global prefix for PRs
71 | # default: empty
72 | #pr_prefix: "Bug #12345"
73 |
74 | # allow to close stale PRs
75 | # default: True
76 | #close_prs: True
--------------------------------------------------------------------------------
/rostful/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | # EXCEPTION CLASSES
5 | # should be used to return anything that is not 2xx, python style.
6 |
7 |
8 | class NoPyrosClient(Exception):
9 | status_code = 500
10 |
11 | def __init__(self, message, status_code=None, traceback=None):
12 | Exception.__init__(self)
13 | self.message = message
14 | if status_code is not None:
15 | self.status_code = status_code
16 | self.traceback = traceback
17 |
18 | def to_dict(self):
19 | rv = dict({})
20 | rv['message'] = self.message
21 | rv['traceback'] = self.traceback
22 | return rv
23 |
24 |
25 | class WrongMessageFormat(Exception):
26 | status_code = 400
27 |
28 | def __init__(self, message, status_code=None, traceback=None):
29 | Exception.__init__(self)
30 | self.message = message
31 | if status_code is not None:
32 | self.status_code = status_code
33 | self.traceback = traceback
34 |
35 | def to_dict(self):
36 | rv = dict({})
37 | rv['message'] = self.message
38 | rv['traceback'] = self.traceback
39 | return rv
40 |
41 |
42 | class ServiceTimeout(Exception):
43 | status_code = 504
44 |
45 | def __init__(self, message, status_code=None, traceback=None):
46 | Exception.__init__(self)
47 | self.message = message
48 | if status_code is not None:
49 | self.status_code = status_code
50 | self.traceback = traceback
51 |
52 | def to_dict(self):
53 | rv = dict({})
54 | rv['message'] = self.message
55 | rv['traceback'] = self.traceback
56 | return rv
57 |
58 |
59 | class ServiceNotFound(Exception):
60 | status_code = 404
61 |
62 | def __init__(self, message, status_code=None, traceback=None):
63 | Exception.__init__(self)
64 | self.message = message
65 | if status_code is not None:
66 | self.status_code = status_code
67 | self.traceback = traceback
68 |
69 | def to_dict(self):
70 | rv = dict({})
71 | rv['message'] = self.message
72 | rv['traceback'] = self.traceback
73 | return rv
74 |
--------------------------------------------------------------------------------
/rostful/static/js/jquery-mobile/images/icons-svg/gear-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/rostful/static/js/jquery-mobile/images/icons-svg/gear-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/rostful/templates/service.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block title %}Service {{ service.fullname }}{% endblock %}
3 | {% block header %}Service {{ service.fullname }}{% endblock %}
4 |
5 | {% block GET %}
6 |
7 | {% endblock %}
8 |
9 | {% block GET_ajax %}
10 |
11 | {% endblock %}
12 |
13 | {% from 'macros.html' import generate_inputs, generate_msgdata, dynamic_array_input %}
14 |
15 | {% block UTIL_js %}
16 |
21 | {% endblock %}
22 |
23 | {% block POST %}
24 |
47 |
48 |
49 |
50 |
54 |
55 |
56 |
57 | {% endblock %}
58 |
59 | {% block POST_ajax %}
60 |
94 | {% endblock %}
95 |
96 |
97 | {% from 'macros.html' import content_navbar %}
98 | {% block footer %}{{ content_navbar() }}{% endblock %}
99 |
100 |
101 |
--------------------------------------------------------------------------------
/tests/test_rostful/test_app.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import mock
4 | import sys
5 | import os
6 |
7 | #sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'rostful')))
8 |
9 | import unittest
10 | import nose
11 |
12 | from pyros.server.ctx_server import pyros_ctx
13 |
14 |
15 | from rostful import create_app, set_pyros_client, ServiceNotFound, NoPyrosClient
16 |
17 |
18 | # Basic Test class for an simple Flask wsgi app
19 | class TestAppNoPyros(unittest.TestCase):
20 |
21 | @classmethod
22 | def setUpClass(cls):
23 | pass
24 |
25 | @classmethod
26 | def tearDownClass(cls):
27 | pass
28 |
29 | def setUp(self):
30 | # forcing dev config to not rely on the complex ROS/python/flask path mess,
31 | # tests can be started in all kinds of weird ways (nose auto import, etc.)
32 | #config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'instance', 'rostful.cfg')
33 | #self.app = create_app(configfile_override=config_path)
34 | # TESTS needs to work for BOTH installed and source package -> we have to rely on flask instance configuration mechanism...
35 | self.app = create_app()
36 | self.app.debug = True
37 | self.app.config['TESTING'] = True
38 | self.app.testing = True # required to check for exceptions
39 |
40 | def tearDown(self):
41 | pass
42 |
43 | def test_index_root(self):
44 | with self.app.test_client() as client:
45 | res = client.get('/', follow_redirects=True)
46 | nose.tools.assert_equals(res.status_code, 200)
47 |
48 | def test_frontend_root(self):
49 | with self.app.test_client() as client:
50 | res = client.get('/frontend')
51 | nose.tools.assert_equals(res.status_code, 200)
52 |
53 | def test_crossdomain(self):
54 | with self.app.test_client() as client:
55 | res = client.get('/', follow_redirects=True)
56 | nose.tools.assert_equals(res.status_code, 200)
57 | # Not working. TODO : recheck after switching to WSGI Cors
58 | #nose.tools.assert_equals(res.headers['Access-Control-Allow-Origin'], '*')
59 | #nose.tools.assert_equals(res.headers['Access-Control-Max-Age'], '21600')
60 | #nose.tools.assert_true('HEAD' in res.headers['Access-Control-Allow-Methods'])
61 | #nose.tools.assert_true('OPTIONS' in res.headers['Access-Control-Allow-Methods'])
62 | #nose.tools.assert_true('GET' in res.headers['Access-Control-Allow-Methods'])
63 |
64 | def test_error(self):
65 | with self.app.test_client() as client:
66 | res = client.get('/api/v0.1/non-existent')
67 | # TODO : dig into flask restful to find out how we should handle custom errors http://flask-restful-cn.readthedocs.io/en/0.3.4/extending.html
68 | nose.tools.assert_equal(res.status_code, 404)
69 |
70 |
71 | class TestAppPyros(TestAppNoPyros):
72 |
73 | @classmethod
74 | def setUpClass(cls):
75 | pass
76 |
77 | @classmethod
78 | def tearDownClass(cls):
79 | pass
80 |
81 | # overloading run to instantiate the pyros context manager
82 | # this will call setup and teardown
83 | def run(self, result=None):
84 | # argv is rosargs but these have no effect on client, so no need to pass anything here
85 | with pyros_ctx(name='rostful', argv=[], mock_client=True) as node_ctx:
86 | self.node_ctx = node_ctx
87 | super(TestAppPyros, self).run(result)
88 |
89 | def setUp(self):
90 | super(TestAppPyros, self).setUp()
91 | set_pyros_client(self.app, self.node_ctx.client)
92 |
93 | def tearDown(self):
94 | super(TestAppPyros, self).tearDown()
95 |
96 | def test_index_root(self):
97 | super(TestAppPyros, self).test_index_root()
98 | # verify pyros mock client was actually called
99 | self.node_ctx.client.topics.assert_called_once_with()
100 | self.node_ctx.client.services.assert_called_once_with()
101 | self.node_ctx.client.params.assert_called_once_with()
102 |
103 | def test_crossdomain(self):
104 | super(TestAppPyros, self).test_crossdomain()
105 | # verify pyros mock client was actually called
106 | self.node_ctx.client.topics.assert_called_once_with()
107 | self.node_ctx.client.services.assert_called_once_with()
108 | self.node_ctx.client.params.assert_called_once_with()
109 |
110 | def test_error(self):
111 | with self.app.test_client() as client:
112 | res = client.get('/api/v0.1/non-existent')
113 | nose.tools.assert_equal(res.status_code, 404)
114 | # TODO : dig into flask restful to find out how we should handle custom errors http://flask-restful-cn.readthedocs.io/en/0.3.4/extending.html
115 | # verify pyros mock client was actually called
116 | # self.node_ctx.client.topics.assert_called_once_with()
117 | # self.node_ctx.client.services.assert_called_once_with()
118 | # This is not implemented yet
119 | #self.node_ctx.client.params.assert_called_once_with()
120 |
121 |
122 | if __name__ == '__main__':
123 |
124 | import nose
125 | nose.runmodule()
126 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 | import subprocess
3 |
4 | # using setuptools : http://pythonhosted.org/setuptools/
5 |
6 | with open('rostful/_version.py') as vf:
7 | exec(vf.read())
8 |
9 |
10 | # Best Flow :
11 | # Clean previous build & dist
12 | # $ gitchangelog >CHANGELOG.rst
13 | # change version in code and changelog
14 | # $ python setup.py prepare_release
15 | # WAIT FOR TRAVIS CHECKS
16 | # $ python setup.py publish
17 | # => TODO : try to do a simpler "release" command
18 |
19 | #
20 | # And then for ROS, directly (tag is already in git repo):
21 | # bloom-release --rosdistro kinetic --track kinetic pyros_interfaces_ros
22 |
23 |
24 | # Clean way to add a custom "python setup.py "
25 | # Ref setup.py command extension : https://blog.niteoweb.com/setuptools-run-custom-code-in-setup-py/
26 | class PrepareReleaseCommand(setuptools.Command):
27 | """Command to release this package to Pypi"""
28 | description = "prepare a release of pyros"
29 | user_options = []
30 |
31 | def initialize_options(self):
32 | """init options"""
33 | pass
34 |
35 | def finalize_options(self):
36 | """finalize options"""
37 | pass
38 |
39 | def run(self):
40 | """runner"""
41 |
42 | # TODO :
43 | # $ gitchangelog >CHANGELOG.rst
44 | # change version in code and changelog
45 | subprocess.check_call("git commit CHANGELOG.rst rostful/_version.py -m 'v{0}'".format(__version__), shell=True)
46 | subprocess.check_call("git push", shell=True)
47 |
48 | print("You should verify travis checks, and you can publish this release with :")
49 | print(" python setup.py publish")
50 | sys.exit()
51 |
52 |
53 | # Clean way to add a custom "python setup.py "
54 | # Ref setup.py command extension : https://blog.niteoweb.com/setuptools-run-custom-code-in-setup-py/
55 | class PublishCommand(setuptools.Command):
56 | """Command to release this package to Pypi"""
57 | description = "releases pyros to Pypi"
58 | user_options = []
59 |
60 | def initialize_options(self):
61 | """init options"""
62 | # TODO : register option
63 | pass
64 |
65 | def finalize_options(self):
66 | """finalize options"""
67 | pass
68 |
69 | def run(self):
70 | """runner"""
71 | # TODO : clean build/ and dist/ before building...
72 | subprocess.check_call("python setup.py sdist", shell=True)
73 | subprocess.check_call("python setup.py bdist_wheel", shell=True)
74 | # OLD way:
75 | # os.system("python setup.py sdist bdist_wheel upload")
76 | # NEW way:
77 | # Ref: https://packaging.python.org/distributing/
78 | subprocess.check_call("twine upload dist/*", shell=True)
79 |
80 | subprocess.check_call("git tag -a {0} -m 'version {0}'".format(__version__), shell=True)
81 | subprocess.check_call("git push --tags", shell=True)
82 | sys.exit()
83 |
84 |
85 |
86 | # https://hynek.me/articles/conditional-python-dependencies/
87 | import sys
88 | sys_requires = []
89 | if sys.version_info[0:3] <= (2, 7, 9) or (3, 0) < sys.version_info[0:2] <= (3, 4):
90 | sys_requires += ['backports.ssl-match-hostname']
91 |
92 |
93 | setuptools.setup(
94 | name='rostful',
95 | version=__version__,
96 | description='REST API for ROS',
97 | url='http://github.com/asmodehn/rostful',
98 | author='AlexV',
99 | author_email='asmodehn@gmail.com',
100 | license='BSD',
101 | packages=[
102 | 'rostful',
103 | 'rostful.api_0_1',
104 | 'rostful.api_0_2',
105 | 'rostful.frontend',
106 | #'rostful.tests', # not embedding tests in package for now...
107 | ],
108 | package_dir={
109 | },
110 | entry_points={
111 | 'console_scripts': [
112 | 'rostful = rostful.__main__:cli'
113 | ]
114 | },
115 | # this is better than using package data ( since behavior is a bit different from distutils... )
116 | include_package_data=True, # use MANIFEST.in during install.
117 | package_data={ # TODO : might be better to do this in MANIFEST
118 | 'rostful': [
119 | 'static/favicon.ico',
120 | 'static/js/moment/*',
121 | 'static/js/jquery/*',
122 | 'static/js/jquery-mobile/jquery.*',
123 | 'static/js/jquery-mobile/images/ajax-loader.gif',
124 | 'static/js/jquery-mobile/images/icons-png/*',
125 | 'static/js/jquery-mobile/images/icons-svg/*',
126 | 'templates/*.html',
127 | 'templates/security/*.html',
128 | 'templates/security/email/*',
129 | ],
130 | },
131 | install_requires=sys_requires + [
132 | #'futures>=3.0.2',
133 | 'Flask>=0.10.1',
134 | 'Flask-Cors>=3.0.2',
135 | 'Flask-Restful>=0.3.4',
136 | 'Flask-reverse-proxy',
137 | 'click>=6.2.0',
138 | 'webargs>=1.3.4',
139 | 'pyros>=0.4.3',
140 | #'pyros_setup>=0.1.5', # pyros should provide this...
141 | #'pyros_config>=0.1.4', # pyros should provide this...
142 | 'tornado>=4.2.1, <5.0', # untested with tornado > 5.0
143 | 'simplejson',
144 | 'tblib>=1.2',
145 | ],
146 | cmdclass={
147 | 'prepare_release': PrepareReleaseCommand,
148 | 'publish': PublishCommand,
149 | },
150 | zip_safe=False, # TODO testing...
151 | )
152 |
153 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | |Build Status| |Documentation Status| |Updates| |Python 3|
2 |
3 | ROSTful
4 | =======
5 |
6 | ROStful - A REST API for ROS.
7 |
8 | We follow a `feature branching workflow `_
9 |
10 | Rostful is intended to be the outside layer of ROS (and eventualy any multiprocess system). Meaning it will interface ROS with other systems, over the internet, via HTTP and through a REST API.
11 | As such, this should be used as any python program, from a virtual environment. We also need to install the specific pyros interface for the system we want to expose.
12 |
13 | ```
14 | $ mkvirtualenv rostful
15 | (rostful)$ pip install rostful pyros[ros]
16 | (rostful)$ python -m rostful flask
17 | ```
18 |
19 | A ROS package is provided as a third party release, for ease of deployment within a ROS system.
20 | However it is heavily recommended to do development the "python way", dynamically, using virtual environments, with quick iterations.
21 | The rostful PyPI package is released more often than the ROS package, and will have latest updates available for use.
22 |
23 |
24 | PYTHON VIRTUALENV SETUP
25 | =======================
26 |
27 | How to setup your python virtual environment on Ubuntu (tested on Xenial 16.04)
28 | * Install and Setup virtualenvwrapper if needed
29 | ```
30 | sudo apt install virtualenvwrapper
31 | ```
32 | * Create your virtual environment for your project
33 | ```
34 | $ mkvirtualenv myproject
35 | ```
36 | * Populate it to use rostful. The catkin dependency is temporarily needed to be able to use the setup.py currently provided in rostful.
37 | ```
38 | (myproject)$ pip install rostful pyros[ros]
39 | (myproject)$ pip install rostful
40 | ```
41 |
42 |
43 | Try it now
44 | ----------
45 |
46 | Go check the `examples`_
47 |
48 | Overview
49 | ---------
50 |
51 | ROStful is a lightweight web server for making ROS services, topics, and
52 | actions available as RESTful web services.
53 |
54 | ROStful web services primarily use the `rosbridge`_ JSON mapping for ROS
55 | messages. However, binary serialized ROS messages can be used to
56 | increase performance.
57 |
58 | The purpose of ROStful is different from `rosbridge`_: rosbridge
59 | provides an API for ROS through JSON using web sockets. ROStful allows
60 | specific services, topics, and actions to be provided as web services
61 | (using plain get and post requests) without exposing underlying ROS
62 | concepts. The ROStful client, however, additionally provides a modicum
63 | of multi-master functionality. The client proxy is a node that connects
64 | to a ROStful web service and exposes the services, topics, and actions
65 | locally over ROS.
66 |
67 | The ROStful server has no dependencies on 3rd party libraries, and is
68 | WSGI-compatible and can therefore be used with most web servers like
69 | Apache and IIS.
70 |
71 | ROStful web services
72 | ~~~~~~~~~~~~~~~~~~~~~
73 |
74 | A ROStful web service is a web service that uses ROS data structures for
75 | input and output. These include services, topics, and actions.
76 |
77 | Service methods accept as input a ROS message over HTTP POST and return
78 | a ROS message in the response. The input and output cannot be defined
79 | directly with ROS messages; it must use a ROS service definition.
80 |
81 | Methods denoted as topics may use any ROS message, but are limited to
82 | accepting that message via HTTP POST or returning it, taking no input,
83 | via HTTP GET. Topic methods do not need to allow both methods. A topic
84 | method allowing POST is described as a “subscribing” method, and one
85 | that allows GET is a “publishing” method.
86 |
87 | Methods denoted as actions consist of a set of subsidiary topic methods,
88 | the subscribing-only ``goal`` and ``cancel`` methods, and the
89 | publishing-only ``status``, ``result``, and ``feedback`` methods. These
90 | methods are located at the url ``/``, where
91 | the suffix is the subsidiary method name.
92 |
93 | The ROStful server
94 | ~~~~~~~~~~~~~~~~~~
95 |
96 | The ROStful server can provide services, topics, and actions that are
97 | locally available over ROS as ROStful web services. Topics may be
98 | specified as publishing, subscribing, or both.
99 |
100 | ROStful uses the rosbridge JSON mapping by default, but binary
101 | serialized ROS messages can be sent with the ``Content-Header`` set to
102 | ``application/vnd.ros.msg``. Giving this MIME type in the ``Accept``
103 | header for queries without input (i.e., publishing topic methods) will
104 | cause the server to return serialized messages.
105 |
106 |
107 |
108 | What will not be in Rostful
109 | ===========================
110 | - Security related stuff ( Authentication/Authorization ) implementation.
111 | We will not provide here any Authentication/Authorization mechanisms without ROS providing one first.
112 | And even after that, the implications of such an implementation would probably fit better in another specific microservice, that we would rely on in rostful.
113 |
114 |
115 |
116 | .. _examples: https://github.com/asmodehn/rostful-examples
117 | .. _rosbridge: http://wiki.ros.org/rosbridge_suite
118 |
119 | .. |Build Status| image:: https://travis-ci.org/asmodehn/rostful.svg?branch=master
120 | :target: https://travis-ci.org/asmodehn/rostful
121 | :alt: Build Status
122 |
123 | .. |Documentation Status| image:: https://readthedocs.org/projects/rostful/badge/?version=latest
124 | :target: http://rostful.readthedocs.io/en/latest/?badge=latest
125 | :alt: Documentation Status
126 |
127 | .. |Updates| image:: https://pyup.io/repos/github/asmodehn/rostful/shield.svg
128 | :target: https://pyup.io/repos/github/asmodehn/rostful/
129 | :alt: Updates
130 |
131 | .. |Python 3| image:: https://pyup.io/repos/github/asmodehn/rostful/python-3-shield.svg
132 | :target: https://pyup.io/repos/github/asmodehn/rostful/
133 | :alt: Python 3
134 |
--------------------------------------------------------------------------------
/rostful/frontend/flask_views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import re
5 |
6 | # Reference for package structure since this is a flask app : http://flask.pocoo.org/docs/0.10/patterns/packages/
7 | from rostful import get_pyros_client
8 |
9 | import time
10 |
11 | from pyros.client.client import PyrosServiceTimeout
12 |
13 | CONFIG_PATH = '_rosdef'
14 | SRV_PATH = '_srv'
15 | MSG_PATH = '_msg'
16 |
17 |
18 | def get_suffix(path):
19 | suffixes = '|'.join([re.escape(s) for s in [CONFIG_PATH, SRV_PATH, MSG_PATH]])
20 | match = re.search(r'/(%s)$' % suffixes, path)
21 | return match.group(1) if match else ''
22 |
23 | # TODO : remove ROS usage here, keep this a pure Flask App as much as possible
24 |
25 |
26 | ROS_MSG_MIMETYPE = 'application/vnd.ros.msg'
27 | def ROS_MSG_MIMETYPE_WITH_TYPE(rostype):
28 | if isinstance(rostype, type):
29 | name = rostype.__name__
30 | module = rostype.__module__.split('.')[0]
31 | rostype = module + '/' + name
32 | return 'application/vnd.ros.msg; type=%s' % rostype
33 |
34 |
35 | #req should be a flask request
36 | #TODO : improve package design...
37 | def request_wants_ros(req):
38 | best = req.accept_mimetypes.best_match([ROS_MSG_MIMETYPE,'application/json'])
39 | return best == ROS_MSG_MIMETYPE and req.accept_mimetypes[best] > req.accept_mimetypes['application/json']
40 | #implementation ref : http://flask.pocoo.org/snippets/45/
41 |
42 |
43 | def get_json_bool(b):
44 | if b:
45 | return 'true'
46 | else:
47 | return 'false'
48 |
49 |
50 | def get_query_bool(query_string, param_name):
51 | return re.search(r'(^|&)%s((=(true|1))|&|$)' % param_name, query_string, re.IGNORECASE)
52 |
53 |
54 | from flask import Flask, request, make_response, render_template, jsonify, redirect
55 | from flask.views import MethodView
56 | from flask_restful import reqparse
57 | import flask_restful as restful
58 |
59 | from . import app_blueprint, current_app
60 |
61 |
62 | from webargs.flaskparser import FlaskParser, use_kwargs
63 |
64 | parser = FlaskParser()
65 |
66 | import urllib
67 | from pyros.client.client import PyrosException
68 | from rostful.exceptions import ServiceNotFound, ServiceTimeout, WrongMessageFormat, NoPyrosClient
69 |
70 |
71 |
72 | class Timeout(object):
73 | """
74 | Small useful timeout class
75 | """
76 | def __init__(self, seconds):
77 | self.seconds = seconds
78 |
79 | def __enter__(self):
80 | self.die_after = time.time() + self.seconds
81 | return self
82 |
83 | def __exit__(self, type, value, traceback):
84 | pass
85 |
86 | @property
87 | def timed_out(self):
88 | return time.time() > self.die_after
89 |
90 |
91 | """
92 | View for frontend pages
93 | """
94 | # TODO: maybe consider http://www.flaskapi.org/
95 | # TODO: or maybe better https://github.com/OAI/OpenAPI-Specification
96 | # TODO: or probably best : https://github.com/rantav/flask-restful-swagger
97 |
98 | # TODO : maybe this can remove the need for a separate blueprint all together...
99 | # http://flask.pocoo.org/snippets/45/
100 | # https://flask-restful.readthedocs.io/en/0.3.2/extending.html#content-negotiation
101 |
102 |
103 | @app_blueprint.route('/', strict_slashes=False, endpoint='ros_list')
104 | def ros_list():
105 | current_app.logger.debug('in ros_list ')
106 |
107 | try:
108 | node_client = get_pyros_client() # we retrieve pyros client from app context
109 | return render_template(
110 | 'index.html',
111 | pathname2url=urllib.pathname2url,
112 | topics=node_client.topics(),
113 | services=node_client.services(),
114 | params=node_client.params(),
115 | )
116 | except NoPyrosClient as exc:
117 | # silently handle the case when we dont have pyros running
118 | return render_template(
119 | 'index.html',
120 | pathname2url=urllib.pathname2url,
121 | topics=[],
122 | services=[],
123 | params=[],
124 | )
125 | except Exception as exc:
126 | # TODO : properly display exception (debug mode at least, currently "localhost didn’t send any data. ERR_EMPTY_RESPONSE")
127 | # failing request if unknown exception triggered
128 | raise
129 |
130 |
131 | @app_blueprint.route('/', strict_slashes=False, endpoint='ros_interface')
132 | def ros_interface(rosname):
133 | current_app.logger.debug('in ros_interface with rosname: %r', rosname)
134 |
135 | node_client = get_pyros_client() # we retrieve pyros client from app context
136 |
137 | # we might need to add "/" to rosname passed as url to match absolute service/topics names listed
138 | if not rosname.startswith("/"):
139 | rosname = "/" + rosname
140 |
141 | services = None
142 | topics = None
143 | params = None
144 | with Timeout(30) as t:
145 | while not t.timed_out and (services is None or topics is None or params is None):
146 | try:
147 | params = node_client.params()
148 | except PyrosServiceTimeout:
149 | params = None
150 | try:
151 | services = node_client.services()
152 | except PyrosServiceTimeout:
153 | services = None
154 | try:
155 | topics = node_client.topics()
156 | except PyrosServiceTimeout:
157 | topics = None
158 |
159 | if t.timed_out:
160 | raise ServiceNotFound("Cannot list services, topics or params. No response from pyros.")
161 |
162 | if rosname in services:
163 | mode = 'service'
164 | service = services[rosname]
165 | return render_template('service.html', service=service)
166 | elif rosname in topics:
167 | mode = 'topic'
168 | topic = topics[rosname]
169 | return render_template('topic.html', topic=topic)
170 | elif rosname in params:
171 | mode = 'param'
172 | param = params[rosname]
173 | return render_template('param.html', param=param)
174 | else:
175 | raise ServiceNotFound("{0} not found among Pyros exposed services, topics and params".format(rosname))
176 |
177 |
--------------------------------------------------------------------------------
/.gitchangelog.rc:
--------------------------------------------------------------------------------
1 | ##
2 | ## Format
3 | ##
4 | ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
5 | ##
6 | ## Description
7 | ##
8 | ## ACTION is one of 'chg', 'fix', 'new'
9 | ##
10 | ## Is WHAT the change is about.
11 | ##
12 | ## 'chg' is for refactor, small improvement, cosmetic changes...
13 | ## 'fix' is for bug fixes
14 | ## 'new' is for new features, big improvement
15 | ##
16 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
17 | ##
18 | ## Is WHO is concerned by the change.
19 | ##
20 | ## 'dev' is for developpers (API changes, refactors...)
21 | ## 'usr' is for final users (UI changes)
22 | ## 'pkg' is for packagers (packaging changes)
23 | ## 'test' is for testers (test only related changes)
24 | ## 'doc' is for doc guys (doc only changes)
25 | ##
26 | ## COMMIT_MSG is ... well ... the commit message itself.
27 | ##
28 | ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
29 | ##
30 | ## They are preceded with a '!' or a '@' (prefer the former, as the
31 | ## latter is wrongly interpreted in github.) Commonly used tags are:
32 | ##
33 | ## 'refactor' is obviously for refactoring code only
34 | ## 'minor' is for a very meaningless change (a typo, adding a comment)
35 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
36 | ## 'wip' is for partial functionality but complete subfunctionality.
37 | ##
38 | ## Example:
39 | ##
40 | ## new: usr: support of bazaar implemented
41 | ## chg: re-indentend some lines !cosmetic
42 | ## new: dev: updated code to be compatible with last version of killer lib.
43 | ## fix: pkg: updated year of licence coverage.
44 | ## new: test: added a bunch of test around user usability of feature X.
45 | ## fix: typo in spelling my name in comment. !minor
46 | ##
47 | ## Please note that multi-line commit message are supported, and only the
48 | ## first line will be considered as the "summary" of the commit message. So
49 | ## tags, and other rules only applies to the summary. The body of the commit
50 | ## message will be displayed in the changelog without reformatting.
51 |
52 |
53 | ##
54 | ## ``ignore_regexps`` is a line of regexps
55 | ##
56 | ## Any commit having its full commit message matching any regexp listed here
57 | ## will be ignored and won't be reported in the changelog.
58 | ##
59 | ignore_regexps = [
60 | r'@minor', r'!minor',
61 | r'@cosmetic', r'!cosmetic',
62 | r'@refactor', r'!refactor',
63 | r'@wip', r'!wip',
64 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
65 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
66 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
67 | ]
68 |
69 |
70 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a
71 | ## list of regexp
72 | ##
73 | ## Commit messages will be classified in sections thanks to this. Section
74 | ## titles are the label, and a commit is classified under this section if any
75 | ## of the regexps associated is matching.
76 | ##
77 | section_regexps = [
78 | ('New', [
79 | r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
80 | ]),
81 | ('Changes', [
82 | r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
83 | ]),
84 | ('Fix', [
85 | r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
86 | ]),
87 |
88 | ('Other', None ## Match all lines
89 | ),
90 |
91 | ]
92 |
93 |
94 | ## ``body_process`` is a callable
95 | ##
96 | ## This callable will be given the original body and result will
97 | ## be used in the changelog.
98 | ##
99 | ## Available constructs are:
100 | ##
101 | ## - any python callable that take one txt argument and return txt argument.
102 | ##
103 | ## - ReSub(pattern, replacement): will apply regexp substitution.
104 | ##
105 | ## - Indent(chars=" "): will indent the text with the prefix
106 | ## Please remember that template engines gets also to modify the text and
107 | ## will usually indent themselves the text if needed.
108 | ##
109 | ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
110 | ##
111 | ## - noop: do nothing
112 | ##
113 | ## - ucfirst: ensure the first letter is uppercase.
114 | ## (usually used in the ``subject_process`` pipeline)
115 | ##
116 | ## - final_dot: ensure text finishes with a dot
117 | ## (usually used in the ``subject_process`` pipeline)
118 | ##
119 | ## - strip: remove any spaces before or after the content of the string
120 | ##
121 | ## Additionally, you can `pipe` the provided filters, for instance:
122 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
123 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
124 | #body_process = noop
125 | body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
126 |
127 |
128 | ## ``subject_process`` is a callable
129 | ##
130 | ## This callable will be given the original subject and result will
131 | ## be used in the changelog.
132 | ##
133 | ## Available constructs are those listed in ``body_process`` doc.
134 | subject_process = (strip |
135 | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
136 | ucfirst | final_dot)
137 |
138 |
139 | ## ``tag_filter_regexp`` is a regexp
140 | ##
141 | ## Tags that will be used for the changelog must match this regexp.
142 | ##
143 | tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$'
144 |
145 |
146 | ## ``unreleased_version_label`` is a string
147 | ##
148 | ## This label will be used as the changelog Title of the last set of changes
149 | ## between last valid tag and HEAD if any.
150 | unreleased_version_label = "%%version%% (unreleased)"
151 |
152 |
153 | ## ``output_engine`` is a callable
154 | ##
155 | ## This will change the output format of the generated changelog file
156 | ##
157 | ## Available choices are:
158 | ##
159 | ## - rest_py
160 | ##
161 | ## Legacy pure python engine, outputs ReSTructured text.
162 | ## This is the default.
163 | ##
164 | ## - mustache()
165 | ##
166 | ## Template name could be any of the available templates in
167 | ## ``templates/mustache/*.tpl``.
168 | ## Requires python package ``pystache``.
169 | ## Examples:
170 | ## - mustache("markdown")
171 | ## - mustache("restructuredtext")
172 | ##
173 | ## - makotemplate()
174 | ##
175 | ## Template name could be any of the available templates in
176 | ## ``templates/mako/*.tpl``.
177 | ## Requires python package ``mako``.
178 | ## Examples:
179 | ## - makotemplate("restructuredtext")
180 | ##
181 | output_engine = rest_py
182 | #output_engine = mustache("restructuredtext")
183 | #output_engine = mustache("markdown")
184 | #output_engine = makotemplate("restructuredtext")
185 |
186 |
187 | ## ``include_merge`` is a boolean
188 | ##
189 | ## This option tells git-log whether to include merge commits in the log.
190 | ## The default is to include them.
191 | include_merge = False
192 |
--------------------------------------------------------------------------------