WriterCommand Guide

This document goes over an example of creating the remarking.JSONWriterCommandExample.

To start, let’s make sure we have a clear goal.

Note

We want to create a new writer command that outputs highlights and documents as a JSON string.

remarking (run|persist) json_example my_book_name

Running the above should output our JSON string.

remarking will take care of writing the output where we want it, we just need to provide it with the proper JSON string.

In order to achieve our goal, we need to implement 2 interfaces:

Before we start implementing them, let’s start by setting up a quick feedback loop.

Iterate quickly

Detection

remarking automatically detects new writer commands that are added to the remarking.cli.commands package.

Skeleton

We’ll start by creating a file called remarking/cli/commands/json_writer_command_example.py and copying this skeleton into it:

import typing as T
from typing import List

from remarking import Writer
from remarking import WriterCommand
from remarking import ClickOption
from remarking import CommandLineLogger
from remarking import Highlight
from remarking import Document


class JSONWriterExample(Writer):
    """ Writer a json string to output.  """

    def __init__(self,
                 documents: List[Document],
                 highlights: List[Highlight],
                 ) -> None:
        pass

    def write(self, logger: CommandLineLogger) -> None:
        """ Write to output. Invoked by remarking after extraction is ran on documents. """



class JSONWriterCommandExample(WriterCommand):
    """ JSON Writer Command for: `json_example` output writer. """

    def name(self) -> str:
        """ Return the name of the command as referenced on the command line. """
        return "json_example"

    def options(self) -> List[ClickOption]:
        """ Return a list of click options to use for the command.

        The list can be constructed from the return value of :func:`click.option`.

        These options will be added to the default options for the run or persist command.
        """
        return []

    def long_description(self) -> str:
        """ The long description for your command. Shown when running ``--help`` """
        return ""

    def short_description(self) -> str:
        """ The short description of your command. """
        return ""

    def writer(self,
               documents: List[Document],
               highlights: List[Highlight],
               **kwargs: T.Any) -> Writer:
        """ Parse options and return a configured instance of a concrete :class:`Writer` class. """
        return JSONWriterExample(documents, highlights)

Let’s give this a test to make sure it is found by remarking.

remarking run json_example library

This should run without errors, but will not produce any output as we are not yet writing any highlights to output!

The remarking.WriterCommand Interface

The remarking.WriterCommand interface is where most of the command line parsing work happens.

We can find the methods we need to implement by looking at the abstractmethods defined on remarking.WriterCommand.

We have already created the implementation stubs in the skeleton above.

Let’s go over each of the methods in the skeleton and implement them.

name

The remarking.WriterCommand.name() method should return the name by which the command will be referred to on the command line.

Let’s set this to "json_example" for the purpose of this example.

def name(self) -> str:
    """ Return the name of the command as referenced on the command line. """
    return "json_example"

options

This should be a list of command line options that your command accepts. The options should be the return value of click.option().

To learn more about click options check out click’s options section.

Note

text

Keep in mind we use options in a special way.

We do not decorate the options method, instead we return a list of click.option() objects.

Let’s add some fun options to illustrate the usage:

import click

def options(self) -> List[ClickOption]:
    """ Return a list of click options to use for the command.

    The list can be constructed from the return value of :func:`click.option`.

    These options will be added to the default options for the run or persist command.
    """
    return [
        click.option("--fun/--no-fun",
                     "is_fun",
                     default=False,
                     help="Have fun"
                   ),
        click.option("--compress/--no-compress",
                     "perform_compression",
                     default=False,
                     help="If compression should be performed on the output."
                   )
    ]

This adds the --fun/--no-fun boolean flags as well as the --compress/--no-compress flags. These flags are added to the already existing arguments needed to run remarking such as the --token and --output options.

Let’s check if our options are there by running:

remarking run json_example --help

This should show the help for our new options.

long_description

This is used for the long description of the command when running:

remarking (run|persist) json_example

Let’s make it long, but not too long:

def long_description(self) -> str:
    """ The long description for your command. Shown when running ``--help`` """
    return """

    Returns documents and highlights as a JSON string

    Has additional compression and fun options that
    """

short_description

Next, we have the short description. This is used when listing all the available writer commands.

Let’s make it short and sweet.

