Tuesday, December 6, 2016

[xtopdf] Wildcard text files to PDF with xtopdf and glob

By Vasudev Ram



First joker card image attribution

This is another in my series of applications that use xtopdf (source), my Python toolkit for PDF creation from other formats.

[ Here's a good overview of xtopdf, its uses, supported platforms and formats, etc. ]

I called this app WildcardTextToPDFpy.. It lets you specify a wildcard for text files, and then converts each of the text files matching the wildcard (like grades*2016.txt or monthly*sales.txt) [1], into corresponding PDF files - with the same names but with '.pdf' appended.

[1] For example, the wildcard grades*2016.txt could match grades-math-2016.txt and grades-bio-2016.txt (think a school or college), while monthly*sales.txt might match monthly-car-sales.txt and monthly-bike-sales.txt (think a vehicle dealership), so a PDF file will be generated for each text file matching the given wildcard.

The program uses the iglob function from the glob module in Python's standard library, similar to how this recent other post:

Simple directory lister with multiple wildcard arguments

used the glob function. The difference is that glob returns a list, while iglob returns a generator, so it will return the matching filenames lazily, on demand, as shown here:
$ python
>>> g1 = glob.glob('text*.txt')
>>> g1
['text1.txt', 'text2.txt', 'text3.txt']
>>>>>> g2 = glob.iglob('text*.txt')
>>> g2
<generator object iglob at 0x027C2850>
>>> next(g2)
'text1.txt'
>>> for f in g2:
...     print f
...
text2.txt
text3.txt

Here is the code for WildcardTextToPDF.py:
from __future__ import print_function

# WildcardTextToPDF.py
# Convert the text files specified by a filename wildcard,
# like '*.txt' or 'foo*bar*baz.txt', to PDF files.
# Each text file's content goes to a separate PDF file, with the 
# PDF file name being the full text file name (including the 
# '.txt' part), with '.pdf' appended.
# Requires:
# - xtopdf: https://bitbucket.org/vasudevram/xtopdf
# - ReportLab: https://www.reportlab.com/ftp/reportlab-1.21.1.tar.gz
# Author: Vasudev Ram
# Copyright 2016 Vasudev Ram
# Product store: https://gumroad.com/vasudevram
# Web site: https://vasudevram.github.io
# Blog: http://jugad2.blogspot.com

import sys
import os
import glob
from PDFWriter import PDFWriter

def usage(argv):
    sys.stderr.write("Usage: python {} txt_filename_pattern\n".format(argv[0]))
    sys.stderr.write("E.g. python {} foo*.txt\n".format(argv[0]))

def text_to_pdf(txt_filename):
    pw = PDFWriter(txt_filename + '.pdf')
    pw.setFont('Courier', 12)
    pw.setHeader('{} converted to PDF'.format(txt_filename))
    pw.setFooter('PDF conversion by xtopdf: https://google.com/search?q=xtopdf')

    with open(txt_filename) as txt_fil:
        for line in txt_fil:
            pw.writeLine(line.strip('\n'))
        pw.savePage()

def main():
    if len(sys.argv) != 2:
        usage(sys.argv)
        sys.exit(0)

    try:
        for filename in glob.glob(sys.argv[1]):
            print("Converting {} to {}".format(filename, filename + '.pdf'))
            text_to_pdf(filename)
    except Exception as e:
        print("Caught Exception: type: {}, message: {}".format(\
            e.__class__, str(e)))

if __name__ == '__main__':
    main()
And here are the relevant files before and after running the program, with the program's output in between:
$ dir text*.txt/b
text1.txt
text2.txt
text3.txt

$ python WildcardTextToPDF2.py text*.txt
Converting text1.txt to text1.txt.pdf
Converting text2.txt to text2.txt.pdf
Converting text3.txt to text3.txt.pdf

$ dir text?.txt*/od/b
text1.txt
text2.txt
text3.txt
text1.txt.pdf
text2.txt.pdf
text3.txt.pdf
Finally, here's a cropped screenshot of the third output file, text3.txt.pdf, in Foxit PDF Reader (a lightweight PDF reader that I use):


Also look up:

[xtopdf] Batch convert text files to PDF (with xtopdf and fileinput)

for another variation on the approach to converting text files to PDF.

Speaking of generators, also check out this other post about them:

Python generators are pluggable

The image at the top of the post is of the earliest Joker card by Samuel Hart c. 1863, according to Wikipedia:

Joker (playing card)

Enjoy.

- Vasudev Ram - Online Python training and consulting

Get updates on my software products / ebooks / courses.

Jump to posts: Python   DLang   xtopdf

Subscribe to my blog by email

My ActiveState recipes

FlyWheel - Managed WordPress Hosting



Saturday, December 3, 2016

Simple directory lister with multiple wildcard arguments

By Vasudev Ram

$ python file_glob.py f[!2-5]*xt

I was browsing the Python standard library (got to use those batteries!) and thought of writing this simple utility - a command-line directory lister that supports multiple wildcard filename arguments, like *, ?, etc. - as the OS shells bash on Unix and CMD on Windows do. It uses the glob function from the glob module. The Python documentation for glob says:

[ No tilde expansion is done, but *, ?, and character ranges expressed with [] will be correctly matched. This is done by using the os.listdir() and fnmatch.fnmatch() functions in concert. ]

Note: no environment variable expansion is done either, but see os.path.expanduser() and os.path.expandvars() in the stdlib.

I actually wrote this program just to try out glob (and fnmatch before it), not to make a production or even throwaway tool, but it turns out that, due to the functionality of glob(), even this simple program is somewhat useful, as the examples of its use below show, particularly with multiple arguments, etc.

Of course, this is not a full-fledged directory lister like DIR (Windows) or ls (Unix) but many of those features can be implemented in this or similar tools, by using the stat module (which I've used in my PySiteCreator and other programs); the Python stat must be a wrapper over the C library with the same name, at least on Unix (AFAIK the native Windows SDK's directory and file system manipulation functions are different from POSIX ones, though the C standard library on Windows has many C stdio functions for compatibility and convenience).

Here is the code for the program, file_glob.py:
from __future__ import print_function
'''
file_glob.py
Lists filenames matching one or more wildcard patterns.
Author: Vasudev Ram
Copyright 2016 Vasudev Ram
Web site: https://vasudevram.github.io
Blog: http://jugad2.blogspot.com
Product store: https://gumroad.com/vasudevram
'''

import sys
import glob

sa = sys.argv
lsa = len(sys.argv)

if lsa < 2:
    print("{}: Must give one or more filename wildcard arguments.".
        format(sa[0]))
    sys.exit(1)

for arg in sa[1:]:
    print("Files matching pattern {}:".format(arg))
    for filename in glob.glob(arg):
            print(filename)

I ran it multiple times with these files in my current directory. All of them are regular files except dir1 which is a directory.
$ dir /b
dir1
f1.txt
f2.txt
f3.txt
f4.txt
f5.txt
f6.txt
f7.txt
f8.txt
f9.txt
file_glob.py
o1.txt
out1
out3
test_fnmatch1.py
test_fnmatch2.py
test_glob.py~
Here are a few different runs of the program with 0, 1 or 2 arguments, giving different outputs based on the patterns used.
$ python file_glob.py
Must give one or more filename wildcard arguments.
$ python file_glob.py a
Files matching pattern a

$ python file_glob.py *1
Files matching pattern *1:
dir1
out1
$ python file_glob.py *txt
Files matching pattern *txt:
f1.txt
f2.txt
f3.txt
f4.txt
f5.txt
f6.txt
f7.txt
f8.txt
f9.txt
o1.txt
$ python file_glob.py *txt *1
Files matching pattern *txt:
f1.txt
f2.txt
f3.txt
f4.txt
f5.txt
f6.txt
f7.txt
f8.txt
f9.txt
o1.txt
Files matching pattern *1:
dir1
out1
$ python file_glob.py *txt *py
Files matching pattern *txt:
f1.txt
f2.txt
f3.txt
f4.txt
f5.txt
f6.txt
f7.txt
f8.txt
f9.txt
o1.txt
Files matching pattern *py:
file_glob.py
test_fnmatch1.py
test_fnmatch2.py
$ python file_glob.py f[2-5]*xt
Files matching pattern f[2-5]*xt:
f2.txt
f3.txt
f4.txt
f5.txt
$ python file_glob.py f[!2-5]*xt
Files matching pattern f[!2-5]*xt:
f1.txt
f6.txt
f7.txt
f8.txt
f9.txt
$ python file_glob.py *mat*
Files matching pattern *mat*:
test_fnmatch1.py
test_fnmatch2.py
$ python file_glob.py *e* *[5-8]*
Files matching pattern *e*:
file_glob.py
test_fnmatch1.py
test_fnmatch2.py
test_glob.py~
Files matching pattern *[5-8]*:
f5.txt
f6.txt
f7.txt
f8.txt
$ python file_glob.py *[1-4]*
Files matching pattern *[1-4]*:
dir1
f1.txt
f2.txt
f3.txt
f4.txt
o1.txt
out1
out3
test_fnmatch1.py
test_fnmatch2.py
$ python file_glob.py a *txt b
Files matching pattern a:
Files matching pattern *txt:
f1.txt
f2.txt
f3.txt
f4.txt
f5.txt
f6.txt
f7.txt
f8.txt
f9.txt
o1.txt
Files matching pattern b:
As you can see from the runs, it works, including for ranges of wildcard characters, and the negation of them too (using the ! character inside the square brackets before the range).

Enjoy.

- Vasudev Ram - Online Python training and consulting

Get updates on my software products / ebooks / courses.

Jump to posts: Python   DLang   xtopdf

Subscribe to my blog by email

My ActiveState recipes

Managed WordPress Hosting by FlyWheel


Sunday, November 27, 2016

Video: Interview: GoingNative 6: Walter Bright and Andrei Alexandrescu - D Programming Language

By Vasudev Ram


I watched this video (from 2013) a few days ago:

GoingNative 6: Walter Bright and Andrei Alexandrescu - D Programming Language

Going Native [1] is a tech event, and I guess 6 refers to the 6th year or edition of it. I first saw this video on Channel 9, one of Microsoft's technology sites. That site has multiple links to the video, including downloadable, in different qualities, so you may want to check out getting it from there. [2]

[1] There seems to be rising interest in "native" (programming) languages these days (before the rise of "scripting" languages, all languages were native, i.e. they ran, after compilation, on the "metal" (i.e. the CPU), not on a language VM or interpreter, and we just called them languages, no fancy-shmancy names, ha ha :). Part of the reason for the rise in interest, as mentioned in the video, is that we are entering (or have entered) a world of multi-core and concurrency, and due to Moore's law ending (?) Anyway, native languages like D, Go and Rust are picking up these days, whatever the reason. I've used native languages like Pascal and C a lot at work earlier, so I welcome this return of greater interest in native languages, though I'm not stopping using languages like Python anytime soon either.

[2] And to download the video at the command line, check out youtube-dl, a YouTube downloader in Python. Yes, downloading a video about native languages with a tool written in a scripting language - go figure :)

