=====================
Development tutorial
=====================
In this minimalistic tutorial we will make a Spynl plugin called `my-package` with one endpoint.
It will be served in a dev environment, documented, tested and translated.
We will build the code part by part, but you can browse :ref:`my-package-index`.
The `spynl` CLI has support for common tasks in the development cycle, which
we'll use as they become relevant.
A plugin with one endpoint
----------------------------
First, you probably want to `make a virtual environment and activate it `_.
Remember to use Python3, preferrably >= 3.5.
Then we install Spynl:
.. code:: shell
$ pip install spynl
Now we'll create a folder for our package
.. code:: shell
$ cd $VIRTUAL_ENV/src
$ mkdir my-package
$ touch my-package/setup.py my-package/hello_world.py
Here is the content of our (very) simple `setup.py`, where we state that
`my-package` is a Spynl plugin:
.. code:: python
from setuptools import setup
setup(name='my-package',
entry_points={
'spynl.plugins': [
'hello_world = 1',
]
}
)
This says that `hello_world.py` should be plugged into the Spynl application.
What this means is that Spynl calls Pyramid's `config.include `_ function, passing the `hello_world`
module to it. Therefore, Pyramid expects a function `hello_world.includeme`,
which we'll write below.
`spynl_plugins` is a list, so we could add other modules if that would suit our code
organisation in `my-package`. For that matter, we could also add other packages
who also define `spynl.plugins` entry points.
And here is `hello_world.py`. We write one endpoint and the registration for it:
.. code:: python
def hello(request):
return dict(message="Hello, world!")
def includeme(config):
config.add_endpoint(hello, 'hello')
The `hello` function is a pretty vanilla endpoint. It returns a dictionary.
This would mean Spynl returns it as `application/json` (it's default response
type), but it could also be served as XML or even YAML (read more about
:ref:`serialisation`).
The `includeme` function gets a Pyramid
`config `_
object, on which we can in principle do everything one can do when writing a pure
Pyramid application. We don't need anything but `config.add_endpoint` however,
which is actually unique to Spynl (it does some extra magic w.r.t. documentation
and route management).
Finally, we develop our package so Spynl knows about it and serve the application:
.. code:: shell
$ python setup.py develop
$ spynl dev.serve
(As you see, the `spynl` CLI command works from anywhere when you have your
virtual environment activated).
The endpoint http://localhost:6543/hello answers:
.. code:: json
{
"status": "ok",
"message": "Hello, world!"
}
Adding documentation for the endpoint
-----------------------------------------
Now let's document the endpoint for frontend developers:
.. code:: python
def hello(request):
"""
Say hello to the world.
---
get:
description: >
####Response
JSON keys | Content Type | Description\n
--------- | ------------ | -----------\n
status | string | 'ok' or 'error'\n
message | string | Hello, world!\n
tags:
- my-package
show-try: true
"""
return dict(message="Hello, world!")
def includeme(config):
config.add_endpoint(hello, 'hello')
Then, the Swagger doc at http://localhost/about/endpoints actually lists our endpoint:
.. image:: img/hello-swagger.png
Click on the endpoint to see details or try it out:
.. image:: img/hello-swagger2.png
We are not using Swagger to its full potential here w.r.t. to its schema
capabilities, we know. We chose not to, you can choose otherwise.
Serve on localhost
-----------------------
You already saw how to serve the app:
.. code:: bash
$ spynl dev.serve
Getting help about spynl CLI tasks
--------------------------------------
Now that we begin using the `sspynl` CLI, we should note that for each CLI task,
you can get help:
.. code:: shell
$ spynl --help dev.serve
Usage: spynl [--core-opts] dev.serve [other tasks here ...]
Docstring:
Run a local server. The ini-file development.ini is searched for in
installed Spynl plugins. If there is none, minimal.ini is used.
Options:
none
Testing the endpoint
-----------------------
Let's write a simple test in `my-package/test_hello.py`:
.. code:: python
import pytest
from webtest import TestApp
from spynl.main import main
@pytest.fixture(scope="session")
def app():
spynl_app = main(None)
return TestApp(spynl_app)
def test_hello(app):
response = app.get('/hello', status=200)
assert response.json['message'] == "Hello, world!"
Then, we can run:
.. code:: shell
$ spynl dev.test
I hope you saw this (the dot says it succeeded):
.. code:: shell
[spynl dev.test] Testing package: my-package
============================= test session starts ==============================
platform linux -- Python 3.5.2, pytest-3.0.5, py-1.4.32, pluggy-0.4.0
rootdir: /home/nicolas/workspace/spynl-git/venv/src/my-package, inifile:
plugins: sugar-0.8.0, cov-2.4.0, raisesregexp-2.1
collected 1 items
test_hello.py .
Adding translations
------------------------
Then we have support for translating the app. Let us add a translatable string
to the hello_world endpoint:
.. code:: python
from spynl.main.locale import SpynlTranslationString as _
def hello(request):
return dict(message=_('hello-msg', default="Hello, world!"))
We can now refresh the translation catalogue of our package:
.. code:: shell
$ spynl dev.translate --packages my-package --languages nl --action refresh
[spynl dev.translate] Package: my-package ...
[spynl dev.translate] Creating locale folder ...
running extract_messages
extracting messages from hello_world.py
extracting messages from setup.py
extracting messages from test_hello.py
writing PO template file to ./locale/messages.pot
[spynl dev.translate] File ./locale/nl/LC_MESSAGES/my-package.po does not exist. Initializing.
running init_catalog
creating catalog ./locale/nl/LC_MESSAGES/my-package.po based on ./locale/messages.pot
[spynl dev.translate] Done with language nl.
--------------------------------------------------
Spynl created all necessary folders and initialised a catalogue. Now a human
needs to translate our string to Dutch. Make this change in
`my-package/locale/nl/LC_MESSAGES/my-package.po`:
.. code:: shell
#: hello_world.py:23
msgid "hello-msg"
msgstr "Hallo, Wereld!"
Then we can compile the catalogue, so that Spynl will serve Dutch when it
should:
.. code:: shell
$ spynl dev.translate --packages my-package --languages nl
[spynl dev.translate] Package: my-package ...
[spynl dev.translate] Located locale folder in /home/nicolas/workspace/spynl/venv/src/my-package ...
running compile_catalog
compiling catalog /home/nicolas/workspace/spynl/venv/src/my-package/locale/nl/LC_MESSAGES/my-package.po to /home/nicolas/workspace/spynl/venv/src/my-package/locale/nl/LC_MESSAGES/my-package.mo
[spynl dev.translate] Done with language nl.
--------------------------------------------------
There are ony two actions, `refresh` and `compile`.
If the `--action` parameter is not given, `spynl dev.translate` compiles.
The compilation step is not necessary and you don't have to include the binary
.mo file in your SCM. When we build a Docker image on Jenkins (see below),
Jenkins runs the compile action.
we need to tell Pyramid that the new locale directory exists. Add this
to the `include_me` function in `my-package/hello_world.py`:
.. code:: python
config.add_translation_dirs('%s/src/my-package/locale'
% os.environ['VIRTUAL_ENV'])
Now we want to see our app serve Dutch. We still need to configure the list
of languages we serve in our app. This is a great opportunity to start
using our own `.ini` file. Copy Spynl's `minimal.ini` to `my-package/development.ini`
and add the `spynl.languages` setting in the `[app:main]` section:
.. code:: shell
[app:main]
use = egg:spynl
spynl.pretty = 1
spynl.languages = nl,en
It is crucial which language is first in this list. Because `nl` is first, we'll
get a Dutch reply from Spynl, e.g.by visiting http://localhost:6543:
.. code: json
{
"message": "Geen endpoint gevonden voor pad '/'.",
"status": "error",
"type": "HTTPNotFound"
}
FIXME: However, http://localhost:6543/hello still returns english ...
Tab completion for the spynl CLI
---------------------------------
Now that we're `spynl` power users, it's time to reveal an important feature:
There is tab completion for the `spynl` CLI. To activate it, run
.. code:: shell
$ source $VIRTUAL_ENV/lib/python3.5/site-packages/spynl/spynl/cli/zsh.completion
(you might need to adapt the path to spynl, it depends on your environment and
method of installation)
You can list (a subset of) tasks by pressing TAB and if the task is complete also the available options.
To see options, type a dash ("-") and the press TAB.
This is available for `bash` and `fish` as well, simply replace `zsh` in the
command.
Installing the package from SCM
---------------------------------
Of course, we will want to use Source Code Management (SCM) for our own code,
e.g. on github or bitbucket. `spynl` provides a task called `dev.install` which
makes it easy to get started in a new dev environment with developing your app
further.
Let's assume your project uses git as SCM and lives in a bitbucket repo:
.. code:: shell
$ spynl dev.install --scm-url git@bitbucket.org:my-team/my-package.git
`spynl dev.install` will clone the code and develop it.
In general, Spynl also supports mercurial repositories.
There are some configuration options here (try `spynl --help dev.install` for
all of them). For example, let's assume you work want to work with a feature
branch and you want/need to specify in which directory the code should be installed:
.. code:: shell
$ spynl dev.install --scm-url git@bitbucket.org:my-team/my-package.git --revision me/some-feature --src_path path/to/my/virtualenv/src
`spynl dev.install` can also install non-Python dependencies for you or do any
other things pre- or post-installation. See `setup.sh.template` in the main Spynl repo. (TODO: point to actual documentation of `setup.sh`).