<commit_before>"""Copyright 2008 Orbitz WorldWide

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License."""

from datetime import datetime,timedelta
from time import daylight
from django.utils import timezone

months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
weekdays = ['sun','mon','tue','wed','thu','fri','sat']

def parseATTime(s, tzinfo=None):
  if tzinfo is None:
    tzinfo = timezone.get_current_timezone()
  s = s.strip().lower().replace('_','').replace(',','').replace(' ','')
  if s.isdigit():
    if len(s) == 8 and int(s[:4]) > 1900 and int(s[4:6]) < 13 and int(s[6:]) < 32:
      pass #Fall back because its not a timestamp, its YYYYMMDD form
    else:
      return datetime.fromtimestamp(int(s),tzinfo)
  elif ':' in s and len(s) == 13:
    return tzinfo.localize(datetime.strptime(s,'%H:%M%Y%m%d'), daylight)
  if '+' in s:
    ref,offset = s.split('+',1)
    offset = '+' + offset
  elif '-' in s:
    ref,offset = s.split('-',1)
    offset = '-' + offset
  else:
    ref,offset = s,''
  return (parseTimeReference(ref) + parseTimeOffset(offset)).astimezone(tzinfo)

def parseTimeReference(ref):
  if not ref or ref == 'now': return timezone.now()

  #Time-of-day reference
  i = ref.find(':')
  hour,min = 0,0
  if i != -1:
    hour = int( ref[:i] )
    min = int( ref[i+1:i+3] )
    ref = ref[i+3:]
    if ref[:2] == 'am': ref = ref[2:]
    elif ref[:2] == 'pm':
      hour = (hour + 12) % 24
      ref = ref[2:]
  if ref.startswith('noon'):
    hour,min = 12,0
    ref = ref[4:]
  elif ref.startswith('midnight'):
    hour,min = 0,0
    ref = ref[8:]
  elif ref.startswith('teatime'):
    hour,min = 16,0
    ref = ref[7:]

  refDate = timezone.now().replace(hour=hour,minute=min,second=0)

  #Day reference
  if ref in ('yesterday','today','tomorrow'): #yesterday, today, tomorrow
    if ref == 'yesterday':
      refDate = refDate - timedelta(days=1)
    if ref == 'tomorrow':
      refDate = refDate + timedelta(days=1)
  elif ref.count('/') == 2: #MM/DD/YY[YY]
    m,d,y = map(int,ref.split('/'))
    if y < 1900: y += 1900
    if y < 1970: y += 100
    refDate = refDate.replace(year=y)

    try: # Fix for Bug #551771
      refDate = refDate.replace(month=m)
      refDate = refDate.replace(day=d)
    except:
      refDate = refDate.replace(day=d)
      refDate = refDate.replace(month=m)

  elif len(ref) == 8 and ref.isdigit(): #YYYYMMDD
    refDate = refDate.replace(year= int(ref[:4]))

    try: # Fix for Bug #551771
      refDate = refDate.replace(month= int(ref[4:6]))
      refDate = refDate.replace(day= int(ref[6:8]))
    except:
      refDate = refDate.replace(day= int(ref[6:8]))
      refDate = refDate.replace(month= int(ref[4:6]))

  elif ref[:3] in months: #MonthName DayOfMonth
    refDate = refDate.replace(month= months.index(ref[:3]) + 1)
    if ref[-2:].isdigit():
      refDate = refDate.replace(day= int(ref[-2:]))
    elif ref[-1:].isdigit():
      refDate = refDate.replace(day= int(ref[-1:]))
    else:
      raise Exception("Day of month required after month name")
  elif ref[:3] in weekdays: #DayOfWeek (Monday, etc)
    todayDayName = refDate.strftime("%a").lower()[:3]
    today = weekdays.index( todayDayName )
    twoWeeks = weekdays * 2
    dayOffset = today - twoWeeks.index(ref[:3])
    if dayOffset < 0: dayOffset += 7
    refDate -= timedelta(days=dayOffset)
  elif ref:
    raise Exception("Unknown day reference")
  return refDate

def parseTimeOffset(offset):
  if not offset:
    return timedelta()

  t = timedelta()

  if offset[0].isdigit():
    sign = 1
  else:
    sign = { '+' : 1, '-' : -1 }[offset[0]]
    offset = offset[1:]

  while offset:
    i = 1
    while offset[:i].isdigit() and i <= len(offset): i += 1
    num = int(offset[:i-1])
    offset = offset[i-1:]
    i = 1
    while offset[:i].isalpha() and i <= len(offset): i += 1
    unit = offset[:i-1]
    offset = offset[i-1:]
    unitString = getUnitString(unit)
    if unitString == 'months':
      unitString = 'days'
      num = num * 30
    if unitString == 'years':
      unitString = 'days'
      num = num * 365
    t += timedelta(**{ unitString : sign * num})

  return t

def getUnitString(s):
  if s.startswith('s'): return 'seconds'
  if s.startswith('min'): return 'minutes'
  if s.startswith('h'): return 'hours'
  if s.startswith('d'): return 'days'
  if s.startswith('w'): return 'weeks'
  if s.startswith('mon'): return 'months'
  if s.startswith('y'): return 'years'
  raise Exception("Invalid offset unit '%s'" % s)
<commit_msg>Update attime.py<commit_after>"""Copyright 2008 Orbitz WorldWide

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License."""

import pytz
from datetime import datetime,timedelta
from time import daylight
from django.utils import timezone
from django.conf import settings

months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
weekdays = ['sun','mon','tue','wed','thu','fri','sat']

def parseATTime(s, tzinfo=None):
  if tzinfo is None:
    tzinfo = pytz.timezone(settings.TIME_ZONE)
  s = s.strip().lower().replace('_','').replace(',','').replace(' ','')
  if s.isdigit():
    if len(s) == 8 and int(s[:4]) > 1900 and int(s[4:6]) < 13 and int(s[6:]) < 32:
      pass #Fall back because its not a timestamp, its YYYYMMDD form
    else:
      return datetime.fromtimestamp(int(s),tzinfo)
  elif ':' in s and len(s) == 13:
    return tzinfo.localize(datetime.strptime(s,'%H:%M%Y%m%d'), daylight)
  if '+' in s:
    ref,offset = s.split('+',1)
    offset = '+' + offset
  elif '-' in s:
    ref,offset = s.split('-',1)
    offset = '-' + offset
  else:
    ref,offset = s,''
  return (parseTimeReference(ref) + parseTimeOffset(offset)).astimezone(tzinfo)

def parseTimeReference(ref):
  if not ref or ref == 'now': return timezone.now()

  #Time-of-day reference
  i = ref.find(':')
  hour,min = 0,0
  if i != -1:
    hour = int( ref[:i] )
    min = int( ref[i+1:i+3] )
    ref = ref[i+3:]
    if ref[:2] == 'am': ref = ref[2:]
    elif ref[:2] == 'pm':
      hour = (hour + 12) % 24
      ref = ref[2:]
  if ref.startswith('noon'):
    hour,min = 12,0
    ref = ref[4:]
  elif ref.startswith('midnight'):
    hour,min = 0,0
    ref = ref[8:]
  elif ref.startswith('teatime'):
    hour,min = 16,0
    ref = ref[7:]

  refDate = timezone.now().replace(hour=hour,minute=min,second=0)

  #Day reference
  if ref in ('yesterday','today','tomorrow'): #yesterday, today, tomorrow
    if ref == 'yesterday':
      refDate = refDate - timedelta(days=1)
    if ref == 'tomorrow':
      refDate = refDate + timedelta(days=1)
  elif ref.count('/') == 2: #MM/DD/YY[YY]
    m,d,y = map(int,ref.split('/'))
    if y < 1900: y += 1900
    if y < 1970: y += 100
    refDate = refDate.replace(year=y)

    try: # Fix for Bug #551771
      refDate = refDate.replace(month=m)
      refDate = refDate.replace(day=d)
    except:
      refDate = refDate.replace(day=d)
      refDate = refDate.replace(month=m)

  elif len(ref) == 8 and ref.isdigit(): #YYYYMMDD
    refDate = refDate.replace(year= int(ref[:4]))

    try: # Fix for Bug #551771
      refDate = refDate.replace(month= int(ref[4:6]))
      refDate = refDate.replace(day= int(ref[6:8]))
    except:
      refDate = refDate.replace(day= int(ref[6:8]))
      refDate = refDate.replace(month= int(ref[4:6]))

  elif ref[:3] in months: #MonthName DayOfMonth
    refDate = refDate.replace(month= months.index(ref[:3]) + 1)
    if ref[-2:].isdigit():
      refDate = refDate.replace(day= int(ref[-2:]))
    elif ref[-1:].isdigit():
      refDate = refDate.replace(day= int(ref[-1:]))
    else:
      raise Exception("Day of month required after month name")
  elif ref[:3] in weekdays: #DayOfWeek (Monday, etc)
    todayDayName = refDate.strftime("%a").lower()[:3]
    today = weekdays.index( todayDayName )
    twoWeeks = weekdays * 2
    dayOffset = today - twoWeeks.index(ref[:3])
    if dayOffset < 0: dayOffset += 7
    refDate -= timedelta(days=dayOffset)
  elif ref:
    raise Exception("Unknown day reference")
  return refDate

