Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre programming (publicaciones antiguas, página 40)

rstpdf love: syntax highlighting

This mini-sprint is do­ing won­ders for rst2pdf. Now on SVN: pyg­ments-based syn­tax high­light­ing. Ex­am­ple here: rst2pdf's code, in a PDF by rst2pdf.

This friday will see a new rst2pdf release

Fol­low­ing my new pol­i­cy of one re­lease ev­ery fri­day, in 6 days you will see a rst2pdf re­lease. But not any re­lease: a great re­lease.

What will be new?

  • Sup­­port for page num­ber/­sec­­tion names/­sec­­tion num­bers in head­­ers and foot­er­s.

  • Cus­­tom in­­ter­pret­ed text roles (that means in­­­line styling ;-)

  • Stylesheets de­fined in ex­ter­­nal files. The syn­­tax is JSON which may look a bit strange, but it works great.

  • A Man­u­al!

  • Easy True Type font em­bed­d­ing.

  • May­be: syn­­tax high­­­light­ing di­rec­­tive via pyg­­ments. I know I could make it work us­ing the Im­age­­For­­mat­ter, but then you can't copy the code. There is a do­cu­til­s-sand­box project that does ex­ac­t­­ly what I wan­t.

I in­tend to call this re­lease 0.3.0, but maybe I will jump high­er, since there is not much more left to im­ple­men­t.

Some more rst2pdf love, time-based releases of my code

Since re­vi­sion #17_ you can dis­play Page num­bers in head­ers and foot­ers (on­ly!) by us­ing this syn­tax:

.. header::

   This is the header. Page ###Page###

This is the content

.. footer::

   This is the footer. Page ###Page###

It has some is­sues if your page num­ber is big­ger than 99999999999 or your head­er/­foot­er is a lit­tle longer than one line when us­ing the place­hold­er, be­cause the space re­quired is cal­cu­lat­ed with the place­hold­er in­stead of with the num­ber, but those are re­al­ly mar­gin­al cas­es.

Next in line, a de­cent way to de­fine cus­tom stylesheet­s.


As for "time-based re­leas­es", I in­tend to re­lease a new ver­sion of some­thing ev­ery fri­day.

Since I have about a dozen projects in dif­fer­ent stages of us­abil­i­ty, I ex­pect this will push me a bit more to­wards show­ing this stuff in­stead of it rot­ting in my hard drive and un­known svn re­pos.

Creating Reports from Python

I need so­me ad­vi­ce. I need to crea­te an appli­ca­tion for schools that takes stu­dent da­ta (per­so­nal in­for­ma­tio­n, sub­jec­ts, gra­des, etc) and pro­du­ces their gra­de re­por­t. I need to crea­te a printed co­p­y, and keep a his­to­ric re­cor­d.

As a first ste­p, I thou­ght on ge­ne­ra­ting them in PDF via re­por­tla­b, but I want opi­nion­s. For exam­ple, I can ge­ne­ra­te the PDF, print it and re­ge­ne­ra­te it if I need to re­print it. What other op­tins do you see? It's ba­si­ca­lly text wi­th ta­ble­s. Re­por­tla­b? La­TeX? So­me other tool?

To this I re­plied I su­gges­ted Res­truc­tu­red Text whi­ch if you fo­llow my blog should sur­pri­se noone at all ;-)

In this sto­ry I wi­ll try to bring to­ge­ther all the pie­ces to turn a chunk of py­thon da­ta in­to a ni­ce PDF re­por­t. Ho­pe it´s use­ful for so­meo­ne!

Why not use reportlab directly?

He­re´s an exam­ple I pos­ted in that th­rea­d: how to crea­te a PDF wi­th two pa­ra­gra­phs, using res­truc­tu­red tex­t:

This is a paragraph. It has several lines, but what it says does not matter.
I can press enter anywhere, because
it ends only on a blank
line. Like this.

This is another paragraph.

And he­re´s what you need to do in re­por­tla­b:

# -*- coding: utf-8 -*-
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
styles = getSampleStyleSheet()
def go():
  doc = SimpleDocTemplate("phello.pdf")
  Story = [Spacer(1,2*inch)]
  style = styles["Normal"]
  p = Paragraph('''This is a paragraph. It has several lines, but what it says does not matter.
I can press enter anywhere, because
it ends when the string ends.''', style)
  Story.append(p)
  p = Paragraph('''This is another paragraph.''', style)
  Story.append(p)
  Story.append(Spacer(1,0.2*inch))
  doc.build(Story)

go()

Of cour­se, you could wri­te a pro­gram that takes text se­pa­ra­ted in pa­ra­gra­phs as its in­pu­t, and crea­tes the re­por­tlab Pa­ra­gra­ph ele­men­ts, pu­ts them in the Sto­ry and buil­ds the do­cu­men­t.... but then you are rein­ven­ting the res­truc­tu­red text par­se­r, on­ly wor­se!

Res­truc­tu­red text is da­ta. Re­por­tlab pro­gra­ms are co­de. Da­ta is ea­sier to ge­ne­ra­te than co­de.

So, how do you do a report?

You crea­te a fi­le wi­th the da­ta in it, pro­ce­ss it via one of the many rs­t->­pdf pa­ths (I su­ggest my rs­t2­pdf scrip­t, but feel free to use the other 9 al­ter­na­ti­ve­s).

Su­ppo­se you ha­ve the fo­llo­wing da­ta:

frobtimes = [[1,3],[3,5],[9,8]]

And you want to pro­du­ce this re­por­t:

Frobniz performance
===================

* 1 frobniz: 3 seconds

* 3 frobniz: 5 seconds