The video shows an interviewer talking to both Walter Bright and Andrei Alexandrescu. Walter, who developed the first native C++ compiler, is the original creator of the D programming language and Andrei, who was a C++ expert and book author, joined him some years later to help in developing the language. The interview was somewhat wide-ranging and a bit long, and quite interesting. Many aspects of the language and the plans and in-progress work for it were talked about.

The video is also embedded below.



Happy holidays to my readers.

- Vasudev Ram - Online Python training and consulting

Get updates on my software products / ebooks / courses.

Jump to posts: Python   DLang   xtopdf

Black Flyday at Flywheel Wordpress Managed Hosting - get 3 months free on the annual plan.

Subscribe to my blog by email

My ActiveState recipes



Friday, November 25, 2016

Processing DSV data (Delimiter-Separated Values) with Python

By Vasudev Ram



I needed to process some DSV files recently. Here is a Python program I wrote for it, with a few changes over the original. E.g. I do not show the processing of the data here; I only read and print it. Also, I support two different command-line options (and ways) to specify the delimiter character.

DSV (Delimiter-separated values) is a common tabular text data format, with one record per line, and some number of fields per record, where the fields are separated or delimited by some specific character. Some common delimiter characters used in DSV files are tab (which makes them TSV files, Tab-Separated Values, common on Unix), comma (CSV files, Comma-Separated Values, a common spreadsheet and database import-export format), the pipe character (|), the colon (:) and others.