def parseTimeOffset(offset):
  if not offset:
    return timedelta()

  t = timedelta()

  if offset[0].isdigit():
    sign = 1
  else:
    sign = { '+' : 1, '-' : -1 }[offset[0]]
    offset = offset[1:]

  while offset:
    i = 1
    while offset[:i].isdigit() and i <= len(offset): i += 1
    num = int(offset[:i-1])
    offset = offset[i-1:]
    i = 1
    while offset[:i].isalpha() and i <= len(offset): i += 1
    unit = offset[:i-1]
    offset = offset[i-1:]
    unitString = getUnitString(unit)
    if unitString == 'months':
      unitString = 'days'
      num = num * 30
    if unitString == 'years':
      unitString = 'days'
      num = num * 365
    t += timedelta(**{ unitString : sign * num})

  return t

def getUnitString(s):
  if s.startswith('s'): return 'seconds'
  if s.startswith('min'): return 'minutes'
  if s.startswith('h'): return 'hours'
  if s.startswith('d'): return 'days'
  if s.startswith('w'): return 'weeks'
  if s.startswith('mon'): return 'months'
  if s.startswith('y'): return 'years'
  raise Exception("Invalid offset unit '%s'" % s)
<|endoftext|>


<commit_before>"""Backend unit testing for the FLL Pit Display."""

from fllipit import APP, Team
from coverage import coverage

import unittest
import os

class BasicTestCase(unittest.TestCase):

    """Basic test case."""

    def test_index(self):
        """Verfy that the index page is present."""
        tester = APP.test_client(self)
        response = tester.get('/', content_type='html/text')
        self.assertEqual(response.status_code, 200)

    def test_playoff_ladder(self):
        """Verfy that the index page is present."""
        tester = APP.test_client(self)
        response = tester.get('/ladder', content_type='html/text')
        self.assertEqual(response.status_code, 200)

    def test_api(self):
        """Verify that the API endpoint is present."""
        tester = APP.test_client(self)
        response = tester.get('/api/teams', content_type='html/text')
        self.assertEqual(response.status_code, 200)

    def test_bestScore(self):

        # Three scores entered
        team1 = Team(round1=350, round2=440, round3=500, round4=410, round5=120)
        team2 = Team(round1=350, round2=440, round3=120, round4=410, round5=120)
        team3 = Team(round1=350, round2=298, round3=305, round4=325, round5=120)
        team4 = Team(round1=350, round2=298, round3=305, round4=400, round5=120)
        team5 = Team(round1=250, round2=298, round3=305, round4=400, round5=410)

        self.assertEqual(500, team1.bestScore)
        self.assertEqual(440, team1.secondBestScore)
        self.assertEqual(410, team1.worstScore)

        self.assertEqual(440, team2.bestScore)
        self.assertEqual(410, team2.secondBestScore)
        self.assertEqual(350, team2.worstScore)

        self.assertEqual(350, team3.bestScore)
        self.assertEqual(325, team3.secondBestScore)
        self.assertEqual(305, team3.worstScore)

        self.assertEqual(400, team4.bestScore)
        self.assertEqual(350, team4.secondBestScore)
        self.assertEqual(305, team4.worstScore)

        self.assertEqual(410, team5.bestScore)
        self.assertEqual(400, team5.secondBestScore)
        self.assertEqual(305, team5.worstScore)

    def test_bestScore_incomplete(self):
        """Verify that the code can determine the best score for a team when not all 3 scores are entered"""

        # No scores entered yet
        team0 = Team()

        # One score entered
        team1 = Team(round1=350)

        # Two scores entered
        team2 = Team(round1=350, round2=440)
        team3 = Team(round1=350, round2=120)

        self.assertEqual(0, team0.bestScore)
        self.assertEqual(0, team0.secondBestScore)
        self.assertEqual(0, team0.worstScore)

        self.assertEqual(350, team1.bestScore)
        self.assertEqual(0, team1.secondBestScore)
        self.assertEqual(0, team1.worstScore)

        self.assertEqual(440, team2.bestScore)
        self.assertEqual(350, team2.secondBestScore)
        self.assertEqual(0, team2.worstScore)

        self.assertEqual(350, team3.bestScore)
        self.assertEqual(120, team3.secondBestScore)
        self.assertEqual(0, team3.worstScore)

if __name__ == '__main__':
    unittest.main()<commit_msg>Add unit test to ensure rankings break ties with penalties<commit_after>"""Backend unit testing for the FLL Pit Display."""

import unittest
import os

from coverage import coverage

from fllipit import APP, Team
from api import rankTeams

class BasicTestCase(unittest.TestCase):

    """Basic test case."""

    def test_index(self):
        """Verfy that the index page is present."""
        tester = APP.test_client(self)
        response = tester.get('/', content_type='html/text')
        self.assertEqual(response.status_code, 200)

    def test_playoff_ladder(self):
        """Verfy that the index page is present."""
        tester = APP.test_client(self)
        response = tester.get('/ladder', content_type='html/text')
        self.assertEqual(response.status_code, 200)

    def test_api(self):
        """Verify that the API endpoint is present."""
        tester = APP.test_client(self)
        response = tester.get('/api/teams', content_type='html/text')
        self.assertEqual(response.status_code, 200)

    def test_bestScore(self):

        # Three scores entered
        team1 = Team(round1=350, round2=440, round3=500, round4=410, round5=120)
        team2 = Team(round1=350, round2=440, round3=120, round4=410, round5=120)
        team3 = Team(round1=350, round2=298, round3=305, round4=325, round5=120)
        team4 = Team(round1=350, round2=298, round3=305, round4=400, round5=120)
        team5 = Team(round1=250, round2=298, round3=305, round4=400, round5=410)

        self.assertEqual(500, team1.bestScore)
        self.assertEqual(440, team1.secondBestScore)
        self.assertEqual(410, team1.worstScore)

        self.assertEqual(440, team2.bestScore)
        self.assertEqual(410, team2.secondBestScore)
        self.assertEqual(350, team2.worstScore)

        self.assertEqual(350, team3.bestScore)
        self.assertEqual(325, team3.secondBestScore)
        self.assertEqual(305, team3.worstScore)

        self.assertEqual(400, team4.bestScore)
        self.assertEqual(350, team4.secondBestScore)
        self.assertEqual(305, team4.worstScore)

        self.assertEqual(410, team5.bestScore)
        self.assertEqual(400, team5.secondBestScore)
        self.assertEqual(305, team5.worstScore)

    def test_bestScore_incomplete(self):
        """Verify that the code can determine the best score for a team when not all 3 scores are entered"""

        # No scores entered yet
        team0 = Team()

        # One score entered
        team1 = Team(round1=350)

        # Two scores entered
        team2 = Team(round1=350, round2=440)
        team3 = Team(round1=350, round2=120)

        self.assertEqual(0, team0.bestScore)
        self.assertEqual(0, team0.secondBestScore)
        self.assertEqual(0, team0.worstScore)

        self.assertEqual(350, team1.bestScore)
        self.assertEqual(0, team1.secondBestScore)
        self.assertEqual(0, team1.worstScore)

        self.assertEqual(440, team2.bestScore)
        self.assertEqual(350, team2.secondBestScore)
        self.assertEqual(0, team2.worstScore)

        self.assertEqual(350, team3.bestScore)
        self.assertEqual(120, team3.secondBestScore)
        self.assertEqual(0, team3.worstScore)

    def test_bestScore_penlaties(self):
        """Verify that the code can determine the best score for a team when not all 3 scores are entered"""

        # No scores entered yet

        # One score entered
        team1 = Team(number=1, round1=350, round1Penalties=0)
        team2 = Team(number=2, round1=350, round1Penalties=1)

        teams = [team1, team2]
        sortedTeams = rankTeams(teams)

        self.assertEqual(sortedTeams[0].number, 1)
        self.assertEqual(sortedTeams[1].number, 2)

if __name__ == '__main__':
    unittest.main()<|endoftext|>


<commit_before># SFTP storage backend for Django.
# Author: Brent Tubbs <brent.tubbs@gmail.com>
# License: MIT
#
# Modeled on the FTP storage by Rafal Jonca <jonca.rafal@gmail.com>
#
# Settings:
#
# SFTP_STORAGE_HOST - The hostname where you want the files to be saved.
#
# SFTP_STORAGE_ROOT - The root directory on the remote host into which files
# should be placed.  Should work the same way that STATIC_ROOT works for local
# files.  Must include a trailing slash.
#
# SFTP_STORAGE_PARAMS (Optional) - A dictionary containing connection
# parameters to be passed as keyword arguments to
# paramiko.SSHClient().connect() (do not include hostname here).  See
# http://www.lag.net/paramiko/docs/paramiko.SSHClient-class.html#connect for
# details
#
# SFTP_STORAGE_INTERACTIVE (Optional) - A boolean indicating whether to prompt
# for a password if the connection cannot be made using keys, and there is not
# already a password in SFTP_STORAGE_PARAMS.  You can set this to True to
# enable interactive login when running 'manage.py collectstatic', for example.
#
#   DO NOT set SFTP_STORAGE_INTERACTIVE to True if you are using this storage
#   for files being uploaded to your site by users, because you'll have no way
#   to enter the password when they submit the form..
#
# SFTP_STORAGE_FILE_MODE (Optional) - A bitmask for setting permissions on
# newly-created files.  See http://docs.python.org/library/os.html#os.chmod for
# acceptable values.
#
# SFTP_STORAGE_DIR_MODE (Optional) - A bitmask for setting permissions on
# newly-created directories.  See
# http://docs.python.org/library/os.html#os.chmod for acceptable values.
#
#   Hint: if you start the mode number with a 0 you can express it in octal
#   just like you would when doing "chmod 775 myfile" from bash.
#
# SFTP_STORAGE_UID (Optional) - uid of the account that should be set as owner
# of the files on the remote host.  You have to be root to set this.
#
# SFTP_STORAGE_GID (Optional) - gid of the group that should be set on the
# files on the remote host.  You have to be a member of the group to set this.