def short_description(self) -> str:
    """ The short description of your command """
    return "Output results as a JSON String"

Let’s test our new descriptions by having a look at the output of

remarking run --help

and

remarking run json_example --help

writer

The final method we need to implement will lead us into the implementation of our JSONWriterExample class.

The JSONWriterCommandExample.writer method should return an instance of remarking.Writer.

In our case this means the concrete implementation JSONWriterExample.

The concrete implementation instance is passed documents and highlights processed by remarking for them to be output as JSON.

Along the way, the JSONWriterCommandExample.writer will also need to parse the kwargs argument for the options that it specified in the options method.

Let’s have a look at what this all looks like:

def writer(self,
           documents: List[Document],
           highlights: List[Highlight],
           **kwargs: T.Any) -> Writer:
    """ Parse options and return a configured instance of a concrete :class:`Writer` class. """
    is_fun = kwargs['is_fun']
    perform_compression = kwargs['perform_compression']
    return JSONWriterExample(documents, highlights, is_fun, perform_compression)

As you can see, we reach into the kwargs dictionary to extract the named options we declared in options method.

You’ll notice the JSONWriterExample constructor receives documents and highlights in addition to the options we set.

Note

Having separate writer logic and command logic makes it easier to have clean code.

We won’t be able to test this until we modify the JSONWriterExample class with the correct constructor.

Let’s create the JSONWriterExample shortly, but before that, let’s go over what we have so far.

Final State

Our JSONWriterCommandExample class should resemble:

import typing as T
from typing import List

import click
from remarking import Writer
from remarking import WriterCommand
from remarking import ClickOption
from remarking import CommandLineLogger
from remarking import Highlight
from remarking import Document

class JSONWriterCommandExample(WriterCommand):
    """ JSON Writer Command for: `json_example` output writer. """

    def name(self) -> str:
        """ Return the name of the command as referenced on the command line. """
        return "json_example"

    def options(self) -> List[ClickOption]:
        """ Return a list of click options to use for the command.

        The list can be constructed from the return value of :func:`click.option`.

        These options will be added to the default options for the run or persist command.
        """
        return [
            click.option("--fun/--no-fun",
                         "is_fun",
                         default=False,
                         help="Have fun"
                       ),
            click.option("--compress/--no-compress",
                         "perform_compression",
                         default=False,
                         help="If compression should be performed on the output."
                       )
        ]

    def long_description(self) -> str:
        """ The long description for your command. Shown when running ``--help`` """
        return """

        Returns documents and highlights as a JSON string

        Has additional compression and fun options that
        """

    def short_description(self) -> str:
        """ The short description of your command. """
        return "Output results as a JSON String"


    def writer(self,
               documents: List[Document],
               highlights: List[Highlight],
               **kwargs: T.Any) -> Writer:
        """ Parse options and return a configured instance of a concrete :class:`Writer` class. """
        is_fun = kwargs['is_fun']
        perform_compression = kwargs['perform_compression']
        return JSONWriterExample(documents, highlights, is_fun, perform_compression)

Let’s move on to defining our JSONWriterExample

The remarking.Writer Interface

The last class we need to define is the implementation of remarking.Writer: JSONWriterExample.

We previously added our definition of JSONWriterExample at the top of remarking/cli/commands/json_writer_command_example.py

class JSONWriterExample(Writer):
    """ Writer a json string to output.  """

    def __init__(self,
                 documents: List[Document],
                 highlights: List[Highlight],
                 ) -> None:
        pass

    def write(self, logger: CommandLineLogger) -> None:
        """ Write to output. Invoked by remarking after extraction is ran on documents. """

The only method we must implement is JSONWriterExample.write. We also need to make sure we implement a constructor that can take the documents, highlights and the options we set in JSONWriterCommandExample.

Let’s match the signature of JSONWriterExample to the one we used in JSONWriterCommandExample.writer:

def __init__(self,
             documents: List[Document],
             highlights: List[Highlight],
             is_fun: bool,
             perform_compression: bool) -> None:
    self.is_fun = is_fun
    self.perform_compression = perform_compression
    self.data = {
       'documents': [doc.to_dict() for doc in documents],
       'highlights': [highlight.to_dict() for highlight in highlights]
    }

Note that we also create a dictionary with the highlights and documents keys.

