File: builtin_backends.rst

package info (click to toggle)
python-stone 3.3.9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,036 kB
  • sloc: python: 22,311; objc: 498; sh: 23; makefile: 11
file content (388 lines) | stat: -rw-r--r-- 13,967 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
****************
Builtin Backends
****************

Using a backend, you can convert the data types and routes in your spec into
objects in your programming language of choice.

Stone includes backends for an assortment of languages, including:

* `Python <#python-guide>`_
* Python `Type Stubs <https://www.python.org/dev/peps/pep-0484/#id42>`_
* Javascript
* Objective-C
* Swift
* Typescript

If you're looking to write your own backend, see `Backend Reference
<backend_ref.rst>`_. We would love to see a contribution of a PHP or Ruby
backend.

Compile with the CLI
====================

Compiling a spec and generating code is done using the ``stone``
command-line interface (CLI)::

    $ stone -h
    usage: stone [-h] [-v] [--clean-build] [-f FILTER_BY_ROUTE_ATTR]
                 [-w WHITELIST_NAMESPACE_ROUTES | -b BLACKLIST_NAMESPACE_ROUTES]
                 backend output [spec [spec ...]]

    StoneAPI

    positional arguments:
      backend               Either the name of a built-in backend or the path to
                            a backend module. Paths to backend modules must
                            end with a .stoneg.py extension. The following
                            backends are built-in: js_client, js_types,
                            tsd_client, tsd_types, python_types, python_client,
                            swift_client
      output                The folder to save generated files to.
      spec                  Path to API specifications. Each must have a .stone
                            extension. If omitted or set to "-", the spec is read
                            from stdin. Multiple namespaces can be provided over
                            stdin by concatenating multiple specs together.

    optional arguments:
      -h, --help            show this help message and exit
      -v, --verbose         Print debugging statements.
      --clean-build         The path to the template SDK for the target language.
      -f FILTER_BY_ROUTE_ATTR, --filter-by-route-attr FILTER_BY_ROUTE_ATTR
                            Removes routes that do not match the expression. The
                            expression must specify a route attribute on the left-
                            hand side and a value on the right-hand side. Use
                            quotes for strings and bytes. The only supported
                            operators are "=" and "!=". For example, if "hide" is
                            a route attribute, we can use this filter:
                            "hide!=true". You can combine multiple expressions
                            with "and"/"or" and use parentheses to enforce
                            precedence.
      -w WHITELIST_NAMESPACE_ROUTES, --whitelist-namespace-routes WHITELIST_NAMESPACE_ROUTES
                            If set, backends will only see the specified
                            namespaces as having routes.
      -b BLACKLIST_NAMESPACE_ROUTES, --blacklist-namespace-routes BLACKLIST_NAMESPACE_ROUTES
                            If set, backends will not see any routes for the
                            specified namespaces.

We'll generate code based on an ``calc.stone`` spec with the following
contents::

    namespace calc

    route eval(Expression, Result, EvalError)

    struct Expression
        "This expression is limited to a binary operation."
        op Operator = add
        left Int64
        right Int64

    union Operator
        add
        sub
        mult
        div Boolean
            "If value is true, rounds up. Otherwise, rounds down."

    struct Result
        answer Int64

    union EvalError
        overflow

Python Guide
============

This section explains how to use the pre-packaged Python backends and work
with the Python classes that have been generated from a spec.

There are two different Python backends: ``python_types`` and
``python_client``. The former generates Python classes for the data types
defined in your spec. The latter generates a single Python class with a method
per route, which is useful for building SDKs.

We'll use the ``python_types`` backend::

    $ stone python_types . calc.stone

This runs the backend on the ``calc.stone`` spec. Its output target is
``.`` which is the current directory. A Python module is created for
each declared namespace, so in this case only ``calc.py`` is created.

Three additional modules are copied into the target directory. The first,
``stone_validators.py``, contains classes for validating Python values against
their expected Stone types. You will not need to explicitly import this module,
but the auto-generated Python classes depend on it. The second,
``stone_serializers.py``, contains a pair of ``json_encode()`` and
``json_decode()`` functions. You will need to import this module to serialize
your objects. The last is ``stone_base.py`` which shouldn't be used directly.

In the following sections, we'll interact with the classes generated in
``calc.py``. For simplicity, we'll assume we've opened a Python interpreter
with the following shell command::

    $ python -i calc.py

For non-test projects, we recommend that you set the generation target to a
path within a Python package, and use Python's import facility.

Primitive Types
---------------

The following table shows the mapping between a Stone `primitive type
<lang_ref.rst#primitive-types>`_ and its corresponding type in Python.

========================== ============== =====================================
Primitive                  Python 2.x / 3    Notes
========================== ============== =====================================
Bytes                      bytes
Boolean                    bool
Float{32,64}               float          long type within range is converted.
Int{32,64}, UInt{32,64}    long
List                       list
String                     unicode / str  str type is converted to unicode.
Timestamp                  datetime
========================== ============== =====================================

Struct
------

For each struct in your spec, you will see a corresponding Python class of the
same name.

In our example, ``Expression``, ``Operator``, ``Answer``, ``EvalError``, and
are Python classes. They have an attribute (getter/setter/deleter property) for
each field defined in the spec. You can instantiate these classes and specify
field values either in the constructor or by assigning to an attribute::

    >>> expr = Expression(op=Operator.add, left=1, right=1)

If you assign a value that fails validation, an exception is raised::

    >>> expr.op = '+'
    Traceback (most recent call last)
    ...
    ValidationError: expected type Operator or subtype, got string

Accessing a required field (non-optional with no default) that has not been set
raises an error::

    >>> res = Result()
    >>> res.answer
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "calc.py", line 221, in answer
        raise AttributeError("missing required field 'answer'")
    AttributeError: missing required field 'answer'