import os
import stat
import paramiko
import posixpath
import getpass
from datetime import datetime
from django.conf import settings
from django.core.files.storage import Storage
from django.core.files.base import File
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

class SFTPStorage(Storage):

    def __init__(self):
        self._host = settings.SFTP_STORAGE_HOST

        # if present, settings.SFTP_STORAGE_PARAMS should be a dict with params
        # matching the keyword arguments to paramiko.SSHClient().connect().  So
        # you can put username/password there.  Or you can omit all that if
        # you're using keys.
        self._params = getattr(settings, 'SFTP_STORAGE_PARAMS', {})
        self._interactive = getattr(settings, 'SFTP_STORAGE_INTERACTIVE',
                                    False)

        self._file_mode = getattr(settings, 'SFTP_STORAGE_FILE_MODE', None)
        self._dir_mode = getattr(settings, 'SFTP_STORAGE_DIR_MODE', None)

        self._uid = getattr(settings, 'SFTP_STORAGE_UID', None)
        self._gid = getattr(settings, 'SFTP_STORAGE_GID', None)

        self._root_path = settings.SFTP_STORAGE_ROOT

        # for now it's all posix paths.  Maybe someday we'll support figuring
        # out if the remote host is windows.
        self._pathmod = posixpath

        # set up connection
        self._connect()

    def _connect(self):
        self._ssh = paramiko.SSHClient()

        # automatically add host keys from current user.
        self._ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts")))

        # and automatically add new host keys for hosts we haven't seen before.
        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            self._ssh.connect(self._host, **self._params)
        except paramiko.AuthenticationException, e:
            if self._interactive and 'password' not in self._params:
                # If authentication has failed, and we haven't already tried
                # username/password, and configuration allows it, then try
                # again with username/password.
                if 'username' not in self._params:
                    self._params['username'] = getpass.getuser()
                self._params['password'] = getpass.getpass()
                self._connect()
            else:
                raise paramiko.AuthenticationException, e

        if not hasattr(self, '_sftp'):
            self._sftp = self._ssh.open_sftp()

    def _join(self, *args):
        # Use the path module for the remote host type to join a path together
        return self._pathmod.join(*args)

    def _remote_path(self, name):
        return self._join(self._root_path, name)

    def _open(self, name, mode='rb'):
        return SFTPStorageFile(name, self, mode)

    def _read(self, name):
        remote_path = self._remote_path(name)
        f = self._sftp.open(remote_path, 'rb')

    def _chown(self, path, uid=None, gid=None):
        """Set uid and/or gid for file at path."""
        # Paramiko's chown requires both uid and gid, so look them up first if
        # we're only supposed to set one.
        if uid is None or gid is None:
            attr = self._sftp.stat(path)
            uid = uid or attr.st_uid
            gid = gid or attr.st_gid
        self._sftp.chown(path, uid, gid)

    def _mkdir(self, path):
        """Create directory, recursing up to create parent dirs if
        necessary."""
        parent = self._pathmod.dirname(path)
        if not self.exists(parent):
            self._mkdir(parent)
        self._sftp.mkdir(path)

        if self._dir_mode is not None:
            self._sftp.chmod(path, self._dir_mode)

        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)

    def _save(self, name, content):
        """Save file via SFTP."""
        content.open()
        path = self._remote_path(name)
        dirname = self._pathmod.dirname(path)
        if not self.exists(dirname):
            self._mkdir(dirname)

        f = self._sftp.open(path, 'wb')
        f.write(content.file.read())
        f.close()

        # set file permissions if configured
        if self._file_mode is not None:
            self._sftp.chmod(path, self._file_mode)
        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)
        return name

    def delete(self, name):
        remote_path = self._remote_path(name)
        self._sftp.remove(remote_path)

    def exists(self, name):
        # Try to retrieve file info.  Return true on success, false on failure.
        remote_path = self._remote_path(name)
        try:
            self._sftp.stat(remote_path)
            return True
        except IOError:
            return False

    def _isdir_attr(self, item):
        # Return whether an item in sftp.listdir_attr results is a directory
        if item.st_mode is not None:
            return stat.S_IFMT(item.st_mode) == stat.S_IFDIR
        else:
            return False

    def listdir(self, path):
        remote_path = self._remote_path(path)
        dirs, files = [], []
        for item in self._sftp.listdir_attr(remote_path):
            if self._isdir_attr(item):
                dirs.append(item.filename)
            else:
                files.append(item.filename)
        return dirs, files

    def size(self, name):
        remote_path = self._remote_path(name)
        return self._sftp.stat(remote_path).st_size

    def accessed_time(self, name):
        remote_path = self._remote_path(name)
        utime = self._sftp.stat(remote_path).st_atime
        return datetime.fromtimestamp(utime)

    def modified_time(self, name):
        remote_path = self._remote_path(name)
        utime = self._sftp.stat(remote_path).st_mtime
        return datetime.fromtimestamp(utime)

    def url(self, name):
        remote_path = self._remote_path(name)
        return 'sftp://%s/%s' % (self._host, remote_path)

class SFTPStorageFile(File):
    def __init__(self, name, storage, mode):
        self._name = name
        self._storage = storage
        self._mode = mode
        self._is_dirty = False
        self.file = StringIO()
        self._is_read = False

    @property
    def size(self):
        if not hasattr(self, '_size'):
            self._size = self._storage.size(self._name)
        return self._size

    def read(self, num_bytes=None):
        if not self._is_read:
            self.file = self._storage._read(self._name)
            self._is_read = True

        return self.file.read(num_bytes)

    def write(self, content):
        if 'w' not in self._mode:
            raise AttributeError("File was opened for read-only access.")
        self.file = StringIO(content)
        self._is_dirty = True
        self._is_read = True

    def close(self):
        if self._is_dirty:
            self._storage._save(self._name, self.file.getvalue())
        self.file.close()
<commit_msg>fixed error on SFTPStorage.read()<commit_after># SFTP storage backend for Django.
# Author: Brent Tubbs <brent.tubbs@gmail.com>
# License: MIT
#
# Modeled on the FTP storage by Rafal Jonca <jonca.rafal@gmail.com>
#
# Settings:
#
# SFTP_STORAGE_HOST - The hostname where you want the files to be saved.
#
# SFTP_STORAGE_ROOT - The root directory on the remote host into which files
# should be placed.  Should work the same way that STATIC_ROOT works for local
# files.  Must include a trailing slash.
#
# SFTP_STORAGE_PARAMS (Optional) - A dictionary containing connection
# parameters to be passed as keyword arguments to
# paramiko.SSHClient().connect() (do not include hostname here).  See
# http://www.lag.net/paramiko/docs/paramiko.SSHClient-class.html#connect for
# details
#
# SFTP_STORAGE_INTERACTIVE (Optional) - A boolean indicating whether to prompt
# for a password if the connection cannot be made using keys, and there is not
# already a password in SFTP_STORAGE_PARAMS.  You can set this to True to
# enable interactive login when running 'manage.py collectstatic', for example.
#
#   DO NOT set SFTP_STORAGE_INTERACTIVE to True if you are using this storage
#   for files being uploaded to your site by users, because you'll have no way
#   to enter the password when they submit the form..
#
# SFTP_STORAGE_FILE_MODE (Optional) - A bitmask for setting permissions on
# newly-created files.  See http://docs.python.org/library/os.html#os.chmod for
# acceptable values.
#
# SFTP_STORAGE_DIR_MODE (Optional) - A bitmask for setting permissions on
# newly-created directories.  See
# http://docs.python.org/library/os.html#os.chmod for acceptable values.
#
#   Hint: if you start the mode number with a 0 you can express it in octal
#   just like you would when doing "chmod 775 myfile" from bash.
#
# SFTP_STORAGE_UID (Optional) - uid of the account that should be set as owner
# of the files on the remote host.  You have to be root to set this.
#
# SFTP_STORAGE_GID (Optional) - gid of the group that should be set on the
# files on the remote host.  You have to be a member of the group to set this.

import os
import stat
import paramiko
import posixpath
import getpass
from datetime import datetime
from django.conf import settings
from django.core.files.storage import Storage
from django.core.files.base import File
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

