2. Examples

There are many uses of Rickle, and some of the functionality is described here through examples.

2.1. Simple Config

The most basic usage of a Rickle is to use it as a config object. Let’s create a scenario in which this might be useful. Say you have a common API served through a Flask app. You need 10 versions of the API, each having the same code base but with different databases in the back, and some different endpoint configurations. Below we follow an example app with 10 different configs saved as YAML files.

2.1.1. Basic usage

Let’s make our first simple config in YAML, call it config_US.yaml.

APP:
   details:
       name: user_api
       doc_page: '/doc'
       version: '1.0.0'
   database:
       host: 127.0.0.1
       user: local
       passw: ken-s3nt_me
   endpoints:
       status:
           description: Gets the status for a region in the country.
           params:
               region: US
               language: en-US
       users:
           description: Gets the users for a given city.
           params:
               city: Seattle

As an example, we will have the simple API:

from flask import Flask, Resource
from flask_restx import Api
from rickled import BaseRickle
from some_database import DBConnection

config = BaseRickle('./config_US.yaml')

app = Flask(config.APP.details.name)
api = Api(
    app,
    version=config.APP.details.version,
    doc=config.APP.details.doc_page,
)

conf_status_ep = config.APP.endpoints.get('status')
if conf_status_ep:
    @api.route('/status')
    class Status(Resource):
        @api.doc(description=conf_status_ep.description)
        @api.param('region', conf_status_ep.params.region)
        @api.param('language', conf_status_ep.params.language)
        def get(self):
            return some_function_here(request.args['region'], request.args['language'])

conf_users_ep = config.APP.endpoints.get('users')
if conf_users_ep:
    @api.route('/users')
    class Users(Resource):
        @api.doc(description=conf_users_ep.description)
        @api.param('city', conf_users_ep.params.city)
        def get(self):
            with DBConnection(host=config.APP.database.host,
                              user=config.APP.database.user,
                              passw=config.APP.database.passw
                             ) as conn:
                results = conn.exec(f"SELECT * FROM users WHERE city = '{request.args['city']}'")
            return results

Here we can see that the config YAML file is loaded as a Rickle. In the creation of the Flask API, we load details from the Rickle. We then get the settings for the endpoint “status”. If the endpoint is not defined in the YAML, we simply don’t create it. That gives us the power to create a new YAML config for another country where the “status” endpoint does not exist.

2.1.2. Create from different things

The config does not have to be loaded from a YAML file. It does not even have to be loaded.

# Create an empty Rickle
config = BaseRickle()

# Loaded from a JSON file
config = BaseRickle('./config_ZA.json')

# Create from a Python dictionary
d = {
    'APP' : {
        'details': {
            'name': 'user_api',
            'doc_page': '/doc',
            'version': '1.0.0'
        }
        'database': {
            'host': '127.0.0.1',
            'user': 'local',
            'passw': 'ken-s3nt_me'
       }
        'endpoints': {}
    }
}
config = BaseRickle(d)

# Create from a YAML string (or a JSON string)
yaml_string = """
APP:
    details:
        name: user_api
        doc_page: '/doc'
        version: '1.0.0'
    database:
        host: 127.0.0.1
        user: local
        passw: ken-s3nt_me
    endpoints: null
"""

config = BaseRickle(yaml_string)

2.1.3. Add global arguments

For the less likely event that you need to modify the YAML string dynamically before loading, arguments can be given as follows.

APP:
   details:
       name: user_api
       doc_page: _|documentation_endpoint|_
       version: '1.0.0'

And then the string will be searched and replaced before the YAML is loaded and a Rickle is constructed.

# Create an empty Rickle
config = BaseRickle()

# Loaded from a JSON file
config = BaseRickle('./config_ZA.json', documentation_endpoint='/za_docs')

This will in effect change the YAML to the following (before loading it).

APP:
   details:
       name: user_api
       doc_page: /za_docs
       version: '1.0.0'

Even though the possibilities are opened up here, there are probably better ways to solve this (such as using ENV vars as shown later in this examples page).

2.1.4. Load multiple files

We are not limited to only loading configs from one YAML (or JSON) file. Multiple files can be loaded into one Rickle at once. Be sure to not have duplicate keys in the same root.

Let’s create the same config but split it into two, because we probably have the same DB connection details for all 10 countries.

Here we have a file db_conf.yaml:

database:
    host: 127.0.0.1
    user: local
    passw: ken-s3nt_me

And now the country config config_SW.yaml:

details:
    name: user_api
    doc_page: /docs
    version: '1.0.0'

Notice how here we don’t have the root APP, but only to show the example.

We can now load both into the same Rickle:

# Load a list of YAML files
config = BaseRickle(['./db_conf.yaml', './config_SW.yaml'])

print(config.database.host)
print(config.details.version)

