├── .circleci ├── config.yml └── requirements.txt ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── alembic.ini ├── dev.py ├── docker-compose.yaml ├── docs ├── about.md ├── deployment.md ├── glossary.md ├── hacking.md ├── logging_plugins.md ├── logo-readme-github.png ├── migrations.md ├── releasing.md └── useful-snippets.md ├── example.yaml ├── plugins ├── README.md ├── conftest.py ├── routemaster-prometheus │ ├── MANIFEST.in │ ├── README.md │ ├── routemaster_prometheus │ │ └── __init__.py │ ├── setup.py │ └── version.py ├── routemaster-sentry │ ├── MANIFEST.in │ ├── README.rst │ ├── routemaster_sentry │ │ └── __init__.py │ ├── setup.py │ └── version.py ├── routemaster-statsd │ ├── MANIFEST.in │ ├── README.rst │ ├── routemaster_statsd │ │ └── __init__.py │ ├── setup.py │ └── version.py └── tests │ └── test_logging_plugins.py ├── routemaster ├── __init__.py ├── __main__.py ├── app.py ├── cli.py ├── config │ ├── __init__.py │ ├── exceptions.py │ ├── loader.py │ ├── model.py │ ├── schema.yaml │ └── tests │ │ ├── test_context_trigger_firing.py │ │ ├── test_database_config.py │ │ ├── test_loading.py │ │ └── test_next_states.py ├── conftest.py ├── context.py ├── cron.py ├── cron_processors.py ├── db │ ├── __init__.py │ ├── initialisation.py │ ├── model.py │ ├── model.pyi │ └── tests │ │ └── test_model_repr.py ├── exit_conditions │ ├── __init__.py │ ├── __main__.py │ ├── analysis.py │ ├── error_display.py │ ├── evaluator.py │ ├── exceptions.py │ ├── operations.py │ ├── parser.py │ ├── peephole.py │ ├── prepositions.py │ ├── program.py │ ├── tests │ │ ├── test_integration.py │ │ ├── test_program.py │ │ └── test_tokenizer.py │ └── tokenizer.py ├── feeds.py ├── gunicorn_application.py ├── logging │ ├── __init__.py │ ├── base.py │ ├── plugins.py │ ├── python_logger.py │ ├── split_logger.py │ └── tests │ │ ├── test_loggers.py │ │ └── test_logging_plugin_system.py ├── middleware.py ├── migrations │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 091a6e84d9ac_initial_state_machine.py │ │ ├── 3a6f69dca9d7_rename_context_to_metadata.py │ │ ├── 3ab04cd7bb80_add_edge_tracking.py │ │ ├── 3d0b226940eb_initial_history.py │ │ ├── 3eb4f3b419c6_create_trigger_to_sync_updated_field.py │ │ ├── 4fe851fbcdc3_initial_label.py │ │ ├── 4ff14db28f2d_rename_labels_table.py │ │ ├── 6fb8896f0729_fix_trigger_timezone_awareness.py │ │ ├── 814a6b555eb9_make_fields_non_nullable_by_default.py │ │ ├── 9871e5c166b4_drop_data_warehouse_models_for_now.py │ │ ├── ab899b70c758_timezone_aware_created_and_updated_.py │ │ ├── e1fec9622785_add_metadata_triggers_processed_flag_to_.py │ │ ├── e7d5ad06c0d1_add_updated_field_to_label.py │ │ └── ead8463bff97_add_a_deleted_column_to_labels.py ├── server │ ├── __init__.py │ ├── endpoints.py │ └── tests │ │ ├── test_endpoints.py │ │ └── test_propagate_exceptions.py ├── state_machine │ ├── __init__.py │ ├── actions.py │ ├── api.py │ ├── exceptions.py │ ├── gates.py │ ├── tests │ │ ├── test_actions.py │ │ ├── test_gates.py │ │ ├── test_state_machine.py │ │ ├── test_state_machine_utils.py │ │ └── test_transitions.py │ ├── transitions.py │ ├── types.py │ └── utils.py ├── tests │ ├── test_app.py │ ├── test_cli.py │ ├── test_context.py │ ├── test_cron.py │ ├── test_cron_processors.py │ ├── test_feeds.py │ ├── test_gunicorn_application.py │ ├── test_layering.py │ ├── test_middleware.py │ ├── test_text_utils.py │ ├── test_time_utils.py │ ├── test_timezones.py │ ├── test_utils.py │ ├── test_validation.py │ └── test_webhook_runner.py ├── text_utils.py ├── time_utils.py ├── timezones.py ├── utils.py ├── validation.py └── webhooks.py ├── scripts ├── build │ └── default_config.yaml ├── database │ └── create_databases.sh ├── install_for_development ├── linting │ ├── requirements.in │ └── requirements.txt ├── testing │ └── requirements.txt └── typechecking │ └── requirements.txt ├── setup.cfg ├── setup.py ├── test_data ├── action_and_gate_invalid.yaml ├── disconnected.yaml ├── invalid_feed_name_in_exit_condition.yaml ├── invalid_top_level_context_name_in_exit_condition.yaml ├── invalid_top_level_context_name_in_path.yaml ├── multiple_feeds_same_name_invalid.yaml ├── nested_kwargs_logging_plugin_invalid.yaml ├── next_states_not_constant_or_context_invalid.yaml ├── next_states_shorthand.yaml ├── no_state_machines_invalid.yaml ├── not_action_or_gate_invalid.yaml ├── not_time_or_context_invalid.yaml ├── not_yaml.json ├── path_format_context_trigger_invalid.yaml ├── plugins │ └── logger_plugin.py ├── realistic.yaml ├── time_and_context_invalid.yaml ├── trigger_interval_format_invalid.yaml ├── trigger_time_format_invalid.yaml ├── trigger_timezone_name_invalid.yaml └── trivial.yaml ├── tox.ini └── version.py /.circleci/requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements for top level CI operations. 2 | # Don't include Routemaster's own requirements here. 3 | pip==21.1.2 4 | setuptools==57.0.0 5 | tox==2.9.1 6 | coveralls==1.10.0 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | docs/ 3 | test_data/ 4 | build/* 5 | .tox/ 6 | .cache/ 7 | .coverage* 8 | .eggs/ 9 | .git/ 10 | .mypy_cache/ 11 | .python-version 12 | config.dev.yaml 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yml,yaml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.dev.yaml 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/* 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | // Please keep the list alphabetically sorted within each group 7 | "recommendations": [ 8 | "EditorConfig.EditorConfig", 9 | "ms-python.python", 10 | 11 | // Generally useful tools which are useful here 12 | "bradymholt.pgformatter", 13 | "stkb.rewrap", 14 | "wmaurer.change-case" 15 | ], 16 | 17 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 18 | "unwantedRecommendations": [] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": false, 3 | "python.linting.flake8Enabled": true, 4 | "python.linting.mypyEnabled": true, 5 | "python.pythonPath": "${env:VIRTUAL_ENV}/bin/python3", 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-stretch 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | WORKDIR /routemaster/app 6 | 7 | COPY routemaster/migrations/ routemaster/migrations/ 8 | 9 | COPY dist/ . 10 | 11 | # Install first-party plugins (inactive by default). 12 | COPY plugins/routemaster-sentry/dist/ . 13 | COPY plugins/routemaster-prometheus/dist/ . 14 | COPY plugins/routemaster-statsd/dist/ . 15 | 16 | RUN pip install --no-cache-dir *.whl 17 | 18 | COPY scripts/build/default_config.yaml config.yaml 19 | COPY alembic.ini alembic.ini 20 | 21 | EXPOSE 2017 22 | 23 | ENV PROMETHEUS_MULTIPROC_DIR /tmp/routemaster/prometheus 24 | 25 | CMD ["routemaster", "--config-file=config.yaml", "serve"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Thread Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include version.py 2 | include routemaster/config/schema.yaml 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Routemaster](docs/logo-readme-github.png) 2 | 3 | - - - 4 | 5 | [![CircleCI](https://circleci.com/gh/thread/routemaster.svg?style=shield&circle-token=3973777302b4f7f00f5b9eb1c07e3c681ea94f35)](https://circleci.com/gh/thread/routemaster) [![Coverage Status](https://coveralls.io/repos/github/thread/routemaster/badge.svg?branch=master)](https://coveralls.io/github/thread/routemaster?branch=master) 6 | 7 | State machines as a service. 8 | 9 | Routemaster is designed to enable the creation of complex, but clearly defined, 10 | state machines used by multiple separate services. 11 | 12 | 13 | ##### Useful Links 14 | 15 | - [About Routemaster](docs/about.md) 16 | - [Glossary](docs/glossary.md) 17 | - [Hacking on Routemaster](docs/hacking.md) 18 | - [Deployment](docs/deployment.md) 19 | - [Migrations FAQ](docs/migrations.md) 20 | - [Routemaster on PyPI](https://pypi.python.org/pypi/routemaster) 21 | - [Routemaster on Docker Hub](https://hub.docker.com/r/thread/routemaster/) 22 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | [alembic] 2 | script_location = ./routemaster/migrations 3 | timezone = UTC 4 | 5 | [loggers] 6 | keys = root,sqlalchemy,alembic 7 | 8 | [handlers] 9 | keys = console 10 | 11 | [formatters] 12 | keys = generic 13 | 14 | [logger_root] 15 | level = WARN 16 | handlers = console 17 | qualname = 18 | 19 | [logger_sqlalchemy] 20 | level = WARN 21 | handlers = 22 | qualname = sqlalchemy.engine 23 | 24 | [logger_alembic] 25 | level = INFO 26 | handlers = 27 | qualname = alembic 28 | 29 | [handler_console] 30 | class = StreamHandler 31 | args = (sys.stderr,) 32 | level = NOTSET 33 | formatter = generic 34 | 35 | [formatter_generic] 36 | format = %(levelname)-5.5s [%(name)s] %(message)s 37 | datefmt = %H:%M:%S 38 | -------------------------------------------------------------------------------- /dev.py: -------------------------------------------------------------------------------- 1 | """ 2 | Development interactive script. 3 | 4 | Use with `python -i dev.py` for a useful interactive shell. 5 | """ 6 | import layer_loader 7 | 8 | from routemaster.db import * # noqa: F403, F401 9 | from routemaster.app import App 10 | from routemaster.config import yaml_load, load_config 11 | 12 | 13 | def app_from_config(config_path): 14 | """ 15 | Create an `App` instance with a session from a given config path. 16 | 17 | By default, will use the example.yaml file. 18 | """ 19 | config = load_config( 20 | layer_loader.load_files( 21 | [config_path], 22 | loader=yaml_load, 23 | ), 24 | ) 25 | 26 | class InteractiveApp(App): 27 | """ 28 | App for use in interactive shell only. 29 | 30 | Provides a global database session. 31 | """ 32 | 33 | def __init__(self, *args, **kwargs): 34 | super().__init__(*args, **kwargs) 35 | self._session = self._sessionmaker() 36 | 37 | @property 38 | def session(self): 39 | """Return the database session.""" 40 | return self._session 41 | 42 | return InteractiveApp(config) 43 | 44 | 45 | app = app_from_config('example.yaml') 46 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: postgres 6 | environment: 7 | POSTGRES_USER: routemaster 8 | 9 | routemaster: 10 | build: . 11 | image: routemaster 12 | command: 13 | - routemaster 14 | - --config-file=config.dev.yaml 15 | - serve 16 | - --debug 17 | volumes: 18 | - .:/routemaster/app 19 | ports: 20 | - "2017:2017" 21 | environment: 22 | DB_NAME: routemaster 23 | DB_USER: routemaster 24 | DB_HOST: db 25 | DB_PORT: 5432 26 | depends_on: 27 | - db 28 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ### Docker 4 | 5 | Deploying with Docker is the recommended deployment strategy. 6 | 7 | Either create your own Dockerfile that inherits from 8 | [`thread/routemaster`](https://hub.docker.com/r/thread/routemaster/) and 9 | replaces the file `/routemaster/config/config.yaml` with your configuration, or 10 | deploy the `routemaster` image directly, mounting your own config in place. 11 | 12 | You will need to expose the database to the running container, and provide 13 | connection through environment variables: `DB_HOST`, `DB_PORT`, `DB_NAME`, 14 | `DB_USER`, `DB_PASS`. 15 | 16 | 17 | ##### Migrations 18 | 19 | Migrations are not run automatically by the Docker container. It is recommended 20 | that you include a migration process in your deployment. A basic version of 21 | this is: 22 | 23 | ```shell 24 | docker stop routemaster 25 | docker run --rm thread/routemaster alembic upgrade head 26 | docker start routemaster 27 | ``` 28 | 29 | 30 | ### Python 31 | 32 | Routemaster and its plugins are packaged as Python packages and deployed to 33 | PyPI. You can install and run Routemaster on any machine with Python 3.6 by 34 | using `pip`. 35 | -------------------------------------------------------------------------------- /docs/glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | |Term|Definition| 4 | |---|---| 5 | |routemaster|A state machine service| 6 | |label|A single named entity moving through a single state machine. Note the primary key is `(state_machine, label)`, i.e. labels have no meaning between separate state machines.| 7 | |metadata|Arbitrary metadata associated with a label, used to decide how that label should progress through a state machine. The exit condition DSL can access this through the `metadata` prefix.| 8 | |feed|A dynamic data source, retrieved lazily, and accessible through the `feed` prefix.| 9 | |system|The prefix for injected constants in the exit condition DSL.| 10 | |context|The context in which exit condition programs run.| 11 | |state|A single node in the state machine.| 12 | |action|A type of state that triggers a side-effect in an external service upon a label entering it. A label remaining here implies that routemaster has been unable to successfully trigger the side-effect.| 13 | |gate|A type of state that restricts a label from continuing, based on an **exit condition**.| 14 | |exit condition|A conditional statement that resolves to either true or false, deciding whether a label can leave the associated state. Can be dependent on the **context**, external data feeds, the current time, or the duration that a label has been in the state| 15 | -------------------------------------------------------------------------------- /docs/hacking.md: -------------------------------------------------------------------------------- 1 | # Hacking on Routemaster 2 | 3 | You'll need to create a database for developing against and for running tests 4 | against. This can be done by running the `scripts/database/create_databases.sh` 5 | script. Full details of how the database, models & migrations are handled can 6 | be found in the [migrations docs](./migrations.md). Routemaster requires 7 | Postgres. 8 | 9 | 10 | ### Tox 11 | 12 | Testing, linting and type checking are done by `tox`, which manages its own 13 | virtual environments for you. 14 | 15 | The following should be sufficient to run a full test and lint process as done 16 | by the CI. 17 | 18 | ```shell 19 | $ pip install tox 20 | $ tox 21 | ``` 22 | 23 | 24 | ### Testing 25 | 26 | To run the tests outside of `tox` (i.e. to be able to pass complex parameters 27 | to pytest): 28 | 29 | ```shell 30 | $ pip install -r scripts/testing/requirements.txt 31 | $ py.test 32 | ``` 33 | 34 | 35 | ### Linting 36 | 37 | To run the linting outside of `tox` (i.e. possibly for integration with an 38 | editor): 39 | 40 | ```shell 41 | $ pip install -r scripts/linting/requirements.txt 42 | $ flake8 routemaster 43 | ``` 44 | 45 | 46 | ### Type checking 47 | 48 | To run the type checking outside of `tox` (again possibly for editor 49 | integration): 50 | 51 | ```shell 52 | $ pip install -r scripts/linting/requirements.txt 53 | $ mypy -p routemaster 54 | ``` 55 | 56 | 57 | ##### Running after changed dependencies 58 | 59 | `tox` uses virtualenvs to contain the tests. These are not created every time 60 | the tests are run, which means we have to reset them manually if the 61 | requirements (or `setup.py` "install requires") change. 62 | 63 | Run tox with the "recreate" flag to force a reinstall of the dependencies. 64 | 65 | ```shell 66 | $ tox --recreate 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/logging_plugins.md: -------------------------------------------------------------------------------- 1 | # Logging Plugins 2 | 3 | Logging plugins are Routemaster's mechanism for exposing logs and metrics 4 | about its performance and behaviour. 5 | 6 | Plugins are all inactive by default, and must be enabled in your Routemaster 7 | configuration file like so: 8 | 9 | ```yaml 10 | plugins: 11 | logging: 12 | - class: routemaster.logging:PythonLogger 13 | kwargs: 14 | log_level: DEBUG 15 | ``` 16 | 17 | ## Available logging plugins 18 | 19 | ### Python Logger 20 | 21 | Routemaster comes with the above `PythonLogger` that uses Python's logging 22 | system, and can be configured with a log level. It outputs basic information 23 | to stdout. 24 | 25 | 26 | ### Sentry Logger 27 | 28 | Maintained in the Routemaster repo is a Sentry plugin. This can be configured 29 | with a Sentry "dsn", and will report exceptions to Sentry. 30 | 31 | ```yaml 32 | plugins: 33 | logging: 34 | - class: routemaster_sentry.logger:SentryLogger 35 | kwargs: 36 | dsn: https://xxxxxxx:xxxxxxx@sentry.io/xxxxxxx 37 | ``` 38 | 39 | 40 | ### Prometheus Logger 41 | 42 | The Prometheus logger is also maintained in the Routemaster repo. It provides 43 | a Prometheus compatible scrape target for metrics at the path `/metrics`, on 44 | the same port as the API. 45 | 46 | Several metrics about the behaviour and performance of Routemaster are 47 | exposed, including timings and status codes of the API, webhook requests and 48 | feed requests, as well as the processing of jobs in the internal cron system. 49 | 50 | 51 | ```yaml 52 | plugins: 53 | logging: 54 | - class: routemaster_prometheus.logger:PrometheusLogger 55 | ``` 56 | 57 | ### StatsD Logger 58 | 59 | The StatsD logger is also maintained in the Routemaster repo. It exports metrics to a provided statsd server over UDP. 60 | 61 | Several metrics about the behaviour and performance of Routemaster are 62 | exposed, including timings and status codes of the API, webhook requests and 63 | feed requests, as well as the processing of jobs in the internal cron system. 64 | 65 | 66 | ```yaml 67 | plugins: 68 | logging: 69 | - class: routemaster_statsd.logger:StatsDLogger 70 | ``` 71 | 72 | The target StatsD server can be configured through the plugin keyword arguments or using the `STATSD_HOST` and `STATSD_PORT` environment variables. 73 | 74 | ## Implementing a logging plugin 75 | 76 | A logging plugin must inherit from `routemaster.logging.BaseLogger`, and can 77 | override any of the methods on it. 78 | 79 | It should be noted that some of the core methods are context managers, and 80 | wrap a piece of functionality in Routemaster. If overridden, these methods 81 | _must_ yield to perform their behaviour, and are also required to not swallow 82 | any exceptions except those generated within the method itself. This is to 83 | ensure that other plugins will see exceptions generated by Routemaster and 84 | will get a chance to act on them. 85 | -------------------------------------------------------------------------------- /docs/logo-readme-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thread/routemaster/6c69148203575112fff4246f243d8c663459fb78/docs/logo-readme-github.png -------------------------------------------------------------------------------- /docs/migrations.md: -------------------------------------------------------------------------------- 1 | # Migrations setup 2 | 3 | Routemaster uses [`alembic`][alembic] for its migrations, and supports 4 | Postgres for its data storage. 5 | 6 | 7 | ### I need to set up my database up for the first time 8 | 9 | 1. Create a database for Routemaster to use. 10 | 11 | Note: if you already have a local PostgreSQL database server configured, 12 | then you may be able to just run the `scripts/database/create_databases.sh` 13 | script. 14 | 15 | 2. Set up access credentials for the database in your environment variables. 16 | 17 | You'll probably want to set `DB_USER` (defaults to `routemaster`) and 18 | `DB_PASS` for running, though you also need to set `PG_USER` and `PG_PASS` 19 | for the tests to use. 20 | 21 | A convenient way to do this is to add these to a `.env` file in the root of 22 | the repo and then source that file as part of `postactivate` in your 23 | virtualenv. 24 | 25 | 3. Run `alembic upgrade head` 26 | 27 | 28 | ### I need to apply migrations to bring myself up to date 29 | 30 | Run `alembic upgrade head`. This is equivalent it `manage.py migrate` in 31 | Django. 32 | 33 | 34 | ### I have edited the models and need to create a migration 35 | 36 | 1. Run `alembic revision --autogenerate -m ""` 37 | 2. Edit the file it just created, to sanity check and tidy it up. 38 | 3. `alembic upgrade head` to apply it. 39 | 40 | If you are familiar with Django migrations there are a few differences to 41 | `makemigrations` to note: 42 | 43 | * The revision message is not an optional feature 44 | * Due to the greater flexibility SQLAlchemy has compared with the 45 | Django ORM, migration systems have a much harder time reliably detecting 46 | changes and generating DDL. This means you _always_ need to check and revise 47 | what Alembic generates in its migrations. 48 | * The revision IDs are _not_, in general, date-ordered or sequential: the IDs 49 | are random and the sequencing is declared programmatically in the migrations. 50 | You can get the "sensible" history with `alembic history`. 51 | 52 | [alembic]: http://alembic.zzzcomputing.com/en/latest/ 53 | -------------------------------------------------------------------------------- /docs/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing Routemaster Versions 2 | 3 | Releases are handled by CircleCI. To perform a release: 4 | 5 | 1. Push the commit to master that you wish to release. 6 | 2. Wait for the build to complete successfully. 7 | 3. Tag the commit appropriately, and `git push --tags`. 8 | 4. Wait for CircleCI to rebuild, and then release. 9 | 10 | 11 | ### Versions 12 | 13 | The tag for a release must be the version number to be published. 14 | 15 | We roughly follow semantic versioning. The public API should be considered to 16 | be the information provided by the web API, the file format for configuration, 17 | and the _logical_ behaviour of the state machine. 18 | 19 | Note that state machine behaviour changes such as optimisations, or fixes to 20 | be more correct, should not be considered breaking changes. 21 | 22 | 23 | ### What's in a release? 24 | 25 | A release consists of: 26 | 27 | - A `routemaster` package uploaded, with the specified version number, to 28 | PyPI. 29 | - A package for each plugin in the `plugins` directory, with the _same 30 | version number_, uploaded to PyPI. 31 | - A Docker image built with all of the above installed in it, and pushed to 32 | `thread/routemaster` on Docker Hub. 33 | -------------------------------------------------------------------------------- /docs/useful-snippets.md: -------------------------------------------------------------------------------- 1 | # Useful snippets 2 | 3 | ## Database queries 4 | 5 | ### Number of labels in each state 6 | ``` 7 | SELECT latest_states.new_state, COUNT(*) AS num_labels_in_state 8 | FROM ( 9 | SELECT history.label_name, history.new_state 10 | FROM history 11 | JOIN ( 12 | SELECT label_name, MAX(id) AS max_id 13 | FROM history 14 | GROUP BY label_name 15 | ) states 16 | ON 17 | history.id = states.max_id AND 18 | history.label_name = states.label_name 19 | ) latest_states 20 | GROUP BY latest_states.new_state; 21 | ``` 22 | 23 | ### Latest state for all labels 24 | ``` 25 | SELECT history.label_name, history.created, history.new_state 26 | FROM history 27 | JOIN ( 28 | SELECT label_name, MAX(id) AS max_id 29 | FROM history 30 | GROUP BY label_name 31 | ) latest_states 32 | ON 33 | history.id = latest_states.max_id AND 34 | history.label_name = latest_states.label_name; 35 | ``` 36 | 37 | ### Number of labels in a specific state 38 | 39 | ``` 40 | SELECT COUNT(*) 41 | FROM history 42 | JOIN ( 43 | SELECT label_name, MAX(id) AS max_id 44 | FROM history 45 | GROUP BY label_name 46 | ) latest_states 47 | ON 48 | history.id = latest_states.max_id AND 49 | history.label_name = latest_states.label_name 50 | WHERE history.new_state = 'STATE_NAME'; 51 | ``` 52 | -------------------------------------------------------------------------------- /example.yaml: -------------------------------------------------------------------------------- 1 | state_machines: 2 | user_lifecycle: 3 | feeds: 4 | - name: jacquard 5 | url: http://localhost:1212/