class SFTPStorage(Storage):

    def __init__(self):
        self._host = settings.SFTP_STORAGE_HOST

        # if present, settings.SFTP_STORAGE_PARAMS should be a dict with params
        # matching the keyword arguments to paramiko.SSHClient().connect().  So
        # you can put username/password there.  Or you can omit all that if
        # you're using keys.
        self._params = getattr(settings, 'SFTP_STORAGE_PARAMS', {})
        self._interactive = getattr(settings, 'SFTP_STORAGE_INTERACTIVE',
                                    False)

        self._file_mode = getattr(settings, 'SFTP_STORAGE_FILE_MODE', None)
        self._dir_mode = getattr(settings, 'SFTP_STORAGE_DIR_MODE', None)

        self._uid = getattr(settings, 'SFTP_STORAGE_UID', None)
        self._gid = getattr(settings, 'SFTP_STORAGE_GID', None)

        self._root_path = settings.SFTP_STORAGE_ROOT

        # for now it's all posix paths.  Maybe someday we'll support figuring
        # out if the remote host is windows.
        self._pathmod = posixpath

        # set up connection
        self._connect()

    def _connect(self):
        self._ssh = paramiko.SSHClient()

        # automatically add host keys from current user.
        self._ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts")))

        # and automatically add new host keys for hosts we haven't seen before.
        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            self._ssh.connect(self._host, **self._params)
        except paramiko.AuthenticationException, e:
            if self._interactive and 'password' not in self._params:
                # If authentication has failed, and we haven't already tried
                # username/password, and configuration allows it, then try
                # again with username/password.
                if 'username' not in self._params:
                    self._params['username'] = getpass.getuser()
                self._params['password'] = getpass.getpass()
                self._connect()
            else:
                raise paramiko.AuthenticationException, e

        if not hasattr(self, '_sftp'):
            self._sftp = self._ssh.open_sftp()

    def _join(self, *args):
        # Use the path module for the remote host type to join a path together
        return self._pathmod.join(*args)

    def _remote_path(self, name):
        return self._join(self._root_path, name)

    def _open(self, name, mode='rb'):
        return SFTPStorageFile(name, self, mode)

    def _read(self, name):
        remote_path = self._remote_path(name)
        return self._sftp.open(remote_path, 'rb')

    def _chown(self, path, uid=None, gid=None):
        """Set uid and/or gid for file at path."""
        # Paramiko's chown requires both uid and gid, so look them up first if
        # we're only supposed to set one.
        if uid is None or gid is None:
            attr = self._sftp.stat(path)
            uid = uid or attr.st_uid
            gid = gid or attr.st_gid
        self._sftp.chown(path, uid, gid)

    def _mkdir(self, path):
        """Create directory, recursing up to create parent dirs if
        necessary."""
        parent = self._pathmod.dirname(path)
        if not self.exists(parent):
            self._mkdir(parent)
        self._sftp.mkdir(path)

        if self._dir_mode is not None:
            self._sftp.chmod(path, self._dir_mode)

        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)

    def _save(self, name, content):
        """Save file via SFTP."""
        content.open()
        path = self._remote_path(name)
        dirname = self._pathmod.dirname(path)
        if not self.exists(dirname):
            self._mkdir(dirname)

        f = self._sftp.open(path, 'wb')
        f.write(content.file.read())
        f.close()

        # set file permissions if configured
        if self._file_mode is not None:
            self._sftp.chmod(path, self._file_mode)
        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)
        return name

    def delete(self, name):
        remote_path = self._remote_path(name)
        self._sftp.remove(remote_path)

    def exists(self, name):
        # Try to retrieve file info.  Return true on success, false on failure.
        remote_path = self._remote_path(name)
        try:
            self._sftp.stat(remote_path)
            return True
        except IOError:
            return False

    def _isdir_attr(self, item):
        # Return whether an item in sftp.listdir_attr results is a directory
        if item.st_mode is not None:
            return stat.S_IFMT(item.st_mode) == stat.S_IFDIR
        else:
            return False

    def listdir(self, path):
        remote_path = self._remote_path(path)
        dirs, files = [], []
        for item in self._sftp.listdir_attr(remote_path):
            if self._isdir_attr(item):
                dirs.append(item.filename)
            else:
                files.append(item.filename)
        return dirs, files

    def size(self, name):
        remote_path = self._remote_path(name)
        return self._sftp.stat(remote_path).st_size

    def accessed_time(self, name):
        remote_path = self._remote_path(name)
        utime = self._sftp.stat(remote_path).st_atime
        return datetime.fromtimestamp(utime)

    def modified_time(self, name):
        remote_path = self._remote_path(name)
        utime = self._sftp.stat(remote_path).st_mtime
        return datetime.fromtimestamp(utime)

    def url(self, name):
        remote_path = self._remote_path(name)
        return 'sftp://%s/%s' % (self._host, remote_path)

class SFTPStorageFile(File):
    def __init__(self, name, storage, mode):
        self._name = name
        self._storage = storage
        self._mode = mode
        self._is_dirty = False
        self.file = StringIO()
        self._is_read = False

    @property
    def size(self):
        if not hasattr(self, '_size'):
            self._size = self._storage.size(self._name)
        return self._size

    def read(self, num_bytes=None):
        if not self._is_read:
            self.file = self._storage._read(self._name)
            self._is_read = True

        return self.file.read(num_bytes)

    def write(self, content):
        if 'w' not in self._mode:
            raise AttributeError("File was opened for read-only access.")
        self.file = StringIO(content)
        self._is_dirty = True
        self._is_read = True

    def close(self):
        if self._is_dirty:
            self._storage._save(self._name, self.file.getvalue())
        self.file.close()
<|endoftext|>


<commit_before># SFTP storage backend for Django.
# Author: Brent Tubbs <brent.tubbs@gmail.com>
# License: MIT
#
# Modeled on the FTP storage by Rafal Jonca <jonca.rafal@gmail.com>
#
# Settings:
#
# SFTP_STORAGE_HOST - The hostname where you want the files to be saved.
#
# SFTP_STORAGE_ROOT - The root directory on the remote host into which files
# should be placed.  Should work the same way that STATIC_ROOT works for local
# files.  Must include a trailing slash.
#
# SFTP_STORAGE_PARAMS (Optional) - A dictionary containing connection
# parameters to be passed as keyword arguments to
# paramiko.SSHClient().connect() (do not include hostname here).  See
# http://www.lag.net/paramiko/docs/paramiko.SSHClient-class.html#connect for
# details
#
# SFTP_STORAGE_INTERACTIVE (Optional) - A boolean indicating whether to prompt
# for a password if the connection cannot be made using keys, and there is not
# already a password in SFTP_STORAGE_PARAMS.  You can set this to True to
# enable interactive login when running 'manage.py collectstatic', for example.
#
#   DO NOT set SFTP_STORAGE_INTERACTIVE to True if you are using this storage
#   for files being uploaded to your site by users, because you'll have no way
#   to enter the password when they submit the form..
#
# SFTP_STORAGE_FILE_MODE (Optional) - A bitmask for setting permissions on
# newly-created files.  See http://docs.python.org/library/os.html#os.chmod for
# acceptable values.
#
# SFTP_STORAGE_DIR_MODE (Optional) - A bitmask for setting permissions on
# newly-created directories.  See
# http://docs.python.org/library/os.html#os.chmod for acceptable values.
#
#   Hint: if you start the mode number with a 0 you can express it in octal
#   just like you would when doing "chmod 775 myfile" from bash.
#
# SFTP_STORAGE_UID (Optional) - uid of the account that should be set as owner
# of the files on the remote host.  You have to be root to set this.
#
# SFTP_STORAGE_GID (Optional) - gid of the group that should be set on the
# files on the remote host.  You have to be a member of the group to set this.

import os
import stat
import paramiko
import posixpath
import getpass
from datetime import datetime
from django.conf import settings
from django.core.files.storage import Storage
from django.core.files.base import File
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