Again, in this example the root APP is missing as it is a slightly different example.

In this example we can create 10 config files and always load the same DB connection settings, instead of copying it to each config file.

2.1.5. Referencing in YAML

What is especially powerful of YAML is the ability to add references. If we had a lot of duplication, we can simply reference the same values.

APP:
   details:
       name: user_api
       doc_page: '/doc'
       version: '1.0.0'
   database:
       host: 127.0.0.1
       user: local
       passw: ken-s3nt_me
   default_params:
      db_version: &db_version '1.1.0'
      language: &language 'en-US'
   endpoints:
      status:
         description: Gets the status for a region in the country.
         params:
            region: US
            language: *language
            db_version: *db_version
      users:
         description: Gets the users for a given city.
         params:
            city: Seattle
            language: *language
            db_version: *db_version

2.1.6. Strings, Repr

A Rickle can have a string representation, which will be in YAML format.

rick = Rickle('test.yaml')

print(str(rick))
>> database:
     host: 127.0.0.1
     user: local
     passw: ken-s3nt_me

Str will give the serialised version where repr will give a raw view.

2.1.7. Dict, Items, Values

A Rickle can act like a Python dictionary, like the following examples:

rick = Rickle('test.yaml')

rick.items()
>> [(k, v)]

rick.values()
>> [v, v]

rick.keys()
>> [k, k]

rick.get('k', default=0.42)
>> 72

rick['new'] = 0.99
rick['new']
>> 0.99

A Rickle can also be converted to a Python dictionary:

rick = Rickle('test.yaml')

rick.dict()
>> {'k' : 'v'}

2.1.8. To YAML, JSON

A rickle can also be dumped to YAML or JSON.

rick = Rickle('test.yaml')

rick.to_yaml_file('other.yaml')
rick.to_json_file('other.json')
rick.to_yaml_string()
rick.to_json_string()

2.2. Extended usage

2.2.1. Add environment var

Using the Rickle class, instead of the BasicRickle, we can add a lot more extended types. One being the environment variable.

Here we have a file db_conf.yaml again, but this time we are loading the values from OS env:

database:
   host:
      type: env
      load: DB_HOST
      default: 127.0.0.1
   user:
      type: env
      load: DB_USERNAME
   passw:
      type: env
      load: DB_PASSWORD

Note that we can define a default value. The default is always None, so no exception is raised if the env var does not exist.

2.2.2. Add lambdas

Another extension that could potentially be very useful is adding lambdas to a Rickle. This is not without security risks. If lambdas are loaded that you did not author yourself and do not know what they do, they can do anything.

A Rickle can be loaded without lambdas or functions by passing the load_lambda argument at creation. But this is not a foolproof safety measure. Even with load_lambda=False, if you load other sources such as API results or other files, they can reference other calls that do execute the lambda functions.

The safest way to load unknown sources is to not load them. However, you can always define the following ENV variable:

RICKLE_SAFE_LOAD=1

Again, the best way to load lambdas is to load what you trust.

Example of a lambda:

datenow:
   type: lambda
   import:
      - "from datetime import datetime as dd"
   load: "print(dd.utcnow().strftime('%Y-%m-%d'))"

The lambda can be used by calling datenow(). Lambdas can also have arguments:

datenow:
   type: lambda
   args:
      message: Hello World
   import:
      - "from datetime import datetime as dd"
   load: "print(dd.utcnow().strftime('%Y-%m-%d'), message)"

And can be used as datenow(message='Hello friend').

2.2.3. Add functions

Functions are a further extension to lambdas. They allow self referencing to the Rickle, and are multi line blocks.

get_area:
   type: function
   name: get_area
   args:
      x: 10
      y: 10
      z: null
      f: 0.7
   import:
      - math
   load: >
      def get_area(x, y, z, f):
         if not z is None:
            area = (x * y) + (x * z) + (y * z)
            area = 2 * area
         else:
            area = x * y
         return math.floor(area * f)

And then the function can be called as follows.

rick = Rickle('test.yaml', load_lambda=True)

rick.get_area(x=52, y=34.9, z=10, f=0.8)

A self reference to the Rickle can also be added.

const:
   f: 0.7
get_area:
   type: function
   name: get_area
   is_method: true
   args:
      x: 10
      y: 10
      z: null
   import:
      - math
   load: >
      def get_area(self, x, y, z):
         if not z is None:
            area = (x * y) + (x * z) + (y * z)
            area = 2 * area
         else:
            area = x * y
         return math.floor(area * self.const.f)

In this example rickle.const.f is used in the function.

This will only work if the attribute referred to is found on the same level. The following example won’t work.

const:
   f: 0.7
one_higher:
   get_area:
      type: function
      name: get_area
      is_method: true
      args:
         x: 10
         y: 10
         z: null
      import:
         - math
      load: >
         def get_area(self, x, y, z):
            if not z is None:
               area = (x * y) + (x * z) + (y * z)
               area = 2 * area
            else:
               area = x * y
            return math.floor(area * self.const.f)
