]> git.phdru.name Git - m_lib.git/commitdiff
Initial import
authorOleg Broytman <phd@phdru.name>
Sat, 30 Nov 2013 21:54:41 +0000 (01:54 +0400)
committerOleg Broytman <phd@phdru.name>
Sat, 30 Nov 2013 21:54:41 +0000 (01:54 +0400)
Start to move m_lib to git.

34 files changed:
README.txt [new file with mode: 0644]
m_lib/__init__.py [new file with mode: 0644]
m_lib/clock/__init__.py [new file with mode: 0644]
m_lib/clock/mkclock.py [new file with mode: 0755]
m_lib/clock/tstclock.py [new file with mode: 0755]
m_lib/defenc.py [new file with mode: 0755]
m_lib/flog.py [new file with mode: 0755]
m_lib/lazy/__init__.py [new file with mode: 0644]
m_lib/lazy/dict.py [new file with mode: 0644]
m_lib/lazy/imports.py [new file with mode: 0644]
m_lib/m_path.py [new file with mode: 0755]
m_lib/m_shutil.py [new file with mode: 0755]
m_lib/mcrypt.py [new file with mode: 0755]
m_lib/md5wrapper.py [new file with mode: 0755]
m_lib/metaclasses.py [new file with mode: 0755]
m_lib/net/__init__.py [new file with mode: 0644]
m_lib/net/ftp/__init__.py [new file with mode: 0644]
m_lib/net/ftp/ftpparse.py [new file with mode: 0755]
m_lib/net/ftp/ftpscan.py [new file with mode: 0755]
m_lib/net/sms.py [new file with mode: 0644]
m_lib/net/www/__init__.py [new file with mode: 0644]
m_lib/net/www/dtml.py [new file with mode: 0644]
m_lib/net/www/html.py [new file with mode: 0644]
m_lib/net/www/serverpush.py [new file with mode: 0644]
m_lib/net/www/url_lib.py [new file with mode: 0644]
m_lib/net/www/util.py [new file with mode: 0644]
m_lib/net/www/xml.py [new file with mode: 0644]
m_lib/opdate.py [new file with mode: 0755]
m_lib/opstring.py [new file with mode: 0755]
m_lib/rus/__init__.py [new file with mode: 0644]
m_lib/rus/lat2rus.py [new file with mode: 0755]
m_lib/rus/rus2lat.py [new file with mode: 0755]
m_lib/tty_menu.py [new file with mode: 0755]
setup.py [new file with mode: 0755]

diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..bc86232
--- /dev/null
@@ -0,0 +1,41 @@
+   defenc.py - get default encoding.
+
+   flog.py - simple file logger.
+
+   lazy/ - lazy evaluation modules - lazy dictionary and lazy import.
+
+   mcrypt.py - crypt module supplement function gen_salt().
+
+   metaclasses.py - borrowed from Michele Simionato (mis6@pitt.edu)
+      to solve "TypeError: metatype conflict among bases".
+
+   md5wrapper.py - just an MD5 wrapper.
+
+   m_path.py - simple convenient get_homedir().
+
+   m_shutil.py - additional shell utilities (currently only mkhier
+      function).
+
+   net/ftp/ - modules related to FTP - ftpparse (pure-python parser of LIST
+      command output) and ftpscan - recursive scanner of FTP directories.
+
+   net/sms.py - Send SMS to subscribers of Moscow operators (Beeline, MTS,
+      Megafone) using their Web or SMTP gateways.
+
+   net/www/ - modules related to Web/HTTP/HTML/XML/DTML/etc.
+
+   opdate.py - additional date/time manipulation routines
+      In this module Date is a number of days since 1/1/1600 (up to 31 Dec 3999)
+      I am not sure about how easy it might be to extend the modules beyond
+      these bounds. Time is just a number of seconds since midnight. User can
+      add/subtract dates and times, calculate diffs ("how many days, months
+      and years passed since 21 Dec 1967?") and so on. DateTime <==> UTC (GMT)
+      conversion routines provided, too.
+      This module required flognat's strptime.py.
+
+   opstring.py - additional string manipulation routines
+      (character padding, encoding conversions).
+
+   rus/ - work with russian cyrillic - convert text to/from translit.
+
+   tty_menu.py - extremely dumb text-mode menues.
diff --git a/m_lib/__init__.py b/m_lib/__init__.py
new file mode 100644 (file)
index 0000000..438c49c
--- /dev/null
@@ -0,0 +1 @@
+"Broytman Library for Python, Copyright (C) 1996-2001 PhiloSoft Design"
diff --git a/m_lib/clock/__init__.py b/m_lib/clock/__init__.py
new file mode 100644 (file)
index 0000000..7719ef8
--- /dev/null
@@ -0,0 +1 @@
+"Broytman Clock Library for Python, Copyright (C) 1996-2001 PhiloSoft Design"
diff --git a/m_lib/clock/mkclock.py b/m_lib/clock/mkclock.py
new file mode 100755 (executable)
index 0000000..f65c766
--- /dev/null
@@ -0,0 +1,39 @@
+#! /usr/bin/env python
+"""
+   Test if current interpreter do not have clock() and define it as need
+
+   Written by Broytman, Jul 1997. Copyright (C) 1997 PhiloSoft Design
+"""
+
+import sys, time
+
+print "Testing...",
+sys.stdout.flush()
+
+time.sleep(3)
+print '\n' + " "*len("Testing...") + '\n',
+
+need_clock = time.clock() <> 3
+
+outfile = open("clock.py", 'w')
+
+if need_clock:
+   print "Generaing clock.py with custom clock()"
+   outfile.write("""\"\"\"
+   Define clock() for systems that do not have it
+\"\"\"
+
+from time import time
+_clock = time()
+
+def clock():
+   return int(time() - _clock)
+""")
+else:
+   print "Generaing clock.py with standard clock()"
+   outfile.write("""\"\"\"
+   Define clock() shim
+\"\"\"
+
+from time import clock
+   """)
diff --git a/m_lib/clock/tstclock.py b/m_lib/clock/tstclock.py
new file mode 100755 (executable)
index 0000000..827fa1d
--- /dev/null
@@ -0,0 +1,13 @@
+#! /usr/bin/env python
+"""
+   Define clock() for systems that do not have it
+
+   Written by Broytman, Jul 1997. Copyright (C) 1997 PhiloSoft Design
+"""
+
+from clock import clock
+from time import sleep
+
+print "Testing..."
+sleep(3)
+print "Clock:", clock()
diff --git a/m_lib/defenc.py b/m_lib/defenc.py
new file mode 100755 (executable)
index 0000000..01d8990
--- /dev/null
@@ -0,0 +1,39 @@
+#! /usr/bin/env python
+"""Get default encoding
+
+   Written by Oleg Broytman. Copyright (C) 2007, 2008 PhiloSoft Design.
+"""
+
+__all__ = ['default_encoding']
+
+import sys
+
+try:
+   import locale
+   use_locale = True
+except ImportError:
+   use_locale = False
+
+if use_locale:
+   # Get the default charset.
+   try:
+      lcAll = locale.getdefaultlocale()
+   except locale.Error, err:
+      print >>sys.stderr, "WARNING:", err
+      lcAll = []
+
+   if len(lcAll) == 2:
+      default_encoding = lcAll[1]
+   else:
+      try:
+         default_encoding = locale.getpreferredencoding()
+      except locale.Error, err:
+         print >>sys.stderr, "WARNING:", err
+         default_encoding = sys.getdefaultencoding()
+else:
+   default_encoding = sys.getdefaultencoding()
+
+default_encoding = default_encoding.lower()
+
+if __name__ == "__main__":
+   print default_encoding
diff --git a/m_lib/flog.py b/m_lib/flog.py
new file mode 100755 (executable)
index 0000000..b46bae5
--- /dev/null
@@ -0,0 +1,61 @@
+#! /usr/bin/env python
+"""File logger
+
+   Written by Broytman, Dec 1997. Copyright (C) 1997 PhiloSoft Design.
+"""
+
+
+from time import *
+
+
+class FLog:
+   def __init__(self, f, overwrite = 0, timeformat = "%a %d %b %Y %T"):
+      if type(f) == type(''): # If f is string - use it as file's name
+         if overwrite:
+            mode = 'w'
+         else:
+            mode = 'a'
+         self.outfile = open(f, mode)
+      else:
+         self.outfile = f     # else assume it is opened file (fileobject) or
+                              # "compatible" object (must has write() method)
+      self.f = f
+      self.timeformat = timeformat
+
+
+   def __del__(self):
+      self.close()
+
+
+   def close(self):
+      if type(self.f) == type(''): # If f was opened - close it
+         self.outfile.close()
+
+
+   def log(self, str):
+      self.outfile.write("%s %s\n" % (strftime(self.timeformat, localtime(time())), str))
+
+
+   __call__ = log
+
+
+   def flush(self):
+      self.outfile.flush()
+
+
+def makelog(f):
+   return FLog(f, 1)
+
+
+def openlog(f):
+   return FLog(f)
+
+
+def test():
+   log = makelog("test.log")
+   log.log("Log opened")
+   log("Log closed")
+   log.close()
+
+if __name__ == "__main__":
+   test()
diff --git a/m_lib/lazy/__init__.py b/m_lib/lazy/__init__.py
new file mode 100644 (file)
index 0000000..4dedfba
--- /dev/null
@@ -0,0 +1,6 @@
+"Broytman WWW Library for Python, Copyright (C) 1996-2003 PhiloSoft Design"
+
+
+__all__ = [
+   "dict", "imports"
+]
diff --git a/m_lib/lazy/dict.py b/m_lib/lazy/dict.py
new file mode 100644 (file)
index 0000000..c6750e4
--- /dev/null
@@ -0,0 +1,37 @@
+"Lazy dictionaries calculate self content upon first access"
+
+class LazyDict:
+   "Abstract parent of all lazy dictionaries"
+
+   def _init(self):
+      raise NotImplementedError
+
+   def __getattr__(self, attr):
+      if self.data is None: self._init()
+      return getattr(self.data, attr)
+
+   def __getitem__(self, key):
+      if self.data is None: self._init()
+      return self.data[key]
+
+   def __setitem__(self, key, value):
+      if self.data is None: self._init()
+      self.data[key] = value
+
+
+class LazyDictInitFunc(LazyDict):
+   "Lazy dict that initializes itself by calling supplied init function"
+
+   def __init__(self, init=None, *args, **kw):
+      self.init = init
+      self.data = None
+      self.args = args
+      self.kw = kw
+
+   def _init(self):
+      init = self.init
+      if init is None:
+         data = {} # just a new empty dict
+      else:
+         data = init(*self.args, **self.kw)
+      self.data = data
diff --git a/m_lib/lazy/imports.py b/m_lib/lazy/imports.py
new file mode 100644 (file)
index 0000000..99eb032
--- /dev/null
@@ -0,0 +1,23 @@
+"Lazy imoport - import the module upon first request"
+
+
+try:
+   from mx.Misc import LazyModule
+
+except ImportError:
+
+   class LazyModule:
+      def __init__(self, module_name, locals, globals=None):
+         self.module = None
+         self.module_name = module_name
+         self.locals = locals
+         if globals is None:
+            globals = locals
+         self.globals = globals
+
+      def __getattr__(self, attr):
+         if self.module is None:
+            self.module = module = __import__(self.module_name, self.globals, self.locals)
+         else:
+            module = self.module
+         return getattr(module, attr)
diff --git a/m_lib/m_path.py b/m_lib/m_path.py
new file mode 100755 (executable)
index 0000000..6d20720
--- /dev/null
@@ -0,0 +1,26 @@
+#! /usr/bin/env python
+
+#
+# useful function(s) for manipulating paths
+#
+# Author: Oleg Broytman <phd@phd.pp.ru>
+# Copyright (C) 2002 PhiloSoft Design
+#
+
+
+_homedir = None
+
+def get_homedir():
+   global _homedir
+   if _homedir is None:
+      import sys, os
+      _homedir = os.path.abspath(os.path.dirname(sys.argv[0]))
+   return _homedir
+
+
+def test():
+   print get_homedir()
+
+
+if __name__ == "__main__":
+   test()
diff --git a/m_lib/m_shutil.py b/m_lib/m_shutil.py
new file mode 100755 (executable)
index 0000000..10e5ebf
--- /dev/null
@@ -0,0 +1,38 @@
+#! /usr/bin/env python
+"""Broytman's shell utilities. Additional to shutil.py (standard library module)
+
+   Written by Broytman, Oct 1997. Copyright (C) 1997 PhiloSoft Design.
+"""
+
+
+import os, string
+
+
+mkhier_error = "m_shutil.mkhier_error"
+
+def mkhier(path): # Python implementation of UNIX' mkdir -p /path/to/dir
+   if os.path.isdir(path):
+      return # It's Ok to have the directory already created
+
+   if os.path.exists(path):
+      raise mkhier_error, "`%s' is file" % path
+
+   list_dirs = string.split(path, os.sep)
+   #print list_dirs
+   for i in range(0, len(list_dirs)):
+      new_path = string.join(list_dirs[0:i+1], os.sep)
+      if (new_path <> '') and (not os.path.exists(new_path)):
+         #print "Making", new_path
+         os.mkdir(new_path)
+
+
+def mcd(dir):
+   os.mkdir(dir)
+   os.chdir(dir)
+
+
+def test():
+   mkhier("I.AM/creating/TEST/dir")
+
+if __name__ == "__main__":
+   test()
diff --git a/m_lib/mcrypt.py b/m_lib/mcrypt.py
new file mode 100755 (executable)
index 0000000..d6ae1dc
--- /dev/null
@@ -0,0 +1,51 @@
+#! /usr/bin/env python
+
+#
+# useful function(s) for module crypt
+#
+# Author: Oleg Broytman <phd@phd.pp.ru>
+# Copyright (C) 1998-1999 PhiloSoft Design
+#
+
+
+import random, crypt
+
+
+saltchars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789./"
+len_salt = len(saltchars)
+
+def gen_salt():
+   """
+      There are some difference among modern unicies. BSD/OS, for example,
+      uses MD5 hash, and ignores salt completely. FreeBSD uses 3 different
+      versions of crypt() - with standard salt, with extended 9-byte salt,
+      and MD5 (again, ignoring salt at all).
+      This function generates salt for standard "Broken DES"-based crypt().
+   """
+   r1 = random.randint(0, len_salt-1)
+   r2 = random.randint(0, len_salt-1)
+   return "%s%s" % (saltchars[r1], saltchars[r2])
+
+
+def test():
+   passwd = raw_input("Enter password: ")
+   salt = gen_salt()
+   encrypted = crypt.crypt(passwd, salt)
+
+   pwd_file = open("test.pwd", 'w')
+   pwd_file.write("%s:%s" % ("user", encrypted))
+   pwd_file.close()
+   print "Password file written"
+
+   import string
+   pwd_file = open("test.pwd", 'r')
+   username, encrypted = string.split(pwd_file.readline()[:-1], ':')
+   pwd_file.close()
+
+   if crypt.crypt(encrypted, encrypted):
+      print "Password verified Ok"
+   else:
+      print "BAD password"
+
+if __name__ == "__main__":
+   test()
diff --git a/m_lib/md5wrapper.py b/m_lib/md5wrapper.py
new file mode 100755 (executable)
index 0000000..0c1a5f1
--- /dev/null
@@ -0,0 +1,53 @@
+#! /usr/bin/env python
+"""User wrapper for md5 builtin object"""
+
+__author__ = "Oleg Broytman <phd@phdru.name>"
+__copyright__ = "Copyright (C) 1997-2012 PhiloSoft Design"
+__license__ = "GNU GPL"
+
+__all__ = ['md5wrapper']
+
+import sys
+if sys.version < '2.5':
+    from md5 import md5
+else:
+    from hashlib import md5
+
+class md5wrapper:
+   def __init__(self, init=None):
+      if init:
+         self._md5 = md5(init)
+      else:
+         self._md5 = md5()
+
+   def update(self, data):
+      self._md5.update(data)
+
+   def digest(self):
+      return self._md5.digest()
+
+   def __repr__(self):
+      str = self.digest()
+      return "%02x"*len(str) % tuple(map(ord, str))
+         # This nice was suggested by Guido
+
+   def md5file(self, f):
+      if type(f) == type(''): # If f is string - use it as file's name
+         infile = open(f, 'r')
+      else:
+         infile = f           # else assume it is file or file-like object
+
+      try:
+         while 1:
+            buf = infile.read(16*1024)
+            if not buf: break
+            self.update(buf)
+
+      finally:
+         if type(f) == type(''): # If f was opened - close it
+            infile.close()
+
+if __name__ == "__main__":
+   print "This must print exactly the string"
+   print "Test: 900150983cd24fb0d6963f7d28e17f72"
+   print "Test:", md5wrapper("abc")
diff --git a/m_lib/metaclasses.py b/m_lib/metaclasses.py
new file mode 100755 (executable)
index 0000000..27525af
--- /dev/null
@@ -0,0 +1,64 @@
+#! /usr/bin/env python
+
+
+# From: Michele Simionato (mis6@pitt.edu)
+# http://groups.google.com/groups?selm=2259b0e2.0304250413.4be8ee45%40posting.google.com
+# To solve "TypeError: metatype conflict among bases"
+
+
+def _generatemetaclass(bases, metas):
+   "Internal function called by child"
+
+   if metas == (type,): # trivial metaclass
+       metabases = (); metaname = "_"
+   else: # non-trivial metaclass
+       metabases = metas
+       metaname = "_"+''.join([m.__name__ for m in metas])
+   trivial = lambda m: m in metabases or m is type
+
+   for b in bases:
+       meta_b = type(b)
+       if not trivial(meta_b):
+           metabases += (meta_b,)
+           metaname += meta_b.__name__
+
+   if not metabases: # trivial metabase
+       return type
+   elif len(metabases) == 1: # single metabase
+       return metabases[0]
+   else: # multiple metabases
+       return type(metaname, metabases, {}) # creates a new metaclass
+      #shifting the possible conflict to meta-metaclasses
+
+
+def child(*bases, **options):
+   """Class factory avoiding metatype conflicts: if the base classes have
+   metaclasses conflicting within themselves or with the given metaclass,
+   it automatically generates a compatible metaclass and instantiate the
+   child class from it. The recognized keywords in the option dictionary
+   are name, dic and meta."""
+   name = options.get('name', ''.join([b.__name__ for b in bases])+'_')
+   dic = options.get('dic', {})
+   metas = options.get('metas', (type,))
+   return _generatemetaclass(bases, metas)(name, bases, dic)
+
+
+
+def test():
+    class M_A(type): pass
+    class M_B(type): pass
+    A = M_A('A', (), {})
+    B = M_B('B', (), {})
+
+    try:
+       class C(A, B): pass
+    except TypeError:
+       pass
+    else:
+       raise RuntimeError
+
+    C = child(A, B, name='C')
+
+
+if __name__ == "__main__":
+    test()
diff --git a/m_lib/net/__init__.py b/m_lib/net/__init__.py
new file mode 100644 (file)
index 0000000..b557b3a
--- /dev/null
@@ -0,0 +1,6 @@
+"Broytman Net Library for Python, Copyright (C) 1996-2003 PhiloSoft Design"
+
+
+__all__ = [
+   "ftp", "www", "sms"
+]
diff --git a/m_lib/net/ftp/__init__.py b/m_lib/net/ftp/__init__.py
new file mode 100644 (file)
index 0000000..7e30fda
--- /dev/null
@@ -0,0 +1,16 @@
+"Broytman WWW Library for Python, Copyright (C) 1996-2003 PhiloSoft Design"
+
+
+__all__ = [
+   "ftpparse", "ftpscan", "TelnetFTP"
+]
+
+
+from ftplib import FTP
+from telnetlib import IAC
+
+
+class TelnetFTP(FTP):
+    def putline(self, line):
+        line = line.replace(IAC, IAC+IAC)
+        FTP.putline(self, line)
diff --git a/m_lib/net/ftp/ftpparse.py b/m_lib/net/ftp/ftpparse.py
new file mode 100755 (executable)
index 0000000..cb4c569
--- /dev/null
@@ -0,0 +1,278 @@
+#! /usr/bin/env python
+"""Parse output of FTP LIST command.
+   Pure python implementation.
+   Author: Oleg Broytman <phd@phd.pp.ru>.
+   Copyright (C) 2003-2005 PhiloSoft Design.
+   License: GPL.
+   See http://cr.yp.to/ftpparse.html, http://effbot.org/downloads#ftpparse,
+   http://c0re.23.nu/c0de/ftpparsemodule/ and http://www.ocgy.ubc.ca/~tang/treeftp
+
+Currently covered formats:
+   UNIX ls, with or without gid;
+   Windoze FTP Servers;
+   VMS;
+   WFTPD;
+   NetPresenz (Mac);
+   NetWare;
+   DOS;
+
+Definitely not covered:
+   EPLF (show me an FTP server first...);
+   Long VMS filenames, with information split across two lines;
+   NCSA Telnet FTP server. Has LIST = NLST (and bad NLST for directories).
+"""
+
+
+__version__ = "1.1.2"
+__author__ = "Oleg Broytman <phd@phd.pp.ru>"
+__copyright__ = "Copyright (C) 2003-2005 PhiloSoft Design"
+
+
+# ChangeLog:
+# 2005-04-26 version 1.1.2 [phd] Changed some comments and URLs.
+# 2003-07-23 version 1.1.1 [phd] Upgrade to Python 2.2: 0/1 => False/True.
+# 2003-07-17 version 1.1.0 [phd] Great renaming.
+# 2003-07-17 version 1.0.1 [phd] Preserve leading spaces in UNIX format.
+# 2003-02-17 version 1.0.0 [phd] First public version.
+# 2003-02-07 version 0.0.1 [phd] started the project.
+
+
+try:
+   from mx import DateTime
+except ImportError:
+   _parse_datetime = False
+else:
+   _parse_datetime = True
+
+
+class parse_error(Exception): pass
+
+
+class entry:
+   def __init__(self, name=None, perm=None, nlink=None, user=None, group=None, \
+         size=None, mtime=None, links_to=None, file_type=None):
+
+      if mtime:
+         mtime = ' '.join(mtime)
+         if _parse_datetime:
+            try:
+               mtime = DateTime.DateTimeFrom(mtime)
+            except DateTime.Error:
+               pass
+
+      self.name = name
+      self.perm = perm
+      self.nlink = nlink
+      self.user = user
+      self.group = group
+      self.size = size
+      self.mtime = mtime
+      self.links_to = links_to
+      self.file_type = file_type # f - regular file, d - directory, l - symlink
+
+
+   def __str__(self):
+      return """<%s: name=%s, perm=%s, nlink=%s, user=%s, group=%s, size=%s, mtime=%s, links-to=%s, type=%s at 0x%x>""" % (
+      self.__class__.__name__, self.name, self.perm, self.nlink,
+         self.user, self.group, self.size, self.mtime,
+         self.links_to, self.file_type, id(self))
+
+
+def _parse_unix(line, parts):
+   name = None
+   perm = None
+   nlink = None
+   user = None
+   group = None
+   size = None
+   mtime = None
+   links_to = None
+   file_type = None
+
+   perm = parts[0]
+
+   if parts[1][0] == '[': # NetWare
+      if perm[0] == 'd':
+         file_type = 'd'
+      elif perm[0] == '-':
+         file_type = 'f'
+      else:
+         return None
+      perm = perm + ' ' + parts[1]
+      user = parts[2]
+      size = parts[3]
+      mtime = parts[4:7]
+
+      parts = line.split(None, 7) # resplit the original line...
+      name = parts[7]             # ...in case the filename contains whitespaces
+
+   elif parts[1] == "folder": # NetPresenz for the Mac
+      file_type = 'd'
+      size = parts[2]
+      mtime = parts[3:6]
+
+      parts = line.split(None, 6)
+      name = parts[6]
+
+   elif parts[0][0] == 'l': # symlink
+      file_type = 'l'
+      nlink = int(parts[1])
+      user = parts[2]
+      group = parts[3]
+      size = parts[4]
+      mtime = parts[5:8]
+
+      parts = line.split(None, 8)
+      link = parts[8]
+      parts = link.split(" -> ")
+      name = parts[0]
+      links_to = parts[1]
+
+   elif len(parts) > 8:
+      if perm[0] == 'd':
+         file_type = 'd'
+      elif perm[0] == '-':
+         file_type = 'f'
+      else:
+         return None
+      nlink = int(parts[1])
+      user = parts[2]
+      group = parts[3]
+      size = parts[4]
+      mtime = parts[5:8]
+
+      parts = line.split(None, 7)
+      name = parts[7]
+      parts = name.split(' ')[1:]
+      name = ' '.join(parts)
+
+   else:
+      if parts[2].isdigit(): # NetPresenz
+         file_type = 'f'
+         size = parts[3]
+         mtime = parts[4:7]
+      else:
+         if perm[0] == 'd':
+            file_type = 'd'
+         elif perm[0] == '-':
+            file_type = 'f'
+         else:
+            return None
+         nlink = int(parts[1])
+         user = parts[2]
+         size = parts[3]
+         mtime = parts[4:7]
+
+      parts = line.split(None, 7)
+      name = parts[7]
+
+   return entry(name, perm, nlink, user, group, size, mtime, links_to, file_type)
+
+
+def _parse_vms(parts):
+   name = parts[0]
+   perm = parts[5]
+   nlink = parts[1]
+   user = parts[4][1:-1]
+   group = None
+   size = None
+   mtime = parts[2:4]
+   links_to = None
+   file_type = None
+
+   if ',' in user:
+      parts = user.split(',')
+      user = parts[0]
+      group = parts[1]
+
+   return entry(name, perm, nlink, user, group, size, mtime, links_to, file_type)
+
+
+def _parse_dos(parts):
+   name = parts[3]
+   perm = None
+   nlink = None
+   user = None
+   group = None
+   size = None
+   links_to = None
+
+   if _parse_datetime:
+      # Change %m-%d-%y format to %y-%m-%d
+      date = parts[0]
+      date_parts = date.split('-')
+      date = "%s-%s-%s" % (date_parts[2], date_parts[0], date_parts[1])
+      time = parts[1]
+      mtime = [date, time]
+   else:
+      mtime = parts[0:2]
+
+   if parts[2] == "<DIR>":
+      file_type = 'd'
+   else:
+      file_type = 'f'
+      size = int(parts[2])
+
+   return entry(name, perm, nlink, user, group, size, mtime, links_to, file_type)
+
+
+def ftpparse(line):
+   parts = line.split()
+   c = parts[0][0]
+
+   if c == '+': # EPLF format is not supported
+      return None
+
+   if c in ('b', 'c', 'd', 'l', 'p', 's', '-'): # UNIX-like
+      return _parse_unix(line, parts)
+
+   if ';' in parts[0]: # VMS
+      return _parse_vms(parts)
+
+   if '0' <= c <= '9': # DOS
+      return _parse_dos(parts)
+
+
+   #Some useless lines, safely ignored:
+   #"Total of 11 Files, 10966 Blocks." (VMS)
+   #"total 14786" (UNIX)
+   #"DISK$ANONFTP:[ANONYMOUS]" (VMS)
+   #"Directory DISK$PCSA:[ANONYM]" (VMS)
+
+   return None
+
+
+def test():
+   #UNIX-style listing, without inum and without blocks
+   print ftpparse("-rw-r--r--   1 root     other        531 Jan 29 03:26 README")
+   print ftpparse("dr-xr-xr-x   2 root     other        512 Apr  8  1994 etc")
+   print ftpparse("dr-xr-xr-x   2 root     512 Apr  8  1994 etc")
+   print ftpparse("lrwxrwxrwx   1 root     other          7 Jan 25 00:17 bin -> usr/bin")
+   #FTP servers for Windoze:
+   print ftpparse("----------   1 owner    group         1803128 Jul 10 10:18 ls-lR.Z")
+   print ftpparse("d---------   1 owner    group               0 May  9 19:45 Softlib")
+   #WFTPD for DOS:
+   print ftpparse("-rwxrwxrwx   1 noone    nogroup      322 Aug 19  1996 message.ftp")
+   #NetWare:
+   print ftpparse("d [R----F--] supervisor            512       Jan 16 18:53    login")
+   print ftpparse("- [R----F--] rhesus             214059       Oct 20 15:27    cx.exe")
+   #NetPresenz for the Mac:
+   print ftpparse("-------r--         326  1391972  1392298 Nov 22  1995 MegaPhone.sit")
+   print ftpparse("drwxrwxr-x               folder        2 May 10  1996 network")
+
+   #MultiNet (some spaces removed from examples)
+   print ftpparse("00README.TXT;1      2 30-DEC-1996 17:44 [SYSTEM] (RWED,RWED,RE,RE)")
+   print ftpparse("CORE.DIR;1          1  8-SEP-1996 16:09 [SYSTEM] (RWE,RWE,RE,RE)")
+   #non-MutliNet VMS:
+   print ftpparse("CII-MANUAL.TEX;1  213/216  29-JAN-1996 03:33:12  [ANONYMOU,ANONYMOUS]   (RWED,RWED,,)")
+
+   #DOS format
+   print ftpparse("04-27-00  09:09PM       <DIR>          licensed")
+   print ftpparse("07-18-00  10:16AM       <DIR>          pub")
+   print ftpparse("04-14-00  03:47PM                  589 readme.htm")
+
+   print ftpparse("-rw-r--r--   1 root     other        531 Jan 29 03:26 READ ME")
+   print ftpparse("-rw-r--r--   1 root     other        531 Jan 29 03:26  DO NOT READ ME ")
+
+if __name__ == "__main__":
+   test()
diff --git a/m_lib/net/ftp/ftpscan.py b/m_lib/net/ftp/ftpscan.py
new file mode 100755 (executable)
index 0000000..319660b
--- /dev/null
@@ -0,0 +1,283 @@
+#! /usr/bin/env python
+"""Recursive FTP scanners"""
+
+
+import ftplib
+from m_lib.net.ftp.ftpparse import ftpparse
+
+
+class FtpScanError(Exception): pass
+ftpscan_error_mark = object() # error marker
+
+
+class GetFiles:
+   def __init__(self):
+      self.entries = []
+
+   def __call__(self, line):
+      entry = ftpparse(line)
+      if entry:
+         self.entries.append(entry)
+
+   def filter(self, file_type):
+      return filter(lambda e, file_type=file_type: e.file_type == file_type,
+         self.entries)
+
+   def files(self):
+      return self.filter('f')
+
+   def directories(self):
+      return filter(lambda e: e.name not in (".", ".."), self.filter('d'))
+
+
+class ReconnectingFTPCallWrapper:
+   retries = 10 # retries per function call
+
+   def __init__(self, wrapper, func):
+      self.wrapper = wrapper
+      self.func = func
+
+   def __call__(self, *params, **kw):
+      wrapper = self.wrapper
+      func = self.func
+
+      for retry in range(self.retries):
+         try:
+            return func(*params, **kw)
+         except EOFError:
+            pass
+
+         ftp_dir = wrapper._ftp_dir
+         wrapper._tree.append((ftpscan_error_mark, "Connection reset by peer at directory `%s'. Reconnecting..." % ftp_dir))
+
+         ftp = wrapper._ftp
+         ftp.close()
+
+         ftp.connect(wrapper._ftp_server, wrapper._ftp_port)
+         ftp.login(wrapper._login, wrapper._password)
+         ftp.cwd(ftp_dir)
+
+class ReconnectingFTPWrapper:
+   ReconnectingFTPCallWrapperClass = ReconnectingFTPCallWrapper
+
+   def __init__(self, ftp, ftp_server, ftp_port=None, login=None, password=None, ftp_dir='/', tree=None):
+      self._ftp = ftp
+      self._ftp_server = ftp_server
+      self._ftp_port = ftp_port
+      self._login = login
+      self._password = password
+      ftp_dir = [''] + [name for name in ftp_dir.split('/') if name] # remove double slashes //
+      self._ftp_dir = '/'.join(ftp_dir)
+      self._tree = tree
+
+   def cwd(self, new_cwd, do_ftp=True):
+      ftp_dir = self._ftp_dir.split('/')
+      if new_cwd == "..":
+         del ftp_dir[-1]
+      else:
+         ftp_dir.append(new_cwd)
+      self._ftp_dir = '/'.join(ftp_dir)
+      if do_ftp: self._wrap(self._ftp.cwd)(new_cwd)
+
+   def __getattr__(self, attr):
+      value = getattr(self._ftp, attr)
+      if callable(value):
+         return self._wrap(value)
+      return value
+
+   def _wrap(self, func):
+      return self.ReconnectingFTPCallWrapperClass(self, func)
+
+
+def _traverse_ftp(ftp, tree, ftp_dir):
+   get_files = GetFiles()
+   try:
+      ftp.dir(get_files)
+   except ftplib.all_errors, msg:
+      tree.append((ftpscan_error_mark, "Cannot list directory `%s': %s" % (ftp_dir, msg)))
+      return
+   files = get_files.files()
+   directories = get_files.directories()
+
+   if ftp_dir and ftp_dir[-1] == '/':
+      ftp_dir = ftp_dir[:-1] # Prevent paths to contain double slashes //
+
+   tree.append((ftp_dir, files))
+
+   for d in directories:
+      name = d.name
+      full_path = ftp_dir + '/' + name
+      try:
+         ftp.cwd(name)
+      except ftplib.error_perm, msg:
+         tree.append((ftpscan_error_mark, "Cannot enter directory `%s': %s" % (full_path, msg)))
+         if isinstance(ftp, ReconnectingFTPWrapper):
+            ftp.cwd("..", False)
+      except ftplib.all_errors, msg:
+         tree.append((ftpscan_error_mark, "Cannot enter directory `%s': %s" % (full_path, msg)))
+      else:
+         _traverse_ftp(ftp, tree, full_path)
+         ftp.cwd("..")
+
+
+def ftpscan1(ftp_server, ftp_port=None, login=None, password=None,
+      ftp_dir='/', passive=None, FTPClass=ftplib.FTP, reconnect=False,
+      ReconnectingFTPWrapperClass=ReconnectingFTPWrapper):
+   """Recursive FTP scan using one-by-one directory traversing. It is slow
+   but robust - it works with all but very broken FTP servers.
+   """
+   tree = []
+   ftp = FTPClass()
+   if passive is not None:
+      ftp.set_pasv(passive)
+   if reconnect:
+      ftp = ReconnectingFTPWrapperClass(ftp, ftp_server, ftp_port, login, password, ftp_dir, tree)
+   ftp.connect(ftp_server, ftp_port)
+   ftp.login(login, password)
+   if ftp_dir <> '/':
+      ftp.cwd(ftp_dir)
+
+   _traverse_ftp(ftp, tree, ftp_dir)
+   ftp.quit()
+
+   return tree
+
+
+def ftpscanrecursive(ftp_server, ftp_port=None, login=None, password=None,
+      ftp_dir='/', passive=None, FTPClass=ftplib.FTP, reconnect=False):
+   """
+   Recursive FTP scan using fast LIST -R command. Not all servers supports
+   this, though.
+   """
+   ftp = FTPClass()
+   if passive is not None:
+      ftp.set_pasv(passive)
+   ftp.connect(ftp_server, ftp_port)
+   ftp.login(login, password)
+   if ftp_dir <> '/':
+      ftp.cwd(ftp_dir)
+
+   lines = []
+   try:
+      ftp.dir("-R", lines.append)
+   except ftplib.error_perm:
+      # The server does not implement LIST -R and
+      # treats -R as a name of a directory (-:
+      ftp.quit()
+      raise FtpScanError, "the server does not implement recursive listing"
+   ftp.quit()
+
+   tree = []
+   current_dir = ftp_dir
+   files = []
+
+   for line in lines:
+      if line:
+         if line[-1] == ':' and not line.startswith("-rw-"): # directory
+            tree.append((current_dir, files))
+            if line[:2] == "./":
+               line = line[1:] # remove leading dot
+            elif line[0] <> '/':
+               line = '/' + line
+            current_dir = line[:-1]
+            files = []
+         else:
+            if not line.startswith("total "):
+               entry = ftpparse(line)
+               if entry:
+                  if entry.file_type == 'f':
+                     files.append(entry)
+               else:
+                  tree.append((ftpscan_error_mark, "Unrecognised line: `%s'" % line))
+   tree.append((current_dir, files))
+
+   if len(tree) == 1:
+      raise FtpScanError, "the server ignores -R in LIST"
+
+   return tree
+
+
+def ftpscan(ftp_server, ftp_port=None, login=None, password=None,
+      ftp_dir='/', passive=None, FTPClass=ftplib.FTP):
+   try:
+      return ftpscanrecursive(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass)
+   except FtpScanError:
+      try:
+         return ftpscan1(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass)
+      except EOFError:
+         return ftpscan1(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass, True)
+   except EOFError:
+      return ftpscan1(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass, True)
+
+
+def test(ftp_server, func, passive=None, reconnect=False):
+   from time import time
+   start_time = time()
+
+   tree = func(ftp_server, passive=passive, reconnect=reconnect)
+
+   stop_time = time()
+   print stop_time - start_time
+
+   logfname = "%s.list" % ftp_server
+   log = open(logfname, 'w')
+
+   for ftp_dir, files in tree:
+      if ftp_dir == ftpscan_error_mark:
+         log.write("Error:\n")
+         log.write(files)
+         log.write('\n')
+      else:
+         log.write(ftp_dir + '\n')
+         for _file in files:
+            log.write("    ")
+            log.write(_file.name + '\n')
+
+
+def usage(code=0):
+   sys.stderr.write("Usage: %s [-a|-p] [hostname]\n" % sys.argv[0])
+   sys.exit(code)
+
+if __name__ == "__main__":
+   import sys
+   from getopt import getopt, GetoptError
+
+   try:
+      options, arguments = getopt(sys.argv[1:], "hap",
+         ["help", "active", "passive"])
+   except GetoptError:
+      usage(1)
+
+   passive = None
+
+   for option, value in options:
+      if option in ("-h", "--help"):
+         usage()
+      elif option in ("-a", "--active"):
+         passive = False
+      elif option in ("-p", "--passive"):
+         passive = True
+      else:
+         usage(2)
+
+   l = len(arguments)
+   if (l == 0):
+      ftp_server = "localhost"
+   elif l > 1:
+      usage()
+   else:
+      ftp_server = arguments[0]
+
+   print "Scanning", ftp_server
+   try:
+      test(ftp_server, ftpscanrecursive, passive)
+   except FtpScanError, msg:
+      print "Rescanning due to the error:", msg
+      try:
+         test(ftp_server, ftpscan1, passive)
+      except EOFError:
+         print "Rescanning due to the error: connection reset by peer"
+         test(ftp_server, ftpscan1, passive, True)
+   except EOFError:
+      print "Rescanning due to the error: connection reset by peer"
+      test(ftp_server, ftpscan1, passive, True)
diff --git a/m_lib/net/sms.py b/m_lib/net/sms.py
new file mode 100644 (file)
index 0000000..423fd4a
--- /dev/null
@@ -0,0 +1,569 @@
+"SMS Transports by phd, corvin, r_sabitov"
+# -*- coding: koi8-r -*-
+
+
+import socket
+
+from m_lib.opstring import koi2win
+from m_lib.rus.rus2lat import rus2lat
+
+
+debug_level = 0
+use_syslog = 0
+
+def debug(level, message):
+   if level <= debug_level:
+      if use_syslog:
+         import syslog
+         syslog.syslog(message)
+      import sys
+      sys.stderr.write("%s\n" % message)
+
+
+smsSize = 160
+
+#
+# Base transport classes
+#
+
+class Transport(object):
+   def __init__(self, phone, message):
+      self.phone = phone
+      self.message = message
+
+   def calcText(self, maxsize=smsSize, transFunction=None):
+      "Recode text on demand and truncate the result"
+
+      text = self.message
+      if transFunction:
+         text = transFunction(text)
+      text = text[:maxsize - 1]
+      return text
+
+   def calcExpiration(self):
+      from mx import DateTime
+      return DateTime.now() + DateTime.DateTimeDelta(0, 4)
+
+class CP1251(object):
+   "Mixin that converts input text from koi8 to cp1251"
+
+   def calcText(self, maxsize=smsSize):
+      text = super(CP1251, self).calcText(maxsize=maxsize)
+      return koi2win(text)
+
+#
+# HTTP transport
+#
+
+class HTTPTransport(Transport):
+   "Base class for HTTP transport"
+
+   HTTPHeaders = {
+      'User-Agent': 'Mozilla/5.0 (X11; U; Linux 2.2.19 i686; en-US; rv:0.9) Gecko/20010507',
+      'Accept': '*/*'
+   }
+
+   referer = ''
+
+   def post(self, dict):
+      import urllib, urlparse
+      _none, host, uri, _none, _none, _none = urlparse.urlparse(self.url)
+      postdata = urllib.urlencode(dict)
+
+      methodFunc = getattr(self, self.method)
+
+      try:
+         reply = methodFunc(postdata, host, uri)
+      except socket.error:
+         ret = 0
+      else:
+         html = reply[2].fp.read()
+         debug(2, html)
+
+         if html.find(self.ok_match) >= 0:
+            ret = 1
+         else:
+            ret = 0
+
+
+      _debug = 'msg to %s via %s (%s)' % \
+            (self.phone, self.__class__.__name__, ret)
+      debug(1, _debug)
+
+      return ret
+
+   def GET(self, postdata, host, uri, port=80, proxy_host=None, proxy_port=None):
+      "HTTP method GET"
+
+      if postdata:
+         uri = uri + "?" + postdata
+
+      import httplib
+      if proxy_host:
+         http = httplib.HTTP(proxy_host, proxy_port)
+         http.set_debuglevel(debug_level)
+         http.putrequest("GET", 'http://%s%s' % (host, uri))
+      else:
+         http = httplib.HTTP(host, port)
+         http.set_debuglevel(debug_level)
+         http.putrequest("GET", uri)
+
+      http.putheader("Host", host)
+      if self.referer:
+         http.putheader("Referer", self.referer)
+
+      for name, val in self.HTTPHeaders.items():
+         http.putheader(name, val)
+
+      http.endheaders()
+
+      reply = http.getreply()
+      reply[2].fp = http.getfile()
+      return reply
+
+   def POST(self, postdata, host, uri, port=80, proxy_host=None, proxy_port=None):
+      "HTTP method POST"
+
+      import httplib
+      if proxy_host:
+         http = httplib.HTTP(proxy_host, proxy_port)
+         http.set_debuglevel(debug_level)
+         http.putrequest("POST", 'http://%s%s' % (host, uri))
+      else:
+         http = httplib.HTTP(host, port)
+         http.set_debuglevel(debug_level)
+         http.putrequest("POST", uri)
+
+      http.putheader("Host", host)
+      if self.referer:
+         http.putheader("Referer", self.referer)
+      http.putheader('Content-Type', 'application/x-www-form-urlencoded')
+      http.putheader('Content-Length', str(len(postdata)))
+
+      for name, val in self.HTTPHeaders.items():
+         http.putheader(name, val)
+
+      http.endheaders()
+      http.send(postdata)
+
+      reply = http.getreply()
+      reply[2].fp = http.getfile()
+      return reply
+
+class CP1251HTTPTransport(CP1251, HTTPTransport):
+   pass
+
+#
+# SNPP transport
+#
+
+class SNPPTransport(Transport):
+   "Base class for SNPP transport"
+
+   host = 'localhost'
+   port = 444
+   ok_match = '250 Message Sent Successfully'
+
+   def post(self, dict):
+      # raw snpp hack w/o error checking
+      try:
+         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+         sock.connect(('www.extel-gsm.com', 4444))
+         sock_file = sock.makefile('r')
+
+         # 220 Extel Mobile Communications. SNPP Gateway v.0.2.15; Wed May 30 00:43:56 2001
+         # 220 SNPP Gateway Ready
+         debug(2, sock_file.readline())
+         debug(2, sock_file.readline())
+
+         sock.send('PAGE %s\r\n' % dict['phone'])
+         # 250 Pager ID Accepted
+         debug(2, sock_file.readline())
+
+         sock.send('DATA\r\n')
+         # 354 Begin Input; End with <CRLF>'.'<CRLF>
+         debug(2, sock_file.readline())
+
+         sock.send('%s\r\n' % dict['message'])
+         sock.send('.\r\n')
+         # 250 Message OK
+         debug(2, sock_file.readline())
+
+         sock.send('SEND\r\n')
+         # 250 Message Sent Successfully. Msg ID = 692274
+         reply = sock_file.readline()
+         debug(2, reply)
+
+         sock.send('QUIT\r\n')
+         sock.close()
+      except:
+         ret = 0
+      else:
+         if reply.find(self.ok_match) >= 0:
+            ret = 1
+         else:
+            ret = 0
+
+      _debug = 'msg to %s via %s (%s)' % \
+            (self.phone, self.__class__.__name__, ret)
+      debug(1, _debug)
+      return ret
+
+#
+# E-mail trnsport
+#
+
+class EMailTransport(Transport):
+   def __init__(self, phone, message, mail_from=None, gate=None):
+      Transport.__init__(self, phone, message)
+
+      if not mail_from:
+         import getpass
+         try:
+            realuser = getpass.getuser()
+         except AttributeError:
+            # Not all systems have os.environ or getpw...
+            realuser = 'nobody'
+         thishost = socket.getfqdn()
+         mail_from = "%s@%s" % (realuser, thishost)
+         debug(1, mail_from)
+      self.mail_from = mail_from
+
+      self.gate = gate
+
+   def send(self):
+      message = """\
+From: %s
+To: %s@%s
+Subject: SMS
+
+%s
+""" % (self.mail_from, self.phone, self.gate, self.message)
+      self.post(message)
+      debug(2, message)
+      return 1
+
+class SendmailTransport(EMailTransport):
+   def __init__(self, phone, message, mail_from=None, gate=None):
+      EMailTransport.__init__(self, phone, message, mail_from, gate)
+
+      sendmail = self.find_sendmail()
+      if not sendmail:
+         raise ValueError, "cannot find sendmail binary"
+      self.sendmail = sendmail
+
+   def find_sendmail(self):
+      import os
+      for sendmail in ("/usr/lib/sendmail", "/usr/sbin/sendmail"):
+         if os.path.exists(sendmail):
+            return sendmail
+      return None
+
+   def post(self, message):
+      cmd = "%s -oem -oi '%s@%s'" % (self.sendmail, self.phone, self.gate)
+      debug(1, cmd)
+      import os
+      sendmail = os.popen(cmd, 'w')
+      sendmail.write(message)
+      sendmail.close()
+
+class SMTPTransport(EMailTransport):
+   def __init__(self, phone, message, mail_from=None, gate=None, mail_relay="localhost"):
+      EMailTransport.__init__(self, phone, message, mail_from, gate)
+      self.mail_relay = mail_relay
+
+   def post(self, message):
+      debug(1, self.mail_relay)
+      import smtplib
+      smtp = smtplib.SMTP(self.mail_relay)
+      smtp.sendmail(self.mail_from, "%s@%s" % (self.phone, self.gate), message)
+      smtp.close()
+
+
+#
+# Real transports
+#
+
+class MTSru(HTTPTransport):
+   "Russian provider MTS.ru"
+
+   method = "POST"
+   url = "http://www.mts.ru:5051/cgi-bin/cgi.exe"
+   referer = ""
+   ok_match = 'Ваше сообщение отправлено'
+
+   def send(self):
+      dict = {}
+      text = self.calcText()
+      time = self.calcExpiration()
+
+      dict['function'] = 'sms_send'
+      #dict['MMObjectType'] = '0'
+      #dict['MMObjectID'] = ''
+      phone = self.phone
+      if phone[:4] == "8902":
+         phone = "8916%s" % phone[4:]
+      dict['To'] = phone
+      dict['Msg'] = text
+      dict['Hour'] = time.strftime('%k').strip()
+      dict['Min'] = time.strftime('%M')
+      dict['Day'] = time.strftime('%e').strip()
+      dict['Mon'] = str(time.month)
+      dict['Year'] = time.strftime('%Y')
+      dict['count'] = str(len(text))
+      dict['Lang'] = '2'
+
+      return self.post(dict)
+
+   def POST(self, postdata, host, uri, port=80, proxy_host=None, proxy_port=None):
+      postdata = "MMObjectType=0&MMObjectID=&" + postdata
+      return HTTPTransport.POST(self, postdata, host, uri, port, proxy_host, proxy_port)
+
+
+class MTSGSMcom(HTTPTransport):
+   url = "http://www.mtsgsm.com/sms/sent.html"
+   referer = "http://www.mtsgsm.com/sms/"
+   method = "GET"
+   ok_match = 'Ваше сообщение отправлено'
+
+   def send(self):
+      dict = {}
+      text = self.calcText()
+      time = self.calcExpiration()
+
+      dict['Posted'] = '1'
+      dict['To'] = self.phone
+      dict['Msg'] = text
+      dict['count'] = str(len(text))
+      dict['SMSHour'] = time.strftime('%k').strip()
+      dict['SMSMinute'] = time.strftime('%M')
+      dict['SMSDay'] = time.strftime('%e').strip()
+      dict['SMSMonth'] = str(time.month - 1)
+      dict['SMSYear'] = time.strftime('%Y')
+
+      return self.post(dict)
+
+
+class BeeOnLine(CP1251HTTPTransport):
+   "Russian provider BeeOnLine.ru"
+
+   method = "POST"
+   url = "http://www.beeonline.ru/portal/comm/send_sms/simple_send_sms.sms"
+   referer = "http://www.beeonline.ru/portal/comm/send_sms/simple_send_sms.sms"
+   crap = 'BOL '
+   ok_match = koi2win('Ваше сообщение отправлено')
+
+   def __init__(self, phone, message,
+         mode="GSM",      # mode can be either GSM or DAMPS
+         transliterate=1, # turn transliteration of/off
+         reply_to=''):      # send reply to this e-mail
+      Transport.__init__(self, phone, message)
+      if mode not in ('GSM', 'DAMPS'):
+         raise ValueError, "mode (%s) must be either 'GSM' or 'DAMPS'" % mode
+      self.mode = mode
+      self.transliterate = transliterate
+      self.reply_to = reply_to
+
+   def send(self):
+      dict = {}
+      text = self.calcText(smsSize - len(self.crap))
+
+      # hidden
+      #dict['deferto'] = ''
+      #dict['adv_year'] = ''
+      dict['send'] = 'send'
+      dict['destination_number_from'] = 'ordinary' # number, not a BEEpost
+
+      prf = self.phone[:4]
+      if self.mode == "GSM":
+         dict['network_code'] = '3'
+      elif prf == '7095':
+         dict['network_code'] = '2'
+      elif prf == '7901':
+         dict['network_code'] = '1'
+      else:
+         raise RuntimeError, "incorrect combination of mode (%s) and prefix (%s)" % (self.mode, prf)
+
+      dict['phone'] = self.phone[4:]
+      dict['message'] = text
+      dict['mlength'] = str(smsSize - len(self.crap) - len(text))
+
+      if self.mode == "GSM" and not self.transliterate:
+         dict['translit'] = '1' # turn transliteration OFF! :)
+
+      if self.reply_to:
+         dict['send_email'] = '1'
+         dict['reply_addr'] = self.reply_to
+
+      return self.post(dict)
+
+class BeeOnLineSMS(BeeOnLine):
+   "Russian provider BeeOnLine.ru"
+
+   url = "http://www.beeonline.ru/servlet/send/sms/"
+
+   def send(self):
+      dict = {}
+      text = self.calcText(smsSize - len(self.crap))
+
+      dict['prf'] = self.phone[:4]
+      dict['phone'] = self.phone[4:]
+      dict['termtype'] = self.mode[0]
+      dict['message'] = text
+
+      if self.mode == "GSM" and not self.transliterate:
+         dict['translit'] = '1' # turn transliteration OFF! :)
+
+      dict['number_sms'] = "number_sms_send"
+      return self.post(dict)
+
+
+class Pcom(HTTPTransport):
+   "Russian provider PCOM.ru"
+
+   method = "POST"
+   url = "http://www.pcom.ru/online.phtml"
+   referer = "http://www.pcom.ru/online.phtml"
+   crap = ''
+   ok_match = koi2win('поставлено в очередь')
+
+   def calcText(self, maxsize=smsSize):
+      "force translitertaion"
+
+      return HTTPTransport.calcText(self, maxsize=maxsize,
+                           transFunction=rus2lat)
+
+   def send(self):
+      dict = {}
+      text = self.calcText(120 - len(self.crap))
+      expiration = self.calcExpiration()
+      from mx import DateTime
+      now = DateTime.now()
+
+      dict['ACTION'] = 'SENDSMS'
+
+      dict['SMS_START_HOUR'] = now.strftime('%H')
+      dict['SMS_START_MINUTE'] = now.strftime('%M')
+      dict['SMS_START_DAY'] = now.strftime('%d')
+      dict['SMS_START_MONTH'] = now.strftime('%m')
+      dict['SMS_START_YEAR'] = now.strftime('%Y')
+
+      dict['SMS_STOP_HOUR'] = expiration.strftime('%H')
+      dict['SMS_STOP_MINUTE'] = expiration.strftime('%M')
+      dict['SMS_STOP_DAY'] = expiration.strftime('%d')
+      dict['SMS_STOP_MONTH'] = expiration.strftime('%m')
+      dict['SMS_STOP_YEAR'] = expiration.strftime('%Y')
+
+      dict['prefix'] = self.phone[1:4]
+      dict['DN'] = self.phone[4:]
+      dict['MSG'] = text
+
+      return self.post(dict)
+
+
+class ExtelGsmCom(SNPPTransport):
+   "Russian provider Extel-GSM.com"
+
+   host = 'www.extel-gsm.com'
+   port = 4444
+   ok_match = '250 Message Sent Successfully'
+   crap = ''
+
+   prefix = '0119'
+
+   def calcText(self, maxsize=smsSize):
+      "force translitertaion"
+
+      return SNPPTransport.calcText(self, maxsize=maxsize,
+                           transFunction=rus2lat)
+
+   def send(self):
+      dict = {}
+      text = self.calcText(smsSize - len(self.crap))
+      phone = self.phone[5:]
+
+      # 0112 (city code) must be replaced with (0119)
+      dict['phone'] = '+7%s%s' % (self.prefix, phone)
+      dict['message'] = text
+
+      return self.post(dict)
+
+
+class MegafoneMoscow(CP1251HTTPTransport):
+   "Rissian provider megafonmoscow.ru"
+
+   method = "POST"
+   url = "http://www.megafonmoscow.ru/rus/sms.xpml"
+   referer = "http://www.megafonmoscow.ru/rus/sms.xpml"
+   ok_match = koi2win('Сообщение успешно отправлено')
+
+   def send(self):
+      dict = {}
+      text = self.calcText()
+
+      dict['prefix'] = self.phone[:4]
+      dict['addr'] = self.phone[4:]
+      dict['message'] = text
+      dict['send'] = "     Send     "
+
+      if hasattr(self, 'transliterate') and self.transliterate:
+         dict['transliterate'] = '1'
+
+      return self.post(dict)
+
+
+class SkyLinkMsk(CP1251HTTPTransport):
+   "Russian provider SkyLink.Msk.ru"
+
+   method = "POST"
+   url = "http://skylink.msk.ru/inc/sendsms.php"
+   ok_match = koi2win('Сообщение было отправлено')
+
+   def send(self):
+      dict = {}
+      text = self.calcText()
+
+      phone = self.phone
+      if phone[0] == '7':
+         phone = '8' + phone[1:]
+      dict['dest'] = phone
+      dict['smsbody'] = text
+
+      return self.post(dict)
+
+
+BEELINE = BeeOnLineSMS
+#BEELINE = Whizdiary
+MTS = MTSru
+#MTS = MTSGSMcom
+#MTS = Whizdiary
+SONET = Pcom
+EXTEL = ExtelGsmCom
+Megafone = MegafoneMoscow
+SkyLink = SkyLinkMsk
+
+
+Prefix2Provider = {
+   "903": BEELINE,
+   "905": BEELINE,
+   "906": BEELINE,
+
+   "910": MTS,
+   "916": MTS,
+
+   "926": Megafone
+}
+
+
+class SMSError(Exception): pass
+
+def Phone2Provider(phone):
+   prefix = phone[1:4]
+
+   if prefix in ("095", "901"): # 901 is being used by Beeline and SkyLink
+      raise SMSError, "unknown provider for phone %s" % phone
+
+   if Prefix2Provider.has_key(prefix):
+      return Prefix2Provider[prefix]
+
+   raise SMSError, "bad prefix for phone %s" % phone
diff --git a/m_lib/net/www/__init__.py b/m_lib/net/www/__init__.py
new file mode 100644 (file)
index 0000000..27e9a0b
--- /dev/null
@@ -0,0 +1,6 @@
+"Broytman WWW Library for Python, Copyright (C) 1996-2002 PhiloSoft Design"
+
+
+__all__ = [
+   "util", "dtml", "url_lib", "html", "xml", "serverpush"
+]
diff --git a/m_lib/net/www/dtml.py b/m_lib/net/www/dtml.py
new file mode 100644 (file)
index 0000000..f8f42ee
--- /dev/null
@@ -0,0 +1,17 @@
+"DTML utilities"
+
+class standard_html: # Base class for using with ZTemplates
+   def __init__(self, title):
+      self.standard_html_header = """<HTML>
+   <HEAD>
+      <TITLE>
+         %s
+      </TITLE>
+   </HEAD>
+
+<BODY>""" % title
+
+      self.standard_html_footer = """</BODY>
+</HTML>"""
+
+      self.title = title
diff --git a/m_lib/net/www/html.py b/m_lib/net/www/html.py
new file mode 100644 (file)
index 0000000..9a9ef6f
--- /dev/null
@@ -0,0 +1,145 @@
+"HTML parsers"
+
+
+from HTMLParser import HTMLParser as _HTMLParser
+from htmlentitydefs import entitydefs
+
+
+def join_attrs(attrs):
+   attr_list = ['']
+   for attrname, value in attrs:
+      if value is None:
+         attr_list.append('%s' % attrname)
+      else:
+         attr_list.append('%s="%s"' % (attrname, value.strip()))
+
+   return ' '.join(attr_list)
+
+
+class HTMLParser(_HTMLParser):
+
+
+   def __init__(self):
+      _HTMLParser.__init__(self)
+      self.accumulator = ""
+
+
+   def handle_starttag(self, tag, attrs):
+      try:
+         method = getattr(self, 'start_' + tag)
+      except AttributeError:
+         try:
+            method = getattr(self, 'do_' + tag)
+         except AttributeError:
+            self.unknown_starttag(tag, attrs)
+         else:
+            method(attrs)
+      else:
+         method(attrs)
+
+   def handle_endtag(self, tag):
+      try:
+         method = getattr(self, 'end_' + tag)
+      except AttributeError:
+         self.unknown_endtag(tag)
+      else:
+         method()
+
+
+   def handle_data(self, data):
+      if data:
+         self.accumulator = "%s%s" % (self.accumulator, data)
+
+   def handle_comment(self, data):
+      if data:
+         self.accumulator = "%s<!--%s-->" % (self.accumulator, data)
+
+
+   def handle_charref(self, name):
+      self.accumulator = "%s&#%s;" % (self.accumulator, name)
+
+   def handle_entityref(self, name):
+      if entitydefs.has_key(name): # If it is one of the standard SGML entities - close it with semicolon
+         x = ';'
+      else:
+         x = ''
+      self.accumulator = "%s&%s%s" % (self.accumulator, name, x)
+
+
+   # Pass other tags unmodified
+   def unknown_starttag(self, tag, attrs):
+      self.accumulator = "%s<%s%s>" % (self.accumulator, tag, join_attrs(attrs))
+
+   def unknown_endtag(self, tag):
+      self.accumulator = "%s</%s>" % (self.accumulator, tag)
+
+
+# Additional classes for filters
+
+class _allowStartTag:
+    def __init__(self, filter, tag):
+        self.filter = filter
+        self.tag = tag
+
+    def __call__(self, attrs):
+        filter = self.filter
+        filter.accumulator = "%s<%s%s>" % (filter.accumulator, self.tag, join_attrs(attrs))
+
+class _allowEndTag:
+    def __init__(self, filter, tag):
+        self.filter = filter
+        self.tag = tag
+
+    def __call__(self):
+        filter = self.filter
+        filter.accumulator = "%s</%s>" % (filter.accumulator, self.tag)
+
+
+class HTMLFilter(HTMLParser):
+   allowStartTagClass = _allowStartTag
+   allowEndTagClass = _allowEndTag
+
+   def handle_comment(self, data):
+      pass
+
+   # Filter out all tags
+   def unknown_starttag(self, tag, attrs):
+      pass
+
+   def unknown_endtag(self, tag):
+      pass
+
+
+   def allow_startTag(self, tag):
+      setattr(self, "start_%s" % tag, self.allowStartTagClass(self, tag))
+
+   def allow_endTag(self, tag):
+      setattr(self, "end_%s" % tag, self.allowEndTagClass(self, tag))
+
+
+# how to use them:
+
+#class DocHTMLFilter(HTMLFilter):
+#    def __init__(self):
+#        HTMLFilter.__init__(self)
+#
+#        # allow tags <table>, <tr>, <td>
+#        # ... and closing tags
+#
+#        self.allow_startTag('table')
+#        self.allow_endTag('table')
+#
+#        self.allow_startTag('tr')
+#        self.allow_endTag('tr')
+#
+#        self.allow_startTag('td')
+#        self.allow_endTag('td')
+
+def filter_html(str, filter=None):
+    "Process HTML using some HTML parser/filter"
+
+    if filter is None:
+       filter = HTMLFilter()
+
+    filter.feed(str)
+    return filter.accumulator
diff --git a/m_lib/net/www/serverpush.py b/m_lib/net/www/serverpush.py
new file mode 100644 (file)
index 0000000..11f3c65
--- /dev/null
@@ -0,0 +1,28 @@
+"Server Push"
+
+
+import sys, mimetools
+
+class ServerPush:
+   def __init__(self, out=sys.stdout):
+      self.out = out
+      self.output = out.write
+      self.boundary = mimetools.choose_boundary()
+
+
+   def start(self):
+      self.output("""\
+Content-type: multipart/x-mixed-replace;boundary=%s
+
+""" % self.boundary)
+
+
+   def next(self, content_type="text/html"):
+      self.output("""\
+--%s
+Content-type: %s
+""" % (self.boundary, content_type))
+
+
+   def stop(self):
+      self.output("--%s--" % self.boundary)
diff --git a/m_lib/net/www/url_lib.py b/m_lib/net/www/url_lib.py
new file mode 100644 (file)
index 0000000..2ba2d4b
--- /dev/null
@@ -0,0 +1,21 @@
+"url_lib"
+
+
+from urllib import FancyURLopener
+
+class NoAsk_URLopener(FancyURLopener):
+   "URL opener that does not ask for a password interactively"
+
+   def __init__(self, username, password):
+      FancyURLopener.__init__(self)
+
+      self.username = username
+      self.password = password
+      self._asked = 0
+
+   def prompt_user_passwd(self, host, realm):
+      if self._asked:
+         return None, None
+
+      self._asked = 1
+      return self.username, self.password
diff --git a/m_lib/net/www/util.py b/m_lib/net/www/util.py
new file mode 100644 (file)
index 0000000..28b8f09
--- /dev/null
@@ -0,0 +1,84 @@
+"Common WWW/CGI utilities"
+
+
+import sys, os
+
+
+def exception(str = ""):
+   if sys.exc_type == SystemExit: # pass exit() normally
+      return
+   print "Content-Type: text/html"
+   print # Terminate HTTP headers
+
+   import html
+   print html.exception()
+
+   if str:
+      print str
+
+   sys.exit(1)
+
+
+def error(err_str):
+   if not err_str:
+      err_str = "Unknown error"
+
+   print "Content-Type: text/html"
+   print # Terminate HTTP headers
+
+   print str(err_str)
+   sys.exit(1)
+
+
+def get_redirect(_str = ""):
+   server_name = os.environ["SERVER_NAME"]
+   server_port = os.environ["SERVER_PORT"]
+
+   if server_port == "80":
+      server_port = ""
+   else:
+      server_port = ":" + server_port
+
+   return "http://" + server_name + server_port + _str
+
+
+def convert_empty(estr):
+   if estr:
+      _str = str(estr)
+   else:
+      _str = "&nbsp;"
+   return _str
+
+
+def gen_html(title, body):
+   print """
+   <HTML>
+      <HEAD>
+         <TITLE>
+            %s
+         </TITLE>
+      </HEAD>
+
+      <BODY>
+         %s
+      </BODY>
+   </HTML>
+   """ % (title, body)
+
+
+def mkexpires(hours=1, minutes=0, seconds=0):
+   from datetime import datetime, timedelta
+   expire = datetime.now() + timedelta(hours=hours, minutes=minutes, seconds=seconds)
+   return "Expires: %s" % expire.strftime("%a, %d %b %Y %H:%M:%S GMT")
+
+
+def parse_time(t):
+   import time
+   for format in ("%a, %d %b %Y %H:%M:%S GMT", "%A, %d-%b-%y %H:%M:%S GMT", "%A, %d-%b-%Y %H:%M:%S GMT"):
+      try:
+         return time.mktime(time.strptime(t, format)) - time.timezone
+      except (ValueError, OverflowError):
+         pass
+
+   return None
diff --git a/m_lib/net/www/xml.py b/m_lib/net/www/xml.py
new file mode 100644 (file)
index 0000000..532afb6
--- /dev/null
@@ -0,0 +1,58 @@
+"XML parsers"
+
+
+import re, xmllib
+illegal = re.compile('[^\t\r\n -\377]') # illegal chars in content
+xmllib.illegal = illegal # allow cyrillic characters in XML
+
+
+def join_xml_attrs(attrs):
+   attr_list = ['']
+   for attrname, value in attrs.items():
+      attr_list.append('%s="%s"' % (attrname, string.strip(value)))
+
+   return string.join(attr_list, " ")
+
+
+class XMLParser(xmllib.XMLParser):
+   def __init__(self):
+      xmllib.XMLParser.__init__(self)
+      self.accumulator = ""
+
+
+   def handle_data(self, data):
+      if data:
+         self.accumulator = "%s%s" % (self.accumulator, data)
+
+   def handle_comment(self, data):
+      if data:
+         self.accumulator = "%s<!--%s-->" % (self.accumulator, data)
+
+
+   # Pass other tags unmodified
+   def unknown_starttag(self, tag, attrs):
+      self.accumulator = "%s<%s%s>" % (self.accumulator, tag, join_xml_attrs(attrs))
+
+   def unknown_endtag(self, tag):
+      self.accumulator = "%s</%s>" % (self.accumulator, tag)
+
+
+class XMLFilter(XMLParser):
+   def handle_comment(self, data):
+      pass
+
+   # Filter out all tags
+   def unknown_starttag(self, tag, attrs):
+      pass
+
+   def unknown_endtag(self, tag):
+      pass
+
+def filter_xml(str, filter=None):
+    "Process XML using some XML parser/filter"
+
+    if filter is None:
+       filter = XMLFilter()
+
+    filter.feed(str)
+    return filter.accumulator
diff --git a/m_lib/opdate.py b/m_lib/opdate.py
new file mode 100755 (executable)
index 0000000..ac53c81
--- /dev/null
@@ -0,0 +1,480 @@
+#! /usr/bin/env python
+# -*- coding: koi8-r -*-
+
+#
+# opDate - date/time manipulation routines
+# Some ideas came from Turbo Professional/Object Professional (t/o)pDate.PAS
+#
+# Written by Broytman, Nov 1997 - Dec 2003
+# Copyright (C) 1997-2003 PhiloSoft Design
+#
+
+
+from string import *
+from time import *
+from calendar import *
+from opstring import *
+
+
+MinYear = 1600
+MaxYear = 3999
+MinDate = 0x00000000 # = 01/01/1600
+MaxDate = 0x000D6025 # = 12/31/3999
+Date1900 = 0x0001AC05 # = 01/01/1900
+Date1980 = 0x00021E28 # = 01/01/1980
+Date2000 = 0x00023AB1 # = 01/01/2000
+BadDate = 0x7FFFFFFF
+
+Threshold2000 = 1900
+
+MinTime = 0          # = 00:00:00 am
+MaxTime = 86399      # = 23:59:59 pm
+BadTime = 0x7FFFFFFF
+
+SecondsInDay = 86400 # number of seconds in a day
+SecondsInHour = 3600 # number of seconds in an hour
+SecondsInMinute = 60 # number of seconds in a minute
+HoursInDay = 24      # number of hours in a day
+MinutesInHour = 60   # number of minutes in an hour
+
+First2Months = 59    # 1600 was a leap year
+FirstDayOfWeek = 5   # 01/01/1600 was a Saturday
+
+
+# Errors
+class opdate_error(Exception):
+   pass
+
+
+#
+### Date manipulation routines
+#
+
+def IsLeapYear(Year):
+   if ( (Year % 4 == 0) and (Year % 4000 <> 0) and ((Year % 100 <> 0) or (Year % 400 == 0)) ):
+      return True
+   return False
+
+
+def _setYear(Year):
+   # Internal function
+   if Year < 100:
+      Year = Year + 1900
+      if Year < Threshold2000:
+         Year = Year + 100
+   return Year
+
+
+def DaysInMonth(Month, Year):
+   """ Return the number of days in the specified month of a given year """
+   if Month in [1, 3, 5, 7, 8, 10, 12]:
+      return 31
+
+   elif Month in [4, 6, 9, 11]:
+      return 30
+
+   elif Month == 2:
+      return 28+IsLeapYear(_setYear(Year))
+
+   else:
+      raise opdate_error, "bad month `%s'" % str(Month)
+
+
+def ValidDate(Day, Month, Year):
+   """ Verify that day, month, year is a valid date """
+   Year = _setYear(Year)
+
+   if (Day < 1) or (Year < MinYear) or (Year > MaxYear):
+      return False
+   elif (Month >= 1) and (Month <= 12):
+         return Day <= DaysInMonth(Month, Year)
+   else:
+      return False
+
+
+def DMYtoDate(Day, Month, Year):
+   """ Convert from day, month, year to a julian date """
+   Year = _setYear(Year)
+
+   if not ValidDate(Day, Month, Year):
+      return BadDate
+
+   if (Year == MinYear) and (Month < 3):
+      if Month == 1:
+         return Day-1
+      else:
+         return Day+30
+   else:
+      if Month > 2:
+         Month = Month - 3
+      else:
+         Month = Month + 9
+         Year = Year - 1
+      Year = Year - MinYear
+
+      return (((Year / 100)*146097) / 4) + (((Year % 100)*1461) / 4) + (((153*Month)+2) / 5)+Day+First2Months
+
+
+def DateToDMY(Julian):
+   """ Convert from a julian date to day, month, year """
+   if Julian == BadDate:
+      return 0, 0, 0
+
+   if Julian <= First2Months:
+      Year = MinYear
+      if Julian <= 30:
+         Month = 1
+         Day = Julian + 1
+      else:
+         Month = 2
+         Day = Julian-30
+   else:
+      I = (4*(Julian-First2Months))-1
+      J = (4*((I % 146097) / 4))+3
+      Year = (100*(I / 146097))+(J / 1461)
+      I = (5*(((J % 1461)+4) / 4))-3
+      Month = I / 153
+      Day = ((I % 153)+5) / 5
+      if Month < 10:
+         Month = Month + 3
+      else:
+         Month = Month - 9
+         Year = Year + 1
+      Year = Year + MinYear
+
+   return Day, Month, Year
+
+
+def IncDate(Julian, Days, Months, Years):
+   """ Add (or subtract) the number of months, days, and years to a date.
+       Months and years are added before days. No overflow/underflow checks are made
+   """
+   Day, Month, Year = DateToDMY(Julian)
+   Day28Delta = Day-28
+   if Day28Delta < 0:
+      Day28Delta = 0
+   else:
+      Day = 28
+
+   Year = Year + Years
+   Year = Year + Months / 12
+   Month = Month + Months % 12
+   if Month < 1:
+      Month = Month + 12
+      Year = Year - 1
+   elif Month > 12:
+      Month = Month - 12
+      Year = Year + 1
+
+   Julian = DMYtoDate(Day, Month, Year)
+   if Julian <> BadDate:
+      Julian = Julian + Days + Day28Delta
+
+   return Julian
+
+
+def IncDateTrunc(Julian, Months, Years):
+   """ Add (or subtract) the specified number of months and years to a date """
+   Day, Month, Year = DateToDMY(Julian)
+   Day28Delta = Day-28
+   if Day28Delta < 0:
+      Day28Delta = 0
+   else:
+      Day = 28
+
+   Year = Year + Years
+   Year = Year + Months / 12
+   Month = Month + Months % 12
+   if Month < 1:
+      Month = Month + 12
+      Year = Year - 1
+   elif Month > 12:
+      Month = Month - 12
+      Year = Year + 1
+
+   Julian = DMYtoDate(Day, Month, Year)
+   if Julian <> BadDate:
+      MaxDay = DaysInMonth(Month, Year)
+      if Day+Day28Delta > MaxDay:
+         Julian = Julian + MaxDay-Day
+      else:
+         Julian = Julian + Day28Delta
+
+   return Julian
+
+
+def DateDiff(Date1, Date2):
+   """ Return the difference in days,months,years between two valid julian dates """
+   #we want Date2 > Date1
+   if Date1 > Date2:
+      _tmp = Date1
+      Date1 = Date2
+      Date2 = _tmp
+
+   #convert dates to day,month,year
+   Day1, Month1, Year1 = DateToDMY(Date1)
+   Day2, Month2, Year2 = DateToDMY(Date2)
+
+   #days first
+   if Day2 < Day1:
+      Month2 = Month2 - 1
+      if Month2 == 0:
+         Month2 = 12
+         Year2 = Year2 - 1
+      Day2 = Day2 + DaysInMonth(Month2, Year2)
+   Days = abs(Day2-Day1)
+
+   #now months and years
+   if Month2 < Month1:
+      Month2 = Month2 + 12
+      Year2 = Year2 - 1
+   Months = Month2-Month1
+   Years = Year2-Year1
+
+   return Days, Months, Years
+
+
+def DayOfWeek(Julian):
+   """ Return the day of the week for the date. Returns DayType(7) if Julian == BadDate. """
+   if Julian == BadDate:
+      raise opdate_error, "bad date `%s'" % str(Julian)
+   else:
+      return (Julian+FirstDayOfWeek) % 7
+
+
+def DayOfWeekDMY(Day, Month, Year):
+   """ Return the day of the week for the day, month, year """
+   return DayOfWeek( DMYtoDate(Day, Month, Year) )
+
+
+#def MonthStringToMonth(MSt):
+#   """ Convert the month name in MSt to a month (1..12) or -1 on error """
+#   lmn = strptime.LongMonthNames[strptime.LANGUAGE]
+#   smn = strptime.ShortMonthNames[strptime.LANGUAGE]
+#   lmna = LongMonthNamesA
+#
+#   I = FindStr(MSt, lmn)+1 or FindStr(MSt, smn)+1 or \
+#      FindStrUC(MSt, lmn)+1 or FindStrUC(MSt, smn)+1 or \
+#      FindStr(MSt, lmna)+1 or FindStrUC(MSt, lmna)+1
+#
+#   return I-1
+
+
+def Today():
+   """ Returns today's date as a julian """
+   Year, Month, Day = localtime(time())[0:3]
+   return DMYtoDate(Day, Month, Year)
+
+#
+### Time manipulation routines
+#
+
+def TimeToHMS(T):
+   """ Convert a Time variable to Hours, Minutes, Seconds """
+   if T == BadTime:
+      return 0, 0, 0
+
+   else:
+      Hours = T / SecondsInHour
+      T = T - Hours*SecondsInHour
+      Minutes = T / SecondsInMinute
+      T = T - Minutes*SecondsInMinute
+      Seconds = T
+
+      return Hours, Minutes, Seconds
+
+
+def HMStoTime(Hours, Minutes, Seconds):
+   """ Convert Hours, Minutes, Seconds to a Time variable """
+   Hours = Hours % HoursInDay
+   T = Hours*SecondsInHour + Minutes*SecondsInMinute + Seconds
+
+   return T % SecondsInDay
+
+
+def ValidTime(Hours, Minutes, Seconds):
+   """ Return true if Hours:Minutes:Seconds is a valid time """
+   return (0 <= Hours < 24) and (0 <= Minutes < 60) and (0 <= Seconds < 60)
+
+
+def CurrentTime():
+   """ Returns current time in seconds since midnight """
+   Hours, Minutes, Seconds = localtime(time())[3:6]
+   return HMStoTime(Hours, Minutes, Seconds)
+
+
+def TimeDiff(Time1, Time2):
+   """ Return the difference in hours,minutes,seconds between two times """
+   if Time1 > Time2:
+      T = Time1-Time2
+   else:
+      T = Time2-Time1
+
+   Hours, Minutes, Seconds = TimeToHMS(T)
+   return Hours, Minutes, Seconds
+
+
+def IncTime(T, Hours, Minutes, Seconds):
+   """ Add the specified hours,minutes,seconds to T and return the result """
+   T = T + HMStoTime(Hours, Minutes, Seconds)
+   return T % SecondsInDay
+
+
+def DecTime(T, Hours, Minutes, Seconds):
+   """ Subtract the specified hours,minutes,seconds from T and return the result """
+   Hours = Hours % HoursInDay
+   T = T - HMStoTime(Hours, Minutes, Seconds)
+   if T < 0:
+      return T+SecondsInDay
+   else:
+      return T
+
+
+def RoundToNearestHour(T, Truncate = False):
+   """ Round T to the nearest hour, or Truncate minutes and seconds from T """
+   Hours, Minutes, Seconds = TimeToHMS(T)
+   Seconds = 0
+
+   if not Truncate:
+      if Minutes >= (MinutesInHour / 2):
+         Hours = Hours + 1
+
+   Minutes = 0
+   return HMStoTime(Hours, Minutes, Seconds)
+
+
+def RoundToNearestMinute(T, Truncate = False):
+   """ Round T to the nearest minute, or Truncate seconds from T """
+   Hours, Minutes, Seconds = TimeToHMS(T)
+
+   if not Truncate:
+      if Seconds >= (SecondsInMinute / 2):
+         Minutes = Minutes + 1
+
+   Seconds = 0
+   return HMStoTime(Hours, Minutes, Seconds)
+
+
+def DateTimeDiff(DT1, DT2):
+   """ Return the difference in days,seconds between two points in time """
+   # swap if DT1 later than DT2
+   if (DT1[0] > DT2[0]) or ((DT1[0] == DT2[0]) and (DT1[1] > DT2[1])):
+      _tmp = DT1
+      DT1 = DT2
+      DT2 = _tmp
+
+   # the difference in days is easy
+   Days = DT2[0]-DT1[0]
+
+   # difference in seconds
+   if DT2[1] < DT1[1]:
+      # subtract one day, add 24 hours
+      Days = Days - 1
+      DT2[1] = DT2[1] + SecondsInDay
+
+   Secs = DT2[1]-DT1[1]
+   return Days, Secs
+
+
+def IncDateTime(DT1, Days, Secs):
+   """ Increment (or decrement) DT1 by the specified number of days and seconds
+      and put the result in DT2 """
+   DT2 = DT1[:]
+
+   # date first
+   DT2[0] = DT2[0] + Days
+
+   if Secs < 0:
+      # change the sign
+      Secs = -Secs
+
+      # adjust the date
+      DT2[0] = DT2[0] - Secs / SecondsInDay
+      Secs = Secs % SecondsInDay
+
+      if Secs > DT2[1]:
+         # subtract a day from DT2[0] and add a day's worth of seconds to DT2[1]
+         DT2[0] = DT2[0] - 1
+         DT2[1] = DT2[1] + SecondsInDay
+
+      # now subtract the seconds
+      DT2[1] = DT2[1] - Secs
+
+   else:
+      # increment the seconds
+      DT2[1] = DT2[1] + Secs
+
+      # adjust date if necessary
+      DT2[0] = DT2[0] + DT2[1] / SecondsInDay
+
+      # force time to 0..SecondsInDay-1 range
+      DT2[1] = DT2[1] % SecondsInDay
+
+   return DT2
+
+
+#
+### UTC (GMT) stuff
+#
+
+UTC_0Date = DMYtoDate(1, 1, 1970)
+
+
+def DateTimeToGMT(Date, Time = False):
+   Date = Date - UTC_0Date
+   return Date*SecondsInDay + Time
+
+
+def GMTtoDateTime(GMT):
+   q, r = divmod(GMT, SecondsInDay)
+   return q + UTC_0Date, r
+
+
+#
+### Cyrillic stuff
+#
+
+LongMonthNamesA = ['Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня',
+   'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря']
+
+
+#
+### Test stuff
+#
+
+def test():
+   print "Is 1984 leap year?", IsLeapYear(1984)
+   print "Is 1990 leap year?", IsLeapYear(1990)
+
+   print "Days in month 8 year 1996:", DaysInMonth(8, 1996)
+
+   print "Is date 8/12/1996 valid?", ValidDate(8, 12, 1996)
+   print "Is date 40/11/1996 valid?", ValidDate(40, 11, 1996)
+   print "Is date 8/14/1996 valid?", ValidDate(8, 14, 1996)
+
+   print "Date->DMY for 138219:", DateToDMY(138219)
+
+   diff = DateDiff(DMYtoDate(12, 10, 1996), DMYtoDate(12, 10, 1997))
+   print "Date 12/10/1996 and date 12/10/1997 diff: %d years, %d months, %d days" % (diff[2], diff[1], diff[0])
+
+   diff = DateDiff(DMYtoDate(12, 10, 1996), DMYtoDate(12, 11, 1997))
+   print "Date 12/10/1996 and date 12/11/1997 diff: %d years, %d months, %d days" % (diff[2], diff[1], diff[0])
+
+   diff = DateDiff(DMYtoDate(31, 1, 1996), DMYtoDate(1, 3, 1996))
+   print "Date 31/01/1996 and date 01/03/1996 diff: %d years, %d months, %d days" % (diff[2], diff[1], diff[0])
+
+
+   #print "November is %dth month" % MonthStringToMonth("November")
+
+   print "Today is", Today()
+   print "Now is", CurrentTime()
+
+   print "My birthday 21 Dec 1967 is (must be Thursday):", day_name[DayOfWeekDMY(21, 12, 67)]
+
+   gmt = DateTimeToGMT(DMYtoDate(21, 12, 1967), HMStoTime(23, 45, 0))
+   print "21 Dec 1967, 23:45:00 --", gmtime(gmt) # DOS version of gmtime has error processing dates before 1/1/1970 :(
+   D, T = GMTtoDateTime(gmt)
+   print "(gmt) --", DateToDMY(D), TimeToHMS(T)
+
+if __name__ == "__main__":
+   test()
diff --git a/m_lib/opstring.py b/m_lib/opstring.py
new file mode 100755 (executable)
index 0000000..92d193b
--- /dev/null
@@ -0,0 +1,158 @@
+#! /usr/bin/env python
+# -*- coding: koi8-r -*-
+
+#
+# opString - string/pathnames manipulation routines
+# Some ideas came from Turbo Professional/Object Professional (t/o)pString.PAS
+#
+# Written by Broytman, Nov 1997. Copyright (C) 1997 PhiloSoft Design
+#
+
+
+from string import *
+
+
+#
+### Int/string conversion routines
+#
+
+
+def bin(i):
+   """
+      Convert integer to binary string.
+   """
+   s = ''
+   q = i
+   while (1):
+      q, r = divmod(q, 2)
+      s = digits[r] + s
+      if q == 0: break
+
+   return s
+
+
+#
+### String manipulation routines
+#
+
+
+def PadCh(S, Ch, Len):
+   """ Return a string right-padded to length Len with Ch """
+   if len(S) >= Len:
+      return S
+   else:
+      return S + Ch*(Len - len(S))
+
+
+def Pad(S, Len):
+   """ Return a string right-padded to length len with blanks """
+   return PadCh(S, ' ', Len)
+
+
+def LeftPadCh(S, Ch, Len):
+   """ Return a string left-padded to length len with ch """
+   if len(S) >= Len:
+      return S
+   else:
+      return Ch*(Len - len(S)) + S
+
+
+def LeftPad(S, Len):
+   """ Return a string left-padded to length len with blanks """
+   return LeftPadCh(S, ' ', Len)
+
+
+def CenterCh(S, Ch, Width):
+   """ Return a string centered in a string of Ch with specified width """
+   if len(S) >= Width:
+      return S
+   else:
+      l = (Width - len(S)) / 2
+      r = Width - len(S) - l
+      return Ch*l + S + Ch*r
+
+
+def Center(S, Width):
+   """ Return a string centered in a blank string of specified width """
+   return CenterCh(S, ' ', Width)
+
+
+def FindStr(str, list):
+   """ Find given string in the list of strings """
+   for i in range(len(list)):
+      if str == list[i]:
+         return i
+
+   return -1
+
+
+def FindStrUC(str, list):
+   """ Find string ignoring case """
+   str = upper(str)
+   for i in range(len(list)):
+      if str == upper(list[i]):
+         return i
+
+   return -1
+
+
+# Склонения
+
+transl_adict = {
+   "day"  : ["день", "дня", "дней"],
+   "week" : ["неделя", "недели", "недель"],
+   "month": ["месяц", "месяца", "месяцев"],
+   "year" : ["год", "года", "лет"]
+}
+
+transl_adict["days"] = transl_adict["day"]
+transl_adict["weeks"] = transl_adict["week"]
+transl_adict["months"] = transl_adict["month"]
+transl_adict["years"] = transl_adict["year"]
+
+transl_vdict = {
+   1: 0,
+   2: 1, 3: 1, 4: 1,
+   5: 2, 6: 2, 7: 2, 8: 2, 9: 2, 0: 2
+}
+
+def translate_a(val, id):
+   if not transl_adict.has_key(id):
+      return ''
+
+   if 5 <= (val % 100) <= 20:
+      val = 2
+   else:
+      val = transl_vdict[val % 10]
+   return transl_adict[id][val]
+
+
+# Encodings, especially cyrillic. Requires Unicode, hence Python 2.0+
+
+def recode(s, from_encoding, to_encoding, errors = "strict"):
+   return unicode(s, from_encoding, errors).encode(to_encoding, errors)
+
+
+def win2koi(s, errors = "strict"):
+   return recode(s, "cp1251", "koi8-r", errors)
+
+def koi2win(s, errors = "strict"):
+   return recode(s, "koi8-r", "cp1251", errors)
+
+
+#
+### Test stuff
+#
+
+def test():
+   print "bin(0x6) =", bin(0x6)
+   print "bin(0xC) =", bin(0xC)
+
+   print "'Test' left-padded :", LeftPad("Test", 20)
+   print "'Test' right-padded:", PadCh("Test", '*', 20)
+   print "'Test' centered    :", CenterCh("Test", '=', 20)
+
+   print "'Олег':", koi2win(win2koi("Олег"))
+
+if __name__ == "__main__":
+   test()
diff --git a/m_lib/rus/__init__.py b/m_lib/rus/__init__.py
new file mode 100644 (file)
index 0000000..a4f13fa
--- /dev/null
@@ -0,0 +1,6 @@
+"Broytman WWW Library for Python, Copyright (C) 1996-2003 PhiloSoft Design"
+
+
+__all__ = [
+   "rus2lat", "lat2rus"
+]
diff --git a/m_lib/rus/lat2rus.py b/m_lib/rus/lat2rus.py
new file mode 100755 (executable)
index 0000000..abf8af3
--- /dev/null
@@ -0,0 +1,116 @@
+#! /usr/bin/env python
+# -*- coding: koi8-r -*-
+
+#
+# Lat -> Rus translation
+# Written by Broytman. Copyright (C) 2002 PhiloSoft Design
+#
+
+lat2koi_d = {
+   "q": "й",
+   "w": "ц",
+   "e": "у",
+   "r": "к",
+   "t": "е",
+   "y": "н",
+   "u": "г",
+   "i": "ш",
+   "o": "щ",
+   "p": "з",
+   "[": "х",
+   "]": "ъ",
+   "a": "ф",
+   "s": "ы",
+   "d": "в",
+   "f": "а",
+   "g": "п",
+   "h": "р",
+   "j": "о",
+   "k": "л",
+   "l": "д",
+   ";": "ж",
+   "'": "э",
+   "z": "я",
+   "x": "ч",
+   "c": "с",
+   "v": "м",
+   "b": "и",
+   "n": "т",
+   "m": "ь",
+   ",": "б",
+   ".": "ю",
+   "Q": "Й",
+   "W": "Ц",
+   "E": "У",
+   "R": "К",
+   "T": "Е",
+   "Y": "Н",
+   "U": "Г",
+   "I": "Ш",
+   "O": "Щ",
+   "P": "З",
+   "{": "Х",
+   "}": "Ъ",
+   "A": "Ф",
+   "S": "Ы",
+   "D": "В",
+   "F": "А",
+   "G": "П",
+   "H": "Р",
+   "J": "О",
+   "K": "Л",
+   "L": "Д",
+   ":": "Ж",
+   "\"": "Э",
+   "Z": "Я",
+   "X": "Ч",
+   "C": "С",
+   "V": "М",
+   "B": "И",
+   "N": "Т",
+   "M": "Ь",
+   "<": "Б",
+   ">": "Ю",
+   "`": "ё",
+   "~": "Ё",
+   "!": "!",
+   "@": "\"",
+   "#": "#",
+   "$": "*",
+   "%": ":",
+   "^": ",",
+   "&": ".",
+   "*": ";"
+}
+
+
+def make_lat2xxx(encoding="cp1251"):
+   d = {}
+   for k, v in lat2koi_d.items():
+      v = unicode(v, "koi8-r").encode(encoding)
+      d[k] = v
+   return d
+
+
+from m_lib.lazy.dict import LazyDictInitFunc
+lat2win_d = LazyDictInitFunc(make_lat2xxx, encoding="cp1251")
+
+
+def lat2rus(instr, lat2rus_d = lat2koi_d):
+   out = []
+   for c in instr:
+      out.append(lat2rus_d.get(c, c))
+   return ''.join(out)
+
+
+lat2koi = lat2rus
+
+def lat2win(instr):
+   return lat2rus(instr, lat2win_d)
+
+
+if __name__ == "__main__":
+   Test = "Ghbdtn nt,t^ ghtrhfcysq vbh!"
+   print "Test:", Test
+   print "Тест:", lat2koi(Test)
+   print "Тест:", unicode(lat2win(Test), "cp1251").encode("koi8-r")
diff --git a/m_lib/rus/rus2lat.py b/m_lib/rus/rus2lat.py
new file mode 100755 (executable)
index 0000000..dda03da
--- /dev/null
@@ -0,0 +1,105 @@
+#! /usr/bin/env python
+# -*- coding: koi8-r -*-
+
+#
+# Rus -> Lat transliteration (koi2lat and win2lat)
+# Written by Broytman. Copyright (C) 1997-2002 PhiloSoft Design
+#
+
+koi2lat_d = {
+   "А": "A",
+   "Б": "B",
+   "В": "V",
+   "Г": "G",
+   "Д": "D",
+   "Е": "E",
+   "Ж": "Zh",
+   "З": "Z",
+   "И": "I",
+   "Й": "Y",
+   "К": "K",
+   "Л": "L",
+   "М": "M",
+   "Н": "N",
+   "О": "O",
+   "П": "P",
+   "Р": "R",
+   "С": "S",
+   "Т": "T",
+   "У": "U",
+   "Ф": "F",
+   "Х": "H",
+   "Ц": "Ts",
+   "Ч": "Ch",
+   "Ш": "Sh",
+   "Щ": "Sh",
+   "Ъ": "'",
+   "Ь": "'",
+   "Ы": "Y",
+   "Э": "E",
+   "Ю": "Yu",
+   "Я": "Ya",
+   "а": "a",
+   "б": "b",
+   "в": "v",
+   "г": "g",
+   "д": "d",
+   "е": "e",
+   "ж": "zh",
+   "з": "z",
+   "и": "i",
+   "й": "y",
+   "к": "k",
+   "л": "l",
+   "м": "m",
+   "н": "n",
+   "о": "o",
+   "п": "p",
+   "р": "r",
+   "с": "s",
+   "т": "t",
+   "у": "u",
+   "ф": "f",
+   "х": "h",
+   "ц": "ts",
+   "ч": "ch",
+   "ш": "sh",
+   "щ": "sh",
+   "ъ": "'",
+   "ь": "'",
+   "ы": "y",
+   "э": "e",
+   "ю": "yu",
+   "я": "ya"
+}
+
+def make_xxx2lat(encoding="cp1251"):
+   d = {}
+   for k, v in koi2lat_d.items():
+      k = unicode(k, "koi8-r").encode(encoding)
+      d[k] = v
+   return d
+
+
+from m_lib.lazy.dict import LazyDictInitFunc
+win2lat_d = LazyDictInitFunc(make_xxx2lat, encoding="cp1251")
+
+
+def rus2lat(instr, rus2lat_d = koi2lat_d):
+   out = []
+   for c in instr:
+      out.append(rus2lat_d.get(c, c))
+   return ''.join(out)
+
+
+koi2lat = rus2lat
+
+def win2lat(instr):
+   return rus2lat(instr, win2lat_d)
+
+
+if __name__ == "__main__":
+   Test = "Щербаков Игорь Григорьевич. АБВ xyz абв ЬЬЭЮЯ ъьэюя"
+   print "Test:", Test
+   print "Тест:", koi2lat(Test)
+   print "Тест:", win2lat(unicode(Test, "koi8-r").encode("cp1251"))
diff --git a/m_lib/tty_menu.py b/m_lib/tty_menu.py
new file mode 100755 (executable)
index 0000000..35ba43d
--- /dev/null
@@ -0,0 +1,56 @@
+#! /usr/bin/env python
+"""tty menus
+
+   Written by Broytman, Mar 1998. Copyright (C) 1997 PhiloSoft Design.
+"""
+
+
+import string
+
+
+def hmenu(prompt, astring):
+   """
+      Writes prompt and read result
+      until result[0] is one of allowed characters (from astring),
+      and returns the character
+   """
+   while 1:
+      result = raw_input(prompt)
+      if len(result) > 0:
+         c = result[0]
+         if c in astring:
+            return c
+
+
+def vmenu(item_list, prompt, format = "%d. %s"):
+   """
+      Prints numbered list of items and allow user to select one,
+      returns selected number. Returns -1, if user enter non-numeric string.
+   """
+   for i in range(len(item_list)):
+      print format % (i, item_list[i])
+   print
+
+   result = raw_input(prompt)
+
+   try:
+      result = string.atoi(result)
+   except string.atoi_error:
+      result = -1
+
+   return result
+
+
+def test():
+   result = hmenu("Select: d)aily, w)eekly, m)onthly, c)ancel: ", "dwmc")
+   print "Answer is '%s'\n" % result
+
+   os_list = ["DOS", "Windows", "UNIX"]
+   result = vmenu(os_list, "Select OS: ")
+   if 0 <= result < len(os_list):
+      print "Answer is '%s'\n" % os_list[result]
+   else:
+      print "Wrong selection"
+
+if __name__ == "__main__":
+   test()
diff --git a/setup.py b/setup.py
new file mode 100755 (executable)
index 0000000..857eced
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,16 @@
+#! /usr/bin/env python
+
+from distutils.core import setup
+
+setup(name = "m_lib",
+   version = "1.4",
+   description = "Broytman Library for Python",
+   long_description = "Broytman Library for Python, Copyright (C) 1996-2012 PhiloSoft Design",
+   author = "Oleg Broytman",
+   author_email = "phd@phdru.name",
+   url = "http://phdru.name/Software/Python/#m_lib",
+   license = "GPL",
+   platforms = "All",
+   packages = ["m_lib", "m_lib.clock", "m_lib.lazy",
+      "m_lib.net", "m_lib.net.ftp", "m_lib.net.www", "m_lib.rus"]
+)