This will be the dictionary we output as a JSON string.

Let’s make sure everything works by giving our example a run:

remarking run json_example --compress library

Once again, nothing should be displayed, but there should be no errors.

write

The last method we need to implement is write.

The write method receives an instance of remarking.CommandLineLogger called logger that is configured to send output to the correct location.

You should use remarking.CommandLineLogger.output_result() to write any strings for the user to further process.

Check out the class reference for remarking.CommandLineLogger for information on other methods you can use to print statuses.

Our implementation should be something like this:

import zlib
import base64
import json
from remarking.cli.commands.json_writer_command import json_serial

def write(self, logger: CommandLineLogger) -> None:
    """ Write to a logger"""
    json_string = json.dumps(self.data, default=json_serial)
    if self.is_fun:
        json_string = "Only errors for you."

    if self.perform_compression:
         json_string = base64.b64encode(
             zlib.compress(
                 json_string.encode("utf-8")
             )
         ).decode("utf-8")

    logger.output_result(json_string + '\n')

You can see we have utilized the self.is_fun and self.perform_compression that we defined as options in our JSONWriterCommandExample.

We use the json.dumps command to turn our self.data dictionary into a json string. Note that we use the json_serial method from the built-in JSONWriter so that we can handle serializing dates.

Finally, we output our result with CommandLineLogger.output_result().

All together now

The final contents of remarkable/cli/commands/json_writer_command_example.py should be:

import base64
import json

import typing as T
from typing import List

import zlib


import click
from remarking import Writer
from remarking import WriterCommand
from remarking import ClickOption
from remarking import CommandLineLogger
from remarking import Highlight
from remarking import Document

from remarking.cli.commands.json_writer_command import json_serial


class JSONWriterExample(Writer):
    """ Writer a json string to output.  """


    def __init__(self,
                 documents: List[Document],
                 highlights: List[Highlight],
                 is_fun: bool,
                 perform_compression: bool) -> None:
        self.is_fun = is_fun
        self.perform_compression = perform_compression
        self.data = {
           'documents': [doc.to_dict() for doc in documents],
           'highlights': [highlight.to_dict() for highlight in highlights]
        }

    def write(self, logger: CommandLineLogger) -> None:
        """ Write to output. Invoked by remarking after extraction is ran on documents. """
        json_string = json.dumps(self.data, default=json_serial)
        if self.is_fun:
            json_string = "Only errors for you."

        if self.perform_compression:
             json_string = base64.b64encode(
                 zlib.compress(
                     json_string.encode("utf-8")
                 )
             ).decode("utf-8")

        logger.output_result(json_string + '\n')



class JSONWriterCommandExample(WriterCommand):
    """ JSON Writer Command for: `json_example` output writer. """

    def name(self) -> str:
        """ Return the name of the command as referenced on the command line. """
        return "json_example"

    def options(self) -> List[ClickOption]:
        """ Return a list of click options to use for the command.

        The list can be constructed from the return value of :func:`click.option`.

        These options will be added to the default options for the run or persist command.
        """
        return [
            click.option("--fun/--no-fun",
                         "is_fun",
                         default=False,
                         help="Have fun"
                       ),
            click.option("--compress/--no-compress",
                         "perform_compression",
                         default=False,
                         help="If compression should be performed on the output."
                       )
        ]

    def long_description(self) -> str:
        """ The long description for your command. Shown when running ``--help`` """
        return """

        Returns documents and highlights as a JSON string

        Has additional compression and fun options that
        """

    def short_description(self) -> str:
        """ The short description of your command. """
        return "Output results as a JSON String"


    def writer(self,
               documents: List[Document],
               highlights: List[Highlight],
               **kwargs: T.Any) -> Writer:
        """ Parse options and return a configured instance of a concrete :class:`Writer` class. """
        is_fun = kwargs['is_fun']
        perform_compression = kwargs['perform_compression']
        return JSONWriterExample(documents, highlights, is_fun, perform_compression)

Finally we can run our command with:

remarking run json_example library

We can use the compress option to compress our JSON string:

remarking run json_example --compress library

We can also have some fun:

remarking run json_example --fun library

Next Steps

Once you have your new writer command open a pull request as outlined in the contribution doc and someone will be pinged for review!