class SFTPStorage(Storage):

    def __init__(self):
        self._host = settings.SFTP_STORAGE_HOST

        # if present, settings.SFTP_STORAGE_PARAMS should be a dict with params
        # matching the keyword arguments to paramiko.SSHClient().connect().  So
        # you can put username/password there.  Or you can omit all that if
        # you're using keys.
        self._params = getattr(settings, 'SFTP_STORAGE_PARAMS', {})
        self._interactive = getattr(settings, 'SFTP_STORAGE_INTERACTIVE',
                                    False)
        self._file_mode = getattr(settings, 'SFTP_STORAGE_FILE_MODE', None)
        self._dir_mode = getattr(settings, 'SFTP_STORAGE_DIR_MODE', None)

        self._uid = getattr(settings, 'SFTP_STORAGE_UID', None)
        self._gid = getattr(settings, 'SFTP_STORAGE_GID', None)

        self._root_path = settings.SFTP_STORAGE_ROOT

        # for now it's all posix paths.  Maybe someday we'll support figuring
        # out if the remote host is windows.
        self._pathmod = posixpath

    def _connect(self):
        self._ssh = paramiko.SSHClient()

        # automatically add host keys from current user.
        self._ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts")))

        # and automatically add new host keys for hosts we haven't seen before.
        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            self._ssh.connect(self._host, **self._params)
        except paramiko.AuthenticationException, e:
            if self._interactive and 'password' not in self._params:
                # If authentication has failed, and we haven't already tried
                # username/password, and configuration allows it, then try
                # again with username/password.
                if 'username' not in self._params:
                    self._params['username'] = getpass.getuser()
                self._params['password'] = getpass.getpass()
                self._connect()
            else:
                raise paramiko.AuthenticationException, e
        except Exception, e:
            print e

        if not hasattr(self, '_sftp'):
            self._sftp = self._ssh.open_sftp()

    @property
    def sftp(self):
        """Lazy SFTP connection"""
        if not hasattr(self, '_sftp'):
            self._connect()
        return self._sftp

    def _join(self, *args):
        # Use the path module for the remote host type to join a path together
        return self._pathmod.join(*args)

    def _remote_path(self, name):
        return self._join(self._root_path, name)

    def _open(self, name, mode='rb'):
        return SFTPStorageFile(name, self, mode)

    def _read(self, name):
        remote_path = self._remote_path(name)
        return self.sftp.open(remote_path, 'rb')

    def _chown(self, path, uid=None, gid=None):
        """Set uid and/or gid for file at path."""
        # Paramiko's chown requires both uid and gid, so look them up first if
        # we're only supposed to set one.
        if uid is None or gid is None:
            attr = self.sftp.stat(path)
            uid = uid or attr.st_uid
            gid = gid or attr.st_gid
        self.sftp.chown(path, uid, gid)

    def _mkdir(self, path):
        """Create directory, recursing up to create parent dirs if
        necessary."""
        parent = self._pathmod.dirname(path)
        if not self.exists(parent):
            self._mkdir(parent)
        self.sftp.mkdir(path)

        if self._dir_mode is not None:
            self.sftp.chmod(path, self._dir_mode)

        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)

    def _save(self, name, content):
        """Save file via SFTP."""
        content.open()
        path = self._remote_path(name)
        dirname = self._pathmod.dirname(path)
        if not self.exists(dirname):
            self._mkdir(dirname)

        f = self.sftp.open(path, 'wb')
        f.write(content.file.read())
        f.close()

        # set file permissions if configured
        if self._file_mode is not None:
            self.sftp.chmod(path, self._file_mode)
        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)
        return name

    def delete(self, name):
        remote_path = self._remote_path(name)
        self.sftp.remove(remote_path)

    def exists(self, name):
        # Try to retrieve file info.  Return true on success, false on failure.
        remote_path = self._remote_path(name)
        try:
            self.sftp.stat(remote_path)
            return True
        except IOError:
            return False

    def _isdir_attr(self, item):
        # Return whether an item in sftp.listdir_attr results is a directory
        if item.st_mode is not None:
            return stat.S_IFMT(item.st_mode) == stat.S_IFDIR
        else:
            return False

    def listdir(self, path):
        remote_path = self._remote_path(path)
        dirs, files = [], []
        for item in self.sftp.listdir_attr(remote_path):
            if self._isdir_attr(item):
                dirs.append(item.filename)
            else:
                files.append(item.filename)
        return dirs, files

    def size(self, name):
        remote_path = self._remote_path(name)
        return self.sftp.stat(remote_path).st_size

    def accessed_time(self, name):
        remote_path = self._remote_path(name)
        utime = self.sftp.stat(remote_path).st_atime
        return datetime.fromtimestamp(utime)

    def modified_time(self, name):
        remote_path = self._remote_path(name)
        utime = self.sftp.stat(remote_path).st_mtime
        return datetime.fromtimestamp(utime)

    def url(self, name):
        remote_path = self._remote_path(name)
        return 'sftp://%s/%s' % (self._host, remote_path)

class SFTPStorageFile(File):
    def __init__(self, name, storage, mode):
        self._name = name
        self._storage = storage
        self._mode = mode
        self._is_dirty = False
        self.file = StringIO()
        self._is_read = False

    @property
    def size(self):
        if not hasattr(self, '_size'):
            self._size = self._storage.size(self._name)
        return self._size

    def read(self, num_bytes=None):
        if not self._is_read:
            self.file = self._storage._read(self._name)
            self._is_read = True

        return self.file.read(num_bytes)

    def write(self, content):
        if 'w' not in self._mode:
            raise AttributeError("File was opened for read-only access.")
        self.file = StringIO(content)
        self._is_dirty = True
        self._is_read = True

    def close(self):
        if self._is_dirty:
            self._storage._save(self._name, self.file.getvalue())
        self.file.close()
<commit_msg> - allow configurabile known host folder<commit_after># SFTP storage backend for Django.
# Author: Brent Tubbs <brent.tubbs@gmail.com>
# License: MIT
#
# Modeled on the FTP storage by Rafal Jonca <jonca.rafal@gmail.com>
#
# Settings:
#
# SFTP_STORAGE_HOST - The hostname where you want the files to be saved.
#
# SFTP_STORAGE_ROOT - The root directory on the remote host into which files
# should be placed.  Should work the same way that STATIC_ROOT works for local
# files.  Must include a trailing slash.
#
# SFTP_STORAGE_PARAMS (Optional) - A dictionary containing connection
# parameters to be passed as keyword arguments to
# paramiko.SSHClient().connect() (do not include hostname here).  See
# http://www.lag.net/paramiko/docs/paramiko.SSHClient-class.html#connect for
# details
#
# SFTP_STORAGE_INTERACTIVE (Optional) - A boolean indicating whether to prompt
# for a password if the connection cannot be made using keys, and there is not
# already a password in SFTP_STORAGE_PARAMS.  You can set this to True to
# enable interactive login when running 'manage.py collectstatic', for example.
#
#   DO NOT set SFTP_STORAGE_INTERACTIVE to True if you are using this storage
#   for files being uploaded to your site by users, because you'll have no way
#   to enter the password when they submit the form..
#
# SFTP_STORAGE_FILE_MODE (Optional) - A bitmask for setting permissions on
# newly-created files.  See http://docs.python.org/library/os.html#os.chmod for
# acceptable values.
#
# SFTP_STORAGE_DIR_MODE (Optional) - A bitmask for setting permissions on
# newly-created directories.  See
# http://docs.python.org/library/os.html#os.chmod for acceptable values.
#
#   Hint: if you start the mode number with a 0 you can express it in octal
#   just like you would when doing "chmod 775 myfile" from bash.
#
# SFTP_STORAGE_UID (Optional) - uid of the account that should be set as owner
# of the files on the remote host.  You have to be root to set this.
#
# SFTP_STORAGE_GID (Optional) - gid of the group that should be set on the
# files on the remote host.  You have to be a member of the group to set this.

import os
import stat
import paramiko
import posixpath
import getpass
from datetime import datetime
from django.conf import settings
from django.core.files.storage import Storage
from django.core.files.base import File
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

class SFTPStorage(Storage):

    def __init__(self):
        self._host = settings.SFTP_STORAGE_HOST

        # if present, settings.SFTP_STORAGE_PARAMS should be a dict with params
        # matching the keyword arguments to paramiko.SSHClient().connect().  So
        # you can put username/password there.  Or you can omit all that if
        # you're using keys.
        self._params = getattr(settings, 'SFTP_STORAGE_PARAMS', {})
        self._interactive = getattr(settings, 'SFTP_STORAGE_INTERACTIVE',
                                    False)
        self._file_mode = getattr(settings, 'SFTP_STORAGE_FILE_MODE', None)
        self._dir_mode = getattr(settings, 'SFTP_STORAGE_DIR_MODE', None)

        self._uid = getattr(settings, 'SFTP_STORAGE_UID', None)
        self._gid = getattr(settings, 'SFTP_STORAGE_GID', None)

        self._root_path = settings.SFTP_STORAGE_ROOT

        # for now it's all posix paths.  Maybe someday we'll support figuring
        # out if the remote host is windows.
        self._pathmod = posixpath

    def _connect(self):
        self._ssh = paramiko.SSHClient()

        # automatically add host keys from current user.
        if settings.KNOWN_HOST_FOLDER:
            self._ssh.load_host_keys(os.path.join(settings.KNOWN_HOST_FOLDER, "known_hosts"))
        else:
            self._ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts")))

        # and automatically add new host keys for hosts we haven't seen before.
        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            self._ssh.connect(self._host, **self._params)
        except paramiko.AuthenticationException, e:
            if self._interactive and 'password' not in self._params:
                # If authentication has failed, and we haven't already tried
                # username/password, and configuration allows it, then try
                # again with username/password.
                if 'username' not in self._params:
                    self._params['username'] = getpass.getuser()
                self._params['password'] = getpass.getpass()
                self._connect()
            else:
                raise paramiko.AuthenticationException, e
        except Exception, e:
            print e

        if not hasattr(self, '_sftp'):
            self._sftp = self._ssh.open_sftp()

    @property
    def sftp(self):
        """Lazy SFTP connection"""
        if not hasattr(self, '_sftp'):
            self._connect()
        return self._sftp

    def _join(self, *args):
        # Use the path module for the remote host type to join a path together
        return self._pathmod.join(*args)

    def _remote_path(self, name):
        return self._join(self._root_path, name)

    def _open(self, name, mode='rb'):
        return SFTPStorageFile(name, self, mode)

    def _read(self, name):
        remote_path = self._remote_path(name)
        return self.sftp.open(remote_path, 'rb')

    def _chown(self, path, uid=None, gid=None):
        """Set uid and/or gid for file at path."""
        # Paramiko's chown requires both uid and gid, so look them up first if
        # we're only supposed to set one.
        if uid is None or gid is None:
            attr = self.sftp.stat(path)
            uid = uid or attr.st_uid
            gid = gid or attr.st_gid
        self.sftp.chown(path, uid, gid)

    def _mkdir(self, path):
        """Create directory, recursing up to create parent dirs if
        necessary."""
        parent = self._pathmod.dirname(path)
        if not self.exists(parent):
            self._mkdir(parent)
        self.sftp.mkdir(path)

        if self._dir_mode is not None:
            self.sftp.chmod(path, self._dir_mode)

        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)

    def _save(self, name, content):
        """Save file via SFTP."""
        content.open()
        path = self._remote_path(name)
        dirname = self._pathmod.dirname(path)
        if not self.exists(dirname):
            self._mkdir(dirname)

        f = self.sftp.open(path, 'wb')
        f.write(content.file.read())
        f.close()

        # set file permissions if configured
        if self._file_mode is not None:
            self.sftp.chmod(path, self._file_mode)
        if self._uid or self._gid:
            self._chown(path, uid=self._uid, gid=self._gid)
        return name

    def delete(self, name):
        remote_path = self._remote_path(name)
        self.sftp.remove(remote_path)

    def exists(self, name):
        # Try to retrieve file info.  Return true on success, false on failure.
        remote_path = self._remote_path(name)
        try:
            self.sftp.stat(remote_path)
            return True
        except IOError:
            return False

    def _isdir_attr(self, item):
        # Return whether an item in sftp.listdir_attr results is a directory
        if item.st_mode is not None:
            return stat.S_IFMT(item.st_mode) == stat.S_IFDIR
        else:
            return False

    def listdir(self, path):
        remote_path = self._remote_path(path)
        dirs, files = [], []
        for item in self.sftp.listdir_attr(remote_path):
            if self._isdir_attr(item):
                dirs.append(item.filename)
            else:
                files.append(item.filename)
        return dirs, files

    def size(self, name):
        remote_path = self._remote_path(name)
        return self.sftp.stat(remote_path).st_size

    def accessed_time(self, name):
        remote_path = self._remote_path(name)
        utime = self.sftp.stat(remote_path).st_atime
        return datetime.fromtimestamp(utime)

    def modified_time(self, name):
        remote_path = self._remote_path(name)
        utime = self.sftp.stat(remote_path).st_mtime
        return datetime.fromtimestamp(utime)

    def url(self, name):
        remote_path = self._remote_path(name)
        return 'sftp://%s/%s' % (self._host, remote_path)