Other characteristics:

    1. Inheritance in Stone is represented as inheritance in Python.
    2. If a field is nullable and was never set, ``None`` is returned.
    3. If a field has a default but was never set, the default is returned.

Union
-----

For each union in your spec, you will see a corresponding Python class of the
same name.

You do not use a union class's constructor directly. To select a tag with a
void type, use the class attribute of the same name::

    >>> EvalError.overflow
    EvalError('overflow', None)

To select a tag with a value, use the class method of the same name and pass
in an argument to serve as the value::

    >>> Operator.div(False)
    Operator('div', False)

To write code that handles the union options, use the ``is_[tag]()`` methods.
We recommend you exhaustively check all tags, or include an else clause to
ensure that all possibilities are accounted for. For tags that have values,
use the ``get_[tag]()`` method to access the value::

    >>> # assume that op is an instance of Operator
    >>> if op.is_add():
    ...     # handle addition
    ... elif op.is_sub():
    ...     # handle subtraction
    ... elif op.is_mult():
    ...     # handle multiplication
    ... elif op.is_div():
    ...     round_up = op.get_div()
    ...     # handle division

Struct Polymorphism
-------------------

As with regular structs, structs that enumerate subtypes have corresponding
Python classes that behave identically to regular structs.

The difference is apparent when a field has a data type that is a struct with
enumerated subtypes. Expanding on our example from the language reference,
assume the following spec::

    struct Resource
        union
            file File
            folder Folder

        path String

    struct File extends Resource:
        size UInt64

    struct Folder extends Resource:
        "No new fields."

    struct Response
        rsrc Resource

If we instantiate ``Response``, the ``rsrc`` field can only be assigned a
``File`` or ``Folder`` object. It should not be assigned a ``Resource`` object.

An exception to this is on deserialization. Because ``Resource`` is specified
as a catch-all, it's possible when deserializing a ``Response`` to get a
``Resource`` object in the ``rsrc`` field. This indicates that the returned
subtype was unknown because the recipient has an older spec than the sender.
To handle catch-alls, you should use an else clause::

    >>> print resp.rsrc.path  # Guaranteed to work regardless of subtype
    >>> if isinstance(resp, File):
    ...     # handle File
    ... elif isinstance(resp, Folder):
    ...     # handle Folder
    ... else:
    ...     # unknown subtype of Resource

Route
-----

Routes are represented as instances of a ``Route`` object. The generated Python
module for the namespace will have a module-level variable for each route::

    >>> eval
    Route('eval', 1, False, ...)

Route attributes specified in the spec are available as a dict in the ``attrs``
member variable. Route deprecation is stored in the ``deprecated`` member
variable. The name and version of a route are stored in the ``name`` and ``version`` member
variables, respectively.

Serialization
-------------

We can use ``stone_serializers.json_encode()`` to serialize our objects to
JSON::

    >>> import stone_serializers
    >>> stone_serializers.json_encode(eval.result_type, Result(answer=10))
    '{"answer": 10}'

To deserialize, we can use ``json_decode``::

    >>> stone_serializers.json_decode(eval.result_type, '{"answer": 10}')
    Result(answer=10)

There's also ``json_compat_obj_encode`` and ``json_compat_obj_decode`` for
converting to and from Python primitive types rather than JSON strings.

Route Functions
---------------

To generate functions that represent routes, use the ``python_client``
generator::

    $ stone python_client . calc.stone -- -m client -c Client -t myservice

``-m`` specifies the name of the Python module to generate, in this case
``client.py``. The important contents of the file look as follows::

    class Client(object):
        __metaclass__ = ABCMeta

        @abstractmethod
        def request(self, route, namespace, arg, arg_binary=None):
            pass

        # ------------------------------------------
        # Routes in calc namespace

        def calc_eval(self,
                      left,
                      right,
                      op=calc.Operator.add):
            """
            :type op: :class:`myservice.calc.Operator`
            :type left: long
            :type right: long
            :rtype: :class:`myservice.calc.Result`
            :raises: :class:`.exceptions.ApiError`

            If this raises, ApiError will contain:
                :class:`myservice.calc.EvalError`
            """
            arg = calc.Expression(left,
                                  right,
                                  op)
            r = self.request(
                calc.eval,
                'calc',
                arg,
                None,
            )
            return r

``-c`` specified the name of the abstract class to generate. Using this class,
you'll likely want to inherit the class and implement the request function. For
example, an API that goes over HTTP might have the following client::

    import requests  # use the popular HTTP library

    from .stone_serializers import json_decode, json_encode
    from .exceptions import ApiError  # You must implement this

    class MyServiceClient(Client):

        def request(self, route, namespace, arg, arg_binary=None):
            url = 'https://api.myservice.xyz/{}/{}'.format(
                    namespace, route.name)
            r = requests.get(
                url,
                headers={'Content-Type': 'application/json'},
                data=json_encode(route.arg_type, arg))
            if r.status_code != 200:
                raise ApiError(...)
            return json_decode(route.result_type, r.content)

Note that care is taken to ensure that that the return type and exception type
match those that were specified in the automatically generated documentation.

Routes with Version Numbers
---------------------------

There can be multiple versions of routes sharing the same name. For each route with a version
numbers other than 1, the generated module-level route variable and route function have a version
suffix appended in the form of ``{name}_v{version}``.

For example, suppose we add a new version of route ``eval`` in ``calc.stone`` as follows::

    ...

    route eval:2(Expression, ResultV2, EvalError)

    struct ResultV2
        answer String

    ...

The module-level variable for the route will be::

    >>> eval_v2
    Route('eval', 2, False, ...)

And the corresponding route function in ``client.py`` will be ``calc_eval_v2``.