They - DSV files - are described in this section, Data File Metaformats, under Chapter 5: Textuality, of Eric Raymond (ESR)'s book, The Art of Unix Programming, which is a recommended read for anyone interested in Unix (one of the longest-lived operating systems [1]) and in software design and development.

[1] And, speaking a bit loosely, nowaday Unix is also the most widely used OS in the world, by a fair margin, due to its use (as a variant) in Android and iOS based mobile devices, both of which are Unix-based, not to mention Apple MacOS and Linux computers, which also are. Android devices alone number in the billions.

The program, read_dsv.py, is a command-line utility, written in Python, that allows you to specify the delimiter character in one of two ways:

- with a "-c delim_char" option, in which delim_char is an ASCII character,
- with a "-n delim_code" option, in which delim_code is an ASCII code.

It then reads either the files specified as command-line arguments after the -n or -c option, or if no files are given, it reads its standard input.

Here is the code for read_dsv.py:
from __future__ import print_function

"""
read_dsv.py
Author: Vasudev Ram
Web site: https://vasudevram.github.io
Blog: https://jugad2.blogspot.com
Product store: https://gumroad.com/vasudevram
Purpose: Shows how to read DSV data, i.e. 

https://en.wikipedia.org/wiki/Delimiter-separated_values 

from either files or standard input, split the fields of each
line on the delimiter, and process the fields in some way.
The delimiter character is configurable by the user and can
be specified as either a character or its ASCII code.

Reference:
TAOUP (The Art Of Unix Programming): Data File Metaformats:
http://www.catb.org/esr/writings/taoup/html/ch05s02.html
ASCII table: http://www.asciitable.com/
"""

