Merge pull request #182 from hartym/master

Update documentation.
This commit is contained in:
Romain Dorgueil
2017-10-08 13:25:57 +02:00
committed by GitHub
24 changed files with 1681 additions and 111 deletions

View File

@ -10,3 +10,5 @@ dependencies:
- psutil ==5.2.2
- requests ==2.13.0
- stevedore ==1.21.0
# for examples
- pycountry ==17.9.23

24
docs/_templates/alabaster/__init__.py vendored Normal file
View File

@ -0,0 +1,24 @@
import os
from alabaster import _version as version
def get_path():
"""
Shortcut for users whose theme is next to their conf.py.
"""
# Theme directory is defined as our parent directory
return os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
def update_context(app, pagename, templatename, context, doctree):
context['alabaster_version'] = version.__version__
def setup(app):
# add_html_theme is new in Sphinx 1.6+
if hasattr(app, 'add_html_theme'):
theme_path = os.path.abspath(os.path.dirname(__file__))
app.add_html_theme('alabaster', theme_path)
app.connect('html-page-context', update_context)
return {'version': version.__version__,
'parallel_read_safe': True}

2
docs/_templates/alabaster/_version.py vendored Normal file
View File

@ -0,0 +1,2 @@
__version_info__ = (0, 7, 10)
__version__ = '.'.join(map(str, __version_info__))

57
docs/_templates/alabaster/about.html vendored Normal file
View File

@ -0,0 +1,57 @@
{% if theme_logo %}
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/' ~ theme_logo, 1) }}" alt="Logo"/>
{% if theme_logo_name|lower == 'true' %}
<h1 class="logo logo-name">{{ project }}</h1>
{% endif %}
</a>
</p>
{% else %}
<h1 class="logo"><a href="{{ pathto(master_doc) }}">{{ project }}</a></h1>
{% endif %}
{% if theme_description %}
<p class="blurb">{{ theme_description }}</p>
{% endif %}
{% if theme_github_user and theme_github_repo %}
{% if theme_github_button|lower == 'true' %}
<p>
<iframe src="https://ghbtns.com/github-btn.html?user={{ theme_github_user }}&repo={{ theme_github_repo }}&type={{ theme_github_type }}&count={{ theme_github_count }}&size=large&v=2"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
</p>
{% endif %}
{% endif %}
{% if theme_travis_button|lower != 'false' %}
{% if theme_travis_button|lower == 'true' %}
{% set path = theme_github_user + '/' + theme_github_repo %}
{% else %}
{% set path = theme_travis_button %}
{% endif %}
<p>
<a href="https://travis-ci.org/{{ path }}">
<img
alt="https://secure.travis-ci.org/{{ path }}.svg?branch={{ theme_badge_branch }}"
src="https://secure.travis-ci.org/{{ path }}.svg?branch={{ theme_badge_branch }}"
/>
</a>
</p>
{% endif %}
{% if theme_codecov_button|lower != 'false' %}
{% if theme_codecov_button|lower == 'true' %}
{% set path = theme_github_user + '/' + theme_github_repo %}
{% else %}
{% set path = theme_codecov_button %}
{% endif %}
<p>
<a href="https://codecov.io/github/{{ path }}">
<img
alt="https://codecov.io/github/{{ path }}/coverage.svg?branch={{ theme_badge_branch }}"
src="https://codecov.io/github/{{ path }}/coverage.svg?branch={{ theme_badge_branch }}"
/>
</a>
</p>
{% endif %}

9
docs/_templates/alabaster/donate.html vendored Normal file
View File

@ -0,0 +1,9 @@
{% if theme_gratipay_user or theme_gittip_user %}
<h3>Donate</h3>
<p>
Consider supporting the authors on <a href="https://www.gratipay.com/">Gratipay</a>:
<script data-gratipay-username="{{ theme_gratipay_user or theme_gittip_user }}"
data-gratipay-widget="button"
src="//gttp.co/v1.js"></script>
</p>
{% endif %}

82
docs/_templates/alabaster/layout.html vendored Normal file
View File

@ -0,0 +1,82 @@
{%- extends "basic/layout.html" %}
{%- block extrahead %}
{{ super() }}
<link rel="stylesheet" href="{{ pathto('_static/custom.css', 1) }}" type="text/css" />
{% if theme_touch_icon %}
<link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
{% endif %}
{% if theme_canonical_url %}
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
{% endblock %}
{# Disable base theme's top+bottom related navs; we have our own in sidebar #}
{%- block relbar1 %}{% endblock %}
{%- block relbar2 %}{% endblock %}
{# Nav should appear before content, not after #}
{%- block content %}
{%- if theme_fixed_sidebar|lower == 'true' %}
<div class="document">
{{ sidebar() }}
{%- block document %}
<div class="documentwrapper">
{%- if render_sidebar %}
<div class="bodywrapper">
{%- endif %}
<div class="body" role="main">
{% block body %} {% endblock %}
</div>
{%- if render_sidebar %}
</div>
{%- endif %}
</div>
{%- endblock %}
<div class="clearer"></div>
</div>
{%- else %}
{{ super() }}
{%- endif %}
{%- endblock %}
{%- block footer %}
<div class="footer">
{% if show_copyright %}&copy;{{ copyright }}.{% endif %}
{% if theme_show_powered_by|lower == 'true' %}
{% if show_copyright %}|{% endif %}
Powered by <a href="http://sphinx-doc.org/">Sphinx {{ sphinx_version }}</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster {{ alabaster_version }}</a>
{% endif %}
{%- if show_source and has_source and sourcename %}
{% if show_copyright or theme_show_powered_by %}|{% endif %}
<a href="{{ pathto('_sources/' + sourcename, true)|e }}"
rel="nofollow">{{ _('Page source') }}</a>
{%- endif %}
</div>
{% if theme_github_banner|lower != 'false' %}
<a href="https://github.com/{{ theme_github_user }}/{{ theme_github_repo }}" class="github">
<img style="position: absolute; top: 0; right: 0; border: 0;" src="{{ pathto('_static/' ~ theme_github_banner, 1) if theme_github_banner|lower != 'true' else 'https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png' }}" alt="Fork me on GitHub" class="github"/>
</a>
{% endif %}
{% if theme_analytics_id %}
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '{{ theme_analytics_id }}']);
_gaq.push(['_setDomainName', 'none']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
{% endif %}
{%- endblock %}

View File

@ -0,0 +1,10 @@
<h3>{{ _('Navigation') }}</h3>
{{ toctree(includehidden=theme_sidebar_includehidden, collapse=theme_sidebar_collapse) }}
{% if theme_extra_nav_links %}
<hr />
<ul>
{% for text, uri in theme_extra_nav_links.items() %}
<li class="toctree-l1"><a href="{{ uri }}">{{ text }}</a></li>
{% endfor %}
</ul>
{% endif %}

View File

@ -0,0 +1,21 @@
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
{%- for parent in parents %}
<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
{%- endfor %}
{%- if prev %}
<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
}}">{{ prev.title }}</a></li>
{%- endif %}
{%- if next %}
<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
}}">{{ next.title }}</a></li>
{%- endif %}
{%- for parent in parents %}
</ul></li>
{%- endfor %}
</ul></li>
</ul>
</div>