* 9 frobniz: 8 seconds

You could do it this wa­y:

print '''Frobniz performance
==================='''

for ft in frobtimes:
  print '* %d frobniz: %d seconds\n'%(ft[0],ft[1])

And it will work. However, this means you are writing code again! This time, you are reinventing templating
languages.

What you want is to use, sa­y, Mako (or whate­ve­r). It's going to be be­tter than your ho­me­brew so­lu­tion an­ywa­y. He­re's the tem­pla­te for the re­por­t:

${title('Frobniz Performance')}

% for ft in frobtimes:
* ${ft[0]} frobniz: $ft[1] seconds

% endfor

This uses a function title defined thus:

..­co­de-­blo­ck:: py­thon

ti­tle=­lamb­da(­tex­t): tex­t+'n'+'='*­len(­tex­t)+'n­n'

You could ge­ne­ra­li­ze it to su­pport mul­ti­ple hea­ding le­vel­s:

ti­tle=­lamb­da(­tex­t,­le­ve­l): tex­t+'n'+'=-~_#%^'[­le­ve­l]*­len(­tex­t)+'n­n'

Trickier: tables

One ve­ry co­m­mon fea­tu­re of re­por­ts is ta­ble­s. In fac­t, it would be mo­re na­tu­ral to pre­sent our fro­bniz re­port as a ta­ble. The bad news is how ta­bles look like in res­truc­tu­red tex­t:

+---------+----------------+
| Frobniz | Time (seconds) |
+---------+----------------+
|        1|              3 |
+---------+----------------+
|        3|              5 |
+---------+----------------+
|        9|              8 |
+---------+----------------+

Whi­ch is ve­ry pre­tty, but not exac­tly tri­vial to ge­ne­ra­te. But do­n´t wo­rr­y, the­re is a sim­ple so­lu­tion for this, too: CSV ta­ble­s.

Frobniz time measurements

'Fro­bni­z'

'Ti­me(­se­con­d­s)'

1

3

3

5

9

8

And of cour­se, the­re is py­tho­n´s csv mo­du­le if you want to be fan­cy and avoid trou­ble wi­th de­li­mi­ter­s, es­ca­ping and so on:

def table(title,header,data):
  head=StringIO()
  body=StringIO()
  csv_writer = csv.writer(head, dialect='excel')
  csv_writer.writerow(header)

  head=´:header: %s´head.getvalue()

  csv_writer = csv.writer(body, dialect='excel')
  for row in data:
    csv_writer.writerow(row)
  body=body.getvalue()

  return ´´´.. csv-table:: %s
     :header: %s

     %s
     ´´´%(title,head,body)

wi­ll pro­du­ce nea­t, ready for use, csv ta­ble di­rec­ti­ves for res­truc­tu­red tex­t.

How would it work?

This py­thon pro­gram is rea­lly ge­ne­ri­c. All you need is for it to ma­tch a tem­pla­te (an ex­ter­nal text fi­le), wi­th da­ta in the form of a bun­ch of py­thon va­ria­ble­s.

But how do we get the da­ta? We­ll, from a da­ta­ba­se, usua­ll­y. But it can co­me from an­ywhe­re. You could be making a re­port about your de­l.i­cio­.us book­ma­rks, or about fi­les in a fol­de­r, this is rea­lly ge­ne­ric stu­ff.

What would I use to get the da­ta? I would use JSON in the mi­dd­le. I would make my re­port ge­ne­ra­tor take the fo­llo­wing ar­gu­men­ts:

  1. A mako te­m­­pla­­te na­­me.

  2. A JSON da­­ta fi­­le.

That wa­y, the pro­gram wi­ll be com­ple­te­ly ge­ne­ri­c.

So, put all this to­ge­the­r, and the­re's the su­per­du­per ma­gi­cal re­port ge­ne­ra­to­r.

On­ce you get rs­t, pa­ss it th­rou­gh so­me­thing to crea­te PDFs, but sto­re on­ly the rs­t, whi­ch is (al­mos­t) plain tex­t, sear­cha­ble, ea­sy to sto­re, and mu­ch sma­lle­r.

I do­n´t ex­pect su­ch a re­port ge­ne­ra­tor to be over 50 li­nes of co­de, in­clu­ding co­m­men­ts.

Missing pieces

  • Whi­­le res­­tru­c­­tu­­red text is al­­most plain tex­­t, the­­re are spe­­cial cha­­ra­c­­te­r­s, whi­­ch you should es­­ca­­pe. That is le­­ft as an exe­r­­ci­­se to the rea­­der ;-)

  • So­­­meo­­­ne should rea­­lly wri­­te this thing ;-)

Giving rst2pdf some love

Be­cause of a thread in the PyAr list about gen­er­at­ing re­ports from Python, I sug­gest­ed us­ing ReST and my rst2pdf scrip­t.

This caused a few things:

  1. I de­­cid­ed it's a pret­­ty de­­cent piece of code, and it de­serves a re­lease. Mak­ing a re­lease means I need­ed to fix the most em­bar­ras­ing pieces of it. So...

  2. Im­­ple­­men­t­ed the class di­rec­­tive, so it can have cus­­tom para­­graph styles with very lit­­tle ef­­fort.

  3. Did prop­er com­­mand line pars­ing.

  4. Did prop­er se­­tup­­tools script

  5. Up­­load­­ed to PyPI

  6. Cre­at­ed a re­lease in Google Code.

So, if you want the sim­plest way to gen­er­ate PDF files from a pro­gram in the en­tire python­ic uni­verse... give it a look.


Contents © 2000-2020 Roberto Alsina