import sys
import string

def err_write(message):
    sys.stderr.write(message)

def error_exit(message):
    err_write(message)
    sys.exit(1)

def usage(argv, verbose=False):
    usage1 = \
        "{}: read and process DSV (Delimiter-Separated-Values) data.\n".format(argv[0])
    usage2 = "Usage: python" + \
        " {} [ -c delim_char | -n delim_code ] [ dsv_file ] ...\n".format(argv[0])
    usage3 = [
        "where one of either the -c or -n option must be given,\n",  
        "delim_char is a single ASCII delimiter character, and\n", 
        "delim_code is a delimiter character's ASCII code.\n", 
        "Text lines will be read from specified DSV file(s) or\n", 
        "from standard input, split on the specified delimiter\n", 
        "specified by either the -c or -n option, processed, and\n", 
        "written to standard output.\n", 
    ]
    err_write(usage1)
    err_write(usage2)
    if verbose:
        for line in usage3:
            err_write(line)

def str_to_int(s):
    try:
        return int(s)
    except ValueError as ve:
        error_exit(repr(ve))

def valid_delimiter(delim_code):
    return not invalid_delimiter(delim_code)

def invalid_delimiter(delim_code):
    # Non-ASCII codes not allowed, i.e. codes outside
    # the range 0 to 255.
    if delim_code < 0 or delim_code > 255:
        return True
    # Also, don't allow some specific ASCII codes;
    # add more, if it turns out they are needed.
    if delim_code in (10, 13):
        return True
    return False

def read_dsv(dsv_fil, delim_char):
    for idx, lin in enumerate(dsv_fil):
        fields = lin.split(delim_char)
        assert len(fields) > 0
        # Knock off the newline at the end of the last field,
        # since it is the line terminator, not part of the field.
        if fields[-1][-1] == '\n':
            fields[-1] = fields[-1][:-1]
        # Treat a blank line as a line with one field,
        # an empty string (that is what split returns).
        print("Line", idx, "fields:")
        for idx2, field in enumerate(fields):
            print(str(idx2) + ":", "|" + field + "|")