View File

@ -0,0 +1,706 @@
{% set theme_body_bg = theme_body_bg or theme_base_bg %}
{% set theme_code_highlight_bg = theme_code_highlight_bg or theme_body_bg %}
{% set theme_sidebar_header = theme_sidebar_header or theme_gray_1 %}
{% set theme_sidebar_link = theme_sidebar_link or theme_gray_1 %}
{% set theme_anchor_hover_fg = theme_anchor_hover_fg or theme_gray_1 %}
{% set theme_footnote_border = theme_footnote_border or theme_gray_2 %}
{% set theme_pre_bg = theme_pre_bg or theme_gray_2 %}
{# set up admonition styling #}
{# - basic level #}
{% set theme_admonition_xref_bg = theme_admonition_xref_bg or theme_xref_bg %}
{% set theme_admonition_bg = theme_admonition_bg or theme_gray_2 %}
{% set theme_note_bg = theme_note_bg or theme_gray_2 %}
{% set theme_seealso_bg = theme_seealso_bg or theme_gray_2 %}
{# - critical level #}
{% set theme_danger_bg = theme_danger_bg or theme_pink_1 %}
{% set theme_danger_border = theme_danger_border or theme_pink_2 %}
{% set theme_danger_shadow = theme_danger_shadow or theme_pink_3 %}
{% set theme_error_bg = theme_error_bg or theme_pink_1 %}
{% set theme_error_border = theme_error_border or theme_pink_2 %}
{% set theme_error_shadow = theme_error_shadow or theme_pink_3 %}
{# - warning level #}
{% set theme_caution_bg = theme_caution_bg or theme_pink_1 %}
{% set theme_caution_border = theme_caution_border or theme_pink_2 %}
{% set theme_attention_bg = theme_attention_bg or theme_pink_1 %}
{% set theme_attention_border = theme_attention_border or theme_pink_2 %}
{% set theme_warn_bg = theme_warn_bg or theme_pink_1 %}
{% set theme_warn_border = theme_warn_border or theme_pink_2 %}
{# - normal level #}
{% set theme_important_bg = theme_important_bg or theme_gray_2 %}
{% set theme_tip_bg = theme_tip_bg or theme_gray_2 %}
{% set theme_hint_bg = theme_hint_bg or theme_gray_2 %}
{# /set up admonition styling #}
{% set theme_shadow = theme_shadow or theme_gray_2 %}
{% set theme_topic_bg = theme_topic_bg or theme_gray_2 %}
{% set theme_narrow_sidebar_link = theme_narrow_sidebar_link or theme_gray_3 %}
{% set theme_sidebar_hr = theme_sidebar_hr or theme_gray_3 %}
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: {{ theme_font_family }};
font-size: {{ theme_font_size }};
background-color: {{ theme_base_bg }};
color: {{ theme_base_text }};
margin: 0;
padding: 0;
}
div.document {
width: {{ theme_page_width }};
margin: 30px auto 0 auto;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 {{ theme_sidebar_width }};
}
div.sphinxsidebar {
width: {{ theme_sidebar_width }};
font-size: 14px;
line-height: 1.5;
}
hr {
border: 1px solid {{ theme_hr_border }};
}
div.body {
background-color: {{ theme_body_bg }};
color: {{ theme_body_text }};
padding: 0 30px 0 30px;
}
div.body > .section {
text-align: {{ theme_body_text_align }};
}
div.footer {
width: {{ theme_page_width }};
margin: 20px auto 30px auto;
font-size: 14px;
color: {{ theme_footer_text }};
text-align: right;
}
div.footer a {
color: {{ theme_footer_text }};
}
p.caption {
font-family: {{ theme_caption_font_family }};
font-size: {{ theme_caption_font_size }};
}
{% if theme_show_related|lower == 'false' %}
div.relations {
display: none;
}
{% endif %}
div.sphinxsidebar a {
color: {{ theme_sidebar_link }};
text-decoration: none;
border-bottom: 1px dotted {{ theme_sidebar_link_underscore }};
}
div.sphinxsidebar a:hover {
border-bottom: 1px solid {{ theme_sidebar_link_underscore }};
}
div.sphinxsidebarwrapper {
padding: 18px 10px;
}
div.sphinxsidebarwrapper p.logo {
padding: 0;
margin: -10px 0 0 0px;
text-align: center;
}
div.sphinxsidebarwrapper h1.logo {
margin-top: -10px;
text-align: center;
margin-bottom: 5px;
text-align: {{ theme_logo_text_align }};
}
div.sphinxsidebarwrapper h1.logo-name {
margin-top: 0px;
}
div.sphinxsidebarwrapper p.blurb {
margin-top: 0;
font-style: {{ theme_description_font_style }};
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: {{ theme_head_font_family }};
color: {{ theme_sidebar_header }};
font-size: 24px;
font-weight: normal;
margin: 0 0 5px 0;
padding: 0;
}
div.sphinxsidebar h4 {
font-size: 20px;
}
div.sphinxsidebar h3 a {
color: {{ theme_sidebar_link }};
}
div.sphinxsidebar p.logo a,
div.sphinxsidebar h3 a,
div.sphinxsidebar p.logo a:hover,
div.sphinxsidebar h3 a:hover {
border: none;
}
div.sphinxsidebar p {
color: {{ theme_sidebar_text }};
margin: 10px 0;
}
div.sphinxsidebar ul {
margin: 10px 0;
padding: 0;
color: {{ theme_sidebar_list }};
}
div.sphinxsidebar ul li.toctree-l1 > a {
font-size: 120%;
}
div.sphinxsidebar ul li.toctree-l2 > a {
font-size: 110%;
}
div.sphinxsidebar input {
border: 1px solid {{ theme_sidebar_search_button }};
font-family: {{ theme_font_family }};
font-size: 1em;
}
div.sphinxsidebar hr {
border: none;
height: 1px;
color: {{ theme_sidebar_hr }};
background: {{ theme_sidebar_hr }};
text-align: left;
margin-left: 0;
width: 50%;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: {{ theme_link }};
text-decoration: underline;
}
a:hover {
color: {{ theme_link_hover }};
text-decoration: underline;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: {{ theme_head_font_family }};
font-weight: normal;
margin: 30px 0px 10px 0px;
padding: 0;
}
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
div.body h2 { font-size: 180%; }
div.body h3 { font-size: 150%; }
div.body h4 { font-size: 130%; }
div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: {{ theme_anchor }};
padding: 0 4px;
text-decoration: none;
}
a.headerlink:hover {
color: {{ theme_anchor_hover_fg }};
background: {{ theme_anchor_hover_bg }};
}
div.body p, div.body dd, div.body li {
line-height: 1.4em;
}
div.admonition {
margin: 20px 0px;
padding: 10px 30px;
background-color: {{ theme_admonition_bg }};
border: 1px solid {{ theme_admonition_border }};
}
div.admonition tt.xref, div.admonition code.xref, div.admonition a tt {
background-color: {{ theme_admonition_xref_bg }};
border-bottom: 1px solid {{ theme_admonition_xref_border }};
}
div.admonition p.admonition-title {
font-family: {{ theme_head_font_family }};
font-weight: normal;
font-size: 24px;
margin: 0 0 10px 0;
padding: 0;
line-height: 1;
}
div.admonition p.last {
margin-bottom: 0;
}
div.highlight {
background-color: {{ theme_code_highlight_bg }};
}
dt:target, .highlight {
background: {{ theme_highlight_bg }};
}
div.warning {
background-color: {{ theme_warn_bg }};
border: 1px solid {{ theme_warn_border }};
}
div.danger {
background-color: {{ theme_danger_bg }};
border: 1px solid {{ theme_danger_border }};
-moz-box-shadow: 2px 2px 4px {{ theme_danger_shadow }};
-webkit-box-shadow: 2px 2px 4px {{ theme_danger_shadow }};
box-shadow: 2px 2px 4px {{ theme_danger_shadow }};
}
div.error {
background-color: {{ theme_error_bg }};
border: 1px solid {{ theme_error_border }};
-moz-box-shadow: 2px 2px 4px {{ theme_error_shadow }};
-webkit-box-shadow: 2px 2px 4px {{ theme_error_shadow }};
box-shadow: 2px 2px 4px {{ theme_error_shadow }};
}
div.caution {
background-color: {{ theme_caution_bg }};
border: 1px solid {{ theme_caution_border }};
}
div.attention {
background-color: {{ theme_attention_bg }};
border: 1px solid {{ theme_attention_border }};
}
div.important {
background-color: {{ theme_important_bg }};
border: 1px solid {{ theme_important_border }};
}
div.note {
background-color: {{ theme_note_bg }};
border: 1px solid {{ theme_note_border }};
}
div.tip {
background-color: {{ theme_tip_bg }};
border: 1px solid {{ theme_tip_border }};
}
div.hint {
background-color: {{ theme_hint_bg }};
border: 1px solid {{ theme_hint_border }};
}
div.seealso {
background-color: {{ theme_seealso_bg }};
border: 1px solid {{ theme_seealso_border }};
}
div.topic {
background-color: {{ theme_topic_bg }};
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre, tt, code {
font-family: {{theme_code_font_family}};
font-size: {{ theme_code_font_size }};
}
.hll {
background-color: {{theme_code_highlight}};
margin: 0 -12px;
padding: 0 12px;
display: block;
}
img.screenshot {
}
tt.descname, tt.descclassname, code.descname, code.descclassname {
font-size: 0.95em;
}
tt.descname, code.descname {
padding-right: 0.08em;
}
img.screenshot {
-moz-box-shadow: 2px 2px 4px {{ theme_shadow }};
-webkit-box-shadow: 2px 2px 4px {{ theme_shadow }};
box-shadow: 2px 2px 4px {{ theme_shadow }};
}
table.docutils {
border: 1px solid {{ theme_table_border }};
-moz-box-shadow: 2px 2px 4px {{ theme_shadow }};
-webkit-box-shadow: 2px 2px 4px {{ theme_shadow }};
box-shadow: 2px 2px 4px {{ theme_shadow }};
}
table.docutils td, table.docutils th {
border: 1px solid {{ theme_table_border }};
padding: 0.25em 0.7em;
}
table.field-list, table.footnote {
border: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
table.footnote {
margin: 15px 0;
width: 100%;
border: 1px solid {{ theme_footnote_border }};
background: {{ theme_footnote_bg }};
font-size: 0.9em;
}
table.footnote + table.footnote {
margin-top: -15px;
border-top: none;
}
table.field-list th {
padding: 0 0.8em 0 0;
}
table.field-list td {
padding: 0;
}
table.field-list p {
margin-bottom: 0.8em;
}
/* Cloned from
* https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68
*/
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
table.footnote td.label {
width: .1px;
padding: 0.3em 0 0.3em 0.5em;
}
table.footnote td {
padding: 0.3em 0.5em;
}
dl {
margin: 0;
padding: 0;
}
dl dd {
margin-left: 30px;
}
blockquote {
margin: 0 0 0 30px;
padding: 0;
}
ul, ol {
/* Matches the 30px from the narrow-screen "li > ul" selector below */
margin: 10px 0 10px 30px;
padding: 0;
}
pre {
background: {{ theme_pre_bg }};
padding: 7px 30px;
margin: 15px 0px;
line-height: 1.3em;
}
div.viewcode-block:target {
background: {{ theme_viewcode_target_bg }};
}
dl pre, blockquote pre, li pre {
margin-left: 0;
padding-left: 30px;
}
tt, code {
background-color: {{ theme_code_bg }};
color: {{ theme_code_text }};
/* padding: 1px 2px; */
}
tt.xref, code.xref, a tt {
background-color: {{ theme_xref_bg }};
border-bottom: 1px solid {{ theme_xref_border }};
}
a.reference {
text-decoration: none;
border-bottom: 1px dotted {{ theme_link }};
}
/* Don't put an underline on images */
a.image-reference, a.image-reference:hover {
border-bottom: none;
}
a.reference:hover {
border-bottom: 1px solid {{ theme_link_hover }};
}
a.footnote-reference {
text-decoration: none;
font-size: 0.7em;
vertical-align: top;
border-bottom: 1px dotted {{ theme_link }};
}
a.footnote-reference:hover {
border-bottom: 1px solid {{ theme_link_hover }};
}
a:hover tt, a:hover code {
background: {{ theme_code_hover }};
}
@media screen and (max-width: 870px) {
div.sphinxsidebar {
display: none;
}
div.document {
width: 100%;
}
div.documentwrapper {
margin-left: 0;
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
}
div.bodywrapper {
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
margin-left: 0;
}
ul {
margin-left: 0;
}
li > ul {
/* Matches the 30px from the "ul, ol" selector above */
margin-left: 30px;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.bodywrapper {
margin: 0;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
@media screen and (max-width: 875px) {
body {
margin: 0;
padding: 20px 30px;
}
div.documentwrapper {
float: none;
background: {{ theme_base_bg }};
}
div.sphinxsidebar {
display: block;
float: none;
width: 102.5%;
{%- if theme_fixed_sidebar|lower == 'true' %}
margin: -20px -30px 20px -30px;
{%- else %}
margin: 50px -30px -20px -30px;
{%- endif %}
padding: 10px 20px;
background: {{ theme_narrow_sidebar_bg }};
color: {{ theme_narrow_sidebar_fg }};
}
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
div.sphinxsidebar h3 a {
color: {{ theme_base_bg }};
}
div.sphinxsidebar a {
color: {{ theme_narrow_sidebar_link }};
}
div.sphinxsidebar p.logo {
display: none;
}
div.document {
width: 100%;
margin: 0;
}
div.footer {
display: none;
}
div.bodywrapper {
margin: 0;
}
div.body {
min-height: 0;
padding: 0;
}
.rtd_doc_footer {
display: none;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
{%- if theme_fixed_sidebar|lower == 'true' %}
@media screen and (min-width: 876px) {
div.sphinxsidebar {
position: fixed;
margin-left: 0;
}
}
{%- endif %}
/* misc. */
.revsys-inline {
display: none!important;
}
/* Make nested-list/multi-paragraph items look better in Releases changelog
* pages. Without this, docutils' magical list fuckery causes inconsistent
* formatting between different release sub-lists.
*/
div#changelog > div.section > ul > li > p:only-child {
margin-bottom: 0;
}
/* Hide fugly table cell borders in ..bibliography:: directive output */
table.docutils.citation, table.docutils.citation td, table.docutils.citation th {
border: none;
/* Below needed in some edge cases; if not applied, bottom shadows appear */
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}

View File

@ -0,0 +1 @@
/* This file intentionally left blank. */

88
docs/_templates/alabaster/support.py vendored Normal file
View File

@ -0,0 +1,88 @@
# flake8: noqa
from pygments.style import Style
from pygments.token import Keyword, Name, Comment, String, Error, \
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
# Originally based on FlaskyStyle which was based on 'tango'.
class Alabaster(Style):
background_color = "#f8f8f8" # doesn't seem to override CSS 'pre' styling?
default_style = ""
styles = {
# No corresponding class for the following:
#Text: "", # class: ''
Whitespace: "underline #f8f8f8", # class: 'w'
Error: "#a40000 border:#ef2929", # class: 'err'
Other: "#000000", # class 'x'
Comment: "italic #8f5902", # class: 'c'
Comment.Preproc: "noitalic", # class: 'cp'
Keyword: "bold #004461", # class: 'k'
Keyword.Constant: "bold #004461", # class: 'kc'
Keyword.Declaration: "bold #004461", # class: 'kd'
Keyword.Namespace: "bold #004461", # class: 'kn'
Keyword.Pseudo: "bold #004461", # class: 'kp'
Keyword.Reserved: "bold #004461", # class: 'kr'
Keyword.Type: "bold #004461", # class: 'kt'
Operator: "#582800", # class: 'o'
Operator.Word: "bold #004461", # class: 'ow' - like keywords
Punctuation: "bold #000000", # class: 'p'
# because special names such as Name.Class, Name.Function, etc.
# are not recognized as such later in the parsing, we choose them
# to look the same as ordinary variables.
Name: "#000000", # class: 'n'
Name.Attribute: "#c4a000", # class: 'na' - to be revised
Name.Builtin: "#004461", # class: 'nb'
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
Name.Class: "#000000", # class: 'nc' - to be revised
Name.Constant: "#000000", # class: 'no' - to be revised
Name.Decorator: "#888", # class: 'nd' - to be revised
Name.Entity: "#ce5c00", # class: 'ni'
Name.Exception: "bold #cc0000", # class: 'ne'
Name.Function: "#000000", # class: 'nf'
Name.Property: "#000000", # class: 'py'
Name.Label: "#f57900", # class: 'nl'
Name.Namespace: "#000000", # class: 'nn' - to be revised
Name.Other: "#000000", # class: 'nx'
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
Name.Variable: "#000000", # class: 'nv' - to be revised
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
Number: "#990000", # class: 'm'
Literal: "#000000", # class: 'l'
Literal.Date: "#000000", # class: 'ld'
String: "#4e9a06", # class: 's'
String.Backtick: "#4e9a06", # class: 'sb'
String.Char: "#4e9a06", # class: 'sc'
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
String.Double: "#4e9a06", # class: 's2'
String.Escape: "#4e9a06", # class: 'se'
String.Heredoc: "#4e9a06", # class: 'sh'
String.Interpol: "#4e9a06", # class: 'si'
String.Other: "#4e9a06", # class: 'sx'
String.Regex: "#4e9a06", # class: 'sr'
String.Single: "#4e9a06", # class: 's1'
String.Symbol: "#4e9a06", # class: 'ss'
Generic: "#000000", # class: 'g'
Generic.Deleted: "#a40000", # class: 'gd'
Generic.Emph: "italic #000000", # class: 'ge'
Generic.Error: "#ef2929", # class: 'gr'
Generic.Heading: "bold #000080", # class: 'gh'
Generic.Inserted: "#00A000", # class: 'gi'
Generic.Output: "#888", # class: 'go'
Generic.Prompt: "#745334", # class: 'gp'
Generic.Strong: "bold #000000", # class: 'gs'
Generic.Subheading: "bold #800080", # class: 'gu'
Generic.Traceback: "bold #a40000", # class: 'gt'
}

122
docs/_templates/alabaster/theme.conf vendored Normal file
View File

@ -0,0 +1,122 @@
[theme]
inherit = basic
stylesheet = alabaster.css
pygments_style = alabaster.support.Alabaster
[options]
logo =
logo_name = false
logo_text_align = left
description =
description_font_style = normal
github_user =
github_repo =
github_button = true
github_banner = false
github_type = watch
github_count = true
badge_branch = master
travis_button = false
codecov_button = false
gratipay_user =
gittip_user =
analytics_id =
touch_icon =
canonical_url =
extra_nav_links =
sidebar_includehidden = true
sidebar_collapse = true
show_powered_by = true
show_related = false
gray_1 = #444
gray_2 = #EEE
gray_3 = #AAA
pink_1 = #FCC
pink_2 = #FAA
pink_3 = #D52C2C
base_bg = #fff
base_text = #000
hr_border = #B1B4B6
body_bg =
body_text = #3E4349
body_text_align = left
footer_text = #888
link = #004B6B
link_hover = #6D4100
sidebar_header =
sidebar_text = #555
sidebar_link =
sidebar_link_underscore = #999
sidebar_search_button = #CCC
sidebar_list = #000
sidebar_hr =
anchor = #DDD
anchor_hover_fg =
anchor_hover_bg = #EAEAEA
table_border = #888
shadow =
# Admonition options
## basic level
admonition_bg =
admonition_border = #CCC
note_bg =
note_border = #CCC
seealso_bg =
seealso_border = #CCC
## critical level
danger_bg =
danger_border =
danger_shadow =
error_bg =
error_border =
error_shadow =
## normal level
tip_bg =
tip_border = #CCC
hint_bg =
hint_border = #CCC
important_bg =
important_border = #CCC
## warning level
caution_bg =
caution_border =
attention_bg =
attention_border =
warn_bg =
warn_border =
topic_bg =
code_highlight_bg =
highlight_bg = #FAF3E8
xref_border = #fff
xref_bg = #FBFBFB
admonition_xref_border = #fafafa
admonition_xref_bg =
footnote_bg = #FDFDFD
footnote_border =
pre_bg =
narrow_sidebar_bg = #333
narrow_sidebar_fg = #FFF
narrow_sidebar_link =
font_size = 17px
caption_font_size = inherit
viewcode_target_bg = #ffd
code_bg = #ecf0f3
code_text = #222
code_hover = #EEE
code_font_size = 0.9em
code_font_family = 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace
font_family = 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif
head_font_family = 'Garamond', 'Georgia', serif
caption_font_family = inherit
code_highlight = #FFC
page_width = 940px
sidebar_width = 220px
fixed_sidebar = false

62
docs/_templates/base.html vendored Normal file
View File

@ -0,0 +1,62 @@
{%- extends "alabaster/layout.html" %}
{%- block extrahead %}
{{ super() }}
<style>
div.related {
width: 940px;
margin: 30px auto 0 auto;
}
@media screen and (max-width: 875px) {
div.related {
visibility: hidden;
display: none;
}
}
</style>
{% endblock %}
{%- block footer %}
{{ relbar() }}
<div class="footer">
{% if show_copyright %}&copy;{{ copyright }}.{% endif %}
{% if theme_show_powered_by|lower == 'true' %}
{% if show_copyright %}|{% endif %}
Powered by <a href="http://sphinx-doc.org/">Sphinx {{ sphinx_version }}</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster {{ alabaster_version }}</a>
{% endif %}
{%- if show_source and has_source and sourcename %}
{% if show_copyright or theme_show_powered_by %}|{% endif %}
<a href="{{ pathto('_sources/' + sourcename, true)|e }}"
rel="nofollow">{{ _('Page source') }}</a>
{%- endif %}
</div>
{% if theme_github_banner|lower != 'false' %}
<a href="https://github.com/{{ theme_github_user }}/{{ theme_github_repo }}" class="github">
<img style="position: absolute; top: 0; right: 0; border: 0;"
src="{{ pathto('_static/' ~ theme_github_banner, 1) if theme_github_banner|lower != 'true' else 'https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png' }}"
alt="Fork me on GitHub" class="github"/>
</a>
{% endif %}
{% if theme_analytics_id %}
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '{{ theme_analytics_id }}']);
_gaq.push(['_setDomainName', 'none']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_trackPageview']);
(function () {
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})();
</script>
{% endif %}
{%- endblock %}

View File

@ -1,7 +1,8 @@
{% extends "layout.html" %}
{% set title = _('Bonobo — Data processing for humans') %}
{% block body %}
{% extends "base.html" %}
{% set title = _('Bonobo — Data processing for humans') %}
{% block body %}
<h1 style="text-align: center">
<img class="logo" src="{{ pathto('_static/bonobo.png', 1) }}" title="Bonobo" alt="Bonobo"
style=" width: 128px; height: 128px;"/>
@ -9,7 +10,7 @@
<p>
{% trans %}
<b>Bonobo</b> is an Extract Transform Load framework for the Python (3.5+) language.
<b>Bonobo</b> is an <b>Extract Transform Load</b> (or ETL) framework for the <b>Python (3.5+)</b> language.
{% endtrans %}
</p>

7
docs/_templates/layout.html vendored Normal file
View File

@ -0,0 +1,7 @@
{%- extends "base.html" %}
{%- block content %}
{{ relbar() }}
{{ super() }}
{%- endblock %}

View File

@ -1,10 +1,10 @@
<a href="{{ pathto(master_doc) }}" style="border: none">
<h1 style="text-align: center; margin-top: 0;">
<h1 style="text-align: center; margin: 0;">
<img class="logo" src="{{ pathto('_static/bonobo.png', 1) }}" title="Bonobo" style="width: 48px; height: 48px; vertical-align: bottom"/>
Bonobo
</h1>
</a>
<p>
Data processing for human beings.
<p style="text-align: center">
Data processing for humans.
</p>

View File

@ -75,9 +75,9 @@ html_theme = 'alabaster'
html_theme_options = {
'github_user': 'python-bonobo',
'github_repo': 'bonobo',
'github_button': True,
'show_powered_by': False,
'show_related': True,
'github_button': 'true',
'show_powered_by': 'false',
'show_related': 'true',
}
html_sidebars = {

3
docs/genindex.rst Normal file
View File

@ -0,0 +1,3 @@
Full Index
==========

View File

@ -1,11 +1,211 @@
Graphs
======
Writing graphs
::::::::::::::
Graphs are the glue that ties transformations together. It's the only data-structure bonobo can execute directly. Graphs
must be acyclic, and can contain as much nodes as your system can handle. Although this number can be rather high in
theory, extreme practical cases usually do not exceed hundreds of nodes (and this is already extreme, really).
Debugging graphs
Definitions
:::::::::::
Graph
A directed acyclic graph of transformations, that Bonobo can inspect and execute.
Node
A transformation within a graph. The transformations are stateless, and have no idea whether or not they are
included in a graph, multiple graph, or not at all.
Creating a graph
::::::::::::::::
Graphs should be instances of :class:`bonobo.Graph`. The :func:`bonobo.Graph.add_chain` method can take as many
positional parameters as you want.
.. code-block:: python
import bonobo
graph = bonobo.Graph()
graph.add_chain(a, b, c)
Resulting graph:
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN -> "a" -> "b" -> "c";
}
Non-linear graphs
:::::::::::::::::
Divergences / forks
-------------------
To create two or more divergent data streams ("fork"), you should specify `_input` kwarg to `add_chain`.
.. code-block:: python
import bonobo
graph = bonobo.Graph()
graph.add_chain(a, b, c)
graph.add_chain(f, g, _input=b)
Resulting graph:
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN -> "a" -> "b" -> "c";
"b" -> "f" -> "g";
}
.. note:: Both branch will receive the same data, at the same time.
Convergences / merges
---------------------
To merge two data streams ("merge"), you can use the `_output` kwarg to `add_chain`, or use named nodes (see below).
.. code-block:: python
import bonobo
graph = bonobo.Graph()
# Here we mark _input to None, so normalize won't get the "begin" impulsion.
graph.add_chain(normalize, store, _input=None)
# Add two different chains
graph.add_chain(a, b, _output=normalize)
graph.add_chain(f, g, _output=normalize)
Resulting graph:
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN -> "a" -> "b" -> "normalize";
BEGIN2 [shape="point"];
BEGIN2 -> "f" -> "g" -> "normalize";
"normalize" -> "store"
}
.. note::
This is not a "join" or "cartesian product". Any data that comes from `b` or `g` will go through `normalize`, one at
a time. Think of the graph edges as data flow pipes.
Named nodes
:::::::::::
Using above code to create convergences can lead to hard to read code, because you have to define the "target" stream
before the streams that logically goes to the beginning of the transformation graph. To overcome that, one can use
"named" nodes:
graph.add_chain(x, y, z, _name='zed')
graph.add_chain(f, g, h, _input='zed')
.. code-block:: python
import bonobo
graph = bonobo.Graph()
# Add two different chains
graph.add_chain(a, b, _output="load")
graph.add_chain(f, g, _output="load")
# Here we mark _input to None, so normalize won't get the "begin" impulsion.
graph.add_chain(normalize, store, _input=None, _name="load")
Resulting graph:
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN -> "a" -> "b" -> "normalize (load)";
BEGIN2 [shape="point"];
BEGIN2 -> "f" -> "g" -> "normalize (load)";
"normalize (load)" -> "store"
}
Inspecting graphs
:::::::::::::::::
Bonobo is bundled with an "inspector", that can use graphviz to let you visualize your graphs.
Read `How to inspect and visualize your graph <https://www.bonobo-project.org/how-to/inspect-an-etl-jobs-graph>`_.
Executing graphs
::::::::::::::::
There are two options to execute a graph (which have a similar result, but are targeting different use cases).
* You can use the bonobo command line interface, which is the highest level interface.
* You can use the python API, which is lower level but allows to use bonobo from within your own code (for example, a
django management command).
Executing a graph with the command line interface
-------------------------------------------------
If there is no good reason not to, you should use `bonobo run ...` to run transformation graphs found in your python
source code files.
.. code-block:: shell-session
$ bonobo run file.py
You can also run a python module:
.. code-block:: shell-session
$ bonobo run -m my.own.etlmod
In each case, bonobo's CLI will look for an instance of :class:`bonobo.Graph` in your file/module, create the plumbery
needed to execute it, and run it.
If you're in an interactive terminal context, it will use :class:`bonobo.ext.console.ConsoleOutputPlugin` for display.
If you're in a jupyter notebook context, it will (try to) use :class:`bonobo.ext.jupyter.JupyterOutputPlugin`.
Executing a graph using the internal API
----------------------------------------
To integrate bonobo executions in any other python code, you should use :func:`bonobo.run`. It behaves very similar to
the CLI, and reading the source you should be able to figure out its usage quite easily.

View File

@ -1,13 +1,14 @@
Guides
======
Here are a few guides and best practices to work with bonobo.
This section will guide you through your journey with Bonobo ETL.
.. toctree::
:maxdepth: 2
graphs
introduction
transformations
graphs
services
environment
purity

106
docs/guide/introduction.rst Normal file
View File

@ -0,0 +1,106 @@
Introduction
============
The first thing you need to understand before you use Bonobo, or not, is what it does and what it does not, so you can
understand if it could be a good fit for your use cases.
How it works?
:::::::::::::
**Bonobo** is an **Extract Transform Load** framework aimed at coders, hackers, or any other person who's at ease with
terminals and source code files.
It is a **data streaming** solution, that treat datasets as ordered collections of independant rows, allowing to process
them "first in, first out" using a set of transformations organized together in a directed graph.
Let's take a few examples:
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
END [shape="none" label="..."];
BEGIN -> "A" -> "B" -> "C" -> "END";
}
One of the simplest, by the book, cases, is an extractor sending to a transformation, itself sending to a loader.
Bonobo will send an "impulsion" to all transformations linked to the little black dot on the left, here `A`.
`A`'s main topic will be to extract data from somewhere (a file, an endpoint, a database...) and generate some output.
As soon as the first row of `A`'s output is available, Bonobo will start asking `B` to process it. As soon as the first
row of `B`'s output is available, Bonobo will start asking `C` to process it.
While `B` and `C` are processing, `A` continues to generate data.
This approach can be efficient, depending on your requirements, because you may rely on a lot of services that may be
long to answer or unreliable, and you don't have to handle optimizations, parallelism or retry logic by yourself.
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
END [shape="none" label="..."];
END2 [shape="none" label="..."];
BEGIN -> "A" -> "B" -> "END";
"A" -> "C" -> "END2";
}
In this case, any output row of `A`, will be **sent to both** `B` and `C` simultaneously. Again, `A` will continue its
processing while `B` and `C` are working.
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN2 [shape="point"];
END [shape="none" label="..."];
BEGIN -> "A" -> "C" -> "END";
BEGIN2 -> "B" -> "C";
}
What is it not?
:::::::::::::::
**Bonobo** is not:
* A data science, or statistical analysis tool, which need to treat the dataset as a whole and not as a collection of
independant rows. If this is your need, you probably want to look at `pandas <https://pandas.pydata.org/>`_.
* A workflow or scheduling solution for independant data-engineering tasks. If you're looking to manage your sets of
data processing tasks as a whole, you probably want to look at `airflow <https://airflow.incubator.apache.org/>`_.
Although there is no Bonobo extension yet that handles that, it does make sense to integrate Bonobo jobs in an airflow
(or other similar tool) workflow.
* A big data solution, `as defined by wikipedia <https://en.wikipedia.org/wiki/Big_data>`_. We're aiming at "small
scale" data processing, which can be still quite huge for humans, but not for computers. If you don't know whether or
not this is sufficient for your needs, it probably means you're not in the "big data" land.
Where to jump next?
:::::::::::::::::::
If you did not run through it yet, we highly suggest that you go through the :doc:`tutorial </tutorial/index>` first.
Then, you can jump to the following guides, in no particuliar order:
.. toctree::
:maxdepth: 1
transformations
graphs
services
environment
purity

View File

@ -1,14 +1,10 @@
Services and dependencies
=========================
:Last-Modified: 20 may 2017
You'll want to use external systems within your transformations, including databases, HTTP APIs, other web services,
filesystems, etc.
You'll probably want to use external systems within your transformations. Those systems may include databases, apis
(using http, for example), filesystems, etc.
You can start by hardcoding those services. That does the job, at first.
If you're going a little further than that, you'll feel limited, for a few reasons:
Hardcoding those services is a good first step, but as your codebase grows, will show limits rather quickly.
* Hardcoded and tightly linked dependencies make your transformations hard to test, and hard to reuse.
* Processing data on your laptop is great, but being able to do it on different target systems (or stages), in different
@ -16,70 +12,77 @@ If you're going a little further than that, you'll feel limited, for a few reaso
pre-production environment, or production system. Maybe you have similar systems for different clients and want to select
the system at runtime. Etc.
Service injection
:::::::::::::::::
Definition of service dependencies
::::::::::::::::::::::::::::::::::
To solve this problem, we introduce a light dependency injection system. It allows to define named dependencies in
To solve this problem, we introduce a light dependency injection system. It allows to define **named dependencies** in
your transformations, and provide an implementation at runtime.
Class-based transformations
---------------------------
For function-based transformations, you can use the :func:`bonobo.config.use` decorator to mark the dependencies. You'll
still be able to call it manually, providing the implementation yourself, but in a bonobo execution context, it will
be resolve and injected automatically, as long as you provided an implementation to the executor (more on that below).
To define a service dependency in a class-based transformation, use :class:`bonobo.config.Service`, a special
descriptor (and subclass of :class:`bonobo.config.Option`) that will hold the service names and act as a marker
for runtime resolution of service instances.
.. code-block:: python
Let's define such a transformation:
from bonobo.config import use
@use('orders_database')
def select_all(database):
yield from database.query('SELECT * FROM foo;')
For class based transformations, you can use :class:`bonobo.config.Service`, a special descriptor (and subclass of
:class:`bonobo.config.Option`) that will hold the service names and act as a marker for runtime resolution of service
instances.
.. code-block:: python
from bonobo.config import Configurable, Service
class JoinDatabaseCategories(Configurable):
database = Service('primary_sql_database')
database = Service('orders_database')
def __call__(self, database, row):
def call(self, database, row):
return {
**row,
'category': database.get_category_name_for_sku(row['sku'])
}
This piece of code tells bonobo that your transformation expect a service called "primary_sql_database", that will be
Both pieces of code tells bonobo that your transformation expect a service called "orders_database", that will be
injected to your calls under the parameter name "database".
Function-based transformations
------------------------------
Providing implementations at run-time
-------------------------------------
No implementation yet, but expect something similar to CBT API, maybe using a `@Service(...)` decorator. See
`issue #70 <https://github.com/python-bonobo/bonobo/issues/70>`_.
Provide implementation at run time
----------------------------------
Let's see how to execute it:
Bonobo will expect you to provide a dictionary of all service implementations required by your graph.
.. code-block:: python
import bonobo
graph = bonobo.graph(
*before,
JoinDatabaseCategories(),
*after,
)
graph = bonobo.graph(...)
def get_services():
return {
'orders_database': my_database_service,
}
if __name__ == '__main__':
bonobo.run(
graph,
services={
'primary_sql_database': my_database_service,
}
)
bonobo.run(graph, services=get_services())
A dictionary, or dictionary-like, "services" named argument can be passed to the :func:`bonobo.run` helper. The
"dictionary-like" part is the real keyword here. Bonobo is not a DIC library, and won't become one. So the implementation
provided is pretty basic, and feature-less. But you can use much more evolved libraries instead of the provided
stub, and as long as it works the same (a.k.a implements a dictionary-like interface), the system will use it.
.. note::
A dictionary, or dictionary-like, "services" named argument can be passed to the :func:`bonobo.run` API method.
The "dictionary-like" part is the real keyword here. Bonobo is not a DIC library, and won't become one. So the
implementation provided is pretty basic, and feature-less. But you can use much more evolved libraries instead of
the provided stub, and as long as it works the same (a.k.a implements a dictionary-like interface), the system will
use it.
Command line interface will look at services in two different places:
* A `get_services()` function present at the same level of your graph definition.
* A `get_services()` function in a `_services.py` file in the same directory as your graph's file, allowing to reuse the
same service implementations for more than one graph.
Solving concurrency problems
----------------------------
@ -87,7 +90,7 @@ Solving concurrency problems
If a service cannot be used by more than one thread at a time, either because it's just not threadsafe, or because
it requires to carefully order the calls made (apis that includes nonces, or work on results returned by previous
calls are usually good candidates), you can use the :class:`bonobo.config.Exclusive` context processor to lock the
use of a dependency for a time period.
use of a dependency for the time of the context manager (`with` statement)
.. code-block:: python
@ -101,18 +104,10 @@ use of a dependency for a time period.
api.last_call()
Service configuration (to be decided and implemented)
:::::::::::::::::::::::::::::::::::::::::::::::::::::
* There should be a way to configure default service implementation for a python file, a directory, a project ...
* There should be a way to override services when running a transformation.
* There should be a way to use environment for service configuration.
Future and proposals
::::::::::::::::::::
This is the first proposed implementation and it will evolve, but looks a lot like how we used bonobo ancestor in
production.
This first implementation and it will evolve. Base concepts will stay, though.
May or may not happen, depending on discussions.

View File

@ -1,8 +1,90 @@
Transformations
===============
Here is some guidelines on how to write transformations, to avoid the convention-jungle that could happen without
a few rules.
Transformations are the smallest building blocks in Bonobo ETL.
They are written using standard python callables (or iterables, if you're writing transformations that have no input,
a.k.a extractors).
Definitions
:::::::::::
Transformation
The base building block of Bonobo, anything you would insert in a graph as a node. Mostly, a callable or an iterable.
Extractor
Special case transformation that use no input. It will be only called once, and its purpose is to generate data,
either by itself or by requesting it from an external service.
Loader
Special case transformation that feed an external service with data. For convenience, it can also yield the data but
a "pure" loader would have no output (although yielding things should have no bad side effect).
Callable
Anything one can call, in python. Can be a function, a python builtin, or anything that implements `__call__`
Iterable
Something we can iterate on, in python, so basically anything you'd be able to use in a `for` loop.
Function based transformations
::::::::::::::::::::::::::::::
The most basic transformations are function-based. Which means that you define a function, and it will be used directly
in a graph.
.. code-block:: python
def get_representation(row):
return repr(row)
graph = bonobo.Graph(
[...],
get_representation,
[...],
)
It does not allow any configuration, but if it's an option, prefer it as it's simpler to write.
Class based transformations
:::::::::::::::::::::::::::
For less basic use cases, you'll want to use classes to define some of your transformations. It's also a better choice
to build reusable blocks, as you'll be able to create parametrizable transformations that the end user will be able to
configure at the last minute.
Configurable
------------
.. autoclass:: bonobo.config.Configurable
Options
-------
.. autoclass:: bonobo.config.Option
Services
--------
.. autoclass:: bonobo.config.Service
Methods
-------
.. autoclass:: bonobo.config.Method
ContextProcessors
-----------------
.. autoclass:: bonobo.config.ContextProcessor
Naming conventions
@ -44,50 +126,35 @@ can be used as a graph node, then use camelcase names:
upper = Apply(str.upper)
Function based transformations
::::::::::::::::::::::::::::::
Testing
:::::::
As Bonobo use plain old python objects as transformations, it's very easy to unit test your transformations using your
favourite testing framework. We're using pytest internally for Bonobo, but it's up to you to use the one you prefer.
If you want to test a transformation with the surrounding context provided (for example, service instances injected, and
context processors applied), you can use :class:`bonobo.execution.NodeExecutionContext` as a context processor and have
bonobo send the data to your transformation.
The most basic transformations are function-based. Which means that you define a function, and it will be used directly
in a graph.
.. code-block:: python
def get_representation(row):
return repr(row)
from bonobo.constants import BEGIN, END
from bonobo.execution import NodeExecutionContext
graph = bonobo.Graph(
[...],
get_representation,
with NodeExecutionContext(
JsonWriter(filename), services={'fs': ...}
) as context:
# Write a list of rows, including BEGIN/END control messages.
context.write(
BEGIN,
Bag({'foo': 'bar'}),
Bag({'foo': 'baz'}),
END
)
It does not allow any configuration, but if it's an option, prefer it as it's simpler to write.
Class based transformations
:::::::::::::::::::::::::::
A lot of logic is a bit more complex, and you'll want to use classes to define some of your transformations.
The :class:`bonobo.config.Configurable` class gives you a few toys to write configurable transformations.
Options
-------
.. autoclass:: bonobo.config.Option
Services
--------
.. autoclass:: bonobo.config.Service
Methods
-------
.. autoclass:: bonobo.config.Method
ContextProcessors
-----------------
.. autoclass:: bonobo.config.ContextProcessor
# Out of the bonobo main loop, we need to call `step` explicitely.
context.step()
context.step()

View File

@ -11,6 +11,10 @@ Bonobo
reference/index
faq
contribute/index
.. toctree::
:hidden:
genindex
modindex