class SFTPStorageFile(File):
    def __init__(self, name, storage, mode):
        self._name = name
        self._storage = storage
        self._mode = mode
        self._is_dirty = False
        self.file = StringIO()
        self._is_read = False

    @property
    def size(self):
        if not hasattr(self, '_size'):
            self._size = self._storage.size(self._name)
        return self._size

    def read(self, num_bytes=None):
        if not self._is_read:
            self.file = self._storage._read(self._name)
            self._is_read = True

        return self.file.read(num_bytes)

    def write(self, content):
        if 'w' not in self._mode:
            raise AttributeError("File was opened for read-only access.")
        self.file = StringIO(content)
        self._is_dirty = True
        self._is_read = True

    def close(self):
        if self._is_dirty:
            self._storage._save(self._name, self.file.getvalue())
        self.file.close()
<|endoftext|>


<commit_before># coding: utf-8
"""Gestion des documents txt

Ce module sert aussi de module de base pour d'autres types de documents :
soyez vigilant si vous y apportez des modifications.
Il n'y a aucune dépendance externe particulière.

Si vous voulez définir un module pour gérer une nouvelle extension, il doit
comporter au moins les méthodes documentées ici. Le cas échéant, vous pouvez
user des mécanismes d'héritage de Python en vous basant sur ce module.
"""
import os.path
import subprocess as sp
import shutil
from deps import HTMLTags as h
from deps import bottle as b
from deps.outils import Path, motaleatoire, traiter_erreur, url, err, _
from deps.mistune import markdown
from deps import jrnl as l
from etc import config as cfg
b.TEMPLATE_PATH += [str(modele) for modele in cfg.MODELES]

class Document:
    """Classe gérant les documents texte
    """
    def __init__(
            self,
            chemin,
            formats=None,
            proprietes=None
    ):
        self.chemin = chemin
        self.nom, self.ext = os.path.splitext(chemin.split('/')[-1])
        self.ext = self.ext[1:]
        # Chemin absolu des fichiers temporaires
        self.rnd = cfg.TMP / motaleatoire(6)
        if formats:
            self.fmt = formats
            self.proprietes = {}
            for fmt in self.fmt:
                self.proprietes[fmt] = \
                    self._traiter_options(
                        fmt, self.proprietes_detail[fmt], self.proprietes
                    )
            if proprietes:
                for fmt in proprietes:
                    self._traiter_proprietes(
                        fmt, proprietes[fmt], self.proprietes_liste[fmt]
                    )

    def doit_etre_actualise(self, fichier, actualiser):
        """Teste s'il est nécessaire de rafraîchir le document
        """
        return actualiser == 1 or not fichier.is_file() or (
            actualiser and self.est_obsolete(fichier)
        )

    def est_obsolete(self, fichier):
        """Teste l'obsolescence d'un fichier
        """
        try:
            return fichier.stat().st_mtime < self._fichier().stat().st_mtime
        except FileNotFoundError:
            return True

    def _fichierrelatif(self, ext=None):
        """Chemin vers un fichier portant ce nom avec une autre extension
        """
        return Path(self.chemin).with_suffix(
            '.' + ext if ext else '.' + self.ext if self.ext else ''
        )

    def _fichier(self, ext=None):
        """Chemin absolu
        """
        if ext:
            return (
                cfg.DOCS / ext / self._fichierrelatif(ext)
            )
        else:
            return cfg.DATA / self._fichierrelatif()

    def _fichiertmp(self, ext=None):
        """Fichier temporaire
        """
        return cfg.TMP / self._fichierrelatif(ext if ext else self.ext)

    def _fichiersortie(self, ext=None, chemin=None, indice=''):
        """Fichier de destination pour un export
        """
        chemin = chemin if chemin else ext
        return (
            cfg.PWD / cfg.STATIC / 'docs' /
            chemin / indice / self._fichierrelatif(ext)
        )

    def _traiter_options(self, fmt, options, props):
        """Options

        Renvoie un dictionnaire 'à une dimension' à partir des options définies
        dans la liste des propriétés.
        """
        proprietes = {}
        for prop, val in options.items():
            if type(val[1]) is dict:
                proprietes = dict(
                    proprietes,
                    **self._traiter_options(fmt, val[1], props)
                )
            else:
                if prop in self.proprietes:
                    proprietes[prop] = self.proprietes[prop]
                elif prop in props:
                    proprietes[prop] = props[prop]
                else:
                    proprietes[prop] = val[1]
        return proprietes

    def _traiter_proprietes(self, fmt, proprietes, listeproprietes):
        """Mise en place des propriétés

        Convertit les chaînes de caractères reçues de l'interface dans les
        types de données attendus par le document.
        """
        for prop, val in proprietes.items():
            if prop in listeproprietes:
                typ = type(listeproprietes[prop])
                if typ in (str, int, float):
                    self.proprietes[fmt][prop] = typ(val)
                elif typ is bool:
                    self.proprietes[fmt][prop] = typ(int(val))
                elif typ in (tuple, list):
                    if type(val) in (tuple, list):
                        self.proprietes[fmt][prop] = val
                    else:
                        self.proprietes[fmt][prop] = tuple(
                            type(pr)(i)
                            for i, pr
                            in zip(
                                val.split(','),
                                listeproprietes[prop]
                            )
                        )

    @property
    def dossier(self):
        """Dossier contenant le document
        """
        return self._fichier().parent

    @property
    def dossiertmp(self):
        """Dossier temporaire
        """
        return self._fichiertmp().parent

    @property
    def obsolete(self):  # pylint: disable=R0201
        """Par défaut, on considère qu'un aperçu est à jour
        """
        return False

    @property
    def proprietes_detail(self):
        """Liste hiérarchisée des propriétés utiles à chaque format d'export
        """
        return {fmt: self.fmt[fmt][1] for fmt in self.fmt}

    @property
    def proprietes_liste(self):
        """Liste non hiérarchisée des propriétés
        """
        listeproprietes = {}
        for fmt, props in self.fmt.items():
            listeproprietes[fmt] = self._traiter_options(
                fmt, props[1], self.proprietes[fmt]
            )
        return listeproprietes

    def afficher(self, actualiser=2):  # pylint: disable=W0613
        """Affichage du contenu du document

        Il doit s'agir ou bien d'un simple texte, ou bien de code html.
        """
        return h.CODE(b.html_escape(self.contenu))

    afficher_source = afficher

    def afficher_pdf(self, message_erreur='', actualiser=2):
        """Affichage du document pdf en html (base 64)
        """
        try:
            return h.OBJECT(
                data="{}".format(self.pdf(actualiser=actualiser)),
                Type="application/pdf",
                width="100%",
                height="100%"
            )
        except ErreurCompilation as err:
            traiter_erreur(err)
            return (markdown(
                _(
                    """\
Il y a eu une erreur pendant le traitement du document.
Ceci vient probablement d'une erreur de syntaxe ; si vous êtes absolument
certain du contraire, merci de signaler le problème.

{}

Voici la sortie de la commande :

                    """
                ).format(message_erreur)
            ) + traiter_erreur_compilation(self.dossiertmp))

    @property
    def contenu(self):
        """Contenu du document
        """
        try:
            with self._fichier().open() as doc:
                return doc.read(-1)
        except UnicodeDecodeError:
            raise FichierIllisible(self._fichier())

    def editer(self, creation=False):
        """Page d'édition du document

        Ce n'est pas ici qu'il importe de s'occuper de cosmétique : on renvoie
        le strict nécessaire, en html, pour l'édition d'un document.
        """
        try:
            return b.template(
                'editeur',
                emplacement=self.chemin,
                texte=self.contenu,
                ext=self.ext
            )
        except FileNotFoundError:
            if creation:
                return b.template(
                    'editeur',
                    emplacement=self.chemin,
                    texte='',
                    ext=self.ext
                )
            else:
                b.abort(404)

    def enregistrer(self, contenu):
        """Enregistrement du document
        """
        with self._fichier().open('w') as doc:
            doc.write(contenu)

    def supprimer(self):
        """Suppression du document
        """
        try:
            self._fichier().unlink()
        except FileNotFoundError as err:
            traiter_erreur(err)
        for doc in cfg.DOCS.glob('*/' + str(self._fichierrelatif().with_suffix('')) + '.*'):
            print(doc)
            doc.unlink()

    def exporter(self, fmt):
        """Export dans les différents formats
        """
        return self.fmt[fmt][0]

    def pdf(self, chemin=None, indice='', actualiser=2):
        """Format pdf

        Si un pdf existe déjà dans l'arborescence de musite, il sera
        actualisé en fonction du paramètre *actualiser* :

        - 0 : aucune actualisation ;
        - 1 : actualisation forcée ;
        - 2 : actualisation si le pdf est plus ancien que l'original.
        """
        chemin = chemin if chemin else 'pdf'
        fichierpdf = self._fichiersortie('pdf', chemin=chemin, indice=indice)
        if actualiser == 1 or (
                not fichierpdf.is_file()
                or (
                    actualiser
                    and self.est_obsolete(fichierpdf)
                )
        ):
            self.preparer_pdf(destination=fichierpdf)
        return url(fichierpdf)