def main():
    # Get and check validity of arguments.
    sa = sys.argv
    lsa = len(sa)
    if lsa == 1:
        usage(sa)
        sys.exit(0)
    if lsa == 2:
        # Allow the help option with any letter case.
        if sa[1].lower() in ("-h", "--help"):
            usage(sa, verbose=True)
            sys.exit(0)
        else:
            usage(sa)
            sys.exit(0)

    # If we reach here, lsa is >= 3.
    # Check for valid mandatory options (sic).
    if not sa[1] in ("-c", "-n"):
        usage(sa, verbose=True)
        sys.exit(0)

    # If -c option given ...
    if sa[1] == "-c":
        # If next token is not a single character ...
        if len(sa[2]) != 1:
            error_exit(
            "{}: Error: -c option needs a single character after it.".format(sa[0]))
        if not sa[2] in string.printable:
            error_exit(
            "{}: Error: -c option needs a printable ASCII character after it.".format(\
            sa[0]))
        delim_char = sa[2]
    # else if -n option given ...
    elif sa[1] == "-n":
        delim_code = str_to_int(sa[2])
        if invalid_delimiter(delim_code):
            error_exit(
            "{}: Error: invalid delimiter code {} given for -n option.".format(\
            sa[0], delim_code))
        delim_char = chr(delim_code)
    else:
        # Checking for what should not happen ... a bit of defensive programming here.
        error_exit("{}: Program error: neither -c nor -n option given.".format(sa[0]))

    try:
        # If no filenames given, read sys.stdin ...
        if lsa == 3:
            print("processing sys.stdin")
            dsv_fil = sys.stdin
            read_dsv(dsv_fil, delim_char)
            dsv_fil.close()
        # else (filenames given), read them ...
        else:
            for dsv_filename in sa[3:]:
                print("processing file:", dsv_filename)
                dsv_fil = open(dsv_filename, 'r')
                read_dsv(dsv_fil, delim_char)
                dsv_fil.close()
    except IOError as ioe:
        error_exit("{}: Error: {}".format(sa[0], repr(ioe)))
        
if __name__ == '__main__':
    main()

Here are test runs of the program (both valid and invalid), and the results of each one:

Run it without any arguments. Gives a brief usage message.
$ python read_dsv.py
read_dsv.py: read and process DSV (Delimiter-Separated-Values) data.
Usage: python read_dsv.py [ -c delim_char | -n delim_code ] [ dsv_file ] ...
Run it with a -h option (for help). Gives the verbose usage message.
$ python read_dsv.py -h
read_dsv.py: read and process DSV (Delimiter-Separated-Values) data.
Usage: python read_dsv.py [ -c delim_char | -n delim_code ] [ dsv_file ] ...
where one of either the -c or -n option must be given,
delim_char is a single ASCII delimiter character, and
delim_code is a delimiter character's ASCII code.
Text lines will be read from specified DSV file(s) or
from standard input, split on the specified delimiter
specified by either the -c or -n option, processed, and
written to standard output.
Run it with a -v option (invalid run). Gives the brief usage message.
$ python read_dsv.py -v
read_dsv.py: read and process DSV (Delimiter-Separated-Values) data.
Usage: python read_dsv.py [ -c delim_char | -n delim_code ] [ dsv_file ] ...
Run it with a -c option but no ASCII character argument (invalid run). Gives the brief usage message.
$ python read_dsv.py -c
read_dsv.py: read and process DSV (Delimiter-Separated-Values) data.
Usage: python read_dsv.py [ -c delim_char | -n delim_code ] [ dsv_file ] ...
Run it with a -c option followed by the pipe character (invalid run). The OS (here, Windows) gives an error message because the pipe character cannot be used to end a pipeline.
$ python read_dsv.py -c |
The syntax of the command is incorrect.
Run it with the -c option and the pipe character as the delimiter character, but protected (by double quotes) from interpretation by the OS shell (CMD).
$ python read_dsv.py -c "|" file1.dsv
processing file: file1.dsv
Line 0 fields:
0: |1|
1: |2|
2: |3|
3: |4|
4: |5|
5: |6|
6: |7|
Line 1 fields:
0: |field1|
1: |fld2|
2: | fld3 with spaces around it |
3: |    fld4 with leading spaces|
4: |fld5 with trailing spaces     |
5: |next field is empty|
6: ||
7: |last field|
Line 2 fields:
0: ||
1: |1|
2: |22|
3: |333|
4: |4444|
5: |55555|
6: |666666|
7: |7777777|
8: |88888888|
Line 3 fields:
0: ||
1: ||
2: ||
3: ||
4: ||
5: |                      |
6: ||
7: ||
8: ||
9: ||
10: ||
Run it with the -n option followed by 124, the ASCII code for the pipe character as the delimiter.
$ python read_dsv.py -n 124 file1.dsv
[Gives exact same output as the run above, as it should, because both use the same delimiter and read the same input file.]
Copy file1.dsv to file3.dsv. Change all the pipe characters (delimiters) to colons:
Run it with the -n option followed by 58, the ASCII code for the colon character.
$ python read_dsv.py -n 58 file3.dsv
[Gives exact same output as the run above, as it should, because other than the delimiters (pipe versus colon), the input is the same.]

