Internals

This section covers technical details of how clans works, in more detail than you probably want to know. It might be useful if you are interested in contributing to the clans source, developing an extension, or writing some other application that will use clans as a library.

Extension Hooks

Clans’ extension framework is based on hooks - named points in the execution of various commands where it stops to call other code. Extensions are Python modules that define functions with names matching one or more of these hooks.

Hooks all accept clans’ controller object, ClansSession, as the first argument. Most are also passed a number of other arguments, depending on the context. For example, the post_search hook is passed the ClansSession as well as the results of that search.

To enable a clans extension that you write yourself, make sure the module is on Python’s module search path, so that it’s importable via import my_clans_extension. Then, in clans.cfg, add it to the [extensions] section:

[extensions]
myext=my_clans_extension

On the left side of the equal sign is a casual name for your extension, and the right should be its importable name, using Python dot-syntax if necessary.

Some extensions are packaged with clans, like clans.ext.newlove, and for these it is unnecessary to specify the full importable path. It is worth looking at the included clans.ext.example extension to get an idea of how to write your own.

Warning

I think I’ve implemented this in a moderately intelligent way, but the hook API should not be considered stable prior to clans 1.0.

List of Hooks

Each hook accepts one or more (usually mutable) arguments, and need not return anything. Typically, arguments can be modified in-place or left untouched.

The first argument passed is always the ClansSession object.

post_get_edit_text(cs, plan_text)

This hook is called during plan editing, after the edit text has been retrieved from the server.

The edit text is passed as an immutable unicode string as the second argument.

If this hook returns any value other than None, clans will skip interactive editing.

post_load_commands(cs)

This hook is called right after the standard commands and arguments are defined.

This hook is a good place to add arguments or subcommands to the command table, which you can do by modifying the commands attribute of cs.

For example, to add an argument to an existing command:

cs.commands['love'].add_argument(
           '-t', '--time', dest='time',
           action='store_true', default=False,
           help="Order results by time first seen.")

or, to add a whole new command:

cs.commands.add_command(
           'secrets', secrets, parents=[global_parser],
           description='Glimpse into the souls of others.',
           help='View secrets.')

where secrets is a function you define elsewhere in your extension.

This hook is called after a (quicklove or regular) search, and is passed a list containing the results.

Elements of this list are 3-tuples:

  • the name of the plan on which the term was found (str)
  • the number of instances found (int)
  • a list of snippets.

Note that the snippet list may not be the same length as the number of instances found.

Lists are mutable, so results may be filtered by modifying this list in-place.

This hook is called before a (quicklove or regular) search, and is passed the same arguments as is the search function:

  • the search term
  • planlove, a boolean of whether to restrict search to planlove.
pre_set_edit_text(cs, edited)

This hook is called during plan editing, after the edit text has been modified, but before being submitted to the server.

The modified edit text is passed as an immutable unicode string as the second argument.

Plans ScrAPI

Plans does not have a complete API, so clans utilizes a Plans-specific scraping library to communicate with the Plans server. This is packaged as a separate sub-module so that it can be used in other Python programs independent of clans.

Warning

Code changes on the server side could break the scrAPI at any time, so it should not be considered in any way stable.

The entire thing is built around one class, PlansConnection. Additionally there is the PlansError exception. They can be imported like so:

from clans.scraper import PlansConnection, PlansError

Then we can instantiate a PlansConnection, log into Plans, and begin doing things:

pc = PlansConnection()
pc.plans_login('baldwint', 'not_my_password_lol')
pc.read_plan('gorp')

Method summary

class PlansConnection(cookiejar=None, base_url='https://www.grinnellplans.com')

Encapsulates an active login to plans.

get_autofinger()

Retrieve all levels of the autofinger (autoread) list.

Returns a dictionary where the keys are the group names “Level 1”, “Level 2”, etc. and the values are a list of usernames waiting to be read.

get_edit_text()

Retrieve contents of the edit plan field.

Returns the edit_text of the plan and its md5 hash, as computed on the server side.

plans_login(username='', password='')

Log into plans.

Returns True on success, False on failure. Leave username and password blank to check an existing login.

planwatch(hours=12)

Return plans updated in the last hours hours.

The result is a list of (username, timestamp) 2-tuples.

read_plan(plan)

Retrieve the contents of the specified plan.

Returns two objects: the plan header (as a python dictionary)
the plan text (in HTML format)
search_plans(term, planlove=False)

Search plans for the provided term.

If planlove is True, term is a username, and the search will be for incidences of planlove for that user.

returns: list of plans upon which the search term was found. each list element is a 3-tuple:

  • plan name
  • number of occurrences of search term on the plan
  • list of plan excerpts giving context

the length of the excerpt list may be equal to or less than the number of occurrences of the search term, since overlapping excerpts are consolidated.

set_edit_text(newtext, md5)

Update plan with new content.

To prevent errors, the server does a hash check on the existing plan before replacing it with the new one. We provide an md5 sum to confirm that yes, we really want to update the plan.

Returns info message.

exception PlansError

Exception raised when there is an error talking to plans.