def compiler(commande, fichier, environnement):
    """Appel de commande externe

    Cette méthode est surtout appelée dans les modules dépendant de celui-ci.
    """
    try:
        os.chdir(str(fichier.parent))
        err(environnement + '\n\n')
        err(' '.join(commande) + '\n\n')
        compilation = sp.Popen(
            commande,
            env=environnement,
            stdout=sp.PIPE,
            stderr=sp.PIPE
        )
        sortie, erreurs = compilation.communicate()
        try:
            l.log(
                _('Sortie :')
                + '\n========\n'
                + '\n{}\n\n\n\n'.format(sortie.decode('utf8'))
                + '−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−\n\n\n\n'
                + _('Erreurs :')
                + '\n=========\n'
                + '\n{}\n'.format(erreurs.decode('utf8')),
                mode = 'w'
            )
        except UnicodeDecodeError:
            raise FichierIllisible
        if compilation.returncode:
            print(sortie, erreurs)
            raise ErreurCompilation(sortie, erreurs)
    finally:
        os.chdir(str(cfg.PWD))

def traiter_erreur_compilation(dossier):
    """Réaction en cas d'erreur de compilation
    """
    return Document(dossier.as_posix() + '/log').afficher()

class FichierIllisible(Exception):
    """Exception renvoyée quand le fichier est illisible

    Ici, cette exception est utile si le fichier est un binaire et non un
    document texte.
    """
    pass

class ErreurCompilation(Exception):
    """Exception levée en cas d'erreur de compilation
    """
    def __init__(self, sortie=None, erreurs=None):
        print(sortie, erreurs)
<commit_msg>Oubli<commit_after># coding: utf-8
"""Gestion des documents txt

Ce module sert aussi de module de base pour d'autres types de documents :
soyez vigilant si vous y apportez des modifications.
Il n'y a aucune dépendance externe particulière.

Si vous voulez définir un module pour gérer une nouvelle extension, il doit
comporter au moins les méthodes documentées ici. Le cas échéant, vous pouvez
user des mécanismes d'héritage de Python en vous basant sur ce module.
"""
import os.path
import subprocess as sp
import shutil
from deps import HTMLTags as h
from deps import bottle as b
from deps.outils import Path, motaleatoire, traiter_erreur, url, err, _
from deps.mistune import markdown
from deps import jrnl as l
from etc import config as cfg
b.TEMPLATE_PATH += [str(modele) for modele in cfg.MODELES]

class Document:
    """Classe gérant les documents texte
    """
    def __init__(
            self,
            chemin,
            formats=None,
            proprietes=None
    ):
        self.chemin = chemin
        self.nom, self.ext = os.path.splitext(chemin.split('/')[-1])
        self.ext = self.ext[1:]
        # Chemin absolu des fichiers temporaires
        self.rnd = cfg.TMP / motaleatoire(6)
        if formats:
            self.fmt = formats
            self.proprietes = {}
            for fmt in self.fmt:
                self.proprietes[fmt] = \
                    self._traiter_options(
                        fmt, self.proprietes_detail[fmt], self.proprietes
                    )
            if proprietes:
                for fmt in proprietes:
                    self._traiter_proprietes(
                        fmt, proprietes[fmt], self.proprietes_liste[fmt]
                    )

    def doit_etre_actualise(self, fichier, actualiser):
        """Teste s'il est nécessaire de rafraîchir le document
        """
        return actualiser == 1 or not fichier.is_file() or (
            actualiser and self.est_obsolete(fichier)
        )

    def est_obsolete(self, fichier):
        """Teste l'obsolescence d'un fichier
        """
        try:
            return fichier.stat().st_mtime < self._fichier().stat().st_mtime
        except FileNotFoundError:
            return True

    def _fichierrelatif(self, ext=None):
        """Chemin vers un fichier portant ce nom avec une autre extension
        """
        return Path(self.chemin).with_suffix(
            '.' + ext if ext else '.' + self.ext if self.ext else ''
        )

    def _fichier(self, ext=None):
        """Chemin absolu
        """
        if ext:
            return (
                cfg.DOCS / ext / self._fichierrelatif(ext)
            )
        else:
            return cfg.DATA / self._fichierrelatif()

    def _fichiertmp(self, ext=None):
        """Fichier temporaire
        """
        return cfg.TMP / self._fichierrelatif(ext if ext else self.ext)

    def _fichiersortie(self, ext=None, chemin=None, indice=''):
        """Fichier de destination pour un export
        """
        chemin = chemin if chemin else ext
        return (
            cfg.PWD / cfg.STATIC / 'docs' /
            chemin / indice / self._fichierrelatif(ext)
        )

    def _traiter_options(self, fmt, options, props):
        """Options

        Renvoie un dictionnaire 'à une dimension' à partir des options définies
        dans la liste des propriétés.
        """
        proprietes = {}
        for prop, val in options.items():
            if type(val[1]) is dict:
                proprietes = dict(
                    proprietes,
                    **self._traiter_options(fmt, val[1], props)
                )
            else:
                if prop in self.proprietes:
                    proprietes[prop] = self.proprietes[prop]
                elif prop in props:
                    proprietes[prop] = props[prop]
                else:
                    proprietes[prop] = val[1]
        return proprietes

    def _traiter_proprietes(self, fmt, proprietes, listeproprietes):
        """Mise en place des propriétés

        Convertit les chaînes de caractères reçues de l'interface dans les
        types de données attendus par le document.
        """
        for prop, val in proprietes.items():
            if prop in listeproprietes:
                typ = type(listeproprietes[prop])
                if typ in (str, int, float):
                    self.proprietes[fmt][prop] = typ(val)
                elif typ is bool:
                    self.proprietes[fmt][prop] = typ(int(val))
                elif typ in (tuple, list):
                    if type(val) in (tuple, list):
                        self.proprietes[fmt][prop] = val
                    else:
                        self.proprietes[fmt][prop] = tuple(
                            type(pr)(i)
                            for i, pr
                            in zip(
                                val.split(','),
                                listeproprietes[prop]
                            )
                        )

    @property
    def dossier(self):
        """Dossier contenant le document
        """
        return self._fichier().parent

    @property
    def dossiertmp(self):
        """Dossier temporaire
        """
        return self._fichiertmp().parent

    @property
    def obsolete(self):  # pylint: disable=R0201
        """Par défaut, on considère qu'un aperçu est à jour
        """
        return False

    @property
    def proprietes_detail(self):
        """Liste hiérarchisée des propriétés utiles à chaque format d'export
        """
        return {fmt: self.fmt[fmt][1] for fmt in self.fmt}

    @property
    def proprietes_liste(self):
        """Liste non hiérarchisée des propriétés
        """
        listeproprietes = {}
        for fmt, props in self.fmt.items():
            listeproprietes[fmt] = self._traiter_options(
                fmt, props[1], self.proprietes[fmt]
            )
        return listeproprietes

    def afficher(self, actualiser=2):  # pylint: disable=W0613
        """Affichage du contenu du document

        Il doit s'agir ou bien d'un simple texte, ou bien de code html.
        """
        return h.CODE(b.html_escape(self.contenu))

    afficher_source = afficher

    def afficher_pdf(self, message_erreur='', actualiser=2):
        """Affichage du document pdf en html (base 64)
        """
        try:
            return h.OBJECT(
                data="{}".format(self.pdf(actualiser=actualiser)),
                Type="application/pdf",
                width="100%",
                height="100%"
            )
        except ErreurCompilation as err:
            traiter_erreur(err)
            return (markdown(
                _(
                    """\
Il y a eu une erreur pendant le traitement du document.
Ceci vient probablement d'une erreur de syntaxe ; si vous êtes absolument
certain du contraire, merci de signaler le problème.

{}

Voici la sortie de la commande :

                    """
                ).format(message_erreur)
            ) + traiter_erreur_compilation(self.dossiertmp))

    @property
    def contenu(self):
        """Contenu du document
        """
        try:
            with self._fichier().open() as doc:
                return doc.read(-1)
        except UnicodeDecodeError:
            raise FichierIllisible(self._fichier())

    def editer(self, creation=False):
        """Page d'édition du document

        Ce n'est pas ici qu'il importe de s'occuper de cosmétique : on renvoie
        le strict nécessaire, en html, pour l'édition d'un document.
        """
        try:
            return b.template(
                'editeur',
                emplacement=self.chemin,
                texte=self.contenu,
                ext=self.ext
            )
        except FileNotFoundError:
            if creation:
                return b.template(
                    'editeur',
                    emplacement=self.chemin,
                    texte='',
                    ext=self.ext
                )
            else:
                b.abort(404)

    def enregistrer(self, contenu):
        """Enregistrement du document
        """
        with self._fichier().open('w') as doc:
            doc.write(contenu)

    def supprimer(self):
        """Suppression du document
        """
        try:
            self._fichier().unlink()
        except FileNotFoundError as err:
            traiter_erreur(err)
        for doc in cfg.DOCS.glob('*/' + str(self._fichierrelatif().with_suffix('')) + '.*'):
            print(doc)
            doc.unlink()

    def exporter(self, fmt):
        """Export dans les différents formats
        """
        return self.fmt[fmt][0]

    def pdf(self, chemin=None, indice='', actualiser=2):
        """Format pdf

        Si un pdf existe déjà dans l'arborescence de musite, il sera
        actualisé en fonction du paramètre *actualiser* :

        - 0 : aucune actualisation ;
        - 1 : actualisation forcée ;
        - 2 : actualisation si le pdf est plus ancien que l'original.
        """
        chemin = chemin if chemin else 'pdf'
        fichierpdf = self._fichiersortie('pdf', chemin=chemin, indice=indice)
        if actualiser == 1 or (
                not fichierpdf.is_file()
                or (
                    actualiser
                    and self.est_obsolete(fichierpdf)
                )
        ):
            self.preparer_pdf(destination=fichierpdf)
        return url(fichierpdf)