I added support for the -n option to the program because it makes it more flexible, since you can specify any ASCII character as the delimiter (that makes sense), by giving its ASCII code.

And of course, to find out the values of the ASCII codes for these delimiter characters, I used the char_to_ascii_code.py program from my recent post:

Trapping KeyboardInterrupt and EOFError for program cleanup

You may have noticed that I mentioned delimiter characters and DSV files in that post too. The char_to_ascii_code.py utility shown in that post was created to find the ASCII code for any character (without having to look it up on the web each time).

- Enjoy.

- Vasudev Ram - Online Python training and consulting

- Black Flyday at Flywheel Wordpress Managed Hosting - get 3 months free on the annual plan.

Get updates on my software products / ebooks / courses.

Jump to posts: Python   DLang   xtopdf

Subscribe to my blog by email

My ActiveState recipes



Wednesday, November 16, 2016

Using std.datetime.StopWatch to time sections of D code

By Vasudev Ram




Accuracy_and_precision image attribution

This D program shows how you can use the std.datetime module from Phobos, D's standard library, to time the execution of sections of D code. I use the StopWatch class from that module. I print the elapsed time in both milliseconds and in microseconds in this program. (Depending on the functions used from this module and the OS and hardware platform, it may also be possible to measure time to an accuracy of hecto-nanoseconds.

The D documentation for the StopWatch class says:

[ StopWatch measures time as precisely as possible.
This class uses a high-performance counter. On Windows systems, it uses QueryPerformanceCounter, and on Posix systems, it uses clock_gettime if available, and gettimeofday otherwise.
But the precision of StopWatch differs from system to system. It is impossible for it to be the same from system to system since the precision of the system clock varies from system to system, and other system-dependent and situation-dependent stuff (such as the overhead of a context switch between threads) can also affect StopWatch's accuracy. ]

Here are a couple of links about the difference between accuracy and precision:

On Wikipedia: Accuracy and Precision

On ncse.edu: Accuracy and Precision

The program for timing D code is below, in the file test_stopwatch.d. It is straightforward, so should be somewhat possible for even people unfamiliar with D to understand. It counts from 1 to 1 billion and measures the time taken for that. Note the use of the more human-readable constant 1_000_000_000, using underscores to separate groups of digits, like Perl can. A nice small D feature. Wish more languages had it.
/*********************************************************************
File: test_stopwatch.d
Author: Vasudev Ram
Copyright 2016 Vasudev Ram
Web site: https://vasudevram.github.io
Blog: http://jugad2.blogspot.com
Product store: https://gumroad.com/vasudevram
*********************************************************************/

import std.stdio;
import std.datetime;

void foo() {
    // limit is set to 1000 million which is 1 billion.
    auto limit = 1_000_000_000;
    writeln("Counting from 1 to ", limit);
    foreach (i; 1 .. limit) {
        // This is a do-nothing loop since we are only 
        // interested in counting from 1 to limit.
    }
}

int main(string[] args) {
    // Declare a variable sw of type StopWatch.
    StopWatch sw;
    // Start the stopwatch.
    sw.start();
    // Call function foo() which takes up some time 
    // by counting up to some large number.
    foo();
    // Stop the stopwatch.
    sw.stop();
    // Get the elapsed time in microseconds.
    auto usecs = sw.peek().usecs;
    // Get the elapsed time in milliseconds.
    auto msecs = sw.peek().msecs;
    // Get the elapsed time in seconds.
    auto seconds = sw.peek().seconds;
    writeln("Time in usecs: ", usecs);
    writeln("Time in msecs: ", msecs);
    writeln("Time in seconds: ", seconds);
    return 0;
}
Here is the comand to compile the program.
$ dmd test_stopwatch.d
And here is the output of 3 runs:
$ test_stopwatch
Counting from 1 to 1000000000
Time in usecs: 3136388
Time in msecs: 3136
Time in seconds: 3

$ test_stopwatch
Counting from 1 to 1000000000
Time in usecs: 4231483
Time in msecs: 4231
Time in seconds: 4

$ test_stopwatch
Counting from 1 to 1000000000
Time in usecs: 4603806
Time in msecs: 4603
Time in seconds: 4

- Vasudev Ram - Online Python training and consulting

Get updates on my software products / ebooks / courses.

Jump to posts: Python   DLang   xtopdf

Subscribe to my blog by email

My ActiveState recipes

Managed WordPress Hosting by FlyWheel