Tuesday, February 14, 2017

Intro to Python CSV Processing for Actual Beginners

I've written a lot about CSV processing. Here are some examples http://slott-softwarearchitect.blogspot.com/search/label/csv.

It crops up in my books. A lot.

In all cases, though, I make the implicit assumption that my readers already know a lot of Python. This is a disservice to anyone who's getting started.

Getting Started

You'll need Python 3.6. Nothing else will do if you're starting out.

Go to https://www.continuum.io/downloads and get Python 3.6. You can get the small "miniconda" version to start with. It has some of what you'll need to hack around with CSV files. The full Anaconda version contains a mountain of cool stuff, but it's a big download.

Once you have Python installed, what next? To be sure things are running do this:
  1. Find a command line prompt (terminal window, cmd.exe, whatever it's called on your OS.)
  2. Enter python3.6 (or just python in Windows.)
  3. If Anaconda installed everything properly, you'll have an interaction that looks like this:

MacBookPro-SLott:Python2v3 slott$ python3.5
Python 3.5.1 (v3.5.1:37a07cee5969, Dec  5 2015, 21:12:44) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 

More-or-less. (Yes, the example shows 3.5.1 even though I said you should get 3.6. As soon as the Lynda.com course drops, I'll upgrade. The differences between 3.5 and 3.6 are almost invisible.)

Here's your first interaction.

>>> 355/113
3.1415929203539825

Yep. Python did math. Stuff is happening.

Here's some more.

>>> exit
Use exit() or Ctrl-D (i.e. EOF) to exit
>>> exit()

Okay. That was fun. But it's not data wrangling. When do we get to the good stuff?

To Script or Not To Script

We have two paths when it comes to scripting. You can write script files and run them. This is pretty normal application development stuff. It works well. 

Or.

You can use a Jupyter Notebook. This isn't exactly a script. But. You can use it like a script. It's a good place to start building some code that's useful. You can rerun some (or all) of the notebook to make it script-like.

If you downloaded Anaconda, you have Jupyter. Done. Skip over the next part on installing Jupyter.

Installing Jupyter

If you did not download the full Anaconda -- perhaps because you used the miniconda -- you'll need to add Jupyter.  You can use the command conda install jupyter for this.

Another choice is to use the PIP program to install jupyter. The net effect is the same. It starts like this


MacBookPro-SLott:Python2v3 slott$ pip3 install jupyter
Collecting jupyter
  Downloading jupyter-1.0.0-py2.py3-none-any.whl
Collecting ipykernel (from jupyter)
  Downloading ipykernel-4.5.2-py2.py3-none-any.whl (98kB)

    100% |████████████████████████████████| 102kB 1.3MB/s 

It ends like this.

  Downloading pyparsing-2.1.10-py2.py3-none-any.whl (56kB)
    100% |████████████████████████████████| 61kB 2.1MB/s 
Installing collected packages: ipython-genutils, decorator, traitlets, appnope, appdirs, pyparsing, packaging, setuptools, ptyprocess, pexpect, simplegeneric, wcwidth, prompt-toolkit, pickleshare, ipython, jupyter-core, pyzmq, jupyter-client, tornado, ipykernel, qtconsole, terminado, nbformat, entrypoints, mistune, pandocfilters, testpath, bleach, nbconvert, notebook, widgetsnbextension, ipywidgets, jupyter-console, jupyter
  Found existing installation: setuptools 18.2
    Uninstalling setuptools-18.2:
      Successfully uninstalled setuptools-18.2
  Running setup.py install for simplegeneric ... done
  Running setup.py install for tornado ... done
  Running setup.py install for terminado ... done
  Running setup.py install for pandocfilters ... done
Successfully installed appdirs-1.4.0 appnope-0.1.0 bleach-1.5.0 decorator-4.0.11 entrypoints-0.2.2 ipykernel-4.5.2 ipython-5.2.2 ipython-genutils-0.1.0 ipywidgets-5.2.2 jupyter-1.0.0 jupyter-client-4.4.0 jupyter-console-5.1.0 jupyter-core-4.2.1 mistune-0.7.3 nbconvert-5.1.1 nbformat-4.2.0 notebook-4.4.1 packaging-16.8 pandocfilters-1.4.1 pexpect-4.2.1 pickleshare-0.7.4 prompt-toolkit-1.0.13 ptyprocess-0.5.1 pyparsing-2.1.10 pyzmq-16.0.2 qtconsole-4.2.1 setuptools-34.1.1 simplegeneric-0.8.1 terminado-0.6 testpath-0.3 tornado-4.4.2 traitlets-4.3.1 wcwidth-0.1.7 widgetsnbextension-1.2.6



Now you have Jupyter.

What just happened? You installed a large number of Python packages. All of those packages were required to run Jupyter. You can see jupyter-1.0.0 hidden in the list of packages that were installed.

Starting Jupyter

The Jupyter tool does a number of things. We're going to use the notebook feature to save some code that we can rerun. We can also save notes and do other things in the notebook. When you start the notebook, two things will happen.
  1. The terminal window will start displaying the Jupyter console log.
  2. A browser will pop open showing the local Jupyter notebook home page.
Here's what the console log looks like:

MacBookPro-SLott:Python2v3 slott$ jupyter notebook
[I 08:51:56.746 NotebookApp] Writing notebook server cookie secret to /Users/slott/Library/Jupyter/runtime/notebook_cookie_secret
[I 08:51:56.778 NotebookApp] Serving notebooks from local directory: /Users/slott/Documents/Writing/Python/Python2v3
[I 08:51:56.778 NotebookApp] 0 active kernels 
[I 08:51:56.778 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=2eb40fbb96d7788dd05a49600b1fca4e07cd9c8fe931f9af
[I 08:51:56.778 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).

You can glance at it to see that things are still working. The "Use Control-C to stop this server" is a reminder of how to stop things when you're done.

Your Jupyter home page will have this logo in the corner. Things are working.


You can pick files from this list and edit them. And -- important for what we're going to do -- you can create new notebooks.

On the right side of the web page, you'll see this:


You can create files and folders. That's cool. You can create an interactive terminal session. That's also cool. More important, though, is that you can create a new Python 3 notebook. That's were we'll wrangle with CSV files.

"But Wait," you say. "What directory is it using for this?"

The jupyter server is using the current working directory when you started it.

If you don't like this choice, you have two alternatives.
  • Stop Jupyter. Change directory to your preferred place to keep files. Restart Jupyter.
  • Stop Jupyter. Include the --notebook-dir=your_working_directory option.
The second choice looks like this:

MacBookPro-SLott:Python2v3 slott$ jupyter notebook --notebook-dir=~/Documents/Writing/Python
[I 11:15:42.964 NotebookApp] Serving notebooks from local directory: /Users/slott/Documents/Writing/Python

Now you know where your files are going to be. You can make sure that your .CSV files are here. You will have your ".ipynb" files here also. Lots of goodness in the right place.

Using Jupyter

Here's what a notebook looks like. Here's a screen shot.


First. The notebook was originally called "untitled" which seemed less than ideal. So I clicked on the name and changed it to "csv_wrestling".

Second. There was a box labeled In [ ]:. I entered some Python code to the right of this label. Then I clicked the run cell icon. (It's similar to this emoji --  ⏯ -- but not exactly.)

The In [ ]: changed to In [1]:. A second box appeared labeled Out [1]:. This annotates our dialog with Python: each input and Python's response is tracked. It's pretty nice. We can change our input and rerun the cell. We can add new cells with different things to run. We can run all of the cells. Lots of things are possible based on this idea of a cell with our command. When we run a cell, Python processes the command and we see the output.

For many expressions, a value is displayed.  For some expressions, however, nothing is displayed. For complete statements, nothing is displayed. This means we'll often have to throw the name of a variable in to see the value of that variable.


The rest of the notebook is published separately. It's awkward to work in Blogger when describing a Jupyter notebook. It's much easier to simply post the notebook in GitHub.

The notebook is published here: slott56/introduction-python-csv. You can follow the notebook to build your own copy which reads and writes CSV files.


Tuesday, February 7, 2017

Writing Tools

Read this: http://thesweetsetup.com/apps/our-favorite-pro-writing-app-for-mac/

What I have been doing instead of using these sophisticated, integrated writing tools?

I use OmniOutliner. https://www.omnigroup.com/omnioutliner I've used it for years. It does a lot of things. Most notably, I can create multiple columns so that I can create page budgets for outlines. Acquisition Editors like this. Except, of course, they like it as an DOCX file, which requires a bit of manual juggling to produce.

I use BBEdit and KomodoEdit for a the bulk of my writing. http://www.barebones.com/products/bbedit/index.html
https://www.activestate.com/komodo-ide/downloads/edit

"But wait," you say, "those are text editors."

(Or, more dismissively, "there are merely text editors.")

Correct.  I use RST markup and write in Unicode text.  I use tools to convert the RST text to a variety of other binary formats. See http://docutils.sourceforge.net/docs/user/tools.html for a list of tools. This is what I often use:


How is this better than a tool like Scrivener? It depends -- as always -- on what you're trying to optimize. My pipeline has the (dubious) advantage of being very inexpensive. Except for OmniOutliner and BBEdit, it's all community-edition, free software. If cheap is your goal, I've got cheap.

The cool part is this.

The Mac OS X desktop is an integrated writing environment. I have browser, outliner, writing tool, publishing tool, etc., etc., all readily and immediately available. The "look and feel" isn't consistent, but I'm not sure that's a show-stopper.

The biggest difficulty?

BBEdit doesn't enable the Mac OS X grammar checker. Really. It's switched off. The grammar checker is sometimes handy for preventing a large number of common, dumb writing mistakes. BBEdit shows the word count, which is very helpful for some kinds of writing. I wind up using a second app (i.e. the built-in Mac OS X TextEdit) to make a grammar check pass.

I think, however, the hacker-friendly free-and-open-source tool chain may have reached the end of its service life.

Why Not Use Word?

"After all," you say, "MS-Word does everything."

Agreed. It does everything badly and confusingly. (1) The outliner is hard to use and is firmly tied to the text in a way that breaks outlines all the time. (What's that paragraph doing there? Why is it the wrong outline level?) (2) There are too many useless features. The presence of "advanced" mode is a UX nightmare come true. (3) The character-mode and paragraph-mode formatting rules are baffling (and break the outlining.) (4) The styles are essentially invisible: you have to click on the text and check the style side-bar to be sure that the (invisible) markup is actually right.

The worst thing is that publishers have house style sheets for MS-Word that drive the publishing pipeline. This means that writing involves a weird step where I have to apply the publishers styles to things that are **very** clearly annotated with RST markup. You have to review each word. The words may look right, but have the wrong style applied. This is extremely tiresome to get right.

I intend to stick with plain-text markup. Scrivener supports MultiMarkdown. It's not RST, but it seems to be as rich with built-in semantic categories.

Tuesday, January 31, 2017

Improving the epub format -- hacking your ebooks

From a reader.

I recently purchased a copy of 'Modern Python Cookbook' but I found that the code listings in the epub file were indented which caused a problem when reading on my tablet. (I reverted to epub as the PDF version froze in the Bookari ereader software.)

I unzipped the epub file, created and ran the following script to 'unindent' the code listings then rezipped. (I also tweaked the epub.css file slightly.)

Script:

import os
import codecs
from textwrap import dedent
from bs4 import BeautifulSoup

ENCODING = 'utf8'

def dedent_page(filepath):
    soup = load_soup(filepath)
    code = soup.findAll('pre')
    for c in code:
        # Dedent twice to cater for 'blank' lines with spaces.
        c.string = dedent(dedent(c.text))
    save_soup(filepath, unicode(soup))

def load_soup(filepath):
    with codecs.open(filepath, encoding = ENCODING) as f:
        return BeautifulSoup(f)

def save_soup(filepath, soup):
    with codecs.open(filepath, mode = 'w', encoding = ENCODING) as f:
        f.write(unicode(soup))

if __name__ == "__main__":

    FOLDER = r'ebook\OEBPS'

    html_files = [fn for fn in os.listdir(FOLDER) if fn.endswith('.html')]
    total_files = len(html_files)
    for i, file_name in enumerate(html_files):
        print 'Processing file %s (%s/%s)' % (file_name, i + 1, total_files)
        dedent_page(os.path.join(FOLDER, file_name))

Tuesday, January 17, 2017

Irrelevant Feature Comparison

A Real Email.
So, please consider creating a blog post w/ a title something like "Solving the Fred Flintstone Problem using Monads in Python and Haskell"
First. There's this: https://pypi.python.org/pypi/PyMonad/ and this: http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html. Also, see https://en.wikipedia.org/wiki/Type_class. I think this has been covered nicely.

I can't improve on what's been presented.

Second. I don't see any problems that are solved well by monads in Python. In a lazy, optimized, functional language, monads can be used bind operations into ordered sequences. This is why file parsing and file writing examples of monads abound. They can also be used to bind a number of types so that operator overloading in the presence of strict type checking can be implemented. None of this seems helpful in Python.

Perhaps monads will be helpful with Python type hints. I'll wait and see if a monad definition shows up in the typing module. There, it may be a useful tool for handling dynamic type bindings.

Third. This request is perilously close to a "head-to-head" comparison between languages. The question says "problem", but it is similar to asking to see the exact same algorithm implemented in two different languages. It makes as much sense as comparing Python's built-in complex type with Java's built-in complex type (which Java doesn't have.)

Here's the issue. I replace Fred Flintstone with "Parse JSON Notation".  This is a cool application of monads to recognize the various sub-classes of JSON syntax and emit the correctly-structured document.  See http://fssnip.net/bq/title/JSON-parsing-with-monads.  In Python, this is import json. This isn't informative about the language. If we look at the Python code, we see some operations that might be considered as eligible for a rewrite using monads. But Python isn't compiled and doesn't have the same type-checking issues. The point is that Python has alternatives to monads.

Fourth. It's just asking about a not-required feature to a language. In the spirit of showing the not-required-in-Python features, I'll show the not-required-in-Python GOTO.

Here it is:

def goto(destination):
    global next
    next = destination

def min_none(sequence):
    try:
        return min(sequence)
    except ValueError:
        return None
        
def execute(program, debug=False, stmt=None):
    global next, context
    if stmt is None:
        stmt = min(program.keys())
        context = {'goto': goto}
    while stmt is not None:
        next = min_none(list(filter(lambda x: x>stmt, program.keys())))
        if debug:
            print(">>>", program[stmt])
        exec(program[stmt], globals(), context)
        stmt = next
            
example = {
100: "a = 10",
200: "if a == 0: goto(500)",
250: "print(a)",
300: "a = a - 1",
400: "goto(200)",
500: "print('done'()",
}

execute(example)

This shows how we can concoct an additional feature that isn't really needed in Python.

Given this, we can now compare the GOTO between Python, BASIC, and Haskell. Or maybe we can look at Monads in BASIC vs. Haskell. 

Monday, January 9, 2017

The Depths of Degradation or How to Reduce

Let's talk real-world functional programming. Disclosure: I'm a fan of functional programming in Python. (This: https://www.packtpub.com/application-development/functional-python-programming)

The usual culprits for functional programming are map(), filter(), generator functions, and the various comprehensions. This is very pleasant and can lead to succinct, expressive code.

The reduce operation, however, is sometimes slippery.  The obvious reductions are sum() and prod().  Some slightly less obvious reductions are these three:

sum0 = lambda s: sum(1 for _ in s)
sum1 = lambda s: sum(s)
sum2 = lambda s: sum(n**2 for n in s)

The first is essentially len(s), but stated more formally. It shows how we can add in filter or transformations. If we're working with a collections.Counter object, we can rewrite these three to work with the values() of a counter. This allows us to have a statistics library that works with a sequence of simple items or a Counter of binned items.

(I've left it as an exercise for the reader to create the summaries of Counters.)

The Health Check Question

The context is an RESTful application's /health end-point. When a client does a GET to /health, we want to provide status of the components on which the app depends as well as a summary.

The details are created like this:

components = (component() for component in COMPONENT_LIST)
init_components = [thing.init_app(app) for thing in components]
details = [component.health() for component in init_components]

We have a list of class definitions for each component. We can create instances of each class. We can initialize these by providing the RESTful app. Finally, we can create a list of the various health end-point status codes.

There's a class definition for other RESTful API's. The health check does a transitive GET to a /health end-point. These are all more-or-less identical.

There are also class definitions for the database and the cache and other non-RESTful components. It's all very pretty and very functional.

Note that the three statements aren't adjacent. They're scattered around to fit better with the way Flask works. The component list is in one place. The initialization happens before the first request. The details are computed as requested.

Also. We don't really use a simple list for the details. It's actually a mapping from which we will derive a vector. I've left that detail out because it's a relatively simple complication.

Representation of Health

We represent health with a simple enumeration of values:

from enum import Enum
class Status(Enum):
    OK = "OK"
    DEGRADED = "DEGRADED"
    DOWN = "DOWN"

This provides the essential definition of health for our purposes. We don't drag around details of the degradation; that's something that we have to determine by looking at our consoles and logs and stuff.  Degradation is (a) rare, and (b) nuanced. Some degradations are mere annoyances: one of the servers is being restarted. Other degradations are hints that something else might be going on that needs investigation: database primary server is down and we're running on a secondary.

Summarizing Health

A subset of the details vector, then, looks like this: [Status.OK, Status.OK, Status.DEGRADED].

How can we summarize this?

First, we need some rules.  Like these:

class Status(Enum):
    OK = "OK"
    DEGRADED = "DEGRADED"
    DOWN = "DOWN"

    def depth(self, other):
        if self == self.OK:
            return {self.OK: self.OK,
                    self.DEGRADED: self.DEGRADED,
                    self.DOWN: self.DEGRADED}[other]
        elif self == self.DEGRADED:
            return {self.OK: self.DEGRADED,
                    self.DEGRADED: self.DEGRADED,
                    self.DOWN: self.DEGRADED}[other]
        elif self == self.DOWN:
            return {self.OK: self.DEGRADED,
                    self.DEGRADED: self.DEGRADED,
                    self.DOWN: self.DOWN}[other]


The depth() method implements a comparison operator that defines the relationships. This can be visualized as a table.

depthOKDEGRADEDDOWN
OKOKDEGRADEDDOWN
DEGRADEDDEGRADEDDEGRADEDDEGRADED
DOWNDOWNDEGRADED DOWN


This allows us to define a function that uses reduce to summarize the vector of status values.

from functools import reduce
def summary(sequence): 
    return reduce(lambda a, b: a.depth(b), sequence)

The reduce() function applies a binary operator between items in a vector. We've used lambda a, b: a.depth(b) to turn the the depth() method into a binary operator so it can be used with reduce.

The summary() function is a "depth-reduction" of a vector of status objects. It's defined independently of the actual status objects. The relationships among the status levels are embedded in the class definition where they belong. The actual details of status are pleasantly opaque.

And.

We have an example of map-reduce outside the sphere of big data.

The Integer Alternative

The health rules as shown above are kind of complex. Could they be simplified? The answer is no.

Here's an alternative -- which does not do what we want.

class Status2(IntEnum):
    OK = 1
    DEGRADED = 2
    DOWN = 3
    
summary2 = lambda sequence: max(sequence)

This works in some cases, but it doesn't work in others. Another alternative is to change the order to be OK=1, DOWN=2, DEGRADED=3. This doesn't work, either. I'll leave it as an exercise to write out some of the various combinations of values and see how these differ.

JSON Representation

The final detail is JSONification of the status vector and the summary.

json.dumps({"status": summary(vector).name, "details": [s.name for s in vector]})

This converts the various Status objects to text items that fit the Swagger specification for our /health end-points. The .name attribute reference is required to get the string labels from the enum. An alternative is to customize the JSON encoder to recognize the Enum objects and extract their names.

Conclusion

Map-Reduce is easy. It surfaces in a number of places. The idea helps encapsulate summarization rules.

Tuesday, January 3, 2017

The "Build Script" Idea

In compiled languages, the build script or makefile is pretty important. Java has maven (and gradle and ant) for this job.

Python doesn't really have much for this. Mostly because it's needless.

However.

Some folks like the idea of a build script. I've been asked for suggestions.

First and foremost: Go Slow. A build script is not essential. It's barely even helpful. Python isn't Java. There's no maven/gradle/ant nonsense because it isn't necessary. Make is a poor choice of tools for reasons we'll see below.

For folks new to Python, here's the step that's sometimes important.

python setup.py sdist bdist_wheel upload

This uses the source distribution tools (sdist) to build a "wheel" out of the source code. That's the only thing that's important, and even that's optional. The source is all that really exists, and a Git Pull is the only thing that's truly required.

Really. There's no compilation, and there's no reason to do any processing prior to uploading source.

For folks experienced with Python, this may be obvious. For folks not so experienced, it's difficult to emphasize enough that Python is just source. No "class" files. No "jar" files. No "war" files. No "ear" files. None of that. A wheel is a Zip archive that follows some simple conventions.

Some Preliminary Steps

A modicum of care is a good idea before simply uploading something. There are a few steps that make some sense.

  1. Run pylint to check for obvious code problems. A low pylint score indicates that the code needs to be cleaned up. There's no magically ideal number, but with a few judicious "disable" comments, it's easy to get to 10.00.
  2. Run mypy to check the type hints. If mypy complains, you've got potentially serious problems.
  3. Run py.test and get a coverage report. There's no magically perfect test coverage number: more is better. Even 100% line-of-code coverage doesn't necessarily mean that all of the potential combinations of logic paths have been covered.
  4. Run sphinx to create documentation.
Only py.test has a simple pass-fail aspect. If the unit tests don't pass: that's a clear problem. 

The Script

Using make doesn't work out terribly well. It can be used, but it seems to me to be too confusing to set up properly.

Why? Because we don't have the kind of simple file relationships with which make works out so nicely. If we had simple *.c -> *.o -> *.ar kinds of relationships, make would be perfect. We don't have that, and this seems to make make more trouble than it's worth.  Both pylint and py.test keep history as well as produce reports. Sphinx is make-like already, which is why I'm leery of layering on the complexity.

My preference is something like this:

import pytest
from pylint import epylint as lint
import sphinx
from mypy.api import api

(pylint_stdout, pylint_stderr) = lint.py_run('*.py', return_std=True)
print(pylint_stdout.getvalue())

result = mypy.api.run('*.py')

pytest.main(["futurize_both/tests"])

sphinx.main(['source', 'build/html', '-b', 'singlehtml'])

The point here is to simply run the four tools and then look at the output to see what needs to be fixed. Circumstances will dictate changes to the parameters being used. New features will need different reports than bug fixes. Some parts of a project will have different focus than other parts. Conversion from Python 2 to Python 3 will indicate a shift in focus, also.

The idea of a one-size-fits-all script seems inappropriate. These tools are sophisticated. Each has a distinctive feature set. Tweaking the parameters by editing the build script seems like a simple, flexible solution. I'm not comfortable defining parameter-parsing options for this, since each project I work on seems to be unique.

Important. Right now, mypy-lang in the PyPI repository and mypy in GitHub differ. The GitHub version includes an api module; the PyPI release does not include this. This script may not work for you, depending on which mypy release you're using. This will change in the future, making things nicer. Until then, you may want to run mypy "the hard way" using subprocess.check_call().

In enterprise software development environments, it can make sense to set some thresholds for pylint and pytest coverage. It is very helpful to include type hints everywhere, also. In this context, it might make sense to parse the output from lint, mypy, and py.test to stop processing if some quality thresholds are met.

As noted above: Go Slow. This kind of tool automation isn't required and might actually be harmful if done badly. Arguing over pylint metrics isn't as helpful as writing unit test cases. I worry about teams developing an inappropriate focus on pylint or coverage reports -- and the associated numerology -- to the exclusion of sensible automated testing.

I think tools like https://pypi.python.org/pypi/pytest-bdd might be of more value than a simplistic "automated" tool chain. Automation doesn't seem as helpful as clarity in test design. I like the BDD idea with Gherkin test specifications because the Given-When-Then story outline seems to be very helpful for test design.