def compiler(commande, fichier, environnement):
    """Appel de commande externe

    Cette méthode est surtout appelée dans les modules dépendant de celui-ci.
    """
    try:
        os.chdir(str(fichier.parent))
        err(str(environnement) + '\n\n')
        err(' '.join(commande) + '\n\n')
        compilation = sp.Popen(
            commande,
            env=environnement,
            stdout=sp.PIPE,
            stderr=sp.PIPE
        )
        sortie, erreurs = compilation.communicate()
        try:
            l.log(
                _('Sortie :')
                + '\n========\n'
                + '\n{}\n\n\n\n'.format(sortie.decode('utf8'))
                + '−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−\n\n\n\n'
                + _('Erreurs :')
                + '\n=========\n'
                + '\n{}\n'.format(erreurs.decode('utf8')),
                mode = 'w'
            )
        except UnicodeDecodeError:
            raise FichierIllisible
        if compilation.returncode:
            print(sortie, erreurs)
            raise ErreurCompilation(sortie, erreurs)
    finally:
        os.chdir(str(cfg.PWD))

def traiter_erreur_compilation(dossier):
    """Réaction en cas d'erreur de compilation
    """
    return Document(dossier.as_posix() + '/log').afficher()

class FichierIllisible(Exception):
    """Exception renvoyée quand le fichier est illisible

    Ici, cette exception est utile si le fichier est un binaire et non un
    document texte.
    """
    pass

class ErreurCompilation(Exception):
    """Exception levée en cas d'erreur de compilation
    """
    def __init__(self, sortie=None, erreurs=None):
        print(sortie, erreurs)
<|endoftext|>


<commit_before>import rflint
import sys

ERROR="E"
WARNING="W"
IGNORE="I"

class Rule(object):
    # default severity; subclasses may override
    severity = WARNING
    output_format = "{severity}: {linenumber}, {char}: {message} ({rulename})"

    def __init__(self, controller, severity=None):
        self.controller = controller
        if severity is not None:
            self.severity = severity

    @property
    def name(self):
        return self.__class__.__name__

    def report(self, obj, message, linenum, char_offset=0):
        """Report an error or warning"""
        self.controller.report(linenumber=linenum, filename=obj.path,
                               severity=self.severity, message=message,
                               rulename = self.__class__.__name__,
                               char=char_offset)

    @property
    def doc(self):
        '''Algorithm from https://www.python.org/dev/peps/pep-0257/'''
        if not self.__doc__:
            return ""

        lines = self.__doc__.expandtabs().splitlines()

        # Determine minimum indentation (first line doesn't count):
        indent = sys.maxint
        for line in lines[1:]:
            stripped = line.lstrip()
            if stripped:
                indent = min(indent, len(line) - len(stripped))

        # Remove indentation (first line is special):
        trimmed = [lines[0].strip()]
        if indent < sys.maxint:
            for line in lines[1:]:
                trimmed.append(line[indent:].rstrip())

        # Strip off trailing and leading blank lines:
        while trimmed and not trimmed[-1]:
            trimmed.pop()
        while trimmed and not trimmed[0]:
            trimmed.pop(0)

        # Return a single string:
        return '\n'.join(trimmed)

    def __repr__(self):
        return "%s %s" % (self.severity, self.__class__.__name__)

class TestRule(Rule):
    """Rule that runs against test cases.

    The object that is passed in will be of type rflint.parser.Testcase
    """
    pass

class ResourceRule(Rule):
    """Rule that runs against a resource file

    The object that is passed in will be of type rflint.parser.ResourceFile
    """

class SuiteRule(Rule):
    """Rule that runs against test cases.

    The object that is passed in will be of type rflint.parser.SuiteFile
    """
    pass

class KeywordRule(Rule):
    """Rule that runs against keywords

    The object that is passed in will be of type rflint.parser.Keyword
    """
    pass

class GeneralRule(Rule):
    """Rule that requires a suite or resource file, but may apply to child objects

    This rule is identical to a SuiteRule, but exists in case you want
    to write a rule that accepts a suite but doesn't necessarily apply
    to the suite (ie: you may iterate over tests, or keywords, or some
    other child object)
    """
    pass

<commit_msg>add default implementation of Rule.configure<commit_after>import rflint
import sys

ERROR="E"
WARNING="W"
IGNORE="I"

class Rule(object):
    # default severity; subclasses may override
    severity = WARNING
    output_format = "{severity}: {linenumber}, {char}: {message} ({rulename})"

    def __init__(self, controller, severity=None):
        self.controller = controller
        if severity is not None:
            self.severity = severity

    def configure(self, *args, **kwargs):
        # subclasses can override this if they want to be configurable.
        raise Exception("rule '%s' is not configurable" % self.__class__.__name__)

    @property
    def name(self):
        return self.__class__.__name__

    def report(self, obj, message, linenum, char_offset=0):
        """Report an error or warning"""
        self.controller.report(linenumber=linenum, filename=obj.path,
                               severity=self.severity, message=message,
                               rulename = self.__class__.__name__,
                               char=char_offset)

    @property
    def doc(self):
        '''Algorithm from https://www.python.org/dev/peps/pep-0257/'''
        if not self.__doc__:
            return ""

        lines = self.__doc__.expandtabs().splitlines()

        # Determine minimum indentation (first line doesn't count):
        indent = sys.maxint
        for line in lines[1:]:
            stripped = line.lstrip()
            if stripped:
                indent = min(indent, len(line) - len(stripped))

        # Remove indentation (first line is special):
        trimmed = [lines[0].strip()]
        if indent < sys.maxint:
            for line in lines[1:]:
                trimmed.append(line[indent:].rstrip())

        # Strip off trailing and leading blank lines:
        while trimmed and not trimmed[-1]:
            trimmed.pop()
        while trimmed and not trimmed[0]:
            trimmed.pop(0)

        # Return a single string:
        return '\n'.join(trimmed)

    def __repr__(self):
        return "%s %s" % (self.severity, self.__class__.__name__)

class TestRule(Rule):
    """Rule that runs against test cases.

    The object that is passed in will be of type rflint.parser.Testcase
    """
    pass

class ResourceRule(Rule):
    """Rule that runs against a resource file

    The object that is passed in will be of type rflint.parser.ResourceFile
    """

class SuiteRule(Rule):
    """Rule that runs against test cases.

    The object that is passed in will be of type rflint.parser.SuiteFile
    """
    pass

class KeywordRule(Rule):
    """Rule that runs against keywords

    The object that is passed in will be of type rflint.parser.Keyword
    """
    pass

class GeneralRule(Rule):
    """Rule that requires a suite or resource file, but may apply to child objects

    This rule is identical to a SuiteRule, but exists in case you want
    to write a rule that accepts a suite but doesn't necessarily apply
    to the suite (ie: you may iterate over tests, or keywords, or some
    other child object)
    """
    pass

<|endoftext|>