rick = Rickle('test.yaml', load_lambda=True)

rick.one_higher.get_area(x=52, y=34.9, z=10, f=0.8)

This will result in an AttributeError:

>> Traceback (most recent call last):
>>   File "C:\source\Zipfian Science\rickled\tests\unittest\test_advanced.py", line 183, in test_self_reference
>>     area = r.functions.get_area(x=10, y=10, z=10)
>>   File "<string>", line 1, in <lambda>
>>   File "<string>", line 7, in get_area3ee93073e2f441af9f6a9acac3e21635
>> AttributeError: 'Rickle' object has no attribute 'const'

2.2.4. Add CSV

A local CSV file can be loaded as a list of lists, or as a list of Rickles.

If we have a CSV file with the following contents:

A,B,C,D
j,1,0.2,o
h,2,0.9,o
p,1,1.0,c

Where A,B,C,D are the columns, the following will load a list of three Rickle objects.

csv:
   type: from_csv
   file_path: './tests/placebos/test.csv'
   load_as_rick: true
   fieldnames: null
rick = Rickle('test.yaml')

rick.csv[0].A == 'j'
>> True

rick.csv[0].C == 0.2
>> True

rick.csv[-1].D == 'c'
>> True

If fieldnames is null, the first row in the file is assumed to be the names.

If the file is not loaded as a Rickle, lists of lists are loaded, and this assumes that the first row is not the field names.

csv:
   type: from_csv
   file_path: './tests/placebos/test.csv'
   load_as_rick: false
   fieldnames: null
rick = Rickle('test.yaml')

rick.csv[0]
>> ['A','B','C','D']

rick.csv[-1]
>> ['p',1,1.0,'c']

A third way to load the CSV is to load the columns as lists.

j,1,0.2,o
h,2,0.9,o
p,1,1.0,c
csv:
   type: from_csv
   file_path: './tests/placebos/test.csv'
   load_as_rick: false
   fieldnames: [A, B, C, D]
rick = Rickle('test.yaml')

rick.csv.A
>> ['j','h','p']

rick.csv.C
>> [0.2,0.9,1.0]

2.2.5. Add from file

Other files can also be loaded, either as another Rickle, a binary file, or a plain text file.

another_rick:
   type: from_file
   file_path: './tests/placebos/test_config.json'
   load_as_rick: true
   deep: true
   load_lambda: true

This will load the contents of the file as a Rickle object.

another_rick:
   type: from_file
   file_path: './tests/placebos/test.txt'
   load_as_rick: false
   encoding: UTF-16

This will load the contents as plain text.

another_rick:
   type: from_file
   file_path: './tests/placebos/out.bin'
   is_binary: true

This will load the data as binary.

The data in the file can also be loaded on function call, same as with the add_api_json_call. This is done with the hot_load: true property.

2.2.6. Add from REST API

Data can also be loaded from an API, expecting a JSON response.

crypt_exchanges:
   type: api_json
   url: https://cryptingup.com/api/exchanges
   expected_http_status: 200

This will load the JSON response as a dictionary. But the contents can also be loaded as a Rickle. Note, this can be dangerous, therefore a load_lambda property is defined. However, this response can point to another API call with load_lambda set as true. Only load API responses as Rickles when you trust the contents, or set the ENV RICKLE_SAFE_LOAD=1.

crypt_exchanges:
   type: api_json
   url: https://cryptingup.com/api/exchanges
   expected_http_status: 200
   load_as_rick: true
   deep: true
   load_lambda: false

Other properties that can be defined:

url
http_verb: 'GET' or 'POST'
headers: dictionary type
params: dictionary type
body: dictionary type
load_as_rick: bool
deep: bool
load_lambda: bool
expected_http_status: int
hot_load: bool

The property hot_load will turn this into a function that, when called, does the request with the params/headers.

crypt_exchanges:
   type: api_json
   url: https://cryptingup.com/api/exchanges
   expected_http_status: 200
   hot_load: true

This example will load the results hot off the press.

rick = Rickle('test.yaml')

rick.crypt_exchanges()

Notice how it is called with parentheses because it is now a function (hot_load=true).

2.2.7. Add base 64 encoded

A base 64 string can be loaded as bytes.

encoded:
   type: base64
   load: dG9vIG1hbnkgc2VjcmV0cw==

2.2.8. Add HTML page

Useful when loading up a documentation page.

encoded:
   type: html_page
   url: https://cryptingup.com
   expected_http_status: 200

This will GET the HTML. params and headers can also be given, same as with the API call.

As with the API call, a hot_load property will load the page on call.

2.2.9. Import Python modules

Should you need specific Python modules loaded, you can define the following:

r_modules:
   type: module_import
   import:
      - "math"

2.2.10. Define a class

Whole new classes can be defined. This will have a type and will be initialised with attributes and functions.

TesterClass:
   name: TesterClass
   type: class_definition
   attributes:
      dictionary:
         a: a
         b: b
      list_type:
         - 1
         - 2
         - 3
         - 4
 some_func:
   type: function
   name: some_func
   is_method: true
   args:
     x: 7
     y: 2
   import:
     - "math"
   load: >
     def some_func(self, x, y):
       print(x , y)
       print(self.__class__.__name__)
datenow:
   type: lambda
   import:
     - "from datetime import datetime as dd"
   load: "lambda self: print(dd.utcnow().strftime('%Y-%m-%d'))"
rick = Rickle('test.yaml')

rick.TesterClass.datenow()
>> '1991-02-20'

print(type(rick.TesterClass))
>> <class 'TesterClass'>

2.3. Paths and searching

Another useful piece of functionality is the ability to use paths with Rickles.

2.3.1. Search keys

We can search for paths by using the search_path method.

rickle.search_path('point')
>> ['/config/default/point', '/config/control/point', '/docs/controls/point']

If we search for point, we found all the paths in the Rickle.

2.3.2. Use paths

We can access the attributes by using the paths. If we have the following YAML:

path:
   datenow:
      type: lambda
      import:
         - "from datetime import datetime as dd"
      load: "dd.utcnow().strftime('%Y-%m-%d')"
   level_one:
      level_two:
         member: 42
         list_member:
            - 1
            - 0
            - 1
            - 1
            - 1
   funcs:
      type: function
      name: funcs
      args:
         x: 42
         y: worl
      load: >
          def funcs(x, y):
              _x = int(x)
              return f'Hello {y}, {_x / len(y)}!'

And the we can use paths.

test_rickle = Rickle(yaml, load_lambda=True)

test_rickle('/path/level_one/level_two/member') == 42
>> True

test_rickle('/path/level_one/funcs?x=100&y=world') == 'Hello world, 20.0!'
>> True

test_rickle('/path/datenow')
>> '1991-08-06'

We can even call functions like this, and pass the arguments as parameters.

2.4. Object Rickler

The ObjectRickler is a tool to convert basic Python objects to Rickles, or to create Python objects and merge Rickles into them. This is very experimental should be used as such.

2.4.1. Object to Rickle

A Python object can be converted to a Rickle, taking the attributes visible and functions with as best it can.

class TestObject:

   names = ['Phiber Optik', 'Dark Avenger']
   deep = [
      {'k' : 0.2},
      {'k' : 0.9}
   ]
   __hidden = 'Value'

   def print_names(self):
      for name in self.names:
         print(f'Hello, {name}')

And then using the Rickler:

rickler = ObjectRickler()

test_object = TestObject()

rick = rickler.to_rickle(test_object, deep=True, load_lambda=True)

isinstance(rick, Rickle)
>> True

rick.names
>> ['Phiber Optik', 'Dark Avenger']

rick.deep[0].k
>> 0.2

rick.print_names()
>> Hello Phiber Optik
   Hello Dark Avenger

Note that __hidden will not be a part of the Rickle.

The Python object can also be converted to a dictionary.

obj_dict = rickler.deconstruct(test_object, include_imports=True, include_class_source=True)

obj_dict['names']
>> ['Phiber Optik', 'Dark Avenger']

obj_dict['print_names']
>> {
       "type": "function",
       "name": "print_names",
       "is_method" : True,
       "load": "def print_names(self):\n         for name in self.names:\n            print(f'Hello, {name}')",
       "args": {}
   }

2.4.2. Rickle to object

A Rickle can also be attached to a Python object.

class TestObject:

   names = ['Phiber Optik', 'Dark Avenger']
   deep = [
      {'k' : 0.2},
      {'k' : 0.9}
   ]
   __hidden = 'Value'

   def print_names(self):
      for name in self.names:
         print(f'Hello, {name}')

And then the following Rickle can be defined:

path:
   datenow:
      type: lambda
      import:
         - "from datetime import datetime as dd"
      load: "dd.utcnow().strftime('%Y-%m-%d')"
   level_one:
      level_two:
         member: 42
         list_member:
            - 1
            - 0
            - 1
            - 1
            - 1
   funcs:
      type: function
      name: funcs
      args:
         x: 42
         y: worl
      load: >
          def funcs(x, y):
              _x = int(x)
              return f'Hello {y}, {_x / len(y)}!'

Then added to the object:

rick = Rickle('test.yaml', load_lambda=True)

rickler = ObjectRickler()

obj = rickler.from_rickle(rick, TestObject)

obj.names
>> ['Phiber Optik', 'Dark Avenger']

obj.path.datenow()
>> '1988-11-02'