Common Patterns And Best Practices

While designing and working with Ways, a few re-occuring ideas would appear in production code over and over. This page is a collection of some of those good ideas.

Best Practices

This section is a series of things to include while writing Ways objects that are generally good ideas to do.

Writing mapping and mapping_details

Include a mapping for your plugins whenever possible. If you have some kind of information, a string or a dict, and you don’t know what Context hierarchy it belongs to, mapping and mapping_details are used to “auto-find” the right Context.

Auto-find using mapping

Whenever you have to autofind a Context using ways.api.get_asset(), it’s best to give a string whenever you can because then Ways can exact-match the string to a mapping, like this:

plugins:
    something:
        hierarchy: foo
        mapping: /jobs/{JOB}/shots
value = '/jobs/someJobName_12391231/shots'
asset = ways.api.get_asset(value)

If the mapping of the hierarchy you’re looking for has at least one Token, you can give a dict:

value = {'JOB': 'someJobName_12391231'}
asset = ways.api.get_asset(value)

There’s a pretty obvious problem with that though. If two hierarchies have a mapping that both use the “JOB” Token, Ways will try to return them both, which will cause an error.

plugins:
    something:
        hierarchy: foo
        mapping: /jobs/{JOB}/shots
    another:
        hierarchy: bar
        mapping: generic.{JOB}.string.here
value = {'JOB': 'someJobName_12391231'}
asset = ways.api.get_asset(value)  # Will raise an exception

Both “foo” and “bar” hierarchies use the JOB Token so Ways doesn’t know which one to use.

The good news is, there is a way to distinguish between “foo” and “bar” in this worst-case scenario. Just describe “JOB” using mapping_details.

plugins:
    something:
        hierarchy: foo
        mapping: /jobs/{JOB}/shots
        mapping_details:
            JOB:
                parse:
                    regex: '\d+'
    another:
        hierarchy: bar
        mapping: generic.{JOB}.string.here
        mapping_details:
            JOB:
                parse:
                    regex: '\w+'
value = {'JOB': 'someJobName_12391231'}
asset = ways.api.get_asset(value)
asset.get_hierarchy()
# Result: 'foo'

Because the “foo” hierarchy was defined with regex and it expected some integer, and “bar” is allowed to have non-digit characters, Ways was able to figure out which Context to use for our Asset.

In short, it’s a good idea to define mapping and mapping_details basically always.

Add a UUID

In Ways, the UUID is an optional string that you can add to every plugin. This UUID is useful for searching and debugging so it’s a good idea to include it whenever you can.

plugins:
    hierarchy: foo
    uuid: some_string_that_is_not_used_anywhere_else

A UUID must be unique, even in other Ways-related files. If the same UUID comes up more than once, Ways will raise an exception to let you know.

Filepaths and mapping

If you use Ways for filepaths, make sure to enable the “path” key to avoid OS-related issues.

plugins:
    path_out:
        hierarchy: foo
        mapping: /etc/some/filepath
        path: true

The reason to do this has explained in path so head there if further explanation is needed.

Action Patterns

By now you should know about Actions (If not, read through this Extend Ways Using Actions). Actions are how Ways extends its objects with additional functions.

Because Actions are applied to certain hierarchies, sometimes you may call an Action on an Asset or Context that you think exists but doesn’t. When that happens, AttributeError is raised.

plugins:
    foo:
        hierarchy: some/hierarchy
    another:
        hierarchy: action/hierarchy
class ActionOne(ways.api.Action):

    name = 'some_action'

    @classmethod
    def get_hierarchy(cls):
        return 'some/hierarchy'

    def __call__(self, obj):
        return ['t', 'a', 'b', 'z']


class ActionTwo(ways.api.Action):

    name = 'some_action'

    @classmethod
    def get_hierarchy(cls):
        return 'action/hierarchy'

    def __call__(self, obj):
        return [1, 2, 4, 5.4, 6, -2]

for hierarchy in ['some/hierarchy', 'action/hierarchy', 'bar']:
    context = ways.api.get_context(hierarchy)
    context.actions.some_action()

This will cause you to want to write lots of code using try/except:

try:
    value = context.actions.some_action()
except AttributeError:
    value = []

A better way is to assign a default value for your Action. This value will get returned whenever you call a missing Action.

In a plugin file, you can write this:

/some/plugin/defaults.py

import ways.api

class ActionTwo(ways.api.Action):

    name = 'some_action'

    @classmethod
    def get_hierarchy(cls):
        return 'action/hierarchy'

    def __call__(self, obj):
        return [1, 2, 4, 5.4, 6, -2]

def main():
    '''Add defaults for actions.'''
    ways.api.add_action_default('some_action', [])

Then add the path to /some/plugin/defaults.py to your WAYS_PLUGINS environment variable.

Now, in any file you’d like, you can work as normal.

import ways.api

context = ways.api.get_context('foo/bar')
for item in context.actions.some_action():
    # ...

To summarize, it’s usually a good idea to define a default value in the same file that defines Actions. That way there is always a fallback value.

Note

If you want certain hierarchies to have different default values, specify a hierarchy while you define your default value.

ways.api.add_action_default(‘some_action’, [], hierarchy=’foo/bar’)

Designing Plugins

Appending vs Defining

It’s mentioned in several other pages such as path and Appending To Plugins but you have 3 options to add information to hierarchies. You can either just add the information to the original plugin or append to it, using another absolute plugin or a relative plugin.

plugins:
    root:
        hierarchy: foo
    another:
        hierarchy: bar
        mapping: a_mapping
    absolute_append:
        hierarchy: foo
        data:
            something_to_add: here
    relative_append:
        hierarchy: ''
        mapping: something
        path: true
        uses:
            - foo
            - bar

In this example, the “absolute_append” plugin will append to “root” and “relative_append” appends to “root” and “another” at once. If you need better control over your plugins, using absolute appends will tend to be a very clear, simple way to do it. If you need to make a broad change to many plugins at once, relative appends make more sense to do since you can specify many plugins and add information all in one plugin.

Relative appends have one problem though - you can’t customize what gets appended to both hierarchies.

In the above example, mapping and path are both appending onto “root” and “another”. But say for example you only wanted mapping to append to “root” and not to “another”? It’s not possible - you’d have to split the relative plugin into two relative plugins. At that point, you might as well use absolute appends.

It’s a balancing act and you’ll find yourself gravitating to one style or another.

Asset Swapping

Ways comes with an object called Asset (ways.api.Asset) that is used for basic asset management. If you have your own classes that you’d prefer to use instead, adding those objects to Ways is fairly simple.

Register A Custom Class

An generic Ways Asset expects at least two arguments, the object that represents the information to pass to the Asset and the Context that does with that that information. The Context is optional, as mentioned before.

info = {'foo': 'bar'}
context = 'some/thing/context'
ways.api.get_asset(info, context)

If you have a class that takes two or more arguments, you can use that class directly in place of an Asset.

import ways.api

class SomeNewAssetClass(object):

    '''Some class that will take the place of our Asset.'''

    def __init__(self, info, context):
        '''Create the object.'''
        super(SomeNewAssetClass, self).__init__()
        self.context = context

    def example_method(self):
        '''Run some method.'''
        return 8

    def another_method(self):
        '''Run another method.'''
        return 'bar'
context = ways.api.get_context('some/thing/context')
ways.api.register_asset_class(SomeNewAssetClass, context)
asset = ways.api.get_asset({'JOB': 'something'}, context='some/thing/context')
asset.example_method()
# Result: 8

If the class isn’t designed to work with Ways or takes 0 or 1 arguments, you can still use it. Just add an init function:

import ways.api

class SomeNewAssetClass(object):

    '''Some class that will take the place of our Asset.'''

    def __init__(self):
        '''Create the object.'''
        super(SomeNewAssetClass, self).__init__()

def custom_init(*args, **kwargs):
    return SomeNewAssetClass()

def main():
    '''Register a default Asset class for 'some/thing/context.'''
    context = ways.api.get_context('some/thing/context')
    ways.api.register_asset_class(
        SomeNewAssetClass, context=context, init=custom_init)

By default, you will need to register a class/init function for every hierarchy that you want to swap. So if you had hierarchies like this, “some”, “some/other”, “some/other/child”, and “some/other/child/hierarchy” then you’d need to register the custom class for all 4 hierarchies individually. If you’re prefer to register them for “this hierarchy and all its subhierarchies”, set children to True.

ways.api.register_asset_class(SomeNewAssetClass, context='some', children=True)