#! /usr/bin/env python
# -*- coding: Latin1 -*-

import re
import sys
import psycopg
from mx import DateTime

class ParserError(Exception):
    """
    Ein 'ParserError' wird ausgelöst, falls während des Parsens einer
    Logfile-Zeile ein Fehler auftritt.
    """
    pass

SQL_STATEMENT = """
  INSERT INTO statistics (
    address,       userid,         datetime,         method,
    url,           protocol,       status,           bytes,
    referer,       agent
  ) VALUES (
    %(address)s,   %(userid)s,     '%(datetime)s',   %(method)s,
    %(url)s,       %(protocol)s,   %(status)s,       %(bytes)s,
    %(referer)s,   %(agent)s
  )
  """

class Logline:
    """
    Ein 'Logline'-Objekt enthält die folgenden Attribute:

    address     (string)     # IP/hostname
    userid      (string)     # authentifizierte User-ID
    datetime    (DateTime)   # Zeitstempel
    method      (string)     # HTTP-Methode, z. B. GET
    url         (string)     # URL
    protocol    (string)     # z. B. HTTP/1.1
    status      (int)        # z. B. 200 or 404
    bytes       (int)        # übertragene Bytes (None, falls unbekannt)
    referer     (string)     # Referer (None, falls unbekannt)
    agent       (string)     # HTTP-Client

    Diese Attribute werden im Konstruktor gesetzt, sofern nicht ein
    'ParserError' ausgelöst wird.
    """
    # regulärer Ausdruck, um eine Zeile des Logfiles zu parsen
    _line_regex = re.compile(r"""
      ^
      (?P<address>\S+)
      \s
      \S+                       # ident ignorieren
      \s
      (?P<userid>\S+)
      \s
      \[
          (?P<datetime>
              (?P<day>\d\d) /
              (?P<month>\w{3}) /
              (?P<year>\d{4}) :
              (?P<hour>\d\d) :
              (?P<minute>\d\d) :
              (?P<second>\d\d)
              \s
              (?P<timezone>[+-]\d\d)
              00                # die letzten zwei Ziffern sollten stets 0 sein
          )
      \]
      \s"                       # öffnendes Anführungszeichen
      (?P<method>\w+)
      \s
      (?P<url>[^"]+)
      \s
      (?P<protocol>[^"]+)
      "\s                       # schließendes Anführungszeichen
      (?P<status>\d{3})
      \s
      (?P<bytes>-|\d+)          # kann für best. Requests "-" sein
      # optional, nur im "combined"-Format, nicht im "common"-Format
      (?:   E
          \s
          "(?P<referer>[^"]+)"  # Referer, in Anführungszeichen
          \s
          "(?P<agent>[^"]+)"    # Client, in Anführungszeichen
      )?
      $
      """, re.VERBOSE)

    _month_numbers = {
      "Jan": 1, "Feb": 2, "Mar": 3, "Apr":  4, "May":  5, "Jun":  6,
      "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12}

    def __init__(self, line):
        """
        Parse eine Logfile-Zeile 'line' im "common"- oder "combined"-
        Format und intialisiere diese Instanz entsprechend. Falls 'line'
        nicht verarbeitet werden kann, erzeuge einen 'ParserError'.
        """
        match = self._line_regex.search(line)
        if not match:
            raise ParserError("can't parse line: %s" % line)
        groups = match.groupdict()
        # wandle einige der Gruppen in Ganzzahlen
        for name in ['day', 'year', 'hour', 'minute', 'second',
                     'timezone', 'status']:
            # Umwandlung sollte wegen der "\d"s im reg. Ausdruck immer
            #  funktionieren
            groups[name] = int(groups[name])
        # kopiere das Dictionary 'groups' in diese Instanz
        self.__dict__.update(groups)
        self.month = self._month_numbers[self.month]
        # erstelle Zeitstempel
        try:
            self.datetime = DateTime.DateTime(
                              self.year, self.month, self.day,
                              self.hour, self.minute, self.second)
        except DateTime.RangeError:
            raise ParserError("invalid datetime: %s" % self.datetime)
        self.datetime += DateTime.DateTimeDelta(0, self.timezone)
        # erstelle User-ID
        if self.userid == '-':
            self.userid = None
        # erstelle 'bytes'
        if self.bytes == '-':
            self.bytes = None
        else:
            self.bytes = int(self.bytes)
        # erstelle Referer
        if self.referer == '-':
            self.referer = None

    def save(self, connection):
        """
        Speichere die Daten aus dieser Instanz in der Datenbank.
        Verwende dazu das Connection-Objekt 'connection'.
        """
        cursor = connection.cursor()
        cursor.execute(SQL_STATEMENT, self.__dict__)
        connection.commit()
        cursor.close()

def parse_and_save(filename):
    """
    Parse das Logfile und speichere die enthaltenen Daten in der
    Datenbank.
    """
    logfile = open(filename)
    connection = psycopg.connect("dbname=webstats user=schwa")
    try:
        for line in logfile:
            try:
                logline = Logline(line)
                logline.save(connection)
            except ParserError:
                pass
    finally:
        connection.close()

parse_and_save(sys.argv[1])