diff --git a/.travis.yml b/.travis.yml
index 9f276d6f5..472896fdb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,9 +2,7 @@ language: python
sudo: false
-cache:
- directories:
- - $HOME/.pip-cache/
+cache: pip
python:
- '2.7'
@@ -12,21 +10,7 @@ python:
- '3.5'
install:
- - |
- if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
- export PYENV_ROOT="$HOME/.pyenv"
- if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
- pushd "$PYENV_ROOT" && git pull && popd
- else
- rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
- fi
- export PYPY_VERSION="5.0.1"
- "$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION"
- virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
- source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
- fi
- - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install pycrypto pg8000 pymysql; fi;
- - if [[ $TRAVIS_PYTHON_VERSION != '3.5' ]]; then pip install -e .; fi;
+ - pip install -e .
before_script:
- pip install coverage
diff --git a/CHANGELOG b/CHANGELOG
index e4701ea05..22406bdd9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -10,6 +10,7 @@
"from gluon.storage import Storage"
- tests can only be run with the usual web2py.py --run_system_tests OR with
python -m unittest -v gluon.tests on the root dir
+- updated pymysql driver
## 2.14.6
diff --git a/README.markdown b/README.markdown
index 1c906755e..27fa23690 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,6 +1,6 @@
## Readme
-web2py is a free open source full-stack framework for rapid development of fast, scalable, secure and portable database-driven web-based applications.
+web2py is a free open source full-stack framework for rapid development of fast, scalable, secure and portable database-driven web-based applications.
It is written and programmable in Python. LGPLv3 License
@@ -10,7 +10,7 @@ Learn more at http://web2py.com
cp examples/app.yaml ./
cp handlers/gaehandler.py ./
-
+
Then edit ./app.yaml and replace "yourappname" with yourappname.
## Important reminder about this GIT repo
diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py
index b7831e583..6ad8b332f 100644
--- a/applications/admin/controllers/default.py
+++ b/applications/admin/controllers/default.py
@@ -310,12 +310,13 @@ def __call__(self, value):
regex = re.compile('^\w+$')
if is_manager():
- apps = [f for f in os.listdir(apath(r=request)) if regex.match(f)]
+ apps = [a for a in os.listdir(apath(r=request)) if regex.match(a) and
+ a != '__pycache__']
else:
- apps = [f.name for f in db(db.app.owner == auth.user_id).select()]
+ apps = [a.name for a in db(db.app.owner == auth.user_id).select()]
if FILTER_APPS:
- apps = [f for f in apps if f in FILTER_APPS]
+ apps = [a for a in apps if a in FILTER_APPS]
apps = sorted(apps, key=lambda a: a.upper())
myplatform = platform.python_version()
diff --git a/applications/admin/controllers/pythonanywhere.py b/applications/admin/controllers/pythonanywhere.py
index 33dfda6fd..e61b06ab1 100644
--- a/applications/admin/controllers/pythonanywhere.py
+++ b/applications/admin/controllers/pythonanywhere.py
@@ -4,10 +4,8 @@
import re
import gzip
import tarfile
-from gluon._compat import StringIO
-from xmlrpclib import ProtocolError
from gluon.contrib.simplejsonrpc import ServerProxy
-
+from gluon._compat import StringIO, ProtocolError
def deploy():
response.title = T('Deploy to pythonanywhere')
diff --git a/applications/admin/cron/expire_sessions.py b/applications/admin/cron/expire_sessions.py
index 10a428c3b..34b16091f 100644
--- a/applications/admin/cron/expire_sessions.py
+++ b/applications/admin/cron/expire_sessions.py
@@ -1,17 +1,20 @@
-EXPIRATION_MINUTES=60
-DIGITS=('0','1','2','3','4','5','6','7','8','9')
-import os, time, stat, cPickle, logging
-path = os.path.join(request.folder,'sessions')
+
+import os, time, stat, logging
+from gluon._compat import pickle
+
+EXPIRATION_MINUTES = 60
+
+path = os.path.join(request.folder, 'sessions')
if not os.path.exists(path):
os.mkdir(path)
now = time.time()
-for filename in os.listdir(path):
- fullpath=os.path.join(path,filename)
- if os.path.isfile(fullpath) and filename.startswith(DIGITS):
+for path, dirs, files in os.walk(path, topdown=False):
+ for x in files:
+ fullpath = os.path.join(path, x)
try:
- filetime = os.stat(fullpath)[stat.ST_MTIME] # get it before our io
+ filetime = os.stat(fullpath)[stat.ST_MTIME] # get it before our io
try:
- session_data = cPickle.load(open(fullpath, 'rb+'))
+ session_data = pickle.load(open(fullpath, 'rb+'))
expiration = session_data['auth']['expiration']
except:
expiration = EXPIRATION_MINUTES * 60
@@ -19,3 +22,7 @@
os.unlink(fullpath)
except:
logging.exception('failure to check %s' % fullpath)
+ for d in dirs:
+ dd = os.path.join(path, d)
+ if not os.listdir(dd):
+ os.rmdir(dd)
diff --git a/applications/admin/languages/sr-cr.py b/applications/admin/languages/sr-cr.py
index 56a9a3845..882819b4b 100755
--- a/applications/admin/languages/sr-cr.py
+++ b/applications/admin/languages/sr-cr.py
@@ -5,64 +5,64 @@
'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN',
'"User Exception" debug mode. ': '"User Exception" debug mode. ',
'%s': '%s',
-'%s %%{row} deleted': '%s %%{row} deleted',
-'%s %%{row} updated': '%s %%{row} updated',
+'%s %%{row} deleted': '%s %%{row} је избрисан',
+'%s %%{row} updated': '%s %%{row} је ажуриран',
'%s selected': '%s selected',
'%s students registered': '%s students registered',
'%Y-%m-%d': '%d-%m-%Y',
'%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S',
'(requires internet access)': '(захтијева приступ интернету)',
-'(requires internet access, experimental)': '(requires internet access, experimental)',
-'(something like "it-it")': '(нешто као "it-it")',
+'(requires internet access, experimental)': '(захтијева приступ интернету, експериментално)',
+'(something like "it-it")': '(на примјер "it-it")',
'(version %s)': '(version %s)',
'?': '?',
'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(датотека **gluon/contrib/plural_rules/%s.py** није пронађена)',
-'Abort': 'Abort',
+'Abort': 'Одустани',
'About': 'Информације',
'About application': 'О апликацији',
-'Accept Terms': 'Accept Terms',
+'Accept Terms': 'Прихватање услова коришћења',
'Add breakpoint': 'Add breakpoint',
'Additional code for your application': 'Додатни код за апликацију',
'Admin design page': 'Admin design page',
-'admin disabled because no admin password': 'admin disabled because no admin password',
-'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine',
-'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts',
+'admin disabled because no admin password': 'администрација онемогућена јер нема лозинке',
+'admin disabled because not supported on google app engine': 'администрација онемогућена на google app engine',
+'admin disabled because too many invalid login attempts': 'администрација онемогућена због већег броја неуспјелих покушаја',
'admin disabled because unable to access password file': 'администрација онемогућена јер не могу приступити датотеци са лозинком',
-'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel',
+'Admin is disabled because insecure channel': 'Администрација онемогућена због несигурне везе',
'Admin language': 'Језик администратора',
'Admin versioning page': 'Admin versioning page',
-'administrative interface': 'административни интерфејс',
+'administrative interface': 'административно окружење',
'Administrator Password:': 'Лозинка администратора:',
'and rename it:': 'и преименуј у:',
-'App does not exist or you are not authorized': 'App does not exist or you are not authorized',
+'App does not exist or you are not authorized': 'Апликација не постоји или немате права приступа',
'appadmin': 'appadmin',
-'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel',
-'Application': 'Application',
-'application "%s" uninstalled': 'application "%s" uninstalled',
+'appadmin is disabled because insecure channel': 'администрација онемогућена због несигурне везе',
+'Application': 'Апликација',
+'application "%s" uninstalled': 'апликација "%s" је деинсталирана',
'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode',
-'application compiled': 'application compiled',
-'Application exists already': 'Application exists already',
-'application is compiled and cannot be designed': 'application is compiled and cannot be designed',
+'application compiled': 'апликација је компајлирана',
+'Application exists already': 'Апликација већ постоји',
+'application is compiled and cannot be designed': 'апликација је компајлирана и не може се даље уређивати',
'Application name:': 'Назив апликације:',
-'Application updated via git pull': 'Application updated via git pull',
+'Application updated via git pull': 'Апликација ажурирана преко git pull',
'are not used': 'није кориштено',
'are not used yet': 'није још кориштено',
-'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?',
-'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?',
+'Are you sure you want to delete file "%s"?': 'Да ли сте сигурни да желите избрисати датотеку "%s"?',
+'Are you sure you want to delete plugin "%s"?': 'Да ли сте сигурни да желите избрисати помоћни модул "%s"?',
'Are you sure you want to delete this object?': 'Да ли сте сигурни да желите обрисати?',
-'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?',
-'Are you sure?': 'Are you sure?',
+'Are you sure you want to uninstall application "%s"?': 'Да ли сте сигурни да желите деинсталирати апликацију "%s"?',
+'Are you sure?': 'Да ли сте сигурни?',
'arguments': 'arguments',
'at char %s': 'код слова %s',
'at line %s': 'на линији %s',
-'ATTENTION:': 'ATTENTION:',
+'ATTENTION:': 'ПАЖЊА:',
'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.',
'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.',
-'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!',
+'ATTENTION: you cannot edit the running application!': 'ПАЖЊА: не можете уређивати покренуту апликацију!',
'Autocomplete Python Code': 'Autocomplete Python Code',
-'Available Databases and Tables': 'Available Databases and Tables',
+'Available Databases and Tables': 'Доступне базе података и табеле',
'back': 'назад',
-'Back to the plugins list': 'Back to the plugins list',
+'Back to the plugins list': 'Назад на листу помоћних модула',
'Back to wizard': 'Back to wizard',
'Basics': 'Основе',
'Begin': 'Почетак',
@@ -71,19 +71,19 @@
'breakpoints': 'breakpoints',
'Bulk Register': 'Bulk Register',
'Bulk Student Registration': 'Bulk Student Registration',
-'Cache': 'Cache',
-'cache': 'cache',
+'Cache': 'Кеш',
+'cache': 'кеш',
'Cache Cleared': 'Cache Cleared',
'Cache Keys': 'Cache Keys',
'cache, errors and sessions cleaned': 'кеш, грешке и сесије су обрисани',
-'can be a git repo': 'може бити git repo',
-'Cancel': 'Cancel',
-'Cannot be empty': 'Cannot be empty',
-'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:',
-'cannot create file': 'cannot create file',
+'can be a git repo': 'може бити git репозиторијум',
+'Cancel': 'Откажи',
+'Cannot be empty': 'Не може бити празно',
+'Cannot compile: there are errors in your app:': 'Не могу компајлирати: грешка у апликацији:',
+'cannot create file': 'не могу креирати датотеку',
'cannot upload file "%(filename)s"': 'не мофу отпремити датотеку "%(filename)s"',
-'Change Admin Password': 'Change Admin Password',
-'Change admin password': 'Промијени лзинку администратора',
+'Change Admin Password': 'Промијени лозинку администратора',
+'Change admin password': 'Промијени лозинку администратора',
'change editor settings': 'change editor settings',
'Changelog': 'Changelog',
'check all': 'check all',
@@ -91,66 +91,66 @@
'Check to delete': 'Check to delete',
'Checking for upgrades...': 'Провјеравам могућност надоградње...',
'Clean': 'Прочисти',
-'Clear': 'Clear',
-'Clear CACHE?': 'Clear CACHE?',
-'Clear DISK': 'Clear DISK',
-'Clear RAM': 'Clear RAM',
+'Clear': 'Обриши',
+'Clear CACHE?': 'Обриши CACHE?',
+'Clear DISK': 'Обриши DISK',
+'Clear RAM': 'Обриши RAM',
'Click row to expand traceback': 'Click row to expand traceback',
'Click row to view a ticket': 'Click row to view a ticket',
'code': 'код',
-'Code listing': 'Code listing',
+'Code listing': 'Приказ кода',
'collapse/expand all': 'сакрити/приказати све',
-'Command': 'Command',
-'Comment:': 'Comment:',
+'Command': 'Наредба',
+'Comment:': 'Коментар:',
'Commit': 'Commit',
'Commit form': 'Commit form',
'Committed files': 'Committed files',
'Compile': 'Компајлирај',
-'Compile (all or nothing)': 'Compile (all or nothing)',
-'Compile (skip failed views)': 'Compile (skip failed views)',
-'compiled application removed': 'compiled application removed',
-'Condition': 'Condition',
-'continue': 'continue',
+'Compile (all or nothing)': 'Компајлирај (све или ништа)',
+'Compile (skip failed views)': 'Компајлирај (игнориши грешке)',
+'compiled application removed': 'компајлирана апликација је уклоњена',
+'Condition': 'Стање',
+'continue': 'настави',
'Controllers': 'Контролери',
'controllers': 'контролери',
-'Count': 'Count',
+'Count': 'Редни број',
'Create': 'Креирај',
'create file with filename:': 'Креирај датотеку под називом:',
'Create rules': 'Креирај правила',
-'Create/Upload': 'Create/Upload',
+'Create/Upload': 'Креирај/Преузми',
'created by': 'израдио',
'Created by:': 'Created by:',
'Created On': 'Created On',
'Created on:': 'Created on:',
'crontab': 'crontab',
-'Current request': 'Current request',
-'Current response': 'Current response',
-'Current session': 'Current session',
+'Current request': 'Тренутни захтјев',
+'Current response': 'Тренутни одговор',
+'Current session': 'Тренутна сесија',
'currently running': 'тренутно покренут',
'currently saved or': 'тренутно сачувано или',
'data uploaded': 'data uploaded',
-'Database': 'Database',
+'Database': 'База података',
'Database %s select': 'Database %s select',
-'Database administration': 'Database administration',
+'Database administration': 'Администрација базе података',
'database administration': 'администрација базе података',
-'Database Administration (appadmin)': 'Database Administration (appadmin)',
-'Date and Time': 'Date and Time',
+'Database Administration (appadmin)': 'Администрација базе података (appadmin)',
+'Date and Time': 'Датум и вријеме',
'db': 'db',
-'Debug': 'Debug',
+'Debug': 'Отклони грешку',
'defines tables': 'дефинише табеле',
'Delete': 'Обриши',
'delete': 'обриши',
-'delete all checked': 'delete all checked',
-'delete plugin': 'delete plugin',
-'Delete this file (you will be asked to confirm deletion)': 'Обриши ову даатотеку (бићете упитани за потврду брисања)',
-'Delete:': 'Delete:',
+'delete all checked': 'избриши све означено',
+'delete plugin': 'избриши помоћни модул',
+'Delete this file (you will be asked to confirm deletion)': 'Обриши ову датотеку (бићете упитани за потврду брисања)',
+'Delete:': 'Избриши:',
'deleted after first hit': 'deleted after first hit',
'Demo': 'Demo',
'Deploy': 'Постави',
'Deploy on Google App Engine': 'Постави на Google App Engine',
'Deploy to OpenShift': 'Постави на OpenShift',
-'Deploy to pythonanywhere': 'Deploy to pythonanywhere',
-'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere',
+'Deploy to pythonanywhere': 'Постави на pythonanywhere',
+'Deploy to PythonAnywhere': 'Постави на PythonAnywhere',
'Deployment form': 'Deployment form',
'Deployment Interface': 'Deployment Interface',
'Description:': 'Description:',
@@ -160,10 +160,10 @@
'direction: ltr': 'direction: ltr',
'directory not found': 'directory not found',
'Disable': 'Искључи',
-'Disabled': 'Disabled',
-'disabled in demo mode': 'disabled in demo mode',
-'disabled in GAE mode': 'disabled in GAE mode',
-'disabled in multi user mode': 'disabled in multi user mode',
+'Disabled': 'Искључено',
+'disabled in demo mode': 'онемогућено у демо моду',
+'disabled in GAE mode': 'онемогућено у GAE моду',
+'disabled in multi user mode': 'онемогућено у вишекорисничком моду',
'DISK': 'DISK',
'Disk Cache Keys': 'Disk Cache Keys',
'Disk Cleared': 'Disk Cleared',
@@ -173,20 +173,20 @@
'Docs': 'Docs',
'done!': 'done!',
'Downgrade': 'Downgrade',
-'Download .w2p': 'Download .w2p',
-'Download as .exe': 'Download as .exe',
+'Download .w2p': 'Преузми као .w2p',
+'Download as .exe': 'Преузми као .exe',
'download layouts': 'преузми layouts',
'Download layouts from repository': 'Download layouts from repository',
-'download plugins': 'преузми plugins',
-'Download plugins from repository': 'Download plugins from repository',
-'Edit': 'Уређивање',
+'download plugins': 'преузми помоћне модуле',
+'Download plugins from repository': 'Преузми помоћне модуле из репозиторијум',
+'Edit': 'Уреди',
'edit all': 'уреди све',
'Edit application': 'Уреди апликацију',
'edit controller': 'уреди контролер',
'edit controller:': 'edit controller:',
'Edit current record': 'Edit current record',
-'edit views:': 'уреди views:',
-'Editing %s': 'Editing %s',
+'edit views:': 'уреди приказ:',
+'Editing %s': 'Уређивање %s',
'Editing file "%s"': 'Уређивање датотеке "%s"',
'Editing Language file': 'Уређивање језичке датотеке',
'Editing Plural Forms File': 'Editing Plural Forms File',
@@ -206,21 +206,21 @@
'Exit Fullscreen': 'Exit Fullscreen',
'Expand Abbreviation': 'Expand Abbreviation',
'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)',
-'export as csv file': 'export as csv file',
-'Exports:': 'Exports:',
-'exposes': 'exposes',
-'exposes:': 'exposes:',
+'export as csv file': 'извези као csv датотеку',
+'Exports:': 'Извози:',
+'exposes': 'приказује',
+'exposes:': 'приказује:',
'extends': 'проширује',
'failed to compile file because:': 'нисам могао да компајлирам због:',
'failed to reload module because:': 'failed to reload module because:',
'File': 'Датотека',
-'file "%(filename)s" created': 'file "%(filename)s" created',
-'file "%(filename)s" deleted': 'file "%(filename)s" deleted',
-'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded',
-'file "%s" of %s restored': 'file "%s" of %s restored',
+'file "%(filename)s" created': 'датотека "%(filename)s" је креирана',
+'file "%(filename)s" deleted': 'датотека "%(filename)s" је избрисана',
+'file "%(filename)s" uploaded': 'датотека "%(filename)s" је отпремљена',
+'file "%s" of %s restored': 'датотека "%s" од %s е враћено у претходно стање',
'file changed on disk': 'file changed on disk',
'file does not exist': 'датотека не постоји',
-'file not found': 'file not found',
+'file not found': 'датотека није пронађена',
'file saved on %(time)s': 'file saved on %(time)s',
'file saved on %s': 'датотека сачувана на %s',
'filename': 'filename',
@@ -242,7 +242,7 @@
'Globals##debug': 'Globals##debug',
'Go to Matching Pair': 'Go to Matching Pair',
'go!': 'крени!',
-'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface',
+'Google App Engine Deployment Interface': 'Google App Engine Dинсталационо окружење',
'Google Application Id': 'Google Application Id',
'Goto': 'Goto',
'graph model': 'graph model',
@@ -260,7 +260,7 @@
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.',
'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.',
'import': 'import',
-'Import/Export': 'Import/Export',
+'Import/Export': 'инсталационо окружење',
'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.',
'includes': 'укључује',
'Indent with tabs': 'Indent with tabs',
@@ -270,49 +270,49 @@
'Installed applications': 'Инсталиране апликације',
'Interaction at %s line %s': 'Interaction at %s line %s',
'Interactive console': 'Interactive console',
-'internal error': 'internal error',
-'internal error: %s': 'internal error: %s',
+'internal error': 'унутрашња грешка',
+'internal error: %s': 'унутрашња грешка: %s',
'Internal State': 'Internal State',
'Invalid action': 'Invalid action',
'Invalid application name': 'Invalid application name',
'invalid circular reference': 'invalid circular reference',
'Invalid git repository specified.': 'Invalid git repository specified.',
-'invalid password': 'invalid password',
-'invalid password.': 'погрешна лозинка.',
-'Invalid Query': 'Invalid Query',
+'invalid password': 'Неважећа лозинка',
+'invalid password.': 'неважећа лозинка.',
+'Invalid Query': 'Погрешан упит',
'invalid request': 'invalid request',
'Invalid request': 'Invalid request',
'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)',
-'invalid ticket': 'invalid ticket',
+'invalid ticket': 'погрешан тикет',
'Key': 'Key',
'Key bindings': 'Пречице',
'Key bindings for ZenCoding Plugin': 'Пречице за ZenCoding Plugin',
-'Keyboard shortcuts': 'Keyboard shortcuts',
+'Keyboard shortcuts': 'Пречице на тастатури',
'kill process': 'kill process',
-'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated',
+'language file "%(filename)s" created/updated': 'језичка датотека "%(filename)s" је креирана/ажурирана',
'Language files (static strings) updated': 'Језичке датотеке су ажуриране',
'languages': 'језици',
'Languages': 'Језици',
'Last Revision': 'Last Revision',
'Last saved on:': 'Посљедња измјена:',
'License for': 'Лиценца за',
-'License:': 'License:',
+'License:': 'Лиценца:',
'Line Nr': 'Line Nr',
-'Line number': 'Line number',
-'lists by exception': 'lists by exception',
-'lists by ticket': 'lists by ticket',
-'Loading...': 'Loading...',
+'Line number': 'Линија број',
+'lists by exception': 'прикажи грешке',
+'lists by ticket': 'прикажи тикете',
+'Loading...': 'Преузимам...',
'loading...': 'преузимам...',
-'Local Apps': 'Local Apps',
+'Local Apps': 'Локалне апликације',
'locals': 'locals',
'Locals##debug': 'Locals##debug',
'Login': 'Пријава',
'Login successful': 'Login successful',
-'Login to the Administrative Interface': 'Пријава за административни интерфејс',
-'Login/Register': 'Login/Register',
-'Logout': 'Излаз',
-'lost password': 'lost password',
-'Main Menu': 'Main Menu',
+'Login to the Administrative Interface': 'Пријава за административно окружење',
+'Login/Register': 'Пријава/Регистрација',
+'Logout': 'Одјава',
+'lost password': 'изгубљена лозинка',
+'Main Menu': 'Главни мени',
'Manage': 'Manage',
'Manage %(action)s': 'Manage %(action)s',
'Manage Access Control': 'Manage Access Control',
@@ -321,68 +321,68 @@
'Manage Students': 'Manage Students',
'Match Pair': 'Match Pair',
'Memberships': 'Memberships',
-'merge': 'merge',
+'merge': 'споји',
'Merge Lines': 'Споји линије',
-'Models': 'Models',
-'models': 'models',
+'Models': 'Модели',
+'models': 'Модели',
'Modified On': 'Modified On',
-'Modules': 'Modules',
-'modules': 'modules',
-'Multi User Mode': 'Multi User Mode',
-'new application "%s" created': 'new application "%s" created',
-'new application "%s" imported': 'new application "%s" imported',
+'Modules': 'Модули',
+'modules': 'модули',
+'Multi User Mode': 'Вишекориснички режим рада',
+'new application "%s" created': 'нова апликација "%s" је креирана',
+'new application "%s" imported': 'нова апликација "%s" је увежена',
'New Application Wizard': 'Чаробњак за нове апликације',
'New application wizard': 'Чаробњак за нове апликације',
-'new plugin installed': 'new plugin installed',
-'New plugin installed: %s': 'New plugin installed: %s',
-'New Record': 'New Record',
-'new record inserted': 'new record inserted',
+'new plugin installed': 'нови помоћни модул је инсталиран',
+'New plugin installed: %s': 'Инсталиран нови помоћни модул: %s',
+'New Record': 'Нови запис',
+'new record inserted': 'унешен нови запис',
'New simple application': 'Нова једноставна апликација',
-'next': 'next',
+'next': 'следећи',
'next %s rows': 'next %s rows',
'Next Edit Point': 'Next Edit Point',
-'NO': 'NO',
-'no changes': 'no changes',
-'No databases in this application': 'No databases in this application',
-'No Interaction yet': 'No Interaction yet',
-'no match': 'no match',
-'no package selected': 'no package selected',
-'no permission to uninstall "%s"': 'no permission to uninstall "%s"',
+'NO': 'НЕ',
+'no changes': 'нема промјена',
+'No databases in this application': 'Нема базе података у апликацији',
+'No Interaction yet': 'Нема још интеракције',
+'no match': 'нема подударања',
+'no package selected': 'пакет није одабран',
+'no permission to uninstall "%s"': 'немате овлаштење да деинсталирате "%s"',
'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder',
'Node:': 'Node:',
-'Not Authorized': 'Not Authorized',
-'Not supported': 'Not supported',
+'Not Authorized': 'Немате овлашћење',
+'Not supported': 'Није подржано',
'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.',
"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.",
'online designer': 'онлајн дизајнер',
-'Open new app in new window': 'Open new app in new window',
-'OpenShift Deployment Interface': 'OpenShift Deployment Interface',
+'Open new app in new window': 'Отвори нову апликацију у новом прозору',
+'OpenShift Deployment Interface': 'OpenShift инсталационо окружење',
'OpenShift Output': 'OpenShift Output',
-'or alternatively': 'or alternatively',
+'or alternatively': 'или алтернативно',
'Or Get from URL:': 'Or Get from URL:',
-'or import from csv file': 'or import from csv file',
+'or import from csv file': 'или увези помоћу csv датотеке',
'Original/Translation': 'Оргинал/Превод',
-'Overview': 'Overview',
-'Overwrite installed app': 'Пребриши постојећу апликацију',
+'Overview': 'Преглед',
+'Overwrite installed app': 'Замјени већ постојећу апликацију',
'Pack all': 'Запакуј све',
-'Pack compiled': 'Pack compiled',
-'Pack custom': 'Pack custom',
-'pack plugin': 'pack plugin',
-'password changed': 'password changed',
-'Past revisions': 'Past revisions',
+'Pack compiled': 'Запакуј компајлирано',
+'Pack custom': 'Прилагођено паковање',
+'pack plugin': 'запакуј помоћни модул',
+'password changed': 'лозинка је промијењена',
+'Past revisions': 'Претходне корекције',
'Path to appcfg.py': 'Path to appcfg.py',
'Path to local openshift repo root.': 'Path to local openshift repo root.',
-'Peeking at file': 'Peeking at file',
-'Permission': 'Permission',
-'Permissions': 'Permissions',
-'Please': 'Please',
+'Peeking at file': 'Преглед датотеке',
+'Permission': 'Дозвола',
+'Permissions': 'Дозволе',
+'Please': 'Молим',
'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...',
-'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted',
-'Plugin "%s" in application': 'Plugin "%s" in application',
+'plugin "%(plugin)s" deleted': 'помоћни модул "%(plugin)s" је избрисан',
+'Plugin "%s" in application': 'Помоћни модул "%s" у апликацији',
'plugin not specified': 'plugin not specified',
-'Plugin page': 'Plugin page',
-'plugins': 'plugins',
-'Plugins': 'Plugins',
+'Plugin page': 'Страница помоћних модула',
+'plugins': 'помоћни модули',
+'Plugins': 'Помоћни модули',
'Plural Form #%s': 'Plural Form #%s',
'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Омогућио',
@@ -390,8 +390,8 @@
'Preferences saved on session only': 'Preferences saved on session only',
'previous %s rows': 'previous %s rows',
'Previous Edit Point': 'Previous Edit Point',
-'Private files': 'Private files',
-'private files': 'private files',
+'Private files': 'Приватне датотеке',
+'private files': 'приватне датотеке',
'Project Progress': 'Напредак пројекта',
'Pull': 'Pull',
'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.',
@@ -401,7 +401,7 @@
'pygraphviz library not found': 'pygraphviz library not found',
'PythonAnywhere Apps': 'PythonAnywhere Apps',
'PythonAnywhere Password': 'PythonAnywhere Password',
-'Query:': 'Query:',
+'Query:': 'Упит:',
'RAM': 'RAM',
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram Cleared',
@@ -412,11 +412,11 @@
'refresh': 'refresh',
'register': 'register',
'Reload routes': 'Обнови преусмјерења',
-'Remove compiled': 'Remove compiled',
+'Remove compiled': 'Уклони компајлирано',
'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s',
'Replace': 'Замијени',
'Replace All': 'Замијени све',
-'Repository (%s)': 'Repository (%s)',
+'Repository (%s)': 'Репозиторијум (%s)',
'request': 'request',
'requires distutils, but not installed': 'requires distutils, but not installed',
'requires python-git, but not installed': 'requires python-git, but not installed',
@@ -430,28 +430,28 @@
'reverted to revision %s': 'reverted to revision %s',
'Revision %s': 'Revision %s',
'Revision:': 'Revision:',
-'Role': 'Role',
-'Roles': 'Roles',
-'Rows in Table': 'Rows in Table',
+'Role': 'Улога',
+'Roles': 'Улоге',
+'Rows in Table': 'Записи у табели',
'Rows selected': 'Rows selected',
'rules are not defined': 'правила нису дефинисана',
'rules:': 'правила:',
-'Run tests': 'Run tests',
-'Run tests in this file': 'Run tests in this file',
+'Run tests': 'Покрени тестове',
+'Run tests in this file': 'Покрени тестове у датотеци',
"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')",
'Running on %s': 'Покренути на %s',
'Save': 'Сачувај',
-'Save file:': 'Save file:',
-'Save file: %s': 'Save file: %s',
-'Save model as...': 'Save model as...',
-'Save via Ajax': 'сачувај via Ajax',
+'Save file:': 'Сачувај датотеку:',
+'Save file: %s': 'Сачувај датотеку: %s',
+'Save model as...': 'Сачувај модел као...',
+'Save via Ajax': 'Сачувај преко Ajax',
'Saved file hash:': 'Сачувано као хаш:',
-'Screenshot %s': 'Screenshot %s',
-'Search': 'Search',
-'Select Files to Package': 'Select Files to Package',
+'Screenshot %s': 'Снимак екрана %s',
+'Search': 'Претрага',
+'Select Files to Package': 'Одабери датотеке за паковање',
'session': 'сесија',
-'session expired': 'сесија истекла',
-'Session saved correctly': 'Session saved correctly',
+'session expired': 'сесија је истекла',
+'Session saved correctly': 'Сесија је уредно сачувана',
'Session saved on session only': 'Session saved on session only',
'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s',
'shell': 'shell',
@@ -459,10 +459,10 @@
'Singular Form': 'Singular Form',
'Site': 'Сајт',
'Size of cache:': 'Size of cache:',
-'skip to generate': 'skip to generate',
-'some files could not be removed': 'some files could not be removed',
+'skip to generate': 'прескочи генерисање',
+'some files could not be removed': 'неке датотеке не могу бити уклоњене',
'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying',
-'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed',
+'Sorry, could not find mercurial installed': 'Жалим, mercurial није инсталиран',
'source : db': 'source : db',
'source : filesystem': 'source : filesystem',
'Start a new app': 'Покрени нову апликацију',
@@ -474,15 +474,15 @@
'Static files': 'Static files',
'Statistics': 'Statistics',
'Step': 'Корак',
-'step': 'step',
+'step': 'kорак',
'stop': 'stop',
-'submit': 'submit',
+'submit': 'прихвати',
'Submit': 'Прихвати',
'successful': 'успјешан',
-'switch to : db': 'switch to : db',
-'switch to : filesystem': 'switch to : filesystem',
+'switch to : db': 'пређи на : db',
+'switch to : filesystem': 'пређи на : filesystem',
'Tab width (# characters)': 'Tab width (# characters)',
-'Table': 'Table',
+'Table': 'Табела',
'Temporary': 'Temporary',
'test': 'тест',
'Testing application': 'Тестирање апликације',
@@ -492,16 +492,16 @@
'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller',
'The data representation, define database tables and sets': 'The data representation, define database tables and sets',
'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates',
-'Theme': 'Theme',
-'There are no controllers': 'There are no controllers',
-'There are no models': 'There are no models',
-'There are no modules': 'There are no modules',
-'There are no plugins': 'There are no plugins',
-'There are no private files': 'There are no private files',
-'There are no static files': 'There are no static files',
-'There are no translators': 'There are no translators',
+'Theme': 'Табела',
+'There are no controllers': 'Нема контолера',
+'There are no models': 'Нема модела',
+'There are no modules': 'Нема модула',
+'There are no plugins': 'Нема помоћних модула',
+'There are no private files': 'Нема приватних датотека',
+'There are no static files': 'Нема статичних датотека',
+'There are no translators': 'Нема преводиоца',
'There are no translators, only default language is supported': 'There are no translators, only default language is supported',
-'There are no views': 'There are no views',
+'There are no views': 'Нема преводиоца',
'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app',
'These files are served without processing, your images go here': 'These files are served without processing, your images go here',
"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.",
@@ -512,9 +512,9 @@
'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.',
'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?',
'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".',
-'Ticket': 'Ticket',
-'Ticket ID': 'Ticket ID',
-'Ticket Missing': 'Ticket nedostaje',
+'Ticket': 'Тикет',
+'Ticket ID': 'Тикет ID',
+'Ticket Missing': 'Недостаје тикет',
'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)',
'to previous version.': 'на претходну верзију.',
'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]',
@@ -525,67 +525,67 @@
'Toggle Fullscreen': 'Toggle Fullscreen',
'Traceback': 'Traceback',
'Translation strings for the application': 'Ријечи у апликацији које треба превести',
-'try something like': 'try something like',
-'Try the mobile interface': 'Пробај мобилни интерфејс',
-'try view': 'try view',
+'try something like': 'на примјер',
+'Try the mobile interface': 'Пробај мобилнo окружење',
+'try view': 'пробај приказ',
'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.',
'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.',
-'Unable to check for upgrades': 'Unable to check for upgrades',
+'Unable to check for upgrades': 'Не могу да провјерим могућност надоградње',
'unable to create application "%s"': 'unable to create application "%s"',
-'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"',
-'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"',
-'Unable to determine the line number!': 'Unable to determine the line number!',
-'Unable to download app because:': 'Unable to download app because:',
+'unable to delete file "%(filename)s"': 'не могу избрисати датотеку "%(filename)s"',
+'unable to delete file plugin "%(plugin)s"': 'не могу избрисати помоћни модул "%(plugin)s"',
+'Unable to determine the line number!': 'Не могу да утврдим број реда!',
+'Unable to download app because:': 'Не могу да преузмем апликацију због:',
'unable to download layout': 'unable to download layout',
'unable to download plugin: %s': 'unable to download plugin: %s',
-'Unable to download the list of plugins': 'Unable to download the list of plugins',
-'unable to install plugin "%s"': 'unable to install plugin "%s"',
-'unable to parse csv file': 'unable to parse csv file',
-'unable to uninstall "%s"': 'unable to uninstall "%s"',
-'unable to upgrade because "%s"': 'unable to upgrade because "%s"',
+'Unable to download the list of plugins': 'Не могу да преузмем списак помоћних модула',
+'unable to install plugin "%s"': 'не могу да инсталирам помоћни модул "%s"',
+'unable to parse csv file': 'не могу да расчланим csv датотеку',
+'unable to uninstall "%s"': 'не могу да деинсталирам "%s"',
+'unable to upgrade because "%s"': 'не могу да ажуримам због "%s"',
'uncheck all': 'uncheck all',
'Uninstall': 'Деинсталирај',
'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s',
'update': 'ажурирај',
'update all languages': 'ажурирај све језике',
-'Update:': 'Update:',
-'Upgrade': 'Upgrade',
-'upgrade now to %s': 'upgrade now to %s',
+'Update:': 'Ажурирај:',
+'Upgrade': 'Надоградња',
+'upgrade now to %s': 'ажуриран на %s',
'upload': 'Отпреми',
-'Upload': 'Upload',
+'Upload': 'Преузми',
'Upload a package:': 'Преузми пакет:',
'Upload and install packed application': 'Преузми и инсталирај запаковану апликацију',
'upload file:': 'преузми датотеку:',
-'upload plugin file:': 'преузми плагин датотеку:',
+'upload plugin file:': 'преузми датотеку помоћног модула:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.',
-'User': 'User',
-'Username': 'Username',
-'Users': 'Users',
+'User': 'Корисник',
+'Username': 'Корисничко име',
+'Users': 'Корисници',
'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.',
'variables': 'variables',
'Version': 'Верзија',
'Version %s.%s.%s (%s) %s': 'Верзија %s.%s.%s (%s) %s',
-'Versioning': 'Versioning',
-'Views': 'Views',
-'views': 'views',
-'Warning!': 'Warning!',
-'WARNING:': 'WARNING:',
+'Versioning': 'Креирање верзија',
+'Views': 'Прикази',
+'views': 'прикази',
+'Warning!': 'Упозорење!',
+'WARNING:': 'УПОЗОРЕЊЕ:',
'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:',
'Web Framework': 'Web Framework',
-'web2py Admin Password': 'web2py Admin Password',
-'web2py apps to deploy': 'web2py apps to deploy',
+'web2py Admin Password': 'web2py администраторска лозинка',
+'web2py apps to deploy': 'web2py апликација за инсталацију',
'web2py Debugger': 'web2py Debugger',
'web2py downgrade': 'web2py downgrade',
'web2py is up to date': 'web2py је ажуран',
'web2py online debugger': 'web2py online debugger',
'web2py Recent Tweets': 'web2py Recent Tweets',
-'web2py upgrade': 'web2py upgrade',
-'web2py upgraded; please restart it': 'web2py upgraded; please restart it',
-'Working...': 'Working...',
+'web2py upgrade': 'web2py надоградња',
+'web2py upgraded; please restart it': 'web2py је ажуриран; молим да рестартујете',
+'Working...': 'Извршавам...',
'Wrap with Abbreviation': 'Wrap with Abbreviation',
'WSGI reference name': 'WSGI reference name',
-'YES': 'YES',
-'Yes': 'Yes',
+'YES': 'ДА',
+'Yes': 'Да',
'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button',
'You can inspect variables using the console below': 'You can inspect variables using the console below',
'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out',
diff --git a/applications/admin/languages/sr-lt.py b/applications/admin/languages/sr-lt.py
index 659cb84ef..a807b8ccd 100755
--- a/applications/admin/languages/sr-lt.py
+++ b/applications/admin/languages/sr-lt.py
@@ -5,64 +5,65 @@
'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN',
'"User Exception" debug mode. ': '"User Exception" debug mode. ',
'%s': '%s',
-'%s %%{row} deleted': '%s %%{row} deleted',
-'%s %%{row} updated': '%s %%{row} updated',
+'%s %%{row} deleted': '%s %%{row} je izbrisan',
+'%s %%{row} updated': '%s %%{row} je ažuriran',
'%s selected': '%s selected',
'%s students registered': '%s students registered',
'%Y-%m-%d': '%d-%m-%Y',
'%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S',
'(requires internet access)': '(zahtijeva pristup internetu)',
-'(requires internet access, experimental)': '(requires internet access, experimental)',
-'(something like "it-it")': '(nešto kao "it-it")',
+'(requires internet access, experimental)': '(zahtijeva pristup internetu, eksperimentalno)',
+'(something like "it-it")': '(na primjer "it-it")',
'(version %s)': '(version %s)',
'?': '?',
'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(datoteka **gluon/contrib/plural_rules/%s.py** nije pronađena)',
-'Abort': 'Abort',
+'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
+'Abort': 'Odustani',
'About': 'Informacije',
'About application': 'O aplikaciji',
-'Accept Terms': 'Accept Terms',
+'Accept Terms': 'Prihvatanje uslova korišćenja',
'Add breakpoint': 'Add breakpoint',
'Additional code for your application': 'Dodatni kod za aplikaciju',
'Admin design page': 'Admin design page',
-'admin disabled because no admin password': 'admin disabled because no admin password',
-'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine',
-'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts',
+'admin disabled because no admin password': 'administracija onemogućena jer nema lozinke',
+'admin disabled because not supported on google app engine': 'administracija onemogućena na google app engine',
+'admin disabled because too many invalid login attempts': 'administracija onemogućena jer zbog većeg broja neuspjelih pokušaja',
'admin disabled because unable to access password file': 'administracija onemogućena jer ne mogu pristupiti datoteci sa lozinkom',
-'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel',
+'Admin is disabled because insecure channel': 'Administracija onemogućena zbog nesigurne veze',
'Admin language': 'Jezik administratora',
'Admin versioning page': 'Admin versioning page',
-'administrative interface': 'administrativni interfejs',
+'administrative interface': 'administrativno okruženje',
'Administrator Password:': 'Lozinka administratora:',
'and rename it:': 'i preimenuj u:',
-'App does not exist or you are not authorized': 'App does not exist or you are not authorized',
+'App does not exist or you are not authorized': 'Aplikacija ne postoji ili nemate prava pristupa',
'appadmin': 'appadmin',
-'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel',
-'Application': 'Application',
-'application "%s" uninstalled': 'application "%s" uninstalled',
+'appadmin is disabled because insecure channel': 'administracija onemogućena zbog nesigurne veze',
+'Application': 'Aplikacija',
+'application "%s" uninstalled': 'aplikacija "%s" je deinstalirana',
'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode',
-'application compiled': 'application compiled',
-'Application exists already': 'Application exists already',
-'application is compiled and cannot be designed': 'application is compiled and cannot be designed',
+'application compiled': 'aplikacija je kompajlirana',
+'Application exists already': 'Aplikacija već postoji',
+'application is compiled and cannot be designed': 'aplikacija je kompajlirana i ne može se dalje uređivati',
'Application name:': 'Naziv aplikacije:',
-'Application updated via git pull': 'Application updated via git pull',
+'Application updated via git pull': 'Aplikacija ažurirana preko git pull',
'are not used': 'nije korišteno',
'are not used yet': 'nije još korišteno',
-'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?',
-'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?',
+'Are you sure you want to delete file "%s"?': 'Da li ste sigurni da želite izbrisati datoteku "%s"?',
+'Are you sure you want to delete plugin "%s"?': 'Da li ste sigurni da želite izbrisati pomoćni modul "%s"?',
'Are you sure you want to delete this object?': 'Da li ste sigurni da želite obrisati?',
-'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?',
-'Are you sure?': 'Are you sure?',
+'Are you sure you want to uninstall application "%s"?': 'Da li ste sigurni da želite deinstalirati aplikaciju "%s"?',
+'Are you sure?': 'Da li ste sigurni?',
'arguments': 'arguments',
'at char %s': 'kod slova %s',
'at line %s': 'na liniji %s',
-'ATTENTION:': 'ATTENTION:',
+'ATTENTION:': 'PAŽNJA:',
'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.',
'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.',
-'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!',
+'ATTENTION: you cannot edit the running application!': 'ATTENTION: ne možete uređivati pokrenutu aplikaciju!',
'Autocomplete Python Code': 'Autocomplete Python Code',
-'Available Databases and Tables': 'Available Databases and Tables',
+'Available Databases and Tables': 'Dostupne baze podataka i tabele',
'back': 'nazad',
-'Back to the plugins list': 'Back to the plugins list',
+'Back to the plugins list': 'Nazad na listu pomoćnih modula',
'Back to wizard': 'Back to wizard',
'Basics': 'Osnove',
'Begin': 'Početak',
@@ -71,19 +72,19 @@
'breakpoints': 'breakpoints',
'Bulk Register': 'Bulk Register',
'Bulk Student Registration': 'Bulk Student Registration',
-'Cache': 'Cache',
-'cache': 'cache',
+'Cache': 'Keš',
+'cache': 'keš',
'Cache Cleared': 'Cache Cleared',
'Cache Keys': 'Cache Keys',
'cache, errors and sessions cleaned': 'keš, greške i sesije su obrisani',
-'can be a git repo': 'može biti git repo',
-'Cancel': 'Cancel',
-'Cannot be empty': 'Cannot be empty',
-'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:',
-'cannot create file': 'cannot create file',
+'can be a git repo': 'može biti git repozitorijum',
+'Cancel': 'Otkaži',
+'Cannot be empty': 'Ne može biti prazno',
+'Cannot compile: there are errors in your app:': 'Ne mogu kompajlirati: greška u aplikaciji:',
+'cannot create file': 'ne mogu kreirati datoteku',
'cannot upload file "%(filename)s"': 'ne mogu otpremiti datoteku "%(filename)s"',
-'Change Admin Password': 'Change Admin Password',
'Change admin password': 'Promijeni lozinku administratora',
+'Change Admin Password': 'Promijeni lozinku administratora',
'change editor settings': 'change editor settings',
'Changelog': 'Changelog',
'check all': 'check all',
@@ -91,66 +92,66 @@
'Check to delete': 'Check to delete',
'Checking for upgrades...': 'Provjeravam mogućnost nadogradnje...',
'Clean': 'Pročisti',
-'Clear': 'Clear',
-'Clear CACHE?': 'Clear CACHE?',
-'Clear DISK': 'Clear DISK',
-'Clear RAM': 'Clear RAM',
+'Clear': 'Obriši',
+'Clear CACHE?': 'Obriši CACHE?',
+'Clear DISK': 'Obriši DISK',
+'Clear RAM': 'Obriši RAM',
'Click row to expand traceback': 'Click row to expand traceback',
'Click row to view a ticket': 'Click row to view a ticket',
'code': 'kod',
-'Code listing': 'Code listing',
+'Code listing': 'Prikaz koda',
'collapse/expand all': 'sakriti/prikazati sve',
-'Command': 'Command',
-'Comment:': 'Comment:',
+'Command': 'Naredba',
+'Comment:': 'Komentar:',
'Commit': 'Commit',
'Commit form': 'Commit form',
'Committed files': 'Committed files',
'Compile': 'Kompajliraj',
-'Compile (all or nothing)': 'Compile (all or nothing)',
-'Compile (skip failed views)': 'Compile (skip failed views)',
-'compiled application removed': 'compiled application removed',
-'Condition': 'Condition',
-'continue': 'continue',
+'Compile (all or nothing)': 'Kompajliraj (sve ili ništa)',
+'Compile (skip failed views)': 'Kompajliraj (ignoriši greške)',
+'compiled application removed': 'kompajlirana aplikacija je uklonjena',
+'Condition': 'Stanje',
+'continue': 'nastavi',
'Controllers': 'Kontroleri',
'controllers': 'kontroleri',
-'Count': 'Count',
+'Count': 'Redni broj',
'Create': 'Kreiraj',
'create file with filename:': 'Kreiraj datoteku pod nazivom:',
'Create rules': 'Kreiraj pravila',
-'Create/Upload': 'Create/Upload',
+'Create/Upload': 'Kreiraj/Preuzmi',
'created by': 'izradio',
'Created by:': 'Created by:',
'Created On': 'Created On',
'Created on:': 'Created on:',
'crontab': 'crontab',
-'Current request': 'Current request',
-'Current response': 'Current response',
-'Current session': 'Current session',
+'Current request': 'Trenutni zahtjev',
+'Current response': 'Trenutni odgovor',
+'Current session': 'Trenutna sesija',
'currently running': 'trenutno pokrenut',
'currently saved or': 'trenutno sačuvano ili',
'data uploaded': 'data uploaded',
-'Database': 'Database',
+'Database': 'Baza podataka',
'Database %s select': 'Database %s select',
-'Database administration': 'Database administration',
'database administration': 'administracija baze podataka',
-'Database Administration (appadmin)': 'Database Administration (appadmin)',
-'Date and Time': 'Date and Time',
+'Database administration': 'Administracija baze podataka',
+'Database Administration (appadmin)': 'Administracija baze podataka (appadmin)',
+'Date and Time': 'Datum i vrijeme',
'db': 'db',
-'Debug': 'Debug',
+'Debug': 'Otkloni grešku',
'defines tables': 'definiše tabele',
-'Delete': 'Obriši',
-'delete': 'obriši',
-'delete all checked': 'delete all checked',
-'delete plugin': 'delete plugin',
-'Delete this file (you will be asked to confirm deletion)': 'Obriši ovu datoteku (bićete upitani za potvrdu brisanja)',
-'Delete:': 'Delete:',
+'delete': 'Izbriši',
+'Delete': 'Izbriši',
+'delete all checked': 'izbriši sve označeno',
+'delete plugin': 'izbriši pomoćni modul',
+'Delete this file (you will be asked to confirm deletion)': 'Izbriši ovu datoteku (bićete upitani za potvrdu brisanja)',
+'Delete:': 'Izbriši:',
'deleted after first hit': 'deleted after first hit',
'Demo': 'Demo',
'Deploy': 'Postavi',
'Deploy on Google App Engine': 'Postavi na Google App Engine',
'Deploy to OpenShift': 'Postavi na OpenShift',
-'Deploy to pythonanywhere': 'Deploy to pythonanywhere',
-'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere',
+'Deploy to pythonanywhere': 'Postavi na pythonanywhere',
+'Deploy to PythonAnywhere': 'Postavi na PythonAnywhere',
'Deployment form': 'Deployment form',
'Deployment Interface': 'Deployment Interface',
'Description:': 'Description:',
@@ -160,33 +161,33 @@
'direction: ltr': 'direction: ltr',
'directory not found': 'directory not found',
'Disable': 'Isključi',
-'Disabled': 'Disabled',
-'disabled in demo mode': 'disabled in demo mode',
-'disabled in GAE mode': 'disabled in GAE mode',
-'disabled in multi user mode': 'disabled in multi user mode',
+'Disabled': 'Isključeno',
+'disabled in demo mode': 'onemogućeno u demo modu',
+'disabled in GAE mode': 'onemogućeno u GAE modu',
+'disabled in multi user mode': 'onemogućeno u višekorisničkom modu',
'DISK': 'DISK',
'Disk Cache Keys': 'Disk Cache Keys',
'Disk Cleared': 'Disk Cleared',
'Display line numbers': 'Display line numbers',
'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.',
-'docs': 'dokumentacija',
'Docs': 'Docs',
+'docs': 'dokumentacija',
'done!': 'done!',
'Downgrade': 'Downgrade',
-'Download .w2p': 'Download .w2p',
-'Download as .exe': 'Download as .exe',
+'Download .w2p': 'Preuzmi kao .w2p',
+'Download as .exe': 'Preuzmi kao .exe',
'download layouts': 'preuzmi layouts',
'Download layouts from repository': 'Download layouts from repository',
-'download plugins': 'preuzmi plugins',
-'Download plugins from repository': 'Download plugins from repository',
-'Edit': 'Uređivanje',
+'download plugins': 'preuzmi pomoćne module',
+'Download plugins from repository': 'Preuzmi pomoćne module iz repozitorijum',
+'Edit': 'Uredi',
'edit all': 'uredi sve',
'Edit application': 'Uredi aplikaciju',
'edit controller': 'uredi controller',
'edit controller:': 'edit controller:',
'Edit current record': 'Edit current record',
-'edit views:': 'uredi views:',
-'Editing %s': 'Editing %s',
+'edit views:': 'uredi prikaz:',
+'Editing %s': 'Uređivanje %s',
'Editing file "%s"': 'Uređivanje datoteke "%s"',
'Editing Language file': 'Uređivanje jezičke datoteke',
'Editing Plural Forms File': 'Editing Plural Forms File',
@@ -206,21 +207,21 @@
'Exit Fullscreen': 'Exit Fullscreen',
'Expand Abbreviation': 'Expand Abbreviation',
'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)',
-'export as csv file': 'export as csv file',
-'Exports:': 'Exports:',
-'exposes': 'exposes',
-'exposes:': 'exposes:',
+'export as csv file': 'izvezi kao csv datoteku',
+'Exports:': 'Izvozi:',
+'exposes': 'prikazuje',
+'exposes:': 'prikazuje:',
'extends': 'proširuje',
'failed to compile file because:': 'nisam mogao da kompajliram zbog:',
'failed to reload module because:': 'failed to reload module because:',
'File': 'Datoteka',
-'file "%(filename)s" created': 'file "%(filename)s" created',
-'file "%(filename)s" deleted': 'file "%(filename)s" deleted',
-'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded',
-'file "%s" of %s restored': 'file "%s" of %s restored',
+'file "%(filename)s" created': 'datoteka "%(filename)s" je kreirana',
+'file "%(filename)s" deleted': 'datoteka "%(filename)s" je izbrisana',
+'file "%(filename)s" uploaded': 'datoteka "%(filename)s" je otpremljena',
+'file "%s" of %s restored': 'datoteka "%s" od %s je vraćeno u prethodno stanje',
'file changed on disk': 'file changed on disk',
'file does not exist': 'datoteka ne postoji',
-'file not found': 'file not found',
+'file not found': 'datoteka nije pronađena',
'file saved on %(time)s': 'file saved on %(time)s',
'file saved on %s': 'datoteka sačuvana na %s',
'filename': 'filename',
@@ -242,11 +243,11 @@
'Globals##debug': 'Globals##debug',
'Go to Matching Pair': 'Go to Matching Pair',
'go!': 'kreni!',
-'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface',
+'Google App Engine Deployment Interface': 'Google App Engine instalaciono okruženje',
'Google Application Id': 'Google Application Id',
'Goto': 'Goto',
-'graph model': 'graph model',
'Graph Model': 'Graph Model',
+'graph model': 'graph model',
'Help': 'Pomoć',
'here': 'here',
'Hide/Show Translated strings': 'Sakriti/Prikazati prevedene riječi',
@@ -260,7 +261,7 @@
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.',
'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.',
'import': 'import',
-'Import/Export': 'Import/Export',
+'Import/Export': 'Uvoz/Izvoz',
'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.',
'includes': 'uključuje',
'Indent with tabs': 'Indent with tabs',
@@ -270,49 +271,49 @@
'Installed applications': 'Instalirane aplikacije',
'Interaction at %s line %s': 'Interaction at %s line %s',
'Interactive console': 'Interactive console',
-'internal error': 'internal error',
-'internal error: %s': 'internal error: %s',
+'internal error': 'unutrašnja greška',
+'internal error: %s': 'unutrašnja greška: %s',
'Internal State': 'Internal State',
'Invalid action': 'Invalid action',
'Invalid application name': 'Invalid application name',
'invalid circular reference': 'invalid circular reference',
'Invalid git repository specified.': 'Invalid git repository specified.',
-'invalid password': 'invalid password',
-'invalid password.': 'pogrešna lozinka.',
-'Invalid Query': 'Invalid Query',
+'invalid password': 'Nevažeća lozinka',
+'invalid password.': 'nevažeća lozinka.',
+'Invalid Query': 'Pogrešan upit',
'invalid request': 'invalid request',
'Invalid request': 'Invalid request',
'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)',
-'invalid ticket': 'invalid ticket',
+'invalid ticket': 'pogrešan tiket',
'Key': 'Key',
'Key bindings': 'Prečice',
-'Key bindings for ZenCoding Plugin': 'Prečice za for ZenCoding Plugin',
-'Keyboard shortcuts': 'Keyboard shortcuts',
+'Key bindings for ZenCoding Plugin': 'Prečice za ZenCoding Plugin',
+'Keyboard shortcuts': 'Prečice na tastaturi',
'kill process': 'kill process',
-'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated',
+'language file "%(filename)s" created/updated': 'jezička datoteka "%(filename)s" je kreirana/ažurirana',
'Language files (static strings) updated': 'Jezičke datoteke su ažurirane',
-'languages': 'jezici',
'Languages': 'Jezici',
+'languages': 'jezici',
'Last Revision': 'Last Revision',
'Last saved on:': 'Posljednja izmjena:',
'License for': 'Licenca za',
-'License:': 'License:',
-'Line Nr': 'Line Nr',
-'Line number': 'Line number',
-'lists by exception': 'lists by exception',
-'lists by ticket': 'lists by ticket',
-'Loading...': 'Loading...',
+'License:': 'Licenca:',
+'Line Nr': 'Linija broj',
+'Line number': 'Linija broj',
+'lists by exception': 'prikaži greške',
+'lists by ticket': 'prikaži tikete',
+'Loading...': 'Preuzimam...',
'loading...': 'preuzimam...',
-'Local Apps': 'Local Apps',
+'Local Apps': 'Lokalne aplikacije',
'locals': 'locals',
'Locals##debug': 'Locals##debug',
'Login': 'Prijava',
'Login successful': 'Login successful',
-'Login to the Administrative Interface': 'Prijava za administrativni interfejs',
-'Login/Register': 'Login/Register',
-'Logout': 'Izlaz',
-'lost password': 'lost password',
-'Main Menu': 'Main Menu',
+'Login to the Administrative Interface': 'Prijava za administrativno okruženje',
+'Login/Register': 'Prijava/Registracija',
+'Logout': 'Odjava',
+'lost password': 'izgubljena lozinka',
+'Main Menu': 'Glavni meni',
'Manage': 'Manage',
'Manage %(action)s': 'Manage %(action)s',
'Manage Access Control': 'Manage Access Control',
@@ -321,68 +322,68 @@
'Manage Students': 'Manage Students',
'Match Pair': 'Match Pair',
'Memberships': 'Memberships',
-'merge': 'merge',
+'merge': 'spoji',
'Merge Lines': 'Spoji linije',
-'Models': 'Models',
-'models': 'models',
+'Models': 'Modeli',
+'models': 'modeli',
'Modified On': 'Modified On',
-'Modules': 'Modules',
-'modules': 'modules',
-'Multi User Mode': 'Multi User Mode',
-'new application "%s" created': 'new application "%s" created',
-'new application "%s" imported': 'new application "%s" imported',
-'New Application Wizard': 'Čarobnjak za nove aplikacije',
+'modules': 'moduli',
+'Modules': 'Moduli',
+'Multi User Mode': 'Višekorisnički režim rada',
+'new application "%s" created': 'nova aplikacija "%s" je kreirana',
+'new application "%s" imported': 'nova aplikacija "%s" je uvežena',
'New application wizard': 'Čarobnjak za nove aplikacije',
-'new plugin installed': 'new plugin installed',
-'New plugin installed: %s': 'New plugin installed: %s',
-'New Record': 'New Record',
-'new record inserted': 'new record inserted',
+'New Application Wizard': 'Čarobnjak za nove aplikacije',
+'new plugin installed': 'novi pomoćni modul je instaliran',
+'New plugin installed: %s': 'Instaliran novi pomoćni modul: %s',
+'New Record': 'Novi zapis',
+'new record inserted': 'unešen novi zapis',
'New simple application': 'Nova jednostavna aplikacija',
-'next': 'next',
+'next': 'sledeći',
'next %s rows': 'next %s rows',
'Next Edit Point': 'Next Edit Point',
-'NO': 'NO',
-'no changes': 'no changes',
-'No databases in this application': 'No databases in this application',
-'No Interaction yet': 'No Interaction yet',
-'no match': 'no match',
-'no package selected': 'no package selected',
-'no permission to uninstall "%s"': 'no permission to uninstall "%s"',
+'NO': 'NE',
+'no changes': 'nema promjena',
+'No databases in this application': 'Nema baze podataka u aplikaciji',
+'No Interaction yet': 'Nema još interakcije',
+'no match': 'nema podudaranja',
+'no package selected': 'paket nije odabran',
+'no permission to uninstall "%s"': 'nemate ovlaštenje da deinstalirate "%s"',
'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder',
'Node:': 'Node:',
-'Not Authorized': 'Not Authorized',
-'Not supported': 'Not supported',
+'Not Authorized': 'Nemate ovlašćenje',
+'Not supported': 'Nije podržano',
'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.',
"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.",
'online designer': 'onlajn dizajner',
-'Open new app in new window': 'Open new app in new window',
-'OpenShift Deployment Interface': 'OpenShift Deployment Interface',
+'Open new app in new window': 'Otvori novu aplikaciju u novom prozoru',
+'OpenShift Deployment Interface': 'OpenShift instalaciono okruženje',
'OpenShift Output': 'OpenShift Output',
-'or alternatively': 'or alternatively',
+'or alternatively': 'ili alternativno',
'Or Get from URL:': 'Or Get from URL:',
-'or import from csv file': 'or import from csv file',
+'or import from csv file': 'ili uvezi pomoću csv datoteke',
'Original/Translation': 'Original/Prevod',
-'Overview': 'Overview',
-'Overwrite installed app': 'Prebriši postojeću aplikaciju',
+'Overview': 'Pregled',
+'Overwrite installed app': 'Zamjeni već postojeću aplikaciju',
'Pack all': 'Zapakuj sve',
-'Pack compiled': 'Pack compiled',
-'Pack custom': 'Pack custom',
-'pack plugin': 'pack plugin',
-'password changed': 'password changed',
-'Past revisions': 'Past revisions',
+'Pack compiled': 'Zapakuj kompajlirano',
+'Pack custom': 'Prilagođeno pakovanje',
+'pack plugin': 'zapakuj pomoćni modul',
+'password changed': 'lozinka je promijenjena',
+'Past revisions': 'Prethodne korekcije',
'Path to appcfg.py': 'Path to appcfg.py',
'Path to local openshift repo root.': 'Path to local openshift repo root.',
-'Peeking at file': 'Peeking at file',
-'Permission': 'Permission',
-'Permissions': 'Permissions',
-'Please': 'Please',
+'Peeking at file': 'Pregled datoteke',
+'Permission': 'Dozvola',
+'Permissions': 'Dozvole',
+'Please': 'Molim',
'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...',
-'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted',
-'Plugin "%s" in application': 'Plugin "%s" in application',
+'plugin "%(plugin)s" deleted': 'Pomoćni modul "%(plugin)s" je izbrisan',
+'Plugin "%s" in application': 'Pomoćni modul "%s" u aplikaciji',
'plugin not specified': 'plugin not specified',
-'Plugin page': 'Plugin page',
-'plugins': 'plugins',
-'Plugins': 'Plugins',
+'Plugin page': 'Stranica pomoćnih modula',
+'Plugins': 'Pomoćni moduli',
+'plugins': 'pomoćni moduli',
'Plural Form #%s': 'Plural Form #%s',
'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Omogućio',
@@ -390,18 +391,18 @@
'Preferences saved on session only': 'Preferences saved on session only',
'previous %s rows': 'previous %s rows',
'Previous Edit Point': 'Previous Edit Point',
-'Private files': 'Private files',
-'private files': 'private files',
+'private files': 'privatne datoteke',
+'Private files': 'Privatne datoteke',
'Project Progress': 'Napredak projekta',
'Pull': 'Pull',
'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.',
'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.',
'Push': 'Push',
'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.',
-'pygraphviz library not found': 'pygraphviz library not found',
+'pygraphviz library not found': 'pygraphviz biblioteka nije pronađena',
'PythonAnywhere Apps': 'PythonAnywhere Apps',
'PythonAnywhere Password': 'PythonAnywhere Password',
-'Query:': 'Query:',
+'Query:': 'Upit:',
'RAM': 'RAM',
'RAM Cache Keys': 'RAM Cache Keys',
'Ram Cleared': 'Ram Cleared',
@@ -416,7 +417,7 @@
'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s',
'Replace': 'Zamijeni',
'Replace All': 'Zamijeni sve',
-'Repository (%s)': 'Repository (%s)',
+'Repository (%s)': 'Repozitorijum (%s)',
'request': 'request',
'requires distutils, but not installed': 'requires distutils, but not installed',
'requires python-git, but not installed': 'requires python-git, but not installed',
@@ -430,78 +431,78 @@
'reverted to revision %s': 'reverted to revision %s',
'Revision %s': 'Revision %s',
'Revision:': 'Revision:',
-'Role': 'Role',
-'Roles': 'Roles',
-'Rows in Table': 'Rows in Table',
+'Role': 'Uloga',
+'Roles': 'Uloge',
+'Rows in Table': 'Zapisi u tabeli',
'Rows selected': 'Rows selected',
'rules are not defined': 'pravila nisu definisana',
'rules:': 'pravila:',
-'Run tests': 'Run tests',
-'Run tests in this file': 'Run tests in this file',
+'Run tests': 'Pokreni testove',
+'Run tests in this file': 'Pokreni testove u datoteci',
"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')",
'Running on %s': 'Pokrenuto na %s',
'Save': 'Sačuvaj',
-'Save file:': 'Save file:',
-'Save file: %s': 'Save file: %s',
-'Save model as...': 'Save model as...',
-'Save via Ajax': 'Sačuvaj via Ajax',
+'Save file:': 'Sačuvaj datoteku:',
+'Save file: %s': 'Sačuvaj datoteku: %s',
+'Save model as...': 'Sačuvaj model kao...',
+'Save via Ajax': 'Sačuvaj preko Ajax',
'Saved file hash:': 'Sačuvano kao haš:',
-'Screenshot %s': 'Screenshot %s',
-'Search': 'Search',
-'Select Files to Package': 'Select Files to Package',
+'Screenshot %s': 'Snimak ekrana %s',
+'Search': 'Pretraga',
+'Select Files to Package': 'Odaberi datoteke za pakovanje',
'session': 'sesija',
-'session expired': 'sesija istekla',
-'Session saved correctly': 'Session saved correctly',
+'session expired': 'sesija je istekla',
+'Session saved correctly': 'Sesija je uredno sačuvana',
'Session saved on session only': 'Session saved on session only',
'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s',
'shell': 'shell',
-'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found',
+'Showing %s to %s of %s %s found': 'Prikazujem %s do %s od %s %s pronađenih',
'Singular Form': 'Singular Form',
-'Site': 'Sajt',
+'Site': 'Početna',
'Size of cache:': 'Size of cache:',
-'skip to generate': 'skip to generate',
-'some files could not be removed': 'some files could not be removed',
+'skip to generate': 'preskoči generisanje',
+'some files could not be removed': 'neke datoteke ne mogu biti uklonjene',
'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying',
-'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed',
+'Sorry, could not find mercurial installed': 'Žalim, mercurial nije instaliran',
'source : db': 'source : db',
'source : filesystem': 'source : filesystem',
'Start a new app': 'Pokreni novu aplikaciju',
'Start searching': 'Pokreni pretragu',
'Start wizard': 'Pokreni čarobnjaka',
'state': 'state',
-'Static': 'Static',
'static': 'static',
+'Static': 'Static',
'Static files': 'Static files',
'Statistics': 'Statistics',
'Step': 'Korak',
-'step': 'step',
+'step': 'korak',
'stop': 'stop',
-'submit': 'submit',
'Submit': 'Prihvati',
+'submit': 'prihvati',
'successful': 'uspješan',
-'switch to : db': 'switch to : db',
-'switch to : filesystem': 'switch to : filesystem',
+'switch to : db': 'pređi na : db',
+'switch to : filesystem': 'pređi na : filesystem',
'Tab width (# characters)': 'Tab width (# characters)',
-'Table': 'Table',
+'Table': 'Tabela',
'Temporary': 'Temporary',
'test': 'test',
-'Testing application': 'Testing application',
+'Testing application': 'Testiranje aplikacije',
'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.',
'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!',
'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!',
'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller',
'The data representation, define database tables and sets': 'The data representation, define database tables and sets',
'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates',
-'Theme': 'Theme',
-'There are no controllers': 'There are no controllers',
-'There are no models': 'There are no models',
-'There are no modules': 'There are no modules',
-'There are no plugins': 'There are no plugins',
-'There are no private files': 'There are no private files',
-'There are no static files': 'There are no static files',
-'There are no translators': 'There are no translators',
+'Theme': 'Teme',
+'There are no controllers': 'Nema kontolera',
+'There are no models': 'Nema modela',
+'There are no modules': 'Nema modula',
+'There are no plugins': 'Nema pomoćnih modula',
+'There are no private files': 'Nema privatnih datoteka',
+'There are no static files': 'Nema statičnih datoteka',
+'There are no translators': 'Nema prevodioca',
'There are no translators, only default language is supported': 'There are no translators, only default language is supported',
-'There are no views': 'There are no views',
+'There are no views': 'Nema stranica prikaza',
'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app',
'These files are served without processing, your images go here': 'These files are served without processing, your images go here',
"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.",
@@ -512,9 +513,9 @@
'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.',
'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?',
'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".',
-'Ticket': 'Ticket',
-'Ticket ID': 'Ticket ID',
-'Ticket Missing': 'Ticket nedostaje',
+'Ticket': 'Tiket',
+'Ticket ID': 'Tiket ID',
+'Ticket Missing': 'Nedostaje tiket',
'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)',
'to previous version.': 'na prethodnu verziju.',
'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]',
@@ -525,67 +526,67 @@
'Toggle Fullscreen': 'Toggle Fullscreen',
'Traceback': 'Traceback',
'Translation strings for the application': 'Riječi u aplikaciji koje treba prevesti',
-'try something like': 'try something like',
-'Try the mobile interface': 'Probaj mobilni interfejs',
-'try view': 'try view',
+'try something like': 'na primjer',
+'Try the mobile interface': 'Probaj mobilno okruženje',
+'try view': 'probaj prikaz',
'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.',
'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.',
-'Unable to check for upgrades': 'Unable to check for upgrades',
+'Unable to check for upgrades': 'Ne mogu da provjerim mogućnost nadogradnje',
'unable to create application "%s"': 'unable to create application "%s"',
-'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"',
-'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"',
-'Unable to determine the line number!': 'Unable to determine the line number!',
-'Unable to download app because:': 'Unable to download app because:',
+'unable to delete file "%(filename)s"': 'ne mogu izbrisati datoteku "%(filename)s"',
+'unable to delete file plugin "%(plugin)s"': 'ne mogu izbrisati pomoćni modul "%(plugin)s"',
+'Unable to determine the line number!': 'Ne mogu da utvrdim broj reda!',
+'Unable to download app because:': 'Ne mogu da preuzmem aplikaciju zbog:',
'unable to download layout': 'unable to download layout',
'unable to download plugin: %s': 'unable to download plugin: %s',
-'Unable to download the list of plugins': 'Unable to download the list of plugins',
-'unable to install plugin "%s"': 'unable to install plugin "%s"',
-'unable to parse csv file': 'unable to parse csv file',
-'unable to uninstall "%s"': 'unable to uninstall "%s"',
-'unable to upgrade because "%s"': 'unable to upgrade because "%s"',
+'Unable to download the list of plugins': 'Ne mogu da preuzmem spisak pomoćnih modula',
+'unable to install plugin "%s"': 'ne mogu da instaliram pomoćni modul "%s"',
+'unable to parse csv file': 'ne mogu da rasčlanim csv datoteku',
+'unable to uninstall "%s"': 'ne mogu da deinstaliram "%s"',
+'unable to upgrade because "%s"': 'ne mogu da ažurimam zbog "%s"',
'uncheck all': 'uncheck all',
'Uninstall': 'Deinstaliraj',
'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s',
'update': 'ažuriraj',
'update all languages': 'ažuriraj sve jezike',
-'Update:': 'Update:',
-'Upgrade': 'Upgrade',
-'upgrade now to %s': 'upgrade now to %s',
+'Update:': 'Ažuriraj:',
+'Upgrade': 'Nadogradnja',
+'upgrade now to %s': 'ažuriran na %s',
'upload': 'Otpremi',
-'Upload': 'Upload',
+'Upload': 'Preuzmi',
'Upload a package:': 'Preuzmi paket:',
'Upload and install packed application': 'Preuzmi i instaliraj zapakovanu aplikaciju',
'upload file:': 'preuzmi datoteku:',
-'upload plugin file:': 'preuzmi plugin datoteku:',
+'upload plugin file:': 'preuzmi datoteku pomoćnog modula:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.',
-'User': 'User',
-'Username': 'Username',
-'Users': 'Users',
+'User': 'Korisnik',
+'Username': 'Korisničko ime',
+'Users': 'Korisnici',
'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.',
-'variables': 'variables',
+'variables': 'promenljive',
'Version': 'Verzija',
'Version %s.%s.%s (%s) %s': 'Verzija %s.%s.%s (%s) %s',
-'Versioning': 'Versioning',
-'Views': 'Views',
-'views': 'views',
-'Warning!': 'Warning!',
-'WARNING:': 'WARNING:',
-'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:',
+'Versioning': 'Kreiranje verzija',
+'views': 'prikazi',
+'Views': 'Prikazi',
+'Warning!': 'Upozorenje!',
+'WARNING:': 'UPOZORENJE:',
+'WARNING: The following views could not be compiled:': 'WARNING: Sledeći prikazi ne mogu biti kompajlirani:',
'Web Framework': 'Web Framework',
-'web2py Admin Password': 'web2py Admin Password',
-'web2py apps to deploy': 'web2py apps to deploy',
+'web2py Admin Password': 'web2py administratorska lozinka',
+'web2py apps to deploy': 'web2py aplikacija za instalaciju',
'web2py Debugger': 'web2py Debugger',
'web2py downgrade': 'web2py downgrade',
'web2py is up to date': 'web2py je ažuran',
'web2py online debugger': 'web2py online debugger',
'web2py Recent Tweets': 'web2py Recent Tweets',
-'web2py upgrade': 'web2py upgrade',
-'web2py upgraded; please restart it': 'web2py upgraded; please restart it',
-'Working...': 'Working...',
+'web2py upgrade': 'web2py nadogradnja',
+'web2py upgraded; please restart it': 'web2py je ažuriran; molim da restartujete',
+'Working...': 'Izvršavam...',
'Wrap with Abbreviation': 'Wrap with Abbreviation',
-'WSGI reference name': 'WSGI reference name',
-'YES': 'YES',
-'Yes': 'Yes',
+'WSGI reference name': 'WSGI referentni naziv',
+'YES': 'DA',
+'Yes': 'Da',
'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button',
'You can inspect variables using the console below': 'You can inspect variables using the console below',
'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out',
diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js
index f3d14b29d..7853e8cb9 100644
--- a/applications/admin/static/js/web2py.js
+++ b/applications/admin/static/js/web2py.js
@@ -617,8 +617,8 @@
}
if (confirm_message) {
if (confirm_message == 'default') {
- confirm_message = w2p_ajax_confirm_message ||
- 'Are you sure you want to delete this object?';
+ confirm_message = !web2py.isUndefined(w2p_ajax_confirm_message) ?
+ w2p_ajax_confirm_message : 'Are you sure you want to delete this object?';
}
if (!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
diff --git a/applications/admin/views/web2py_ajax.html b/applications/admin/views/web2py_ajax.html
index a5de41ea4..28ec0e023 100644
--- a/applications/admin/views/web2py_ajax.html
+++ b/applications/admin/views/web2py_ajax.html
@@ -1,10 +1,12 @@
{{
response.files.insert(0,URL('static','js/jquery.js'))
diff --git a/applications/examples/static/css/stupid.css b/applications/examples/static/css/stupid.css
index 80bf9b09b..3e68beddb 100644
--- a/applications/examples/static/css/stupid.css
+++ b/applications/examples/static/css/stupid.css
@@ -54,8 +54,8 @@ header, main, footer {display:block; with:100%} /* IE fix */
.right {right:0; text-align:right}
.middle div {vertical-align:middle}
.bottom div {vertical-align:bottom}
-.xscroll {overflow-x:scroll}
-.yscroll {overflow-y:scroll}
+.xscroll {overflow-x:scroll !important}
+.yscroll {overflow-y:scroll !important}
.nowrap {white-space:nowrap; overflow-x:hidden}
.fill {width:100%}
.lifted {box-shadow:5px 5px 10px #666}
@@ -75,7 +75,7 @@ input:invalid, input.error, textarea:invalid, textarea.error {background: #ffdfd
/*** grid ***/
.container {margin-right:-20px}
.container>.quarter, .container>.half, .container>.third, .container>.twothirds, .container>.threequarters {display:inline-block; padding: 0 20px 0 0; vertical-align:top}
-.container>.fill{display: inline-block}
+.container>.fill{display: inline-block; padding-right: 20px; margin-right:-10px}
.container img, .container video {max-width:100%}
.max900 {margin-left:auto; margin-right:auto}
diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js
index 15ff515cb..7853e8cb9 100644
--- a/applications/examples/static/js/web2py.js
+++ b/applications/examples/static/js/web2py.js
@@ -562,7 +562,7 @@
var flash = $('.w2p_flash');
web2py.hide_flash();
flash.html(message).addClass(status);
- if (flash.html()) flash.slideDown();
+ if (flash.html()) flash.append(' × ').slideDown();
},
hide_flash: function () {
$('.w2p_flash').fadeOut(0).html('');
@@ -617,8 +617,8 @@
}
if (confirm_message) {
if (confirm_message == 'default') {
- confirm_message = w2p_ajax_confirm_message ||
- 'Are you sure you want to delete this object?';
+ confirm_message = !web2py.isUndefined(w2p_ajax_confirm_message) ?
+ w2p_ajax_confirm_message : 'Are you sure you want to delete this object?';
}
if (!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
diff --git a/applications/examples/views/web2py_ajax.html b/applications/examples/views/web2py_ajax.html
index bf5548688..0f064de43 100644
--- a/applications/examples/views/web2py_ajax.html
+++ b/applications/examples/views/web2py_ajax.html
@@ -1,9 +1,11 @@
{{
response.files.insert(0,URL('static','js/jquery.js'))
diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py
index 0b8b227f9..4d4e965fb 100644
--- a/applications/welcome/controllers/appadmin.py
+++ b/applications/welcome/controllers/appadmin.py
@@ -409,7 +409,7 @@ def ccache():
import copy
import time
import math
- from gluon import portalocker
+ from pydal.contrib import portalocker
ram = {
'entries': 0,
diff --git a/applications/welcome/languages/.DS_Store b/applications/welcome/languages/.DS_Store
deleted file mode 100644
index 0c703e447..000000000
Binary files a/applications/welcome/languages/.DS_Store and /dev/null differ
diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js
index 2bedd1c5c..7853e8cb9 100644
--- a/applications/welcome/static/js/web2py.js
+++ b/applications/welcome/static/js/web2py.js
@@ -201,7 +201,7 @@
showsTime: true,
timeFormat: '24'
});
- $(this).prop('autocomplete', 'off');
+ $(this).attr('autocomplete', 'off');
$(this).data('w2p_datetime', 1);
$(this).trigger('click');
}
@@ -218,7 +218,7 @@
showsTime: false
});
$(this).data('w2p_date', 1);
- $(this).prop('autocomplete', 'off');
+ $(this).attr('autocomplete', 'off');
$(this).trigger('click');
}
});
@@ -227,7 +227,7 @@
if (web2py.isUndefined(active)) {
$(this).timeEntry({
spinnerImage: ''
- }).prop('autocomplete', 'off');
+ }).attr('autocomplete', 'off');
$(this).data('w2p_time', 1);
}
});
@@ -617,8 +617,8 @@
}
if (confirm_message) {
if (confirm_message == 'default') {
- confirm_message = w2p_ajax_confirm_message ||
- 'Are you sure you want to delete this object?';
+ confirm_message = !web2py.isUndefined(w2p_ajax_confirm_message) ?
+ w2p_ajax_confirm_message : 'Are you sure you want to delete this object?';
}
if (!web2py.confirm(confirm_message)) {
web2py.stopEverything(e);
diff --git a/applications/welcome/views/web2py_ajax.html b/applications/welcome/views/web2py_ajax.html
index b8e1a113a..28ec0e023 100644
--- a/applications/welcome/views/web2py_ajax.html
+++ b/applications/welcome/views/web2py_ajax.html
@@ -1,10 +1,12 @@
{{
response.files.insert(0,URL('static','js/jquery.js'))
diff --git a/appveyor.yml b/appveyor.yml
index 80e86c5d8..dde913d43 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -17,8 +17,7 @@ init:
- set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
install:
- - ps: Start-FileDownload https://bootstrap.pypa.io/get-pip.py
- - python get-pip.py
+ - python -m ensurepip
- pip install codecov
- git submodule update --init --recursive
- pip install pycrypto
diff --git a/fabfile.py b/fabfile.py
index 2e7db9e85..00a52c02f 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -136,6 +136,26 @@ def deploy(appname=None, all=False):
if backup:
print 'TO RESTORE: fab restore:%s' % backup
+
+def deploynobackup(appname=None):
+ """fab -H username@host deploy:appname,all"""
+ appname = appname or os.path.split(os.getcwd())[-1]
+ appfolder = applications+'/'+appname
+ zipfile = os.path.join(appfolder, '_update.zip')
+ if os.path.exists(zipfile):
+ os.unlink(zipfile)
+
+ local('zip -r _update.zip */*.py */*/*.py views/*.html views/*/*.html static/*')
+
+ put('_update.zip','/tmp/_update.zip')
+ try:
+ with cd(appfolder):
+ sudo('unzip -o /tmp/_update.zip')
+ sudo('chown -R www-data:www-data *')
+ sudo('echo "%s" > DATE_DEPLOYMENT' % now)
+
+ finally:
+ sudo('rm /tmp/_update.zip')
def restore(backup):
"""fab -H username@host restore:backupfilename"""
diff --git a/gluon/_compat.py b/gluon/_compat.py
index f7b53dde7..a87a5c943 100644
--- a/gluon/_compat.py
+++ b/gluon/_compat.py
@@ -31,6 +31,7 @@
from types import ClassType
import cgi
import cookielib
+ from xmlrpclib import ProtocolError
BytesIO = StringIO
reduce = reduce
hashlib_md5 = hashlib.md5
@@ -94,6 +95,7 @@ def to_native(obj, charset='utf8', errors='strict'):
from urllib.request import FancyURLopener, urlopen
from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode
from http import cookiejar as cookielib
+ from xmlrpc.client import ProtocolError
import html # warning, this is the python3 module and not the web2py html module
hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8'))
iterkeys = lambda d: iter(d.keys())
diff --git a/gluon/admin.py b/gluon/admin.py
index 3eb3bc54e..9615956fb 100644
--- a/gluon/admin.py
+++ b/gluon/admin.py
@@ -435,13 +435,20 @@ def add_path_first(path):
if not global_settings.web2py_runtime_gae:
site.addsitedir(path)
+def try_mkdir(path):
+ if not os.path.exists(path):
+ try:
+ os.mkdir(path)
+ except OSError as e:
+ if e.strerror == 'File exists': # In case of race condition.
+ pass
+ else:
+ raise e
def create_missing_folders():
if not global_settings.web2py_runtime_gae:
for path in ('applications', 'deposit', 'site-packages', 'logs'):
- path = abspath(path, gluon=True)
- if not os.path.exists(path):
- os.mkdir(path)
+ try_mkdir(abspath(path, gluon=True))
"""
OLD sys.path dance
paths = (global_settings.gluon_parent, abspath(
@@ -449,7 +456,7 @@ def create_missing_folders():
"""
paths = (global_settings.gluon_parent, abspath(
'site-packages', gluon=True), '')
- [add_path_first(path) for p in paths]
+ [add_path_first(p) for p in paths]
def create_missing_app_folders(request):
@@ -458,7 +465,5 @@ def create_missing_app_folders(request):
for subfolder in ('models', 'views', 'controllers', 'databases',
'modules', 'cron', 'errors', 'sessions',
'languages', 'static', 'private', 'uploads'):
- path = os.path.join(request.folder, subfolder)
- if not os.path.exists(path):
- os.mkdir(path)
+ try_mkdir(os.path.join(request.folder, subfolder))
global_settings.app_folders.add(request.folder)
diff --git a/gluon/compileapp.py b/gluon/compileapp.py
index 0ee9c18c9..03529434a 100644
--- a/gluon/compileapp.py
+++ b/gluon/compileapp.py
@@ -230,8 +230,8 @@ def LOAD(c=None, f='index', args=None, vars=None,
if isinstance(page, dict):
other_response._vars = page
other_response._view_environment.update(page)
- run_view_in(other_response._view_environment)
- page = other_response.body.getvalue()
+ page = run_view_in(other_response._view_environment)
+
current.request, current.response = original_request, original_response
js = None
if ajax_trap:
@@ -309,8 +309,8 @@ def __call__(self, c=None, f='index', args=None, vars=None,
if isinstance(page, dict):
other_response._vars = page
other_response._view_environment.update(page)
- run_view_in(other_response._view_environment)
- page = other_response.body.getvalue()
+ page = run_view_in(other_response._view_environment)
+
current.request, current.response = original_request, original_response
js = None
if ajax_trap:
@@ -430,13 +430,10 @@ def build_environment(request, response, session, store_current=True):
current.T = t
current.cache = c
- global __builtins__
if is_jython: # jython hack
+ global __builtins__
__builtins__ = mybuiltin()
- elif is_pypy: # apply the same hack to pypy too
- __builtins__ = mybuiltin()
- elif PY2:
- __builtins__['__import__'] = builtin.__import__ # WHY?
+
environment['request'] = request
environment['response'] = response
environment['session'] = session
@@ -444,7 +441,6 @@ def build_environment(request, response, session, store_current=True):
lambda name, reload=False, app=request.application:\
local_import_aux(name, reload, app)
BaseAdapter.set_folder(pjoin(request.folder, 'databases'))
- response._view_environment = copy.copy(environment)
custom_import_install()
return environment
@@ -487,7 +483,7 @@ def compile_views(folder, skip_failed_views=False):
else:
raise Exception("%s in %s" % (e, fname))
else:
- filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_')
+ filename = 'views.%s.py' % fname.replace(os.path.sep, '.')
filename = pjoin(folder, 'compiled', filename)
write_file(filename, data)
save_pyc(filename)
@@ -503,7 +499,7 @@ def compile_models(folder):
path = pjoin(folder, 'models')
for fname in listdir(path, '.+\.py$'):
data = read_file(pjoin(path, fname))
- modelfile = 'models.'+fname.replace(os.path.sep,'.')
+ modelfile = 'models.'+fname.replace(os.path.sep, '.')
filename = pjoin(folder, 'compiled', modelfile)
mktree(filename)
write_file(filename, data)
@@ -581,44 +577,36 @@ def run_models_in(environment):
if not regex.search(fname) and c != 'appadmin':
continue
elif compiled:
- code = read_pyc(model)
- elif is_gae:
- code = getcfs(model, model,
- lambda: compile2(read_file(model), model))
+ f = lambda: read_pyc(model)
else:
- code = getcfs(model, model, None)
- restricted(code, environment, layer=model)
-
+ f = lambda: compile2(read_file(model), model)
+ ccode = getcfs(model, model, f)
+ restricted(ccode, environment, layer=model)
def run_controller_in(controller, function, environment):
"""
Runs the controller.function() (for the app specified by
the current folder).
- It tries pre-compiled controller_function.pyc first before compiling it.
+ It tries pre-compiled controller.function.pyc first before compiling it.
"""
# if compiled should run compiled!
folder = current.request.folder
- path = pjoin(folder, 'compiled')
+ cpath = pjoin(folder, 'compiled')
badc = 'invalid controller (%s/%s)' % (controller, function)
badf = 'invalid function (%s/%s)' % (controller, function)
- if os.path.exists(path):
- filename = pjoin(path, 'controllers.%s.%s.pyc'
+ if os.path.exists(cpath):
+ filename = pjoin(cpath, 'controllers.%s.%s.pyc'
% (controller, function))
if not os.path.exists(filename):
- ### for backward compatibility
- filename = pjoin(path, 'controllers_%s_%s.pyc'
- % (controller, function))
- ### end for backward compatibility
- if not os.path.exists(filename):
- raise HTTP(404,
- rewrite.THREAD_LOCAL.routes.error_message % badf,
- web2py_error=badf)
- restricted(read_pyc(filename), environment, layer=filename)
+ raise HTTP(404,
+ rewrite.THREAD_LOCAL.routes.error_message % badf,
+ web2py_error=badf)
+ ccode = getcfs(filename, filename, lambda: read_pyc(filename))
elif function == '_TEST':
# TESTING: adjust the path to include site packages
- from settings import global_settings
- from admin import abspath, add_path_first
+ from gluon.settings import global_settings
+ from gluon.admin import abspath, add_path_first
paths = (global_settings.gluon_parent, abspath(
'site-packages', gluon=True), abspath('gluon', gluon=True), '')
[add_path_first(path) for path in paths]
@@ -633,7 +621,7 @@ def run_controller_in(controller, function, environment):
environment['__symbols__'] = environment.keys()
code = read_file(filename)
code += TEST_CODE
- restricted(code, environment, layer=filename)
+ ccode = compile2(code, filename)
else:
filename = pjoin(folder, 'controllers/%s.py'
% controller)
@@ -641,17 +629,17 @@ def run_controller_in(controller, function, environment):
raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badc,
web2py_error=badc)
- code = read_file(filename)
+ code = getcfs(filename, filename, lambda: read_file(filename))
exposed = find_exposed_functions(code)
if not function in exposed:
raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badf,
web2py_error=badf)
- code = "%s\nresponse._vars=response._caller(%s)\n" % (code, function)
- if is_gae:
- layer = filename + ':' + function
- code = getcfs(layer, filename, lambda: compile2(code, layer))
- restricted(code, environment, filename)
+ code = "%s\nresponse._vars=response._caller(%s)" % (code, function)
+ layer = "%s:%s" % (filename, function)
+ ccode = getcfs(layer, filename, lambda: compile2(code, layer))
+
+ restricted(ccode, environment, layer=filename)
response = current.response
vars = response._vars
if response.postprocessing:
@@ -668,15 +656,16 @@ def run_view_in(environment):
Executes the view for the requested action.
The view is the one specified in `response.view` or determined by the url
or `view/generic.extension`
- It tries the pre-compiled views_controller_function.pyc before compiling it.
+ It tries the pre-compiled views.controller.function.pyc before compiling it.
"""
request = current.request
response = current.response
view = environment['response'].view
folder = request.folder
- path = pjoin(folder, 'compiled')
+ cpath = pjoin(folder, 'compiled')
badv = 'invalid view (%s)' % view
patterns = response.get('generic_patterns')
+ layer = None
if patterns:
regex = re_compile('|'.join(map(fnmatch.translate, patterns)))
short_action = '%(controller)s/%(function)s.%(extension)s' % request
@@ -686,29 +675,29 @@ def run_view_in(environment):
if not isinstance(view, str):
ccode = parse_template(view, pjoin(folder, 'views'),
context=environment)
- restricted(ccode, environment, 'file stream')
+ layer = 'file stream'
else:
filename = pjoin(folder, 'views', view)
- if os.path.exists(path): # compiled views
- x = view.replace('/', '_')
- files = ['views_%s.pyc' % x]
- is_compiled = os.path.exists(pjoin(path, files[0]))
+ if os.path.exists(cpath): # compiled views
+ x = view.replace('/', '.')
+ files = ['views.%s.pyc' % x]
+ is_compiled = os.path.exists(pjoin(cpath, files[0]))
# Don't use a generic view if the non-compiled view exists.
if is_compiled or (not is_compiled and not os.path.exists(filename)):
if allow_generic:
- files.append('views_generic.%s.pyc' % request.extension)
+ files.append('views.generic.%s.pyc' % request.extension)
# for backward compatibility
if request.extension == 'html':
- files.append('views_%s.pyc' % x[:-5])
+ files.append('views.%s.pyc' % x[:-5])
if allow_generic:
- files.append('views_generic.pyc')
+ files.append('views.generic.pyc')
# end backward compatibility code
for f in files:
- compiled = pjoin(path, f)
+ compiled = pjoin(cpath, f)
if os.path.exists(compiled):
- code = read_pyc(compiled)
- restricted(code, environment, layer=compiled)
- return
+ ccode = getcfs(compiled, compiled, lambda: read_pyc(compiled))
+ layer = compiled
+ break
if not os.path.exists(filename) and allow_generic:
view = 'generic.' + request.extension
filename = pjoin(folder, 'views', view)
@@ -717,17 +706,14 @@ def run_view_in(environment):
rewrite.THREAD_LOCAL.routes.error_message % badv,
web2py_error=badv)
layer = filename
- if is_gae:
- ccode = getcfs(layer, filename,
- lambda: compile2(parse_template(view,
- pjoin(folder, 'views'),
- context=environment), layer))
- else:
- ccode = parse_template(view,
- pjoin(folder, 'views'),
- context=environment)
- restricted(ccode, environment, layer)
+ # Compile the template
+ ccode = parse_template(view,
+ pjoin(folder, 'views'),
+ context=environment)
+ restricted(ccode, environment, layer=layer)
+ # parse_template saves everything in response body
+ return environment['response'].body.getvalue()
def remove_compiled_application(folder):
"""
@@ -752,26 +738,3 @@ def compile_application(folder, skip_failed_views=False):
compile_controllers(folder)
failed_views = compile_views(folder, skip_failed_views)
return failed_views
-
-
-def test():
- """
- Example::
-
- >>> import traceback, types
- >>> environment={'x':1}
- >>> open('a.py', 'w').write('print 1/x')
- >>> save_pyc('a.py')
- >>> os.unlink('a.py')
- >>> if type(read_pyc('a.pyc'))==types.CodeType: print 'code'
- code
- >>> exec read_pyc('a.pyc') in environment
- 1
- """
-
- return
-
-
-if __name__ == '__main__':
- import doctest
- doctest.testmod()
diff --git a/gluon/contrib/aes.py b/gluon/contrib/aes.py
deleted file mode 100644
index cecf2d907..000000000
--- a/gluon/contrib/aes.py
+++ /dev/null
@@ -1,502 +0,0 @@
-"""Simple AES cipher implementation in pure Python following PEP-272 API
-
-Homepage: https://bitbucket.org/intgr/pyaes/
-
-The goal of this module is to be as fast as reasonable in Python while still
-being Pythonic and readable/understandable. It is licensed under the permissive
-MIT license.
-
-Hopefully the code is readable and commented enough that it can serve as an
-introduction to the AES cipher for Python coders. In fact, it should go along
-well with the Stick Figure Guide to AES:
-http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html
-
-Contrary to intuition, this implementation numbers the 4x4 matrices from top to
-bottom for efficiency reasons::
-
- 0 4 8 12
- 1 5 9 13
- 2 6 10 14
- 3 7 11 15
-
-Effectively it's the transposition of what you'd expect. This actually makes
-the code simpler -- except the ShiftRows step, but hopefully the explanation
-there clears it up.
-
-"""
-
-####
-# Copyright (c) 2010 Marti Raudsepp
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-####
-
-
-from array import array
-
-# Globals mandated by PEP 272:
-# http://www.python.org/dev/peps/pep-0272/
-MODE_ECB = 1
-MODE_CBC = 2
-#MODE_CTR = 6
-
-block_size = 16
-key_size = None
-
-def new(key, mode=MODE_CBC, IV=None):
- if mode == MODE_ECB:
- return ECBMode(AES(key))
- elif mode == MODE_CBC:
- if IV is None:
- raise ValueError("CBC mode needs an IV value!")
-
- return CBCMode(AES(key), IV)
- else:
- raise NotImplementedError
-
-#### AES cipher implementation
-
-class AES(object):
- block_size = 16
-
- def __init__(self, key):
- self.setkey(key)
-
- def setkey(self, key):
- """Sets the key and performs key expansion."""
-
- self.key = key
- self.key_size = len(key)
-
- if self.key_size == 16:
- self.rounds = 10
- elif self.key_size == 24:
- self.rounds = 12
- elif self.key_size == 32:
- self.rounds = 14
- else:
- raise ValueError("Key length must be 16, 24 or 32 bytes")
-
- self.expand_key()
-
- def expand_key(self):
- """Performs AES key expansion on self.key and stores in self.exkey"""
-
- # The key schedule specifies how parts of the key are fed into the
- # cipher's round functions. "Key expansion" means performing this
- # schedule in advance. Almost all implementations do this.
- #
- # Here's a description of AES key schedule:
- # http://en.wikipedia.org/wiki/Rijndael_key_schedule
-
- # The expanded key starts with the actual key itself
- exkey = array('B', self.key)
-
- # extra key expansion steps
- if self.key_size == 16:
- extra_cnt = 0
- elif self.key_size == 24:
- extra_cnt = 2
- else:
- extra_cnt = 3
-
- # 4-byte temporary variable for key expansion
- word = exkey[-4:]
- # Each expansion cycle uses 'i' once for Rcon table lookup
- for i in xrange(1, 11):
-
- #### key schedule core:
- # left-rotate by 1 byte
- word = word[1:4] + word[0:1]
-
- # apply S-box to all bytes
- for j in xrange(4):
- word[j] = aes_sbox[word[j]]
-
- # apply the Rcon table to the leftmost byte
- word[0] = word[0] ^ aes_Rcon[i]
- #### end key schedule core
-
- for z in xrange(4):
- for j in xrange(4):
- # mix in bytes from the last subkey
- word[j] ^= exkey[-self.key_size + j]
- exkey.extend(word)
-
- # Last key expansion cycle always finishes here
- if len(exkey) >= (self.rounds+1) * self.block_size:
- break
-
- # Special substitution step for 256-bit key
- if self.key_size == 32:
- for j in xrange(4):
- # mix in bytes from the last subkey XORed with S-box of
- # current word bytes
- word[j] = aes_sbox[word[j]] ^ exkey[-self.key_size + j]
- exkey.extend(word)
-
- # Twice for 192-bit key, thrice for 256-bit key
- for z in xrange(extra_cnt):
- for j in xrange(4):
- # mix in bytes from the last subkey
- word[j] ^= exkey[-self.key_size + j]
- exkey.extend(word)
-
- self.exkey = exkey
-
- def add_round_key(self, block, round):
- """AddRoundKey step in AES. This is where the key is mixed into plaintext"""
-
- offset = round * 16
- exkey = self.exkey
-
- for i in xrange(16):
- block[i] ^= exkey[offset + i]
-
- #print 'AddRoundKey:', block
-
- def sub_bytes(self, block, sbox):
- """SubBytes step, apply S-box to all bytes
-
- Depending on whether encrypting or decrypting, a different sbox array
- is passed in.
- """
-
- for i in xrange(16):
- block[i] = sbox[block[i]]
-
- #print 'SubBytes :', block
-
- def shift_rows(self, b):
- """ShiftRows step. Shifts 2nd row to left by 1, 3rd row by 2, 4th row by 3
-
- Since we're performing this on a transposed matrix, cells are numbered
- from top to bottom::
-
- 0 4 8 12 -> 0 4 8 12 -- 1st row doesn't change
- 1 5 9 13 -> 5 9 13 1 -- row shifted to left by 1 (wraps around)
- 2 6 10 14 -> 10 14 2 6 -- shifted by 2
- 3 7 11 15 -> 15 3 7 11 -- shifted by 3
- """
-
- b[1], b[5], b[ 9], b[13] = b[ 5], b[ 9], b[13], b[ 1]
- b[2], b[6], b[10], b[14] = b[10], b[14], b[ 2], b[ 6]
- b[3], b[7], b[11], b[15] = b[15], b[ 3], b[ 7], b[11]
-
- #print 'ShiftRows :', b
-
- def shift_rows_inv(self, b):
- """Similar to shift_rows above, but performed in inverse for decryption."""
-
- b[ 5], b[ 9], b[13], b[ 1] = b[1], b[5], b[ 9], b[13]
- b[10], b[14], b[ 2], b[ 6] = b[2], b[6], b[10], b[14]
- b[15], b[ 3], b[ 7], b[11] = b[3], b[7], b[11], b[15]
-
- #print 'ShiftRows :', b
-
- def mix_columns(self, block):
- """MixColumns step. Mixes the values in each column"""
-
- # Cache global multiplication tables (see below)
- mul_by_2 = gf_mul_by_2
- mul_by_3 = gf_mul_by_3
-
- # Since we're dealing with a transposed matrix, columns are already
- # sequential
- for i in xrange(4):
- col = i * 4
-
- #v0, v1, v2, v3 = block[col : col+4]
- v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2],
- block[col + 3])
-
- block[col ] = mul_by_2[v0] ^ v3 ^ v2 ^ mul_by_3[v1]
- block[col+1] = mul_by_2[v1] ^ v0 ^ v3 ^ mul_by_3[v2]
- block[col+2] = mul_by_2[v2] ^ v1 ^ v0 ^ mul_by_3[v3]
- block[col+3] = mul_by_2[v3] ^ v2 ^ v1 ^ mul_by_3[v0]
-
- #print 'MixColumns :', block
-
- def mix_columns_inv(self, block):
- """Similar to mix_columns above, but performed in inverse for decryption."""
-
- # Cache global multiplication tables (see below)
- mul_9 = gf_mul_by_9
- mul_11 = gf_mul_by_11
- mul_13 = gf_mul_by_13
- mul_14 = gf_mul_by_14
-
- # Since we're dealing with a transposed matrix, columns are already
- # sequential
- for i in xrange(4):
- col = i * 4
-
- v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2],
- block[col + 3])
- #v0, v1, v2, v3 = block[col:col+4]
-
- block[col ] = mul_14[v0] ^ mul_9[v3] ^ mul_13[v2] ^ mul_11[v1]
- block[col+1] = mul_14[v1] ^ mul_9[v0] ^ mul_13[v3] ^ mul_11[v2]
- block[col+2] = mul_14[v2] ^ mul_9[v1] ^ mul_13[v0] ^ mul_11[v3]
- block[col+3] = mul_14[v3] ^ mul_9[v2] ^ mul_13[v1] ^ mul_11[v0]
-
- #print 'MixColumns :', block
-
- def encrypt_block(self, block):
- """Encrypts a single block. This is the main AES function"""
-
- # For efficiency reasons, the state between steps is transmitted via a
- # mutable array, not returned.
- self.add_round_key(block, 0)
-
- for round in xrange(1, self.rounds):
- self.sub_bytes(block, aes_sbox)
- self.shift_rows(block)
- self.mix_columns(block)
- self.add_round_key(block, round)
-
- self.sub_bytes(block, aes_sbox)
- self.shift_rows(block)
- # no mix_columns step in the last round
- self.add_round_key(block, self.rounds)
-
- def decrypt_block(self, block):
- """Decrypts a single block. This is the main AES decryption function"""
-
- # For efficiency reasons, the state between steps is transmitted via a
- # mutable array, not returned.
- self.add_round_key(block, self.rounds)
-
- # count rounds down from 15 ... 1
- for round in xrange(self.rounds-1, 0, -1):
- self.shift_rows_inv(block)
- self.sub_bytes(block, aes_inv_sbox)
- self.add_round_key(block, round)
- self.mix_columns_inv(block)
-
- self.shift_rows_inv(block)
- self.sub_bytes(block, aes_inv_sbox)
- self.add_round_key(block, 0)
- # no mix_columns step in the last round
-
-
-#### ECB mode implementation
-
-class ECBMode(object):
- """Electronic CodeBook (ECB) mode encryption.
-
- Basically this mode applies the cipher function to each block individually;
- no feedback is done. NB! This is insecure for almost all purposes
- """
-
- def __init__(self, cipher):
- self.cipher = cipher
- self.block_size = cipher.block_size
-
- def ecb(self, data, block_func):
- """Perform ECB mode with the given function"""
-
- if len(data) % self.block_size != 0:
- raise ValueError("Plaintext length must be multiple of 16")
-
- block_size = self.block_size
- data = array('B', data)
-
- for offset in xrange(0, len(data), block_size):
- block = data[offset : offset+block_size]
- block_func(block)
- data[offset : offset+block_size] = block
-
- return data.tostring()
-
- def encrypt(self, data):
- """Encrypt data in ECB mode"""
-
- return self.ecb(data, self.cipher.encrypt_block)
-
- def decrypt(self, data):
- """Decrypt data in ECB mode"""
-
- return self.ecb(data, self.cipher.decrypt_block)
-
-#### CBC mode
-
-class CBCMode(object):
- """Cipher Block Chaining (CBC) mode encryption. This mode avoids content leaks.
-
- In CBC encryption, each plaintext block is XORed with the ciphertext block
- preceding it; decryption is simply the inverse.
- """
-
- # A better explanation of CBC can be found here:
- # http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29
-
- def __init__(self, cipher, IV):
- self.cipher = cipher
- self.block_size = cipher.block_size
- self.IV = array('B', IV)
-
- def encrypt(self, data):
- """Encrypt data in CBC mode"""
-
- block_size = self.block_size
- if len(data) % block_size != 0:
- raise ValueError("Plaintext length must be multiple of 16")
-
- data = array('B', data)
- IV = self.IV
-
- for offset in xrange(0, len(data), block_size):
- block = data[offset : offset+block_size]
-
- # Perform CBC chaining
- for i in xrange(block_size):
- block[i] ^= IV[i]
-
- self.cipher.encrypt_block(block)
- data[offset : offset+block_size] = block
- IV = block
-
- self.IV = IV
- return data.tostring()
-
- def decrypt(self, data):
- """Decrypt data in CBC mode"""
-
- block_size = self.block_size
- if len(data) % block_size != 0:
- raise ValueError("Ciphertext length must be multiple of 16")
-
- data = array('B', data)
- IV = self.IV
-
- for offset in xrange(0, len(data), block_size):
- ctext = data[offset : offset+block_size]
- block = ctext[:]
- self.cipher.decrypt_block(block)
-
- # Perform CBC chaining
- #for i in xrange(block_size):
- # data[offset + i] ^= IV[i]
- for i in xrange(block_size):
- block[i] ^= IV[i]
- data[offset : offset+block_size] = block
-
- IV = ctext
- #data[offset : offset+block_size] = block
-
- self.IV = IV
- return data.tostring()
-
-####
-
-def galois_multiply(a, b):
- """Galois Field multiplicaiton for AES"""
- p = 0
- while b:
- if b & 1:
- p ^= a
- a <<= 1
- if a & 0x100:
- a ^= 0x1b
- b >>= 1
-
- return p & 0xff
-
-# Precompute the multiplication tables for encryption
-gf_mul_by_2 = array('B', [galois_multiply(x, 2) for x in range(256)])
-gf_mul_by_3 = array('B', [galois_multiply(x, 3) for x in range(256)])
-# ... for decryption
-gf_mul_by_9 = array('B', [galois_multiply(x, 9) for x in range(256)])
-gf_mul_by_11 = array('B', [galois_multiply(x, 11) for x in range(256)])
-gf_mul_by_13 = array('B', [galois_multiply(x, 13) for x in range(256)])
-gf_mul_by_14 = array('B', [galois_multiply(x, 14) for x in range(256)])
-
-####
-
-# The S-box is a 256-element array, that maps a single byte value to another
-# byte value. Since it's designed to be reversible, each value occurs only once
-# in the S-box
-#
-# More information: http://en.wikipedia.org/wiki/Rijndael_S-box
-
-aes_sbox = array('B',
- '637c777bf26b6fc53001672bfed7ab76'
- 'ca82c97dfa5947f0add4a2af9ca472c0'
- 'b7fd9326363ff7cc34a5e5f171d83115'
- '04c723c31896059a071280e2eb27b275'
- '09832c1a1b6e5aa0523bd6b329e32f84'
- '53d100ed20fcb15b6acbbe394a4c58cf'
- 'd0efaafb434d338545f9027f503c9fa8'
- '51a3408f929d38f5bcb6da2110fff3d2'
- 'cd0c13ec5f974417c4a77e3d645d1973'
- '60814fdc222a908846eeb814de5e0bdb'
- 'e0323a0a4906245cc2d3ac629195e479'
- 'e7c8376d8dd54ea96c56f4ea657aae08'
- 'ba78252e1ca6b4c6e8dd741f4bbd8b8a'
- '703eb5664803f60e613557b986c11d9e'
- 'e1f8981169d98e949b1e87e9ce5528df'
- '8ca1890dbfe6426841992d0fb054bb16'.decode('hex')
-)
-
-# This is the inverse of the above. In other words:
-# aes_inv_sbox[aes_sbox[val]] == val
-
-aes_inv_sbox = array('B',
- '52096ad53036a538bf40a39e81f3d7fb'
- '7ce339829b2fff87348e4344c4dee9cb'
- '547b9432a6c2233dee4c950b42fac34e'
- '082ea16628d924b2765ba2496d8bd125'
- '72f8f66486689816d4a45ccc5d65b692'
- '6c704850fdedb9da5e154657a78d9d84'
- '90d8ab008cbcd30af7e45805b8b34506'
- 'd02c1e8fca3f0f02c1afbd0301138a6b'
- '3a9111414f67dcea97f2cfcef0b4e673'
- '96ac7422e7ad3585e2f937e81c75df6e'
- '47f11a711d29c5896fb7620eaa18be1b'
- 'fc563e4bc6d279209adbc0fe78cd5af4'
- '1fdda8338807c731b11210592780ec5f'
- '60517fa919b54a0d2de57a9f93c99cef'
- 'a0e03b4dae2af5b0c8ebbb3c83539961'
- '172b047eba77d626e169146355210c7d'.decode('hex')
-)
-
-# The Rcon table is used in AES's key schedule (key expansion)
-# It's a pre-computed table of exponentation of 2 in AES's finite field
-#
-# More information: http://en.wikipedia.org/wiki/Rijndael_key_schedule
-
-aes_Rcon = array('B',
- '8d01020408102040801b366cd8ab4d9a'
- '2f5ebc63c697356ad4b37dfaefc59139'
- '72e4d3bd61c29f254a943366cc831d3a'
- '74e8cb8d01020408102040801b366cd8'
- 'ab4d9a2f5ebc63c697356ad4b37dfaef'
- 'c5913972e4d3bd61c29f254a943366cc'
- '831d3a74e8cb8d01020408102040801b'
- '366cd8ab4d9a2f5ebc63c697356ad4b3'
- '7dfaefc5913972e4d3bd61c29f254a94'
- '3366cc831d3a74e8cb8d010204081020'
- '40801b366cd8ab4d9a2f5ebc63c69735'
- '6ad4b37dfaefc5913972e4d3bd61c29f'
- '254a943366cc831d3a74e8cb8d010204'
- '08102040801b366cd8ab4d9a2f5ebc63'
- 'c697356ad4b37dfaefc5913972e4d3bd'
- '61c29f254a943366cc831d3a74e8cb'.decode('hex')
-)
diff --git a/gluon/contrib/login_methods/cas_auth.py b/gluon/contrib/login_methods/cas_auth.py
index 61d99868b..1a07c5f11 100644
--- a/gluon/contrib/login_methods/cas_auth.py
+++ b/gluon/contrib/login_methods/cas_auth.py
@@ -8,9 +8,11 @@
Tinkered by Szabolcs Gyuris < szimszo n @ o regpreshaz dot eu>
"""
+import xml.dom.minidom as dom
+import xml.parsers.expat as expat
from gluon import current, redirect, URL
-
+from gluon._compat import urlopen, to_native
class CasAuth(object):
"""
@@ -59,7 +61,7 @@ def __init__(self, g=None, # g for backward compatibility ###
# vars commented because of
# https://code.google.com/p/web2py/issues/detail?id=1774
self.cas_my_url = URL(args=current.request.args,
- #vars=current.request.vars,
+ #vars=current.request.vars,
scheme=True)
def login_url(self, next="/"):
@@ -86,7 +88,6 @@ def _CAS_login(self):
exposed as CAS.login(request)
returns a token on success, None on failed authentication
"""
- import urllib
self.ticket = current.request.vars.ticket
if not current.request.vars.ticket:
redirect("%s?service=%s" % (self.cas_login_url,
@@ -95,7 +96,7 @@ def _CAS_login(self):
url = "%s?service=%s&ticket=%s" % (self.cas_check_url,
self.cas_my_url,
self.ticket)
- data = urllib.urlopen(url).read()
+ data = to_native(urlopen(url).read())
if data.startswith('yes') or data.startswith('no'):
data = data.split('\n')
if data[0] == 'yes':
@@ -108,19 +109,16 @@ def _CAS_login(self):
a = b = c = data[1]
return dict(user=a, email=b, username=c)
return None
- import xml.dom.minidom as dom
- import xml.parsers.expat as expat
try:
dxml = dom.parseString(data)
- envelop = dxml.getElementsByTagName(
- "cas:authenticationSuccess")
+ envelop = dxml.getElementsByTagName("cas:authenticationSuccess")
if len(envelop) > 0:
res = dict()
for x in envelop[0].childNodes:
if x.nodeName.startswith('cas:') and len(x.childNodes):
- key = x.nodeName[4:].encode('utf8')
- value = x.childNodes[0].nodeValue.encode('utf8')
- if not key in res:
+ key = to_native(x.nodeName[4:])
+ value = to_native(x.childNodes[0].nodeValue)
+ if key not in res:
res[key] = value
else:
if not isinstance(res[key], list):
@@ -136,5 +134,4 @@ def _CAS_logout(self):
exposed CAS.logout()
redirects to the CAS logout page
"""
- import urllib
redirect("%s?service=%s" % (self.cas_logout_url, self.cas_my_url))
diff --git a/gluon/contrib/pbkdf2.py b/gluon/contrib/pbkdf2.py
index 66b8b287c..b7a7dd42e 100644
--- a/gluon/contrib/pbkdf2.py
+++ b/gluon/contrib/pbkdf2.py
@@ -40,26 +40,15 @@ def safe_str_cmp(a, b):
:copyright: (c) Copyright 2011 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
-from __future__ import print_function
import hmac
-try:
- from hashlib import sha1
-except ImportError:
- # hashlib not available. Use the old sha module.
- import sha as sha1
-
+import hashlib
from struct import Struct
from operator import xor
from itertools import izip, starmap
-from collections import deque
+
_pack_int = Struct('>I').pack
-try:
- from Crypto.Util.strxor import strxor
-except ImportError:
- def strxor(a, b):
- return ''.join(chr(xor(ord(x), ord(y))) for x,y in izip(a, b))
def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None):
"""Like :func:`pbkdf2_bin` but returns a hex encoded string."""
@@ -72,20 +61,20 @@ def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None):
key of `keylen` bytes. By default SHA-1 is used as hash function,
a different hashlib `hashfunc` can be provided.
"""
- hashfunc = hashfunc or sha1
+ hashfunc = hashfunc or hashlib.sha1
mac = hmac.new(data, None, hashfunc)
def _pseudorandom(x, mac=mac):
h = mac.copy()
h.update(x)
- return h.digest()
- buf = deque()
+ return map(ord, h.digest())
+ buf = []
for block in xrange(1, -(-keylen // mac.digest_size) + 1):
rv = u = _pseudorandom(salt + _pack_int(block))
for i in xrange(iterations - 1):
- u = _pseudorandom(u)
- rv = strxor(rv, u)
+ u = _pseudorandom(''.join(map(chr, u)))
+ rv = starmap(xor, izip(rv, u))
buf.extend(rv)
- return ''.join(buf)[:keylen]
+ return ''.join(map(chr, buf))[:keylen]
def test():
@@ -93,14 +82,14 @@ def test():
def check(data, salt, iterations, keylen, expected):
rv = pbkdf2_hex(data, salt, iterations, keylen)
if rv != expected:
- print('Test failed:')
- print(' Expected: %s' % expected)
- print(' Got: %s' % rv)
- print(' Parameters:')
- print(' data=%s' % data)
- print(' salt=%s' % salt)
- print(' iterations=%d' % iterations)
- print()
+ print 'Test failed:'
+ print ' Expected: %s' % expected
+ print ' Got: %s' % rv
+ print ' Parameters:'
+ print ' data=%s' % data
+ print ' salt=%s' % salt
+ print ' iterations=%d' % iterations
+ print
failed.append(1)
# From RFC 6070
@@ -115,8 +104,9 @@ def check(data, salt, iterations, keylen, expected):
check('pass\x00word', 'sa\x00lt', 4096, 16,
'56fa6aa75548099dcc37d7f03425e0c3')
# This one is from the RFC but it just takes for ages
- check('password', 'salt', 16777216, 20,
- 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984')
+ ##check('password', 'salt', 16777216, 20,
+ ## 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984')
+
# From Crypt-PBKDF2
check('password', 'ATHENA.MIT.EDUraeburn', 1, 16,
'cdedb5281bb2f801565a1122b2563515')
diff --git a/gluon/contrib/pbkdf2_ctypes.py b/gluon/contrib/pbkdf2_ctypes.py
index 4d64a12f8..b2a36de31 100644
--- a/gluon/contrib/pbkdf2_ctypes.py
+++ b/gluon/contrib/pbkdf2_ctypes.py
@@ -18,7 +18,6 @@
:license: LGPLv3
"""
-from __future__ import print_function
import ctypes
import ctypes.util
@@ -29,7 +28,7 @@
import sys
__all__ = ['pkcs5_pbkdf2_hmac', 'pbkdf2_bin', 'pbkdf2_hex']
-__version__ = '0.99.3'
+__version__ = '0.99.4'
def _commoncrypto_hashlib_to_crypto_map_get(hashfunc):
hashlib_to_crypto_map = {hashlib.sha1: 1,
diff --git a/gluon/contrib/pg8000/__init__.py b/gluon/contrib/pg8000/__init__.py
index c30a8cb6a..4a997174c 100644
--- a/gluon/contrib/pg8000/__init__.py
+++ b/gluon/contrib/pg8000/__init__.py
@@ -1,3 +1,14 @@
+from .core import (
+ Warning, Bytea, DataError, DatabaseError, InterfaceError, ProgrammingError,
+ Error, OperationalError, IntegrityError, InternalError, NotSupportedError,
+ ArrayContentNotHomogenousError, ArrayContentEmptyError,
+ ArrayDimensionsNotConsistentError, ArrayContentNotSupportedError, utc,
+ Connection, Cursor, Binary, Date, DateFromTicks, Time, TimeFromTicks,
+ Timestamp, TimestampFromTicks, BINARY, Interval)
+from ._version import get_versions
+__version__ = get_versions()['version']
+del get_versions
+
# Copyright (c) 2007-2009, Mathieu Fenniak
# All rights reserved.
#
@@ -28,258 +39,9 @@
__author__ = "Mathieu Fenniak"
-exec("from struct import Struct")
-for fmt in (
- "i", "h", "q", "d", "f", "iii", "ii", "qii", "dii", "ihihih", "ci",
- "bh", "cccc"):
- exec(fmt + "_struct = Struct('!" + fmt + "')")
- exec(fmt + "_unpack = " + fmt + "_struct.unpack_from")
- exec(fmt + "_pack = " + fmt + "_struct.pack")
-
-import datetime
-import time
-from .six import binary_type, integer_types, PY2
-
-min_int2, max_int2 = -2 ** 15, 2 ** 15
-min_int4, max_int4 = -2 ** 31, 2 ** 31
-min_int8, max_int8 = -2 ** 63, 2 ** 63
-
-
-class Warning(Exception):
- """Generic exception raised for important database warnings like data
- truncations. This exception is not currently used by pg8000.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class Error(Exception):
- """Generic exception that is the base exception of all other error
- exceptions.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class InterfaceError(Error):
- """Generic exception raised for errors that are related to the database
- interface rather than the database itself. For example, if the interface
- attempts to use an SSL connection but the server refuses, an InterfaceError
- will be raised.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class DatabaseError(Error):
- """Generic exception raised for errors that are related to the database.
- This exception is currently never raised by pg8000.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class DataError(DatabaseError):
- """Generic exception raised for errors that are due to problems with the
- processed data. This exception is not currently raised by pg8000.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class OperationalError(DatabaseError):
- """
- Generic exception raised for errors that are related to the database's
- operation and not necessarily under the control of the programmer. This
- exception is currently never raised by pg8000.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class IntegrityError(DatabaseError):
- """
- Generic exception raised when the relational integrity of the database is
- affected. This exception is not currently raised by pg8000.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class InternalError(DatabaseError):
- """Generic exception raised when the database encounters an internal error.
- This is currently only raised when unexpected state occurs in the pg8000
- interface itself, and is typically the result of a interface bug.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class ProgrammingError(DatabaseError):
- """Generic exception raised for programming errors. For example, this
- exception is raised if more parameter fields are in a query string than
- there are available parameters.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class NotSupportedError(DatabaseError):
- """Generic exception raised in case a method or database API was used which
- is not supported by the database.
-
- This exception is part of the `DBAPI 2.0 specification
- `_.
- """
- pass
-
-
-class ArrayContentNotSupportedError(NotSupportedError):
- """
- Raised when attempting to transmit an array where the base type is not
- supported for binary data transfer by the interface.
- """
- pass
-
-
-class ArrayContentNotHomogenousError(ProgrammingError):
- """
- Raised when attempting to transmit an array that doesn't contain only a
- single type of object.
- """
- pass
-
-
-class ArrayContentEmptyError(ProgrammingError):
- """Raised when attempting to transmit an empty array. The type oid of an
- empty array cannot be determined, and so sending them is not permitted.
- """
- pass
-
-
-class ArrayDimensionsNotConsistentError(ProgrammingError):
- """
- Raised when attempting to transmit an array that has inconsistent
- multi-dimension sizes.
- """
- pass
-
-
-class Bytea(binary_type):
- """Bytea is a str-derived class that is mapped to a PostgreSQL byte array.
- This class is only used in Python 2, the built-in ``bytes`` type is used in
- Python 3.
- """
- pass
-
-
-class Interval(object):
- """An Interval represents a measurement of time. In PostgreSQL, an interval
- is defined in the measure of months, days, and microseconds; as such, the
- pg8000 interval type represents the same information.
-
- Note that values of the :attr:`microseconds`, :attr:`days` and
- :attr:`months` properties are independently measured and cannot be
- converted to each other. A month may be 28, 29, 30, or 31 days, and a day
- may occasionally be lengthened slightly by a leap second.
-
- .. attribute:: microseconds
-
- Measure of microseconds in the interval.
-
- The microseconds value is constrained to fit into a signed 64-bit
- integer. Any attempt to set a value too large or too small will result
- in an OverflowError being raised.
-
- .. attribute:: days
-
- Measure of days in the interval.
-
- The days value is constrained to fit into a signed 32-bit integer.
- Any attempt to set a value too large or too small will result in an
- OverflowError being raised.
-
- .. attribute:: months
-
- Measure of months in the interval.
-
- The months value is constrained to fit into a signed 32-bit integer.
- Any attempt to set a value too large or too small will result in an
- OverflowError being raised.
- """
-
- def __init__(self, microseconds=0, days=0, months=0):
- self.microseconds = microseconds
- self.days = days
- self.months = months
-
- def _setMicroseconds(self, value):
- if not isinstance(value, integer_types):
- raise TypeError("microseconds must be an integer type")
- elif not (min_int8 < value < max_int8):
- raise OverflowError(
- "microseconds must be representable as a 64-bit integer")
- else:
- self._microseconds = value
-
- def _setDays(self, value):
- if not isinstance(value, integer_types):
- raise TypeError("days must be an integer type")
- elif not (min_int4 < value < max_int4):
- raise OverflowError(
- "days must be representable as a 32-bit integer")
- else:
- self._days = value
-
- def _setMonths(self, value):
- if not isinstance(value, integer_types):
- raise TypeError("months must be an integer type")
- elif not (min_int4 < value < max_int4):
- raise OverflowError(
- "months must be representable as a 32-bit integer")
- else:
- self._months = value
-
- microseconds = property(lambda self: self._microseconds, _setMicroseconds)
- days = property(lambda self: self._days, _setDays)
- months = property(lambda self: self._months, _setMonths)
-
- def __repr__(self):
- return "" % (
- self.months, self.days, self.microseconds)
-
- def __eq__(self, other):
- return other is not None and isinstance(other, Interval) and \
- self.months == other.months and self.days == other.days and \
- self.microseconds == other.microseconds
-
- def __neq__(self, other):
- return not self.__eq__(other)
-
-from .core import Connection
-
-
def connect(
user=None, host='localhost', unix_sock=None, port=5432, database=None,
- password=None, ssl=False, **kwargs):
+ password=None, ssl=False, timeout=None, **kwargs):
"""Creates a connection to a PostgreSQL database.
This function is part of the `DBAPI 2.0 specification
@@ -287,9 +49,7 @@ def connect(
function are not defined by the specification.
:param user:
- The username to connect to the PostgreSQL server with. If this is not
- provided, pg8000 looks first for the PGUSER then the USER environment
- variables.
+ The username to connect to the PostgreSQL server with.
If your server character encoding is not ``ascii`` or ``utf8``, then
you need to provide ``user`` as bytes, eg.
@@ -325,15 +85,24 @@ def connect(
authentication, the connection will fail to open. If this parameter
is provided but not requested by the server, no error will occur.
+ If your server character encoding is not ``ascii`` or ``utf8``, then
+ you need to provide ``user`` as bytes, eg.
+ ``"my_password".encode('EUC-JP')``.
+
:keyword ssl:
Use SSL encryption for TCP/IP sockets if ``True``. Defaults to
``False``.
+ :keyword timeout:
+ Only used with Python 3, this is the time in seconds before the
+ connection to the database will time out. The default is ``None`` which
+ means no timeout.
+
:rtype:
A :class:`Connection` object.
"""
return Connection(
- user, host, unix_sock, port, database, password, ssl)
+ user, host, unix_sock, port, database, password, ssl, timeout)
apilevel = "2.0"
"""The DBAPI level supported, currently "2.0".
@@ -382,10 +151,6 @@ def connect(
STRING = 1043
"""String type oid."""
-if PY2:
- BINARY = Bytea
-else:
- BINARY = bytes
NUMBER = 1700
"""Numeric type oid"""
@@ -396,104 +161,15 @@ def connect(
ROWID = 26
"""ROWID type oid"""
-
-def Date(year, month, day):
- """Constuct an object holding a date value.
-
- This function is part of the `DBAPI 2.0 specification
- `_.
-
- :rtype: :class:`datetime.date`
- """
- return datetime.date(year, month, day)
-
-
-def Time(hour, minute, second):
- """Construct an object holding a time value.
-
- This function is part of the `DBAPI 2.0 specification
- `_.
-
- :rtype: :class:`datetime.time`
- """
- return datetime.time(hour, minute, second)
-
-
-def Timestamp(year, month, day, hour, minute, second):
- """Construct an object holding a timestamp value.
-
- This function is part of the `DBAPI 2.0 specification
- `_.
-
- :rtype: :class:`datetime.datetime`
- """
- return datetime.datetime(year, month, day, hour, minute, second)
-
-
-def DateFromTicks(ticks):
- """Construct an object holding a date value from the given ticks value
- (number of seconds since the epoch).
-
- This function is part of the `DBAPI 2.0 specification
- `_.
-
- :rtype: :class:`datetime.date`
- """
- return Date(*time.localtime(ticks)[:3])
-
-
-def TimeFromTicks(ticks):
- """Construct an objet holding a time value from the given ticks value
- (number of seconds since the epoch).
-
- This function is part of the `DBAPI 2.0 specification
- `_.
-
- :rtype: :class:`datetime.time`
- """
- return Time(*time.localtime(ticks)[3:6])
-
-
-def TimestampFromTicks(ticks):
- """Construct an object holding a timestamp value from the given ticks value
- (number of seconds since the epoch).
-
- This function is part of the `DBAPI 2.0 specification
- `_.
-
- :rtype: :class:`datetime.datetime`
- """
- return Timestamp(*time.localtime(ticks)[:6])
-
-
-def Binary(value):
- """Construct an object holding binary data.
-
- This function is part of the `DBAPI 2.0 specification
- `_.
-
- :rtype: :class:`pg8000.types.Bytea` for Python 2, otherwise :class:`bytes`
- """
- if PY2:
- return Bytea(value)
- else:
- return value
-
-
-from .core import utc, Cursor
-
__all__ = [
Warning, Bytea, DataError, DatabaseError, connect, InterfaceError,
ProgrammingError, Error, OperationalError, IntegrityError, InternalError,
NotSupportedError, ArrayContentNotHomogenousError, ArrayContentEmptyError,
ArrayDimensionsNotConsistentError, ArrayContentNotSupportedError, utc,
- Connection, Cursor]
+ Connection, Cursor, Binary, Date, DateFromTicks, Time, TimeFromTicks,
+ Timestamp, TimestampFromTicks, BINARY, Interval]
"""Version string for pg8000.
.. versionadded:: 1.9.11
"""
-
-from ._version import get_versions
-__version__ = get_versions()['version']
-del get_versions
diff --git a/gluon/contrib/pg8000/_version.py b/gluon/contrib/pg8000/_version.py
index 982cf246c..5677d1e06 100644
--- a/gluon/contrib/pg8000/_version.py
+++ b/gluon/contrib/pg8000/_version.py
@@ -6,22 +6,58 @@
# that just contains the computed version number.
# This file is released into the public domain. Generated by
-# versioneer-0.12 (https://github.com/warner/python-versioneer)
-
-# these strings will be replaced by git during git-archive
-git_refnames = "$Format:%d$"
-git_full = "$Format:%H$"
-
-# these strings are filled in when 'setup.py versioneer' creates _version.py
-tag_prefix = ""
-parentdir_prefix = "pg8000-"
-versionfile_source = "pg8000/_version.py"
+# versioneer-0.15 (https://github.com/warner/python-versioneer)
+import errno
import os
-import sys
import re
import subprocess
-import errno
+import sys
+
+
+def get_keywords():
+ # these strings will be replaced by git during git-archive.
+ # setup.py/versioneer.py will grep for the variable names, so they must
+ # each be defined on a line of their own. _version.py will just call
+ # get_keywords().
+ git_refnames = " (tag: 1.10.6)"
+ git_full = "4098abf6be90683ab10b7b080983ed6f08476485"
+ keywords = {"refnames": git_refnames, "full": git_full}
+ return keywords
+
+
+class VersioneerConfig:
+ pass
+
+
+def get_config():
+ # these strings are filled in when 'setup.py versioneer' creates
+ # _version.py
+ cfg = VersioneerConfig()
+ cfg.VCS = "git"
+ cfg.style = "pep440"
+ cfg.tag_prefix = ""
+ cfg.parentdir_prefix = "pg8000-"
+ cfg.versionfile_source = "pg8000/_version.py"
+ cfg.verbose = False
+ return cfg
+
+
+class NotThisMethod(Exception):
+ pass
+
+
+LONG_VERSION_PY = {}
+HANDLERS = {}
+
+
+def register_vcs_handler(vcs, method): # decorator
+ def decorate(f):
+ if vcs not in HANDLERS:
+ HANDLERS[vcs] = {}
+ HANDLERS[vcs][method] = f
+ return f
+ return decorate
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
@@ -29,6 +65,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
p = None
for c in commands:
try:
+ dispcmd = str([c] + args)
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,
stderr=(subprocess.PIPE if hide_stderr
@@ -39,7 +76,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
if e.errno == errno.ENOENT:
continue
if verbose:
- print("unable to run %s" % args[0])
+ print("unable to run %s" % dispcmd)
print(e)
return None
else:
@@ -47,28 +84,30 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):
print("unable to find command, tried %s" % (commands,))
return None
stdout = p.communicate()[0].strip()
- if sys.version >= '3':
+ if sys.version_info[0] >= 3:
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
- print("unable to run %s (error)" % args[0])
+ print("unable to run %s (error)" % dispcmd)
return None
return stdout
-def versions_from_parentdir(parentdir_prefix, root, verbose=False):
+def versions_from_parentdir(parentdir_prefix, root, verbose):
# Source tarballs conventionally unpack into a directory that includes
# both the project name and a version string.
dirname = os.path.basename(root)
if not dirname.startswith(parentdir_prefix):
if verbose:
- print(
- "guessing rootdir is '%s', but '%s' doesn't start with "
- "prefix '%s'" % (root, dirname, parentdir_prefix))
- return None
- return {"version": dirname[len(parentdir_prefix):], "full": ""}
+ print("guessing rootdir is '%s', but '%s' doesn't start with "
+ "prefix '%s'" % (root, dirname, parentdir_prefix))
+ raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+ return {"version": dirname[len(parentdir_prefix):],
+ "full-revisionid": None,
+ "dirty": False, "error": None}
+@register_vcs_handler("git", "get_keywords")
def git_get_keywords(versionfile_abs):
# the code embedded in _version.py can just fetch the value of these
# keywords. When used from setup.py, we don't want to import _version.py,
@@ -92,14 +131,15 @@ def git_get_keywords(versionfile_abs):
return keywords
-def git_versions_from_keywords(keywords, tag_prefix, verbose=False):
+@register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
if not keywords:
- return {} # keyword-finding function failed to find keywords
+ raise NotThisMethod("no keywords at all, weird")
refnames = keywords["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("keywords are unexpanded, not using")
- return {} # unexpanded, so not in an unpacked git-archive tarball
+ raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
refs = set([r.strip() for r in refnames.strip("()").split(",")])
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
@@ -124,18 +164,20 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose=False):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
- return {
- "version": r,
- "full": keywords["full"].strip()}
- # no suitable tags, so we use the full revision id
+ return {"version": r,
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": None
+ }
+ # no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
- print("no suitable tags, using full revision id")
- return {
- "version": keywords["full"].strip(),
- "full": keywords["full"].strip()}
+ print("no suitable tags, using unknown + full revision id")
+ return {"version": "0+unknown",
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False, "error": "no suitable tags"}
-def git_versions_from_vcs(tag_prefix, root, verbose=False):
+@register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
# this runs 'git' from the root of the source tree. This only gets called
# if the git-archive 'subst' keywords were *not* expanded, and
# _version.py hasn't already been rewritten with a short version string,
@@ -144,52 +186,275 @@ def git_versions_from_vcs(tag_prefix, root, verbose=False):
if not os.path.exists(os.path.join(root, ".git")):
if verbose:
print("no .git in %s" % root)
- return {}
+ raise NotThisMethod("no .git directory")
GITS = ["git"]
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
- stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"],
- cwd=root)
- if stdout is None:
- return {}
- if not stdout.startswith(tag_prefix):
- if verbose:
- print(
- "tag '%s' doesn't start with prefix '%s'" %
- (stdout, tag_prefix))
- return {}
- tag = stdout[len(tag_prefix):]
- stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
- if stdout is None:
- return {}
- full = stdout.strip()
- if tag.endswith("-dirty"):
- full += "-dirty"
- return {"version": tag, "full": full}
-
-
-def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
+ # if there is a tag, this yields TAG-NUM-gHEX[-dirty]
+ # if there are no tags, this yields HEX[-dirty] (no NUM)
+ describe_out = run_command(GITS, ["describe", "--tags", "--dirty",
+ "--always", "--long"],
+ cwd=root)
+ # --long was added in git-1.5.5
+ if describe_out is None:
+ raise NotThisMethod("'git describe' failed")
+ describe_out = describe_out.strip()
+ full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
+ if full_out is None:
+ raise NotThisMethod("'git rev-parse' failed")
+ full_out = full_out.strip()
+
+ pieces = {}
+ pieces["long"] = full_out
+ pieces["short"] = full_out[:7] # maybe improved later
+ pieces["error"] = None
+
+ # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+ # TAG might have hyphens.
+ git_describe = describe_out
+
+ # look for -dirty suffix
+ dirty = git_describe.endswith("-dirty")
+ pieces["dirty"] = dirty
+ if dirty:
+ git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+ # now we have TAG-NUM-gHEX or HEX
+
+ if "-" in git_describe:
+ # TAG-NUM-gHEX
+ mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+ if not mo:
+ # unparseable. Maybe git-describe is misbehaving?
+ pieces["error"] = ("unable to parse git-describe output: '%s'"
+ % describe_out)
+ return pieces
+
+ # tag
+ full_tag = mo.group(1)
+ if not full_tag.startswith(tag_prefix):
+ if verbose:
+ fmt = "tag '%s' doesn't start with prefix '%s'"
+ print(fmt % (full_tag, tag_prefix))
+ pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+ % (full_tag, tag_prefix))
+ return pieces
+ pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+ # distance: number of commits since tag
+ pieces["distance"] = int(mo.group(2))
+
+ # commit: short hex revision ID
+ pieces["short"] = mo.group(3)
+
+ else:
+ # HEX: no tags
+ pieces["closest-tag"] = None
+ count_out = run_command(GITS, ["rev-list", "HEAD", "--count"],
+ cwd=root)
+ pieces["distance"] = int(count_out) # total number of commits
+
+ return pieces
+
+
+def plus_or_dot(pieces):
+ if "+" in pieces.get("closest-tag", ""):
+ return "."
+ return "+"
+
+
+def render_pep440(pieces):
+ # now build up version string, with post-release "local version
+ # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+ # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+ # exceptions:
+ # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += plus_or_dot(pieces)
+ rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ else:
+ # exception #1
+ rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+ pieces["short"])
+ if pieces["dirty"]:
+ rendered += ".dirty"
+ return rendered
+
+
+def render_pep440_pre(pieces):
+ # TAG[.post.devDISTANCE] . No -dirty
+
+ # exceptions:
+ # 1: no tags. 0.post.devDISTANCE
+
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += ".post.dev%d" % pieces["distance"]
+ else:
+ # exception #1
+ rendered = "0.post.dev%d" % pieces["distance"]
+ return rendered
+
+
+def render_pep440_post(pieces):
+ # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that
+ # .dev0 sorts backwards (a dirty tree will appear "older" than the
+ # corresponding clean one), but you shouldn't be releasing software with
+ # -dirty anyways.
+
+ # exceptions:
+ # 1: no tags. 0.postDISTANCE[.dev0]
+
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += plus_or_dot(pieces)
+ rendered += "g%s" % pieces["short"]
+ else:
+ # exception #1
+ rendered = "0.post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ rendered += "+g%s" % pieces["short"]
+ return rendered
+
+
+def render_pep440_old(pieces):
+ # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty.
+
+ # exceptions:
+ # 1: no tags. 0.postDISTANCE[.dev0]
+
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"] or pieces["dirty"]:
+ rendered += ".post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ else:
+ # exception #1
+ rendered = "0.post%d" % pieces["distance"]
+ if pieces["dirty"]:
+ rendered += ".dev0"
+ return rendered
+
+
+def render_git_describe(pieces):
+ # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty
+ # --always'
+
+ # exceptions:
+ # 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ if pieces["distance"]:
+ rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render_git_describe_long(pieces):
+ # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty
+ # --always -long'. The distance/hash is unconditional.
+
+ # exceptions:
+ # 1: no tags. HEX[-dirty] (note: no 'g' prefix)
+
+ if pieces["closest-tag"]:
+ rendered = pieces["closest-tag"]
+ rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+ else:
+ # exception #1
+ rendered = pieces["short"]
+ if pieces["dirty"]:
+ rendered += "-dirty"
+ return rendered
+
+
+def render(pieces, style):
+ if pieces["error"]:
+ return {"version": "unknown",
+ "full-revisionid": pieces.get("long"),
+ "dirty": None,
+ "error": pieces["error"]}
+
+ if not style or style == "default":
+ style = "pep440" # the default
+
+ if style == "pep440":
+ rendered = render_pep440(pieces)
+ elif style == "pep440-pre":
+ rendered = render_pep440_pre(pieces)
+ elif style == "pep440-post":
+ rendered = render_pep440_post(pieces)
+ elif style == "pep440-old":
+ rendered = render_pep440_old(pieces)
+ elif style == "git-describe":
+ rendered = render_git_describe(pieces)
+ elif style == "git-describe-long":
+ rendered = render_git_describe_long(pieces)
+ else:
+ raise ValueError("unknown style '%s'" % style)
+
+ return {"version": rendered, "full-revisionid": pieces["long"],
+ "dirty": pieces["dirty"], "error": None}
+
+
+def get_versions():
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
# __file__, we can work backwards from there to the root. Some
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
# case we can only use expanded keywords.
- keywords = {"refnames": git_refnames, "full": git_full}
- ver = git_versions_from_keywords(keywords, tag_prefix, verbose)
- if ver:
- return ver
+ cfg = get_config()
+ verbose = cfg.verbose
try:
- root = os.path.abspath(__file__)
+ return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
+ verbose)
+ except NotThisMethod:
+ pass
+
+ try:
+ root = os.path.realpath(__file__)
# versionfile_source is the relative path from the top of the source
# tree (where the .git directory might live) to this file. Invert
# this to find the root from __file__.
- for i in range(len(versionfile_source.split(os.sep))):
+ for i in cfg.versionfile_source.split('/'):
root = os.path.dirname(root)
except NameError:
- return default
+ return {"version": "0+unknown", "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to find root of source tree"}
+
+ try:
+ pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
+ return render(pieces, cfg.style)
+ except NotThisMethod:
+ pass
+
+ try:
+ if cfg.parentdir_prefix:
+ return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+ except NotThisMethod:
+ pass
- return (git_versions_from_vcs(tag_prefix, root, verbose)
- or versions_from_parentdir(parentdir_prefix, root, verbose)
- or default)
+ return {"version": "0+unknown", "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to compute version"}
diff --git a/gluon/contrib/pg8000/core.py b/gluon/contrib/pg8000/core.py
index f19481bd8..29f88d9aa 100644
--- a/gluon/contrib/pg8000/core.py
+++ b/gluon/contrib/pg8000/core.py
@@ -1,3 +1,23 @@
+import datetime
+from datetime import timedelta
+from warnings import warn
+import socket
+import threading
+from struct import pack
+from hashlib import md5
+from decimal import Decimal
+from collections import deque, defaultdict
+from itertools import count, islice
+from .six.moves import map
+from .six import b, PY2, integer_types, next, text_type, u, binary_type
+from uuid import UUID
+from copy import deepcopy
+from calendar import timegm
+from distutils.version import LooseVersion
+from struct import Struct
+import time
+
+
# Copyright (c) 2007-2009, Mathieu Fenniak
# All rights reserved.
#
@@ -27,34 +47,6 @@
__author__ = "Mathieu Fenniak"
-import datetime
-from datetime import timedelta
-from . import (
- Interval, min_int2, max_int2, min_int4, max_int4, min_int8, max_int8,
- Bytea, NotSupportedError, ProgrammingError, InternalError, IntegrityError,
- OperationalError, DatabaseError, InterfaceError, Error,
- ArrayContentNotHomogenousError, ArrayContentEmptyError,
- ArrayDimensionsNotConsistentError, ArrayContentNotSupportedError, Warning,
- i_unpack, ii_unpack, iii_unpack, h_pack, d_unpack, q_unpack, d_pack,
- f_unpack, q_pack, i_pack, h_unpack, dii_unpack, qii_unpack, ci_unpack,
- bh_unpack, ihihih_unpack, cccc_unpack, ii_pack, iii_pack, dii_pack,
- qii_pack)
-from warnings import warn
-import socket
-import threading
-from struct import pack
-from hashlib import md5
-from decimal import Decimal
-from collections import deque, defaultdict
-from itertools import count, islice
-from .six.moves import map
-from .six import b, PY2, integer_types, next, PRE_26, text_type, u
-from sys import exc_info
-from uuid import UUID
-from copy import deepcopy
-from calendar import timegm
-import os
-from distutils.version import LooseVersion
try:
from json import loads
@@ -78,10 +70,351 @@ def dst(self, dt):
utc = UTC()
-if PRE_26:
- bytearray = list
+
+class Interval(object):
+ """An Interval represents a measurement of time. In PostgreSQL, an
+ interval is defined in the measure of months, days, and microseconds; as
+ such, the pg8000 interval type represents the same information.
+
+ Note that values of the :attr:`microseconds`, :attr:`days` and
+ :attr:`months` properties are independently measured and cannot be
+ converted to each other. A month may be 28, 29, 30, or 31 days, and a day
+ may occasionally be lengthened slightly by a leap second.
+
+ .. attribute:: microseconds
+
+ Measure of microseconds in the interval.
+
+ The microseconds value is constrained to fit into a signed 64-bit
+ integer. Any attempt to set a value too large or too small will result
+ in an OverflowError being raised.
+
+ .. attribute:: days
+
+ Measure of days in the interval.
+
+ The days value is constrained to fit into a signed 32-bit integer.
+ Any attempt to set a value too large or too small will result in an
+ OverflowError being raised.
+
+ .. attribute:: months
+
+ Measure of months in the interval.
+
+ The months value is constrained to fit into a signed 32-bit integer.
+ Any attempt to set a value too large or too small will result in an
+ OverflowError being raised.
+ """
+
+ def __init__(self, microseconds=0, days=0, months=0):
+ self.microseconds = microseconds
+ self.days = days
+ self.months = months
+
+ def _setMicroseconds(self, value):
+ if not isinstance(value, integer_types):
+ raise TypeError("microseconds must be an integer type")
+ elif not (min_int8 < value < max_int8):
+ raise OverflowError(
+ "microseconds must be representable as a 64-bit integer")
+ else:
+ self._microseconds = value
+
+ def _setDays(self, value):
+ if not isinstance(value, integer_types):
+ raise TypeError("days must be an integer type")
+ elif not (min_int4 < value < max_int4):
+ raise OverflowError(
+ "days must be representable as a 32-bit integer")
+ else:
+ self._days = value
+
+ def _setMonths(self, value):
+ if not isinstance(value, integer_types):
+ raise TypeError("months must be an integer type")
+ elif not (min_int4 < value < max_int4):
+ raise OverflowError(
+ "months must be representable as a 32-bit integer")
+ else:
+ self._months = value
+
+ microseconds = property(lambda self: self._microseconds, _setMicroseconds)
+ days = property(lambda self: self._days, _setDays)
+ months = property(lambda self: self._months, _setMonths)
+
+ def __repr__(self):
+ return "" % (
+ self.months, self.days, self.microseconds)
+
+ def __eq__(self, other):
+ return other is not None and isinstance(other, Interval) and \
+ self.months == other.months and self.days == other.days and \
+ self.microseconds == other.microseconds
+
+ def __neq__(self, other):
+ return not self.__eq__(other)
+def pack_funcs(fmt):
+ struc = Struct('!' + fmt)
+ return struc.pack, struc.unpack_from
+
+i_pack, i_unpack = pack_funcs('i')
+h_pack, h_unpack = pack_funcs('h')
+q_pack, q_unpack = pack_funcs('q')
+d_pack, d_unpack = pack_funcs('d')
+f_pack, f_unpack = pack_funcs('f')
+iii_pack, iii_unpack = pack_funcs('iii')
+ii_pack, ii_unpack = pack_funcs('ii')
+qii_pack, qii_unpack = pack_funcs('qii')
+dii_pack, dii_unpack = pack_funcs('dii')
+ihihih_pack, ihihih_unpack = pack_funcs('ihihih')
+ci_pack, ci_unpack = pack_funcs('ci')
+bh_pack, bh_unpack = pack_funcs('bh')
+cccc_pack, cccc_unpack = pack_funcs('cccc')
+
+
+Struct('!i')
+
+
+min_int2, max_int2 = -2 ** 15, 2 ** 15
+min_int4, max_int4 = -2 ** 31, 2 ** 31
+min_int8, max_int8 = -2 ** 63, 2 ** 63
+
+
+class Warning(Exception):
+ """Generic exception raised for important database warnings like data
+ truncations. This exception is not currently used by pg8000.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class Error(Exception):
+ """Generic exception that is the base exception of all other error
+ exceptions.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class InterfaceError(Error):
+ """Generic exception raised for errors that are related to the database
+ interface rather than the database itself. For example, if the interface
+ attempts to use an SSL connection but the server refuses, an InterfaceError
+ will be raised.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class DatabaseError(Error):
+ """Generic exception raised for errors that are related to the database.
+ This exception is currently never raised by pg8000.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class DataError(DatabaseError):
+ """Generic exception raised for errors that are due to problems with the
+ processed data. This exception is not currently raised by pg8000.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class OperationalError(DatabaseError):
+ """
+ Generic exception raised for errors that are related to the database's
+ operation and not necessarily under the control of the programmer. This
+ exception is currently never raised by pg8000.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class IntegrityError(DatabaseError):
+ """
+ Generic exception raised when the relational integrity of the database is
+ affected. This exception is not currently raised by pg8000.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class InternalError(DatabaseError):
+ """Generic exception raised when the database encounters an internal error.
+ This is currently only raised when unexpected state occurs in the pg8000
+ interface itself, and is typically the result of a interface bug.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class ProgrammingError(DatabaseError):
+ """Generic exception raised for programming errors. For example, this
+ exception is raised if more parameter fields are in a query string than
+ there are available parameters.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class NotSupportedError(DatabaseError):
+ """Generic exception raised in case a method or database API was used which
+ is not supported by the database.
+
+ This exception is part of the `DBAPI 2.0 specification
+ `_.
+ """
+ pass
+
+
+class ArrayContentNotSupportedError(NotSupportedError):
+ """
+ Raised when attempting to transmit an array where the base type is not
+ supported for binary data transfer by the interface.
+ """
+ pass
+
+
+class ArrayContentNotHomogenousError(ProgrammingError):
+ """
+ Raised when attempting to transmit an array that doesn't contain only a
+ single type of object.
+ """
+ pass
+
+
+class ArrayContentEmptyError(ProgrammingError):
+ """Raised when attempting to transmit an empty array. The type oid of an
+ empty array cannot be determined, and so sending them is not permitted.
+ """
+ pass
+
+
+class ArrayDimensionsNotConsistentError(ProgrammingError):
+ """
+ Raised when attempting to transmit an array that has inconsistent
+ multi-dimension sizes.
+ """
+ pass
+
+
+class Bytea(binary_type):
+ """Bytea is a str-derived class that is mapped to a PostgreSQL byte array.
+ This class is only used in Python 2, the built-in ``bytes`` type is used in
+ Python 3.
+ """
+ pass
+
+
+def Date(year, month, day):
+ """Constuct an object holding a date value.
+
+ This function is part of the `DBAPI 2.0 specification
+ `_.
+
+ :rtype: :class:`datetime.date`
+ """
+ return datetime.date(year, month, day)
+
+
+def Time(hour, minute, second):
+ """Construct an object holding a time value.
+
+ This function is part of the `DBAPI 2.0 specification
+ `_.
+
+ :rtype: :class:`datetime.time`
+ """
+ return datetime.time(hour, minute, second)
+
+
+def Timestamp(year, month, day, hour, minute, second):
+ """Construct an object holding a timestamp value.
+
+ This function is part of the `DBAPI 2.0 specification
+ `_.
+
+ :rtype: :class:`datetime.datetime`
+ """
+ return datetime.datetime(year, month, day, hour, minute, second)
+
+
+def DateFromTicks(ticks):
+ """Construct an object holding a date value from the given ticks value
+ (number of seconds since the epoch).
+
+ This function is part of the `DBAPI 2.0 specification
+ `_.
+
+ :rtype: :class:`datetime.date`
+ """
+ return Date(*time.localtime(ticks)[:3])
+
+
+def TimeFromTicks(ticks):
+ """Construct an objet holding a time value from the given ticks value
+ (number of seconds since the epoch).
+
+ This function is part of the `DBAPI 2.0 specification
+ `_.
+
+ :rtype: :class:`datetime.time`
+ """
+ return Time(*time.localtime(ticks)[3:6])
+
+
+def TimestampFromTicks(ticks):
+ """Construct an object holding a timestamp value from the given ticks value
+ (number of seconds since the epoch).
+
+ This function is part of the `DBAPI 2.0 specification
+ `_.
+
+ :rtype: :class:`datetime.datetime`
+ """
+ return Timestamp(*time.localtime(ticks)[:6])
+
+
+def Binary(value):
+ """Construct an object holding binary data.
+
+ This function is part of the `DBAPI 2.0 specification
+ `_.
+
+ :rtype: :class:`pg8000.types.Bytea` for Python 2, otherwise :class:`bytes`
+ """
+ if PY2:
+ return Bytea(value)
+ else:
+ return value
+
+if PY2:
+ BINARY = Bytea
+else:
+ BINARY = bytes
+
FC_TEXT = 0
FC_BINARY = 1
@@ -271,13 +604,13 @@ def timestamp_recv_integer(data, offset, length):
micros = q_unpack(data, offset)[0]
try:
return EPOCH + timedelta(microseconds=micros)
- except OverflowError:
+ except OverflowError as e:
if micros == INFINITY_MICROSECONDS:
return datetime.datetime.max
elif micros == MINUS_INFINITY_MICROSECONDS:
return datetime.datetime.min
else:
- raise exc_info()[1]
+ raise e
# data is double-precision float representing seconds since 2000-01-01
@@ -299,7 +632,7 @@ def timestamp_send_integer(v):
# data is double-precision float representing seconds since 2000-01-01
def timestamp_send_float(v):
- return d_pack(timegm(v.timetuple) + v.microsecond / 1e6 - EPOCH_SECONDS)
+ return d_pack(timegm(v.timetuple()) + v.microsecond / 1e6 - EPOCH_SECONDS)
def timestamptz_send_integer(v):
@@ -325,13 +658,13 @@ def timestamptz_recv_integer(data, offset, length):
micros = q_unpack(data, offset)[0]
try:
return EPOCH_TZ + timedelta(microseconds=micros)
- except OverflowError:
+ except OverflowError as e:
if micros == INFINITY_MICROSECONDS:
return DATETIME_MAX_TZ
elif micros == MINUS_INFINITY_MICROSECONDS:
return DATETIME_MIN_TZ
else:
- raise exc_info()[1]
+ raise e
def timestamptz_recv_float(data, offset, length):
@@ -565,21 +898,19 @@ def execute(self, operation, args=None, stream=None):
.. versionadded:: 1.9.11
"""
try:
- self._c._lock.acquire()
- self.stream = stream
+ with self._c._lock:
+ self.stream = stream
- if not self._c.in_transaction and not self._c.autocommit:
- self._c.execute(self, "begin transaction", None)
- self._c.execute(self, operation, args)
- except AttributeError:
+ if not self._c.in_transaction and not self._c.autocommit:
+ self._c.execute(self, "begin transaction", None)
+ self._c.execute(self, operation, args)
+ except AttributeError as e:
if self._c is None:
raise InterfaceError("Cursor closed")
elif self._c._sock is None:
raise InterfaceError("connection is closed")
else:
- raise exc_info()[1]
- finally:
- self._c._lock.release()
+ raise e
def executemany(self, operation, param_sets):
"""Prepare a database operation, and then execute it against all
@@ -690,28 +1021,26 @@ def setoutputsize(self, size, column=None):
pass
def __next__(self):
- try:
- self._c._lock.acquire()
- return self._cached_rows.popleft()
- except IndexError:
- if self.portal_suspended:
- self._c.send_EXECUTE(self)
- self._c._write(SYNC_MSG)
- self._c._flush()
- self._c.handle_messages(self)
- if not self.portal_suspended:
- self._c.close_portal(self)
+ with self._c._lock:
try:
return self._cached_rows.popleft()
except IndexError:
- if self.ps is None:
- raise ProgrammingError("A query hasn't been issued.")
- elif len(self.ps['row_desc']) == 0:
- raise ProgrammingError("no result set")
- else:
- raise StopIteration()
- finally:
- self._c._lock.release()
+ if self.portal_suspended:
+ self._c.send_EXECUTE(self)
+ self._c._write(SYNC_MSG)
+ self._c._flush()
+ self._c.handle_messages(self)
+ if not self.portal_suspended:
+ self._c.close_portal(self)
+ try:
+ return self._cached_rows.popleft()
+ except IndexError:
+ if self.ps is None:
+ raise ProgrammingError("A query hasn't been issued.")
+ elif len(self.ps['row_desc']) == 0:
+ raise ProgrammingError("no result set")
+ else:
+ raise StopIteration()
if PY2:
Cursor.next = Cursor.__next__
@@ -737,6 +1066,7 @@ def __next__(self):
COPY_DATA = b("d")
COPY_IN_RESPONSE = b("G")
COPY_OUT_RESPONSE = b("H")
+EMPTY_QUERY_RESPONSE = b("I")
BIND = b("B")
PARSE = b("P")
@@ -776,14 +1106,6 @@ def __next__(self):
IDLE_IN_FAILED_TRANSACTION = b("E")
-# Byte1('N') - Identifier
-# Int32 - Message length
-# Any number of these, followed by a zero byte:
-# Byte1 - code identifying the field type (see responseKeys)
-# String - field value
-def data_into_dict(data):
- return dict((s[0:1], s[1:]) for s in data.split(NULL_BYTE))
-
arr_trans = dict(zip(map(ord, u("[] 'u")), list(u('{}')) + [None] * 3))
@@ -895,7 +1217,9 @@ def _getError(self, error):
error.__name__, stacklevel=3)
return error
- def __init__(self, user, host, unix_sock, port, database, password, ssl):
+ def __init__(
+ self, user, host, unix_sock, port, database, password, ssl,
+ timeout):
self._client_encoding = "utf8"
self._commands_with_count = (
b("INSERT"), b("DELETE"), b("UPDATE"), b("MOVE"),
@@ -903,23 +1227,19 @@ def __init__(self, user, host, unix_sock, port, database, password, ssl):
self._lock = threading.Lock()
if user is None:
- try:
- self.user = os.environ['PGUSER']
- except KeyError:
- try:
- self.user = os.environ['USER']
- except KeyError:
- raise InterfaceError(
- "The 'user' connection parameter was omitted, and "
- "neither the PGUSER or USER environment variables "
- "were set.")
+ raise InterfaceError(
+ "The 'user' connection parameter cannot be None")
+
+ if isinstance(user, text_type):
+ self.user = user.encode('utf8')
else:
self.user = user
- if isinstance(self.user, text_type):
- self.user = self.user.encode('utf8')
+ if isinstance(password, text_type):
+ self.password = password.encode('utf8')
+ else:
+ self.password = password
- self.password = password
self.autocommit = False
self._xid = None
@@ -939,41 +1259,38 @@ def __init__(self, user, host, unix_sock, port, database, password, ssl):
else:
raise ProgrammingError(
"one of host or unix_sock must be provided")
+ if not PY2 and timeout is not None:
+ self._usock.settimeout(timeout)
+
if unix_sock is None and host is not None:
self._usock.connect((host, port))
elif unix_sock is not None:
self._usock.connect(unix_sock)
if ssl:
- try:
- self._lock.acquire()
- import ssl as sslmodule
- # Int32(8) - Message length, including self.
- # Int32(80877103) - The SSL request code.
- self._usock.sendall(ii_pack(8, 80877103))
- resp = self._usock.recv(1)
- if resp == b('S'):
- self._usock = sslmodule.wrap_socket(self._usock)
- else:
- raise InterfaceError("Server refuses SSL")
- except ImportError:
- raise InterfaceError(
- "SSL required but ssl module not available in "
- "this python installation")
- finally:
- self._lock.release()
+ with self._lock:
+ try:
+ import ssl as sslmodule
+ # Int32(8) - Message length, including self.
+ # Int32(80877103) - The SSL request code.
+ self._usock.sendall(ii_pack(8, 80877103))
+ resp = self._usock.recv(1)
+ if resp == b('S'):
+ self._usock = sslmodule.wrap_socket(self._usock)
+ else:
+ raise InterfaceError("Server refuses SSL")
+ except ImportError:
+ raise InterfaceError(
+ "SSL required but ssl module not available in "
+ "this python installation")
self._sock = self._usock.makefile(mode="rwb")
- except socket.error:
+ except socket.error as e:
self._usock.close()
- raise InterfaceError("communication error", exc_info()[1])
+ raise InterfaceError("communication error", e)
self._flush = self._sock.flush
self._read = self._sock.read
-
- if PRE_26:
- self._write = self._sock.writelines
- else:
- self._write = self._sock.write
+ self._write = self._sock.write
self._backend_key_data = None
##
@@ -1182,7 +1499,6 @@ def numeric_out(d):
bool: (16, FC_BINARY, bool_send),
int: (705, FC_TEXT, unknown_out),
float: (701, FC_BINARY, d_pack), # float8
- str: (705, FC_TEXT, text_out), # unknown
datetime.date: (1082, FC_TEXT, date_out), # date
datetime.time: (1083, FC_TEXT, time_out), # time
1114: (1114, FC_BINARY, timestamp_send_integer), # timestamp
@@ -1203,10 +1519,12 @@ def numeric_out(d):
if PY2:
self.py_types[Bytea] = (17, FC_BINARY, bytea_send) # bytea
self.py_types[text_type] = (705, FC_TEXT, text_out) # unknown
+ self.py_types[str] = (705, FC_TEXT, bytea_send) # unknown
self.py_types[long] = (705, FC_TEXT, unknown_out) # noqa
else:
self.py_types[bytes] = (17, FC_BINARY, bytea_send) # bytea
+ self.py_types[str] = (705, FC_TEXT, text_out) # unknown
try:
from ipaddress import (
@@ -1240,6 +1558,7 @@ def inet_in(data, offset, length):
READY_FOR_QUERY: self.handle_READY_FOR_QUERY,
ROW_DESCRIPTION: self.handle_ROW_DESCRIPTION,
ERROR_RESPONSE: self.handle_ERROR_RESPONSE,
+ EMPTY_QUERY_RESPONSE: self.handle_EMPTY_QUERY_RESPONSE,
DATA_ROW: self.handle_DATA_ROW,
COMMAND_COMPLETE: self.handle_COMMAND_COMPLETE,
PARSE_COMPLETE: self.handle_PARSE_COMPLETE,
@@ -1272,32 +1591,37 @@ def inet_in(data, offset, length):
self._flush()
self._cursor = self.cursor()
- try:
- self._lock.acquire()
- code = self.error = None
- while code not in (READY_FOR_QUERY, ERROR_RESPONSE):
- code, data_len = ci_unpack(self._read(5))
- self.message_types[code](self._read(data_len - 4), None)
- if self.error is not None:
- raise self.error
- except:
- self._close()
- raise
- finally:
- self._lock.release()
+ with self._lock:
+ try:
+ code = self.error = None
+ while code not in (READY_FOR_QUERY, ERROR_RESPONSE):
+ code, data_len = ci_unpack(self._read(5))
+ self.message_types[code](self._read(data_len - 4), None)
+ if self.error is not None:
+ raise self.error
+ except Exception as e:
+ try:
+ self._close()
+ except Exception:
+ pass
+ raise e
self.in_transaction = False
self.notifies = []
self.notifies_lock = threading.Lock()
def handle_ERROR_RESPONSE(self, data, ps):
- msg_dict = data_into_dict(data)
+ responses = tuple(
+ (s[0:1], s[1:].decode(self._client_encoding)) for s in
+ data.split(NULL_BYTE))
+ msg_dict = dict(responses)
if msg_dict[RESPONSE_CODE] == "28000":
self.error = InterfaceError("md5 password authentication failed")
else:
- self.error = ProgrammingError(
- msg_dict[RESPONSE_SEVERITY], msg_dict[RESPONSE_CODE],
- msg_dict[RESPONSE_MSG])
+ self.error = ProgrammingError(*tuple(v for k, v in responses))
+
+ def handle_EMPTY_QUERY_RESPONSE(self, data, ps):
+ self.error = ProgrammingError("query was empty")
def handle_CLOSE_COMPLETE(self, data, ps):
pass
@@ -1391,11 +1715,8 @@ def handle_NOTIFICATION_RESPONSE(self, data, ps):
# additional_info = data[idx:idx + null]
# psycopg2 compatible notification interface
- try:
- self.notifies_lock.acquire()
+ with self.notifies_lock:
self.notifies.append((backend_pid, condition))
- finally:
- self.notifies_lock.release()
def cursor(self):
"""Creates a :class:`Cursor` object bound to this
@@ -1412,11 +1733,8 @@ def commit(self):
This function is part of the `DBAPI 2.0 specification
`_.
"""
- try:
- self._lock.acquire()
+ with self._lock:
self.execute(self._cursor, "commit", None)
- finally:
- self._lock.release()
def rollback(self):
"""Rolls back the current database transaction.
@@ -1424,11 +1742,8 @@ def rollback(self):
This function is part of the `DBAPI 2.0 specification
`_.
"""
- try:
- self._lock.acquire()
+ with self._lock:
self.execute(self._cursor, "rollback", None)
- finally:
- self._lock.release()
def _close(self):
try:
@@ -1437,12 +1752,15 @@ def _close(self):
self._write(TERMINATE_MSG)
self._flush()
self._sock.close()
- self._usock.close()
- self._sock = None
except AttributeError:
raise InterfaceError("connection is closed")
except ValueError:
raise InterfaceError("connection is closed")
+ except socket.error as e:
+ raise OperationalError(str(e))
+ finally:
+ self._usock.close()
+ self._sock = None
def close(self):
"""Closes the database connection.
@@ -1450,11 +1768,8 @@ def close(self):
This function is part of the `DBAPI 2.0 specification
`_.
"""
- try:
- self._lock.acquire()
+ with self._lock:
self._close()
- finally:
- self._lock.release()
def handle_AUTHENTICATION_REQUEST(self, data, cursor):
assert self._lock.locked()
@@ -1463,7 +1778,7 @@ def handle_AUTHENTICATION_REQUEST(self, data, cursor):
# 0 = AuthenticationOk
# 5 = MD5 pwd
# 2 = Kerberos v5 (not supported by pg8000)
- # 3 = Cleartext pwd (not supported by pg8000)
+ # 3 = Cleartext pwd
# 4 = crypt() pwd (not supported by pg8000)
# 6 = SCM credential (not supported by pg8000)
# 7 = GSSAPI (not supported by pg8000)
@@ -1480,8 +1795,7 @@ def handle_AUTHENTICATION_REQUEST(self, data, cursor):
raise InterfaceError(
"server requesting password authentication, but no "
"password was provided")
- self._send_message(
- PASSWORD, self.password.encode("ascii") + NULL_BYTE)
+ self._send_message(PASSWORD, self.password + NULL_BYTE)
self._flush()
elif auth_code == 5:
##
@@ -1497,8 +1811,8 @@ def handle_AUTHENTICATION_REQUEST(self, data, cursor):
"server requesting MD5 password authentication, but no "
"password was provided")
pwd = b("md5") + md5(
- md5(self.password.encode("ascii") + self.user).
- hexdigest().encode("ascii") + salt).hexdigest().encode("ascii")
+ md5(self.password + self.user).hexdigest().encode("ascii") +
+ salt).hexdigest().encode("ascii")
# Byte1('p') - Identifies the message as a password message.
# Int32 - Message length including self.
# String - The password. Password may be encrypted.
@@ -1536,11 +1850,10 @@ def make_params(self, values):
except KeyError:
try:
params.append(self.inspect_funcs[typ](value))
- except KeyError:
+ except KeyError as e:
raise NotSupportedError(
- "type " + str(exc_info()[1]) +
- "not mapped to pg type")
- return params
+ "type " + str(e) + "not mapped to pg type")
+ return tuple(params)
def handle_ROW_DESCRIPTION(self, data, cursor):
count = h_unpack(data)[0]
@@ -1572,8 +1885,7 @@ def execute(self, cursor, operation, vals):
args = make_args(vals)
params = self.make_params(args)
-
- key = tuple(oid for oid, x, y in params), operation
+ key = operation, params
try:
ps = cache['ps'][key]
@@ -1617,11 +1929,13 @@ def execute(self, cursor, operation, vals):
try:
self._flush()
- except AttributeError:
+ except AttributeError as e:
if self._sock is None:
raise InterfaceError("connection is closed")
else:
- raise exc_info()[1]
+ raise e
+ except socket.error as e:
+ raise OperationalError(str(e))
self.handle_messages(cursor)
@@ -1711,11 +2025,11 @@ def _send_message(self, code, data):
self._write(i_pack(len(data) + 4))
self._write(data)
self._write(FLUSH_MSG)
- except ValueError:
- if str(exc_info()[1]) == "write to closed file":
+ except ValueError as e:
+ if str(e) == "write to closed file":
raise InterfaceError("connection is closed")
else:
- raise exc_info()[1]
+ raise e
except AttributeError:
raise InterfaceError("connection is closed")
@@ -1783,8 +2097,13 @@ def close_portal(self, cursor):
self._flush()
self.handle_messages(cursor)
+ # Byte1('N') - Identifier
+ # Int32 - Message length
+ # Any number of these, followed by a zero byte:
+ # Byte1 - code identifying the field type (see responseKeys)
+ # String - field value
def handle_NOTICE_RESPONSE(self, data, ps):
- resp = data_into_dict(data)
+ resp = dict((s[0:1], s[1:]) for s in data.split(NULL_BYTE))
self.NoticeReceived(resp)
def handle_PARAMETER_STATUS(self, data, ps):
diff --git a/gluon/contrib/pg8000/six.py b/gluon/contrib/pg8000/six.py
index 6ec96ed87..190c0239c 100644
--- a/gluon/contrib/pg8000/six.py
+++ b/gluon/contrib/pg8000/six.py
@@ -1,6 +1,6 @@
"""Utilities for writing code that runs on Python 2 and 3"""
-# Copyright (c) 2010-2013 Benjamin Peterson
+# Copyright (c) 2010-2015 Benjamin Peterson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,8 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -21,22 +21,21 @@
# SOFTWARE.
from __future__ import absolute_import
+
+import functools
+import itertools
import operator
import sys
import types
__author__ = "Benjamin Peterson "
-__version__ = "1.4.1"
+__version__ = "1.10.0"
# Useful for very coarse version differentiation.
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
-
-PRE_26 = PY2 and sys.version_info[1] < 6
-
-IS_JYTHON = sys.platform.lower().count('java') > 0
-
+PY34 = sys.version_info[0:2] >= (3, 4)
if PY3:
string_types = str,
@@ -47,10 +46,10 @@
MAXSIZE = sys.maxsize
else:
- string_types = basestring, # noqa
- integer_types = (int, long) # noqa
+ string_types = basestring,
+ integer_types = (int, long)
class_types = (type, types.ClassType)
- text_type = unicode # noqa
+ text_type = unicode
binary_type = str
if sys.platform.startswith("java"):
@@ -59,6 +58,7 @@
else:
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
+
def __len__(self):
return 1 << 31
try:
@@ -90,9 +90,13 @@ def __init__(self, name):
def __get__(self, obj, tp):
result = self._resolve()
- setattr(obj, self.name, result)
- # This is a bit ugly, but it avoids running this again.
- delattr(tp, self.name)
+ setattr(obj, self.name, result) # Invokes __set__.
+ try:
+ # This is a bit ugly, but it avoids running this again by
+ # removing this descriptor.
+ delattr(obj.__class__, self.name)
+ except AttributeError:
+ pass
return result
@@ -110,6 +114,27 @@ def __init__(self, name, old, new=None):
def _resolve(self):
return _import_module(self.mod)
+ def __getattr__(self, attr):
+ _module = self._resolve()
+ value = getattr(_module, attr)
+ setattr(self, attr, value)
+ return value
+
+
+class _LazyModule(types.ModuleType):
+
+ def __init__(self, name):
+ super(_LazyModule, self).__init__(name)
+ self.__doc__ = self.__class__.__doc__
+
+ def __dir__(self):
+ attrs = ["__doc__", "__name__"]
+ attrs += [attr.name for attr in self._moved_attributes]
+ return attrs
+
+ # Subclasses should override this
+ _moved_attributes = []
+
class MovedAttribute(_LazyDescr):
@@ -136,38 +161,109 @@ def _resolve(self):
return getattr(module, self.attr)
-class _MovedItems(types.ModuleType):
+class _SixMetaPathImporter(object):
+
+ """
+ A meta path importer to import six.moves and its submodules.
+
+ This class implements a PEP302 finder and loader. It should be compatible
+ with Python 2.5 and all existing versions of Python3
+ """
+
+ def __init__(self, six_module_name):
+ self.name = six_module_name
+ self.known_modules = {}
+
+ def _add_module(self, mod, *fullnames):
+ for fullname in fullnames:
+ self.known_modules[self.name + "." + fullname] = mod
+
+ def _get_module(self, fullname):
+ return self.known_modules[self.name + "." + fullname]
+
+ def find_module(self, fullname, path=None):
+ if fullname in self.known_modules:
+ return self
+ return None
+
+ def __get_module(self, fullname):
+ try:
+ return self.known_modules[fullname]
+ except KeyError:
+ raise ImportError("This loader does not know module " + fullname)
+
+ def load_module(self, fullname):
+ try:
+ # in case of a reload
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ mod = self.__get_module(fullname)
+ if isinstance(mod, MovedModule):
+ mod = mod._resolve()
+ else:
+ mod.__loader__ = self
+ sys.modules[fullname] = mod
+ return mod
+
+ def is_package(self, fullname):
+ """
+ Return true, if the named module is a package.
+
+ We need this method to get correct spec objects with
+ Python 3.4 (see PEP451)
+ """
+ return hasattr(self.__get_module(fullname), "__path__")
+
+ def get_code(self, fullname):
+ """Return None
+
+ Required, if is_package is implemented"""
+ self.__get_module(fullname) # eventually raises ImportError
+ return None
+ get_source = get_code # same as get_code
+
+_importer = _SixMetaPathImporter(__name__)
+
+
+class _MovedItems(_LazyModule):
+
"""Lazy loading of moved objects"""
+ __path__ = [] # mark as package
_moved_attributes = [
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
- MovedAttribute(
- "filterfalse", "itertools", "itertools", "ifilterfalse",
- "filterfalse"),
+ MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+ MovedAttribute("intern", "__builtin__", "sys"),
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+ MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
+ MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
- MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
+ MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
MovedAttribute("reduce", "__builtin__", "functools"),
+ MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
MovedAttribute("StringIO", "StringIO", "io"),
+ MovedAttribute("UserDict", "UserDict", "collections"),
+ MovedAttribute("UserList", "UserList", "collections"),
MovedAttribute("UserString", "UserString", "collections"),
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
- MovedAttribute(
- "zip_longest", "itertools", "itertools", "izip_longest",
- "zip_longest"),
+ MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
MovedModule("builtins", "__builtin__"),
MovedModule("configparser", "ConfigParser"),
MovedModule("copyreg", "copy_reg"),
+ MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+ MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
MovedModule("http_cookies", "Cookie", "http.cookies"),
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
MovedModule("html_parser", "HTMLParser", "html.parser"),
MovedModule("http_client", "httplib", "http.client"),
- MovedModule(
- "email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+ MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+ MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
@@ -177,14 +273,14 @@ class _MovedItems(types.ModuleType):
MovedModule("queue", "Queue"),
MovedModule("reprlib", "repr"),
MovedModule("socketserver", "SocketServer"),
+ MovedModule("_thread", "thread", "_thread"),
MovedModule("tkinter", "Tkinter"),
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
- MovedModule(
- "tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
- MovedModule(
- "tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+ MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+ MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+ MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
MovedModule("tkinter_colorchooser", "tkColorChooser",
@@ -194,30 +290,41 @@ class _MovedItems(types.ModuleType):
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
- MovedModule(
- "tkinter_tksimpledialog", "tkSimpleDialog", "tkinter.simpledialog"),
- MovedModule(
- "urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
- MovedModule(
- "urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
- MovedModule(
- "urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+ MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+ "tkinter.simpledialog"),
+ MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+ MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+ MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
- MovedModule("winreg", "_winreg"),
+ MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+ MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
]
+# Add windows specific modules.
+if sys.platform == "win32":
+ _moved_attributes += [
+ MovedModule("winreg", "_winreg"),
+ ]
+
for attr in _moved_attributes:
setattr(_MovedItems, attr.name, attr)
+ if isinstance(attr, MovedModule):
+ _importer._add_module(attr, "moves." + attr.name)
del attr
-moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves")
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = _MovedItems(__name__ + ".moves")
+_importer._add_module(moves, "moves")
-class Module_six_moves_urllib_parse(types.ModuleType):
+class Module_six_moves_urllib_parse(_LazyModule):
+
"""Lazy loading of moved objects in six.moves.urllib_parse"""
_urllib_parse_moved_attributes = [
MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+ MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
@@ -231,18 +338,27 @@ class Module_six_moves_urllib_parse(types.ModuleType):
MovedAttribute("unquote", "urllib", "urllib.parse"),
MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
MovedAttribute("urlencode", "urllib", "urllib.parse"),
+ MovedAttribute("splitquery", "urllib", "urllib.parse"),
+ MovedAttribute("splittag", "urllib", "urllib.parse"),
+ MovedAttribute("splituser", "urllib", "urllib.parse"),
+ MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_params", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_query", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
]
for attr in _urllib_parse_moved_attributes:
setattr(Module_six_moves_urllib_parse, attr.name, attr)
del attr
-sys.modules[__name__ + ".moves.urllib_parse"] = Module_six_moves_urllib_parse(
- __name__ + ".moves.urllib_parse")
-sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse(
- __name__ + ".moves.urllib.parse")
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
+ "moves.urllib_parse", "moves.urllib.parse")
+
+class Module_six_moves_urllib_error(_LazyModule):
-class Module_six_moves_urllib_error(types.ModuleType):
"""Lazy loading of moved objects in six.moves.urllib_error"""
@@ -255,13 +371,14 @@ class Module_six_moves_urllib_error(types.ModuleType):
setattr(Module_six_moves_urllib_error, attr.name, attr)
del attr
-sys.modules[__name__ + ".moves.urllib_error"] = Module_six_moves_urllib_error(
- __name__ + ".moves.urllib_error")
-sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error(
- __name__ + ".moves.urllib.error")
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
+ "moves.urllib_error", "moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
-class Module_six_moves_urllib_request(types.ModuleType):
"""Lazy loading of moved objects in six.moves.urllib_request"""
@@ -280,8 +397,7 @@ class Module_six_moves_urllib_request(types.ModuleType):
MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
- MovedAttribute(
- "HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
@@ -299,18 +415,20 @@ class Module_six_moves_urllib_request(types.ModuleType):
MovedAttribute("urlcleanup", "urllib", "urllib.request"),
MovedAttribute("URLopener", "urllib", "urllib.request"),
MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+ MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
]
for attr in _urllib_request_moved_attributes:
setattr(Module_six_moves_urllib_request, attr.name, attr)
del attr
-sys.modules[__name__ + ".moves.urllib_request"] = \
- Module_six_moves_urllib_request(__name__ + ".moves.urllib_request")
-sys.modules[__name__ + ".moves.urllib.request"] = \
- Module_six_moves_urllib_request(__name__ + ".moves.urllib.request")
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
+ "moves.urllib_request", "moves.urllib.request")
-class Module_six_moves_urllib_response(types.ModuleType):
+class Module_six_moves_urllib_response(_LazyModule):
+
"""Lazy loading of moved objects in six.moves.urllib_response"""
@@ -324,13 +442,14 @@ class Module_six_moves_urllib_response(types.ModuleType):
setattr(Module_six_moves_urllib_response, attr.name, attr)
del attr
-sys.modules[__name__ + ".moves.urllib_response"] = \
- Module_six_moves_urllib_response(__name__ + ".moves.urllib_response")
-sys.modules[__name__ + ".moves.urllib.response"] = \
- Module_six_moves_urllib_response(__name__ + ".moves.urllib.response")
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
+ "moves.urllib_response", "moves.urllib.response")
-class Module_six_moves_urllib_robotparser(types.ModuleType):
+class Module_six_moves_urllib_robotparser(_LazyModule):
+
"""Lazy loading of moved objects in six.moves.urllib_robotparser"""
@@ -341,25 +460,27 @@ class Module_six_moves_urllib_robotparser(types.ModuleType):
setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
del attr
-sys.modules[__name__ + ".moves.urllib_robotparser"] = \
- Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib_robotparser")
-sys.modules[__name__ + ".moves.urllib.robotparser"] = \
- Module_six_moves_urllib_robotparser(
- __name__ + ".moves.urllib.robotparser")
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
+ "moves.urllib_robotparser", "moves.urllib.robotparser")
class Module_six_moves_urllib(types.ModuleType):
- """Create a six.moves.urllib namespace that resembles the Python 3
- namespace"""
- parse = sys.modules[__name__ + ".moves.urllib_parse"]
- error = sys.modules[__name__ + ".moves.urllib_error"]
- request = sys.modules[__name__ + ".moves.urllib_request"]
- response = sys.modules[__name__ + ".moves.urllib_response"]
- robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"]
+ """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+ __path__ = [] # mark as package
+ parse = _importer._get_module("moves.urllib_parse")
+ error = _importer._get_module("moves.urllib_error")
+ request = _importer._get_module("moves.urllib_request")
+ response = _importer._get_module("moves.urllib_response")
+ robotparser = _importer._get_module("moves.urllib_robotparser")
+
+ def __dir__(self):
+ return ['parse', 'error', 'request', 'response', 'robotparser']
-sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib(
- __name__ + ".moves.urllib")
+_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
+ "moves.urllib")
def add_move(move):
@@ -386,11 +507,6 @@ def remove_move(name):
_func_code = "__code__"
_func_defaults = "__defaults__"
_func_globals = "__globals__"
-
- _iterkeys = "keys"
- _itervalues = "values"
- _iteritems = "items"
- _iterlists = "lists"
else:
_meth_func = "im_func"
_meth_self = "im_self"
@@ -400,11 +516,6 @@ def remove_move(name):
_func_defaults = "func_defaults"
_func_globals = "func_globals"
- _iterkeys = "iterkeys"
- _itervalues = "itervalues"
- _iteritems = "iteritems"
- _iterlists = "iterlists"
-
try:
advance_iterator = next
@@ -427,6 +538,9 @@ def get_unbound_function(unbound):
create_bound_method = types.MethodType
+ def create_unbound_method(func, cls):
+ return func
+
Iterator = object
else:
def get_unbound_function(unbound):
@@ -435,6 +549,9 @@ def get_unbound_function(unbound):
def create_bound_method(func, obj):
return types.MethodType(func, obj, obj.__class__)
+ def create_unbound_method(func, cls):
+ return types.MethodType(func, None, cls)
+
class Iterator(object):
def next(self):
@@ -453,24 +570,49 @@ def next(self):
get_function_globals = operator.attrgetter(_func_globals)
-def iterkeys(d, **kw):
- """Return an iterator over the keys of a dictionary."""
- return iter(getattr(d, _iterkeys)(**kw))
+if PY3:
+ def iterkeys(d, **kw):
+ return iter(d.keys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.values(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.items(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.lists(**kw))
+ viewkeys = operator.methodcaller("keys")
-def itervalues(d, **kw):
- """Return an iterator over the values of a dictionary."""
- return iter(getattr(d, _itervalues)(**kw))
+ viewvalues = operator.methodcaller("values")
+ viewitems = operator.methodcaller("items")
+else:
+ def iterkeys(d, **kw):
+ return d.iterkeys(**kw)
+
+ def itervalues(d, **kw):
+ return d.itervalues(**kw)
+
+ def iteritems(d, **kw):
+ return d.iteritems(**kw)
-def iteritems(d, **kw):
- """Return an iterator over the (key, value) pairs of a dictionary."""
- return iter(getattr(d, _iteritems)(**kw))
+ def iterlists(d, **kw):
+ return d.iterlists(**kw)
+ viewkeys = operator.methodcaller("viewkeys")
-def iterlists(d, **kw):
- """Return an iterator over the (key, [values]) pairs of a dictionary."""
- return iter(getattr(d, _iterlists)(**kw))
+ viewvalues = operator.methodcaller("viewvalues")
+
+ viewitems = operator.methodcaller("viewitems")
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+ "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+ "Return an iterator over the (key, [values]) pairs of a dictionary.")
if PY3:
@@ -480,24 +622,29 @@ def b(s):
def u(s):
return s
unichr = chr
- if sys.version_info[1] <= 1:
- def int2byte(i):
- return bytes((i,))
- else:
- # This is about 2x faster than the implementation above on 3.2+
- int2byte = operator.methodcaller("to_bytes", 1, "big")
+ import struct
+ int2byte = struct.Struct(">B").pack
+ del struct
byte2int = operator.itemgetter(0)
indexbytes = operator.getitem
iterbytes = iter
import io
StringIO = io.StringIO
BytesIO = io.BytesIO
+ _assertCountEqual = "assertCountEqual"
+ if sys.version_info[1] <= 1:
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+ else:
+ _assertRaisesRegex = "assertRaisesRegex"
+ _assertRegex = "assertRegex"
else:
def b(s):
return s
+ # Workaround for standalone backslash
def u(s):
- return unicode(s, "unicode_escape") # noqa
+ return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
unichr = unichr
int2byte = chr
@@ -506,27 +653,38 @@ def byte2int(bs):
def indexbytes(buf, i):
return ord(buf[i])
-
- def iterbytes(buf):
- return (ord(byte) for byte in buf)
+ iterbytes = functools.partial(itertools.imap, ord)
import StringIO
StringIO = BytesIO = StringIO.StringIO
+ _assertCountEqual = "assertItemsEqual"
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
_add_doc(b, """Byte literal""")
_add_doc(u, """Text literal""")
+def assertCountEqual(self, *args, **kwargs):
+ return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+ return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+ return getattr(self, _assertRegex)(*args, **kwargs)
+
+
if PY3:
- import builtins
- exec_ = getattr(builtins, "exec")
+ exec_ = getattr(moves.builtins, "exec")
def reraise(tp, value, tb=None):
+ if value is None:
+ value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
- print_ = getattr(builtins, "print")
- del builtins
-
else:
def exec_(_code_, _globs_=None, _locs_=None):
"""Execute code in a namespace."""
@@ -544,26 +702,52 @@ def exec_(_code_, _globs_=None, _locs_=None):
raise tp, value, tb
""")
+
+if sys.version_info[:2] == (3, 2):
+ exec_("""def raise_from(value, from_value):
+ if from_value is None:
+ raise value
+ raise value from from_value
+""")
+elif sys.version_info[:2] > (3, 2):
+ exec_("""def raise_from(value, from_value):
+ raise value from from_value
+""")
+else:
+ def raise_from(value, from_value):
+ raise value
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
def print_(*args, **kwargs):
- """The new-style print function."""
+ """The new-style print function for Python 2.4 and 2.5."""
fp = kwargs.pop("file", sys.stdout)
if fp is None:
return
def write(data):
- if not isinstance(data, basestring): # noqa
+ if not isinstance(data, basestring):
data = str(data)
+ # If the file has an encoding, encode unicode with it.
+ if (isinstance(fp, file) and
+ isinstance(data, unicode) and
+ fp.encoding is not None):
+ errors = getattr(fp, "errors", None)
+ if errors is None:
+ errors = "strict"
+ data = data.encode(fp.encoding, errors)
fp.write(data)
want_unicode = False
sep = kwargs.pop("sep", None)
if sep is not None:
- if isinstance(sep, unicode): # noqa
+ if isinstance(sep, unicode):
want_unicode = True
elif not isinstance(sep, str):
raise TypeError("sep must be None or a string")
end = kwargs.pop("end", None)
if end is not None:
- if isinstance(end, unicode): # noqa
+ if isinstance(end, unicode):
want_unicode = True
elif not isinstance(end, str):
raise TypeError("end must be None or a string")
@@ -571,12 +755,12 @@ def write(data):
raise TypeError("invalid keyword arguments to print()")
if not want_unicode:
for arg in args:
- if isinstance(arg, unicode): # noqa
+ if isinstance(arg, unicode):
want_unicode = True
break
if want_unicode:
- newline = unicode("\n") # noqa
- space = unicode(" ") # noqa
+ newline = unicode("\n")
+ space = unicode(" ")
else:
newline = "\n"
space = " "
@@ -589,22 +773,96 @@ def write(data):
write(sep)
write(arg)
write(end)
+if sys.version_info[:2] < (3, 3):
+ _print = print_
+
+ def print_(*args, **kwargs):
+ fp = kwargs.get("file", sys.stdout)
+ flush = kwargs.pop("flush", False)
+ _print(*args, **kwargs)
+ if flush and fp is not None:
+ fp.flush()
_add_doc(reraise, """Reraise an exception.""")
+if sys.version_info[0:2] < (3, 4):
+ def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
+ updated=functools.WRAPPER_UPDATES):
+ def wrapper(f):
+ f = functools.wraps(wrapped, assigned, updated)(f)
+ f.__wrapped__ = wrapped
+ return f
+ return wrapper
+else:
+ wraps = functools.wraps
+
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
- return meta("NewBase", bases, {})
+ # This requires a bit of explanation: the basic idea is to make a dummy
+ # metaclass for one level of class instantiation that replaces itself with
+ # the actual metaclass.
+ class metaclass(meta):
+
+ def __new__(cls, name, this_bases, d):
+ return meta(name, bases, d)
+ return type.__new__(metaclass, 'temporary_class', (), {})
def add_metaclass(metaclass):
"""Class decorator for creating a class with a metaclass."""
def wrapper(cls):
orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
orig_vars.pop('__dict__', None)
orig_vars.pop('__weakref__', None)
- for slots_var in orig_vars.get('__slots__', ()):
- orig_vars.pop(slots_var)
return metaclass(cls.__name__, cls.__bases__, orig_vars)
return wrapper
+
+
+def python_2_unicode_compatible(klass):
+ """
+ A decorator that defines __unicode__ and __str__ methods under Python 2.
+ Under Python 3 it does nothing.
+
+ To support Python 2 and 3 with a single code base, define a __str__ method
+ returning text and apply this decorator to the class.
+ """
+ if PY2:
+ if '__str__' not in klass.__dict__:
+ raise ValueError("@python_2_unicode_compatible cannot be applied "
+ "to %s because it doesn't define __str__()." %
+ klass.__name__)
+ klass.__unicode__ = klass.__str__
+ klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+ return klass
+
+
+# Complete the moves implementation.
+# This code is at the end of this module to speed up module loading.
+# Turn this module into a package.
+__path__ = [] # required for PEP 302 and PEP 451
+__package__ = __name__ # see PEP 366 @ReservedAssignment
+if globals().get("__spec__") is not None:
+ __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
+# Remove other six meta path importers, since they cause problems. This can
+# happen if six is removed from sys.modules and then reloaded. (Setuptools does
+# this for some reason.)
+if sys.meta_path:
+ for i, importer in enumerate(sys.meta_path):
+ # Here's some real nastiness: Another "instance" of the six module might
+ # be floating around. Therefore, we can't use isinstance() to check for
+ # the six meta path importer, since the other six instance will have
+ # inserted an importer with different class.
+ if (type(importer).__name__ == "_SixMetaPathImporter" and
+ importer.name == __name__):
+ del sys.meta_path[i]
+ break
+ del i, importer
+# Finally, add the importer to the meta path import hook.
+sys.meta_path.append(_importer)
diff --git a/gluon/contrib/pyaes/LICENSE.txt b/gluon/contrib/pyaes/LICENSE.txt
new file mode 100644
index 000000000..0417a6c2a
--- /dev/null
+++ b/gluon/contrib/pyaes/LICENSE.txt
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Richard Moore
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/gluon/contrib/pyaes/README.md b/gluon/contrib/pyaes/README.md
new file mode 100644
index 000000000..05b39f3f5
--- /dev/null
+++ b/gluon/contrib/pyaes/README.md
@@ -0,0 +1,363 @@
+pyaes
+=====
+
+A pure-Python implmentation of the AES block cipher algorithm and the common modes of operation (CBC, CFB, CTR, ECB and OFB).
+
+
+Features
+--------
+
+* Supports all AES key sizes
+* Supports all AES common modes
+* Pure-Python (no external dependancies)
+* BlockFeeder API allows streams to easily be encrypted and decrypted
+* Python 2.x and 3.x support (make sure you pass in bytes(), not strings for Python 3)
+
+
+API
+---
+
+All keys may be 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes) long.
+
+To generate a random key use:
+```python
+import os
+
+# 128 bit, 192 bit and 256 bit keys
+key_128 = os.urandom(16)
+key_192 = os.urandom(24)
+key_256 = os.urandom(32)
+```
+
+To generate keys from simple-to-remember passwords, consider using a _password-based key-derivation function_ such as [scrypt](https://github.com/ricmoo/pyscrypt).
+
+
+### Common Modes of Operation
+
+There are many modes of operations, each with various pros and cons. In general though, the **CBC** and **CTR** modes are recommended. The **ECB is NOT recommended.**, and is included primarilty for completeness.
+
+Each of the following examples assumes the following key:
+```python
+import pyaes
+
+# A 256 bit (32 byte) key
+key = "This_key_for_demo_purposes_only!"
+
+# For some modes of operation we need a random initialization vector
+# of 16 bytes
+iv = "InitializationVe"
+```
+
+
+#### Counter Mode of Operation (recommended)
+
+```python
+aes = pyaes.AESModeOfOperationCTR(key)
+plaintext = "Text may be any length you wish, no padding is required"
+ciphertext = aes.encrypt(plaintext)
+
+# '''\xb6\x99\x10=\xa4\x96\x88\xd1\x89\x1co\xe6\x1d\xef;\x11\x03\xe3\xee
+# \xa9V?wY\xbfe\xcdO\xe3\xdf\x9dV\x19\xe5\x8dk\x9fh\xb87>\xdb\xa3\xd6
+# \x86\xf4\xbd\xb0\x97\xf1\t\x02\xe9 \xed'''
+print repr(ciphertext)
+
+# The counter mode of operation maintains state, so decryption requires
+# a new instance be created
+aes = pyaes.AESModeOfOperationCTR(key)
+decrypted = aes.decrypt(ciphertext)
+
+# True
+print decrypted == plaintext
+
+# To use a custom initial value
+counter = pyaes.Counter(initial_value = 100)
+aes = pyaes.AESModeOfOperationCTR(key, counter = counter)
+ciphertext = aes.encrypt(plaintext)
+
+# '''WZ\x844\x02\xbfoY\x1f\x12\xa6\xce\x03\x82Ei)\xf6\x97mX\x86\xe3\x9d
+# _1\xdd\xbd\x87\xb5\xccEM_4\x01$\xa6\x81\x0b\xd5\x04\xd7Al\x07\xe5
+# \xb2\x0e\\\x0f\x00\x13,\x07'''
+print repr(ciphertext)
+```
+
+
+#### Cipher-Block Chaining (recommended)
+
+```python
+aes = pyaes.AESModeOfOperationCBC(key, iv = iv)
+plaintext = "TextMustBe16Byte"
+ciphertext = aes.encrypt(plaintext)
+
+# '\xd6:\x18\xe6\xb1\xb3\xc3\xdc\x87\xdf\xa7|\x08{k\xb6'
+print repr(ciphertext)
+
+
+# The cipher-block chaining mode of operation maintains state, so
+# decryption requires a new instance be created
+aes = pyaes.AESModeOfOperationCBC(key, iv = iv)
+decrypted = aes.decrypt(ciphertext)
+
+# True
+print decrypted == plaintext
+```
+
+
+#### Cipher Feedback
+
+```python
+# Each block into the mode of operation must be a multiple of the segment
+# size. For this example we choose 8 bytes.
+aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8)
+plaintext = "TextMustBeAMultipleOfSegmentSize"
+ciphertext = aes.encrypt(plaintext)
+
+# '''v\xa9\xc1w"\x8aL\x93\xcb\xdf\xa0/\xf8Y\x0b\x8d\x88i\xcb\x85rmp
+# \x85\xfe\xafM\x0c)\xd5\xeb\xaf'''
+print repr(ciphertext)
+
+
+# The cipher-block chaining mode of operation maintains state, so
+# decryption requires a new instance be created
+aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8)
+decrypted = aes.decrypt(ciphertext)
+
+# True
+print decrypted == plaintext
+```
+
+
+#### Output Feedback Mode of Operation
+
+```python
+aes = pyaes.AESModeOfOperationOFB(key, iv = iv)
+plaintext = "Text may be any length you wish, no padding is required"
+ciphertext = aes.encrypt(plaintext)
+
+# '''v\xa9\xc1wO\x92^\x9e\rR\x1e\xf7\xb1\xa2\x9d"l1\xc7\xe7\x9d\x87(\xc26s
+# \xdd8\xc8@\xb6\xd9!\xf5\x0cM\xaa\x9b\xc4\xedLD\xe4\xb9\xd8\xdf\x9e\xac
+# \xa1\xb8\xea\x0f\x8ev\xb5'''
+print repr(ciphertext)
+
+# The counter mode of operation maintains state, so decryption requires
+# a new instance be created
+aes = pyaes.AESModeOfOperationOFB(key, iv = iv)
+decrypted = aes.decrypt(ciphertext)
+
+# True
+print decrypted == plaintext
+```
+
+
+#### Electronic Codebook (NOT recommended)
+
+```python
+aes = pyaes.AESModeOfOperationECB(key)
+plaintext = "TextMustBe16Byte"
+ciphertext = aes.encrypt(plaintext)
+
+# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3'
+print repr(ciphertext)
+
+# Since there is no state stored in this mode of operation, it
+# is not necessary to create a new aes object for decryption.
+#aes = pyaes.AESModeOfOperationECB(key)
+decrypted = aes.decrypt(ciphertext)
+
+# True
+print decrypted == plaintext
+```
+
+
+### BlockFeeder
+
+Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data.
+
+The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary.
+
+```python
+import pyaes
+
+# Any mode of operation can be used; for this example CBC
+key = "This_key_for_demo_purposes_only!"
+iv = "InitializationVe"
+
+ciphertext = ''
+
+# We can encrypt one line at a time, regardles of length
+encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv))
+for line in file('/etc/passwd'):
+ ciphertext += encrypter.feed(line)
+
+# Make a final call to flush any remaining bytes and add paddin
+ciphertext += encrypter.feed()
+
+# We can decrypt the cipher text in chunks (here we split it in half)
+decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv))
+decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2])
+decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:])
+
+# Again, make a final call to flush any remaining bytes and strip padding
+decrypted += decrypter.feed()
+
+print file('/etc/passwd').read() == decrypted
+```
+
+### Stream Feeder
+
+This is meant to make it even easier to encrypt and decrypt streams and large files.
+
+```python
+import pyaes
+
+# Any mode of operation can be used; for this example CTR
+key = "This_key_for_demo_purposes_only!"
+
+# Create the mode of operation to encrypt with
+mode = pyaes.AESModeOfOperationCTR(key)
+
+# The input and output files
+file_in = file('/etc/passwd')
+file_out = file('/tmp/encrypted.bin', 'wb')
+
+# Encrypt the data as a stream, the file is read in 8kb chunks, be default
+pyaes.encrypt_stream(mode, file_in, file_out)
+
+# Close the files
+file_in.close()
+file_out.close()
+```
+
+Decrypting is identical, except you would use `pyaes.decrypt_stream`, and the encrypted file would be the `file_in` and target for decryption the `file_out`.
+
+### AES block cipher
+
+Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks.
+
+The block cipher requires exactly one block of data to encrypt or decrypt, and each block should be an array with each element an integer representation of a byte.
+
+```python
+import pyaes
+
+# 16 byte block of plain text
+plaintext = "Hello World!!!!!"
+plaintext_bytes = [ ord(c) for c in plaintext ]
+
+# 32 byte key (256 bit)
+key = "This_key_for_demo_purposes_only!"
+
+# Our AES instance
+aes = pyaes.AES(key)
+
+# Encrypt!
+ciphertext = aes.encrypt(plaintext_bytes)
+
+# [55, 250, 182, 25, 185, 208, 186, 95, 206, 115, 50, 115, 108, 58, 174, 115]
+print repr(ciphertext)
+
+# Decrypt!
+decrypted = aes.decrypt(ciphertext)
+
+# True
+print decrypted == plaintext_bytes
+```
+
+What is a key?
+--------------
+
+This seems to be a point of confusion for many people new to using encryption. You can think of the key as the *"password"*. However, these algorithms require the *"password"* to be a specific length.
+
+With AES, there are three possible key lengths, 16-bytes, 24-bytes or 32-bytes. When you create an AES object, the key size is automatically detected, so it is important to pass in a key of the correct length.
+
+Often, you wish to provide a password of arbitrary length, for example, something easy to remember or write down. In these cases, you must come up with a way to transform the password into a key, of a specific length. A **Password-Based Key Derivation Function** (PBKDF) is an algorithm designed for this exact purpose.
+
+Here is an example, using the popular (possibly obsolete?) *crypt* PBKDF:
+
+```
+# See: https://www.dlitz.net/software/python-pbkdf2/
+import pbkdf2
+
+password = "HelloWorld"
+
+# The crypt PBKDF returns a 48-byte string
+key = pbkdf2.crypt(password)
+
+# A 16-byte, 24-byte and 32-byte key, respectively
+key_16 = key[:16]
+key_24 = key[:24]
+key_32 = key[:32]
+```
+
+The [scrypt](https://github.com/ricmoo/pyscrypt) PBKDF is intentionally slow, to make it more difficult to brute-force guess a password:
+
+```
+# See: https://github.com/ricmoo/pyscrypt
+import pyscrypt
+
+password = "HelloWorld"
+
+# Salt is required, and prevents Rainbow Table attacks
+salt = "SeaSalt"
+
+# N, r, and p are parameters to specify how difficult it should be to
+# generate a key; bigger numbers take longer and more memory
+N = 1024
+r = 1
+p = 1
+
+# A 16-byte, 24-byte and 32-byte key, respectively; the scrypt algorithm takes
+# a 6-th parameter, indicating key length
+key_16 = pyscrypt.hash(password, salt, N, r, p, 16)
+key_24 = pyscrypt.hash(password, salt, N, r, p, 24)
+key_32 = pyscrypt.hash(password, salt, N, r, p, 32)
+```
+
+Another possibility, is to use a hashing function, such as SHA256 to hash the password, but this method may be vulnerable to [Rainbow Attacks](http://en.wikipedia.org/wiki/Rainbow_table), unless you use a [salt](http://en.wikipedia.org/wiki/Salt_(cryptography)).
+
+```python
+import hashlib
+
+password = "HelloWorld"
+
+# The SHA256 hash algorithm returns a 32-byte string
+hashed = hashlib.sha256(password).digest()
+
+# A 16-byte, 24-byte and 32-byte key, respectively
+key_16 = hashed[:16]
+key_24 = hashed[:24]
+key_32 = hashed
+```
+
+
+
+
+Performance
+-----------
+
+There is a test case provided in _/tests/test-aes.py_ which does some basic performance testing (its primary purpose is moreso as a regression test).
+
+Based on that test, in **CPython**, this library is about 30x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 80x slower for CFB; and 300x slower for CTR.
+
+Based on that same test, in **Pypy**, this library is about 4x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 12x slower for CFB; and 19x slower for CTR.
+
+The PyCrypto documentation makes reference to the counter call being responsible for the speed problems of the counter (CTR) mode of operation, which is why they use a specially optimized counter. I will investigate this problem further in the future.
+
+
+FAQ
+---
+
+#### Why do this?
+
+The short answer, *why not?*
+
+The longer answer, is for my [pyscrypt](https://github.com/ricmoo/pyscrypt) library. I required a pure-Python AES implementation that supported 256-bit keys with the counter (CTR) mode of operation. After searching, I found several implementations, but all were missing CTR or only supported 128 bit keys. After all the work of learning AES inside and out to implement the library, it was only a marginal amount of extra work to library-ify a more general solution. So, *why not?*
+
+#### How do I get a question I have added?
+
+E-mail me at pyaes@ricmoo.com with any questions, suggestions, comments, et cetera.
+
+
+#### Can I give you my money?
+
+Umm... Ok? :-)
+
+_Bitcoin_ - `18UDs4qV1shu2CgTS2tKojhCtM69kpnWg9`
diff --git a/gluon/contrib/pyaes/__init__.py b/gluon/contrib/pyaes/__init__.py
new file mode 100644
index 000000000..5712f794a
--- /dev/null
+++ b/gluon/contrib/pyaes/__init__.py
@@ -0,0 +1,53 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2014 Richard Moore
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# This is a pure-Python implementation of the AES algorithm and AES common
+# modes of operation.
+
+# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
+# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
+
+
+# Supported key sizes:
+# 128-bit
+# 192-bit
+# 256-bit
+
+
+# Supported modes of operation:
+# ECB - Electronic Codebook
+# CBC - Cipher-Block Chaining
+# CFB - Cipher Feedback
+# OFB - Output Feedback
+# CTR - Counter
+
+# See the README.md for API details and general information.
+
+# Also useful, PyCrypto, a crypto library implemented in C with Python bindings:
+# https://www.dlitz.net/software/pycrypto/
+
+
+VERSION = [1, 3, 0]
+
+from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter
+from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter
+from .blockfeeder import PADDING_NONE, PADDING_DEFAULT
diff --git a/gluon/contrib/pyaes/aes.py b/gluon/contrib/pyaes/aes.py
new file mode 100644
index 000000000..c6e8bc02a
--- /dev/null
+++ b/gluon/contrib/pyaes/aes.py
@@ -0,0 +1,589 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2014 Richard Moore
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# This is a pure-Python implementation of the AES algorithm and AES common
+# modes of operation.
+
+# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
+
+# Honestly, the best description of the modes of operations are the wonderful
+# diagrams on Wikipedia. They explain in moments what my words could never
+# achieve. Hence the inline documentation here is sparer than I'd prefer.
+# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
+
+# Also useful, PyCrypto, a crypto library implemented in C with Python bindings:
+# https://www.dlitz.net/software/pycrypto/
+
+
+# Supported key sizes:
+# 128-bit
+# 192-bit
+# 256-bit
+
+
+# Supported modes of operation:
+# ECB - Electronic Codebook
+# CBC - Cipher-Block Chaining
+# CFB - Cipher Feedback
+# OFB - Output Feedback
+# CTR - Counter
+
+
+# See the README.md for API details and general information.
+
+
+import copy
+import struct
+
+__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB",
+ "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"]
+
+
+def _compact_word(word):
+ return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3]
+
+def _string_to_bytes(text):
+ return list(ord(c) for c in text)
+
+def _bytes_to_string(binary):
+ return "".join(chr(b) for b in binary)
+
+def _concat_list(a, b):
+ return a + b
+
+
+# Python 3 compatibility
+try:
+ xrange
+except Exception:
+ xrange = range
+
+ # Python 3 supports bytes, which is already an array of integers
+ def _string_to_bytes(text):
+ if isinstance(text, bytes):
+ return text
+ return [ord(c) for c in text]
+
+ # In Python 3, we return bytes
+ def _bytes_to_string(binary):
+ return bytes(binary)
+
+ # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first
+ def _concat_list(a, b):
+ return a + bytes(b)
+
+
+# Based *largely* on the Rijndael implementation
+# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf
+class AES(object):
+ '''Encapsulates the AES block cipher.
+
+ You generally should not need this. Use the AESModeOfOperation classes
+ below instead.'''
+
+ # Number of rounds by keysize
+ number_of_rounds = {16: 10, 24: 12, 32: 14}
+
+ # Round constant words
+ rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
+
+ # S-box and Inverse S-box (S is for Substitution)
+ S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
+ Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ]
+
+ # Transformations for encryption
+ T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
+ T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
+ T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
+ T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
+
+ # Transformations for decryption
+ T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
+ T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
+ T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
+ T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
+
+ # Transformations for decryption key expansion
+ U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
+ U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
+ U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
+ U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
+
+ def __init__(self, key):
+
+ if len(key) not in (16, 24, 32):
+ raise ValueError('Invalid key size')
+
+ rounds = self.number_of_rounds[len(key)]
+
+ # Encryption round keys
+ self._Ke = [[0] * 4 for i in xrange(rounds + 1)]
+
+ # Decryption round keys
+ self._Kd = [[0] * 4 for i in xrange(rounds + 1)]
+
+ round_key_count = (rounds + 1) * 4
+ KC = len(key) // 4
+
+ # Convert the key into ints
+ tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ]
+
+ # Copy values into round key arrays
+ for i in xrange(0, KC):
+ self._Ke[i // 4][i % 4] = tk[i]
+ self._Kd[rounds - (i // 4)][i % 4] = tk[i]
+
+ # Key expansion (fips-197 section 5.2)
+ rconpointer = 0
+ t = KC
+ while t < round_key_count:
+
+ tt = tk[KC - 1]
+ tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^
+ (self.S[(tt >> 8) & 0xFF] << 16) ^
+ (self.S[ tt & 0xFF] << 8) ^
+ self.S[(tt >> 24) & 0xFF] ^
+ (self.rcon[rconpointer] << 24))
+ rconpointer += 1
+
+ if KC != 8:
+ for i in xrange(1, KC):
+ tk[i] ^= tk[i - 1]
+
+ # Key expansion for 256-bit keys is "slightly different" (fips-197)
+ else:
+ for i in xrange(1, KC // 2):
+ tk[i] ^= tk[i - 1]
+ tt = tk[KC // 2 - 1]
+
+ tk[KC // 2] ^= (self.S[ tt & 0xFF] ^
+ (self.S[(tt >> 8) & 0xFF] << 8) ^
+ (self.S[(tt >> 16) & 0xFF] << 16) ^
+ (self.S[(tt >> 24) & 0xFF] << 24))
+
+ for i in xrange(KC // 2 + 1, KC):
+ tk[i] ^= tk[i - 1]
+
+ # Copy values into round key arrays
+ j = 0
+ while j < KC and t < round_key_count:
+ self._Ke[t // 4][t % 4] = tk[j]
+ self._Kd[rounds - (t // 4)][t % 4] = tk[j]
+ j += 1
+ t += 1
+
+ # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3)
+ for r in xrange(1, rounds):
+ for j in xrange(0, 4):
+ tt = self._Kd[r][j]
+ self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^
+ self.U2[(tt >> 16) & 0xFF] ^
+ self.U3[(tt >> 8) & 0xFF] ^
+ self.U4[ tt & 0xFF])
+
+ def encrypt(self, plaintext):
+ 'Encrypt a block of plain text using the AES block cipher.'
+
+ if len(plaintext) != 16:
+ raise ValueError('wrong block length')
+
+ rounds = len(self._Ke) - 1
+ (s1, s2, s3) = [1, 2, 3]
+ a = [0, 0, 0, 0]
+
+ # Convert plaintext to (ints ^ key)
+ t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)]
+
+ # Apply round transforms
+ for r in xrange(1, rounds):
+ for i in xrange(0, 4):
+ a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^
+ self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^
+ self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^
+ self.T4[ t[(i + s3) % 4] & 0xFF] ^
+ self._Ke[r][i])
+ t = copy.copy(a)
+
+ # The last round is special
+ result = [ ]
+ for i in xrange(0, 4):
+ tt = self._Ke[rounds][i]
+ result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
+ result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
+ result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
+ result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF)
+
+ return result
+
+ def decrypt(self, ciphertext):
+ 'Decrypt a block of cipher text using the AES block cipher.'
+
+ if len(ciphertext) != 16:
+ raise ValueError('wrong block length')
+
+ rounds = len(self._Kd) - 1
+ (s1, s2, s3) = [3, 2, 1]
+ a = [0, 0, 0, 0]
+
+ # Convert ciphertext to (ints ^ key)
+ t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)]
+
+ # Apply round transforms
+ for r in xrange(1, rounds):
+ for i in xrange(0, 4):
+ a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^
+ self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^
+ self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^
+ self.T8[ t[(i + s3) % 4] & 0xFF] ^
+ self._Kd[r][i])
+ t = copy.copy(a)
+
+ # The last round is special
+ result = [ ]
+ for i in xrange(0, 4):
+ tt = self._Kd[rounds][i]
+ result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
+ result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
+ result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
+ result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF)
+
+ return result
+
+
+class Counter(object):
+ '''A counter object for the Counter (CTR) mode of operation.
+
+ To create a custom counter, you can usually just override the
+ increment method.'''
+
+ def __init__(self, initial_value = 1):
+
+ # Convert the value into an array of bytes long
+ self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ]
+
+ value = property(lambda s: s._counter)
+
+ def increment(self):
+ '''Increment the counter (overflow rolls back to 0).'''
+
+ for i in xrange(len(self._counter) - 1, -1, -1):
+ self._counter[i] += 1
+
+ if self._counter[i] < 256: break
+
+ # Carry the one
+ self._counter[i] = 0
+
+ # Overflow
+ else:
+ self._counter = [ 0 ] * len(self._counter)
+
+
+class AESBlockModeOfOperation(object):
+ '''Super-class for AES modes of operation that require blocks.'''
+ def __init__(self, key):
+ self._aes = AES(key)
+
+ def decrypt(self, ciphertext):
+ raise Exception('not implemented')
+
+ def encrypt(self, plaintext):
+ raise Exception('not implemented')
+
+
+class AESStreamModeOfOperation(AESBlockModeOfOperation):
+ '''Super-class for AES modes of operation that are stream-ciphers.'''
+
+class AESSegmentModeOfOperation(AESStreamModeOfOperation):
+ '''Super-class for AES modes of operation that segment data.'''
+
+ segment_bytes = 16
+
+
+
+class AESModeOfOperationECB(AESBlockModeOfOperation):
+ '''AES Electronic Codebook Mode of Operation.
+
+ o Block-cipher, so data must be padded to 16 byte boundaries
+
+ Security Notes:
+ o This mode is not recommended
+ o Any two identical blocks produce identical encrypted values,
+ exposing data patterns. (See the image of Tux on wikipedia)
+
+ Also see:
+ o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29
+ o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1'''
+
+
+ name = "Electronic Codebook (ECB)"
+
+ def encrypt(self, plaintext):
+ if len(plaintext) != 16:
+ raise ValueError('plaintext block must be 16 bytes')
+
+ plaintext = _string_to_bytes(plaintext)
+ return _bytes_to_string(self._aes.encrypt(plaintext))
+
+ def decrypt(self, ciphertext):
+ if len(ciphertext) != 16:
+ raise ValueError('ciphertext block must be 16 bytes')
+
+ ciphertext = _string_to_bytes(ciphertext)
+ return _bytes_to_string(self._aes.decrypt(ciphertext))
+
+
+
+class AESModeOfOperationCBC(AESBlockModeOfOperation):
+ '''AES Cipher-Block Chaining Mode of Operation.
+
+ o The Initialization Vector (IV)
+ o Block-cipher, so data must be padded to 16 byte boundaries
+ o An incorrect initialization vector will only cause the first
+ block to be corrupt; all other blocks will be intact
+ o A corrupt bit in the cipher text will cause a block to be
+ corrupted, and the next block to be inverted, but all other
+ blocks will be intact.
+
+ Security Notes:
+ o This method (and CTR) ARE recommended.
+
+ Also see:
+ o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29
+ o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2'''
+
+
+ name = "Cipher-Block Chaining (CBC)"
+
+ def __init__(self, key, iv = None):
+ if iv is None:
+ self._last_cipherblock = [ 0 ] * 16
+ elif len(iv) != 16:
+ raise ValueError('initialization vector must be 16 bytes')
+ else:
+ self._last_cipherblock = _string_to_bytes(iv)
+
+ AESBlockModeOfOperation.__init__(self, key)
+
+ def encrypt(self, plaintext):
+ if len(plaintext) != 16:
+ raise ValueError('plaintext block must be 16 bytes')
+
+ plaintext = _string_to_bytes(plaintext)
+ precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ]
+ self._last_cipherblock = self._aes.encrypt(precipherblock)
+
+ return _bytes_to_string(self._last_cipherblock)
+
+ def decrypt(self, ciphertext):
+ if len(ciphertext) != 16:
+ raise ValueError('ciphertext block must be 16 bytes')
+
+ cipherblock = _string_to_bytes(ciphertext)
+ plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ]
+ self._last_cipherblock = cipherblock
+
+ return _bytes_to_string(plaintext)
+
+
+
+class AESModeOfOperationCFB(AESSegmentModeOfOperation):
+ '''AES Cipher Feedback Mode of Operation.
+
+ o A stream-cipher, so input does not need to be padded to blocks,
+ but does need to be padded to segment_size
+
+ Also see:
+ o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29
+ o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3'''
+
+
+ name = "Cipher Feedback (CFB)"
+
+ def __init__(self, key, iv, segment_size = 1):
+ if segment_size == 0: segment_size = 1
+
+ if iv is None:
+ self._shift_register = [ 0 ] * 16
+ elif len(iv) != 16:
+ raise ValueError('initialization vector must be 16 bytes')
+ else:
+ self._shift_register = _string_to_bytes(iv)
+
+ self._segment_bytes = segment_size
+
+ AESBlockModeOfOperation.__init__(self, key)
+
+ segment_bytes = property(lambda s: s._segment_bytes)
+
+ def encrypt(self, plaintext):
+ if len(plaintext) % self._segment_bytes != 0:
+ raise ValueError('plaintext block must be a multiple of segment_size')
+
+ plaintext = _string_to_bytes(plaintext)
+
+ # Break block into segments
+ encrypted = [ ]
+ for i in xrange(0, len(plaintext), self._segment_bytes):
+ plaintext_segment = plaintext[i: i + self._segment_bytes]
+ xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)]
+ cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ]
+
+ # Shift the top bits out and the ciphertext in
+ self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment)
+
+ encrypted.extend(cipher_segment)
+
+ return _bytes_to_string(encrypted)
+
+ def decrypt(self, ciphertext):
+ if len(ciphertext) % self._segment_bytes != 0:
+ raise ValueError('ciphertext block must be a multiple of segment_size')
+
+ ciphertext = _string_to_bytes(ciphertext)
+
+ # Break block into segments
+ decrypted = [ ]
+ for i in xrange(0, len(ciphertext), self._segment_bytes):
+ cipher_segment = ciphertext[i: i + self._segment_bytes]
+ xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)]
+ plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ]
+
+ # Shift the top bits out and the ciphertext in
+ self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment)
+
+ decrypted.extend(plaintext_segment)
+
+ return _bytes_to_string(decrypted)
+
+
+
+class AESModeOfOperationOFB(AESStreamModeOfOperation):
+ '''AES Output Feedback Mode of Operation.
+
+ o A stream-cipher, so input does not need to be padded to blocks,
+ allowing arbitrary length data.
+ o A bit twiddled in the cipher text, twiddles the same bit in the
+ same bit in the plain text, which can be useful for error
+ correction techniques.
+
+ Also see:
+ o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29
+ o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4'''
+
+
+ name = "Output Feedback (OFB)"
+
+ def __init__(self, key, iv = None):
+ if iv is None:
+ self._last_precipherblock = [ 0 ] * 16
+ elif len(iv) != 16:
+ raise ValueError('initialization vector must be 16 bytes')
+ else:
+ self._last_precipherblock = _string_to_bytes(iv)
+
+ self._remaining_block = [ ]
+
+ AESBlockModeOfOperation.__init__(self, key)
+
+ def encrypt(self, plaintext):
+ encrypted = [ ]
+ for p in _string_to_bytes(plaintext):
+ if len(self._remaining_block) == 0:
+ self._remaining_block = self._aes.encrypt(self._last_precipherblock)
+ self._last_precipherblock = [ ]
+ precipherbyte = self._remaining_block.pop(0)
+ self._last_precipherblock.append(precipherbyte)
+ cipherbyte = p ^ precipherbyte
+ encrypted.append(cipherbyte)
+
+ return _bytes_to_string(encrypted)
+
+ def decrypt(self, ciphertext):
+ # AES-OFB is symetric
+ return self.encrypt(ciphertext)
+
+
+
+class AESModeOfOperationCTR(AESStreamModeOfOperation):
+ '''AES Counter Mode of Operation.
+
+ o A stream-cipher, so input does not need to be padded to blocks,
+ allowing arbitrary length data.
+ o The counter must be the same size as the key size (ie. len(key))
+ o Each block independant of the other, so a corrupt byte will not
+ damage future blocks.
+ o Each block has a uniue counter value associated with it, which
+ contributes to the encrypted value, so no data patterns are
+ leaked.
+ o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and
+ Segmented Integer Counter (SIC
+
+ Security Notes:
+ o This method (and CBC) ARE recommended.
+ o Each message block is associated with a counter value which must be
+ unique for ALL messages with the same key. Otherwise security may be
+ compromised.
+
+ Also see:
+
+ o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29
+ o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5
+ and Appendix B for managing the initial counter'''
+
+
+ name = "Counter (CTR)"
+
+ def __init__(self, key, counter = None):
+ AESBlockModeOfOperation.__init__(self, key)
+
+ if counter is None:
+ counter = Counter()
+
+ self._counter = counter
+ self._remaining_counter = [ ]
+
+ def encrypt(self, plaintext):
+ while len(self._remaining_counter) < len(plaintext):
+ self._remaining_counter += self._aes.encrypt(self._counter.value)
+ self._counter.increment()
+
+ plaintext = _string_to_bytes(plaintext)
+
+ encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ]
+ self._remaining_counter = self._remaining_counter[len(encrypted):]
+
+ return _bytes_to_string(encrypted)
+
+ def decrypt(self, crypttext):
+ # AES-CTR is symetric
+ return self.encrypt(crypttext)
+
+
+# Simple lookup table for each mode
+AESModesOfOperation = dict(
+ ctr = AESModeOfOperationCTR,
+ cbc = AESModeOfOperationCBC,
+ cfb = AESModeOfOperationCFB,
+ ecb = AESModeOfOperationECB,
+ ofb = AESModeOfOperationOFB,
+)
diff --git a/gluon/contrib/pyaes/blockfeeder.py b/gluon/contrib/pyaes/blockfeeder.py
new file mode 100644
index 000000000..f4113c37f
--- /dev/null
+++ b/gluon/contrib/pyaes/blockfeeder.py
@@ -0,0 +1,227 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2014 Richard Moore
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation
+from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable
+
+
+# First we inject three functions to each of the modes of operations
+#
+# _can_consume(size)
+# - Given a size, determine how many bytes could be consumed in
+# a single call to either the decrypt or encrypt method
+#
+# _final_encrypt(data, padding = PADDING_DEFAULT)
+# - call and return encrypt on this (last) chunk of data,
+# padding as necessary; this will always be at least 16
+# bytes unless the total incoming input was less than 16
+# bytes
+#
+# _final_decrypt(data, padding = PADDING_DEFAULT)
+# - same as _final_encrypt except for decrypt, for
+# stripping off padding
+#
+
+PADDING_NONE = 'none'
+PADDING_DEFAULT = 'default'
+
+# @TODO: Ciphertext stealing and explicit PKCS#7
+# PADDING_CIPHERTEXT_STEALING
+# PADDING_PKCS7
+
+# ECB and CBC are block-only ciphers
+
+def _block_can_consume(self, size):
+ if size >= 16: return 16
+ return 0
+
+# After padding, we may have more than one block
+def _block_final_encrypt(self, data, padding = PADDING_DEFAULT):
+ if padding == PADDING_DEFAULT:
+ data = append_PKCS7_padding(data)
+
+ elif padding == PADDING_NONE:
+ if len(data) != 16:
+ raise Exception('invalid data length for final block')
+ else:
+ raise Exception('invalid padding option')
+
+ if len(data) == 32:
+ return self.encrypt(data[:16]) + self.encrypt(data[16:])
+
+ return self.encrypt(data)
+
+
+def _block_final_decrypt(self, data, padding = PADDING_DEFAULT):
+ if padding == PADDING_DEFAULT:
+ return strip_PKCS7_padding(self.decrypt(data))
+
+ if padding == PADDING_NONE:
+ if len(data) != 16:
+ raise Exception('invalid data length for final block')
+ return self.decrypt(data)
+
+ raise Exception('invalid padding option')
+
+AESBlockModeOfOperation._can_consume = _block_can_consume
+AESBlockModeOfOperation._final_encrypt = _block_final_encrypt
+AESBlockModeOfOperation._final_decrypt = _block_final_decrypt
+
+
+
+# CFB is a segment cipher
+
+def _segment_can_consume(self, size):
+ return self.segment_bytes * int(size // self.segment_bytes)
+
+# CFB can handle a non-segment-sized block at the end using the remaining cipherblock
+def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT):
+ if padding != PADDING_DEFAULT:
+ raise Exception('invalid padding option')
+
+ faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes)))
+ padded = data + to_bufferable(faux_padding)
+ return self.encrypt(padded)[:len(data)]
+
+# CFB can handle a non-segment-sized block at the end using the remaining cipherblock
+def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT):
+ if padding != PADDING_DEFAULT:
+ raise Exception('invalid padding option')
+
+ faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes)))
+ padded = data + to_bufferable(faux_padding)
+ return self.decrypt(padded)[:len(data)]
+
+AESSegmentModeOfOperation._can_consume = _segment_can_consume
+AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt
+AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt
+
+
+
+# OFB and CTR are stream ciphers
+
+def _stream_can_consume(self, size):
+ return size
+
+def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT):
+ if padding not in [PADDING_NONE, PADDING_DEFAULT]:
+ raise Exception('invalid padding option')
+
+ return self.encrypt(data)
+
+def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT):
+ if padding not in [PADDING_NONE, PADDING_DEFAULT]:
+ raise Exception('invalid padding option')
+
+ return self.decrypt(data)
+
+AESStreamModeOfOperation._can_consume = _stream_can_consume
+AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt
+AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt
+
+
+
+class BlockFeeder(object):
+ '''The super-class for objects to handle chunking a stream of bytes
+ into the appropriate block size for the underlying mode of operation
+ and applying (or stripping) padding, as necessary.'''
+
+ def __init__(self, mode, feed, final, padding = PADDING_DEFAULT):
+ self._mode = mode
+ self._feed = feed
+ self._final = final
+ self._buffer = to_bufferable("")
+ self._padding = padding
+
+ def feed(self, data = None):
+ '''Provide bytes to encrypt (or decrypt), returning any bytes
+ possible from this or any previous calls to feed.
+
+ Call with None or an empty string to flush the mode of
+ operation and return any final bytes; no further calls to
+ feed may be made.'''
+
+ if self._buffer is None:
+ raise ValueError('already finished feeder')
+
+ # Finalize; process the spare bytes we were keeping
+ if not data:
+ result = self._final(self._buffer, self._padding)
+ self._buffer = None
+ return result
+
+ self._buffer += to_bufferable(data)
+
+ # We keep 16 bytes around so we can determine padding
+ result = to_bufferable('')
+ while len(self._buffer) > 16:
+ can_consume = self._mode._can_consume(len(self._buffer) - 16)
+ if can_consume == 0: break
+ result += self._feed(self._buffer[:can_consume])
+ self._buffer = self._buffer[can_consume:]
+
+ return result
+
+
+class Encrypter(BlockFeeder):
+ 'Accepts bytes of plaintext and returns encrypted ciphertext.'
+
+ def __init__(self, mode, padding = PADDING_DEFAULT):
+ BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding)
+
+
+class Decrypter(BlockFeeder):
+ 'Accepts bytes of ciphertext and returns decrypted plaintext.'
+
+ def __init__(self, mode, padding = PADDING_DEFAULT):
+ BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding)
+
+
+# 8kb blocks
+BLOCK_SIZE = (1 << 13)
+
+def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE):
+ 'Uses feeder to read and convert from in_stream and write to out_stream.'
+
+ while True:
+ chunk = in_stream.read(block_size)
+ if not chunk:
+ break
+ converted = feeder.feed(chunk)
+ out_stream.write(converted)
+ converted = feeder.feed()
+ out_stream.write(converted)
+
+
+def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT):
+ 'Encrypts a stream of bytes from in_stream to out_stream using mode.'
+
+ encrypter = Encrypter(mode, padding = padding)
+ _feed_stream(encrypter, in_stream, out_stream, block_size)
+
+
+def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT):
+ 'Decrypts a stream of bytes from in_stream to out_stream using mode.'
+
+ decrypter = Decrypter(mode, padding = padding)
+ _feed_stream(decrypter, in_stream, out_stream, block_size)
diff --git a/gluon/contrib/pyaes/util.py b/gluon/contrib/pyaes/util.py
new file mode 100644
index 000000000..081a37594
--- /dev/null
+++ b/gluon/contrib/pyaes/util.py
@@ -0,0 +1,60 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2014 Richard Moore
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# Why to_bufferable?
+# Python 3 is very different from Python 2.x when it comes to strings of text
+# and strings of bytes; in Python 3, strings of bytes do not exist, instead to
+# represent arbitrary binary data, we must use the "bytes" object. This method
+# ensures the object behaves as we need it to.
+
+def to_bufferable(binary):
+ return binary
+
+def _get_byte(c):
+ return ord(c)
+
+try:
+ xrange
+except:
+
+ def to_bufferable(binary):
+ if isinstance(binary, bytes):
+ return binary
+ return bytes(ord(b) for b in binary)
+
+ def _get_byte(c):
+ return c
+
+def append_PKCS7_padding(data):
+ pad = 16 - (len(data) % 16)
+ return data + to_bufferable(chr(pad) * pad)
+
+def strip_PKCS7_padding(data):
+ if len(data) % 16 != 0:
+ raise ValueError("invalid length")
+
+ pad = _get_byte(data[-1])
+
+ if pad > 16:
+ raise ValueError("invalid padding byte")
+
+ return data[:-pad]
diff --git a/gluon/contrib/pymysql/__init__.py b/gluon/contrib/pymysql/__init__.py
index c1e6d037e..7bf34c1ca 100644
--- a/gluon/contrib/pymysql/__init__.py
+++ b/gluon/contrib/pymysql/__init__.py
@@ -1,7 +1,7 @@
-'''
-PyMySQL: A pure-Python drop-in replacement for MySQLdb.
+"""
+PyMySQL: A pure-Python MySQL client library.
-Copyright (c) 2010 PyMySQL contributors
+Copyright (c) 2010-2016 PyMySQL contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -20,40 +20,32 @@
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-
-'''
-
-VERSION = (0, 5, None)
-
-from constants import FIELD_TYPE
-from converters import escape_dict, escape_sequence, escape_string
-from err import Warning, Error, InterfaceError, DataError, \
- DatabaseError, OperationalError, IntegrityError, InternalError, \
- NotSupportedError, ProgrammingError, MySQLError
-from times import Date, Time, Timestamp, \
- DateFromTicks, TimeFromTicks, TimestampFromTicks
-
+"""
import sys
-try:
- frozenset
-except NameError:
- from sets import ImmutableSet as frozenset
- try:
- from sets import BaseSet as set
- except ImportError:
- from sets import Set as set
+from ._compat import PY2
+from .constants import FIELD_TYPE
+from .converters import escape_dict, escape_sequence, escape_string
+from .err import (
+ Warning, Error, InterfaceError, DataError,
+ DatabaseError, OperationalError, IntegrityError, InternalError,
+ NotSupportedError, ProgrammingError, MySQLError)
+from .times import (
+ Date, Time, Timestamp,
+ DateFromTicks, TimeFromTicks, TimestampFromTicks)
+
+VERSION = (0, 7, 9, None)
threadsafety = 1
apilevel = "2.0"
-paramstyle = "format"
+paramstyle = "pyformat"
-class DBAPISet(frozenset):
+class DBAPISet(frozenset):
def __ne__(self, other):
if isinstance(other, set):
- return super(DBAPISet, self).__ne__(self, other)
+ return frozenset.__ne__(self, other)
else:
return other not in self
@@ -80,40 +72,52 @@ def __hash__(self):
DATETIME = TIMESTAMP
ROWID = DBAPISet()
+
def Binary(x):
"""Return x as a binary type."""
- return str(x)
+ if PY2:
+ return bytearray(x)
+ else:
+ return bytes(x)
+
def Connect(*args, **kwargs):
"""
Connect to the database; see connections.Connection.__init__() for
more information.
"""
- from connections import Connection
+ from .connections import Connection
return Connection(*args, **kwargs)
+from . import connections as _orig_conn
+if _orig_conn.Connection.__init__.__doc__ is not None:
+ Connect.__doc__ = _orig_conn.Connection.__init__.__doc__
+del _orig_conn
+
+
def get_client_info(): # for MySQLdb compatibility
- return '%s.%s.%s' % VERSION
+ return '.'.join(map(str, VERSION))
connect = Connection = Connect
# we include a doctored version_info here for MySQLdb compatibility
-version_info = (1,2,2,"final",0)
+version_info = (1,2,6,"final",0)
NULL = "NULL"
__version__ = get_client_info()
def thread_safe():
- return True # match MySQLdb.thread_safe()
+ return True # match MySQLdb.thread_safe()
def install_as_MySQLdb():
"""
After this function is called, any application that imports MySQLdb or
- _mysql will unwittingly actually use
+ _mysql will unwittingly actually use
"""
sys.modules["MySQLdb"] = sys.modules["_mysql"] = sys.modules["pymysql"]
+
__all__ = [
'BINARY', 'Binary', 'Connect', 'Connection', 'DATE', 'Date',
'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks', 'TimestampFromTicks',
@@ -126,6 +130,5 @@ def install_as_MySQLdb():
'paramstyle', 'threadsafety', 'version_info',
"install_as_MySQLdb",
-
- "NULL","__version__",
- ]
+ "NULL", "__version__",
+]
diff --git a/gluon/contrib/pymysql/_compat.py b/gluon/contrib/pymysql/_compat.py
new file mode 100755
index 000000000..252789ec4
--- /dev/null
+++ b/gluon/contrib/pymysql/_compat.py
@@ -0,0 +1,21 @@
+import sys
+
+PY2 = sys.version_info[0] == 2
+PYPY = hasattr(sys, 'pypy_translation_info')
+JYTHON = sys.platform.startswith('java')
+IRONPYTHON = sys.platform == 'cli'
+CPYTHON = not PYPY and not JYTHON and not IRONPYTHON
+
+if PY2:
+ import __builtin__
+ range_type = xrange
+ text_type = unicode
+ long_type = long
+ str_type = basestring
+ unichr = __builtin__.unichr
+else:
+ range_type = range
+ text_type = str
+ long_type = int
+ str_type = str
+ unichr = chr
diff --git a/gluon/contrib/pymysql/_socketio.py b/gluon/contrib/pymysql/_socketio.py
new file mode 100755
index 000000000..6a11d42e4
--- /dev/null
+++ b/gluon/contrib/pymysql/_socketio.py
@@ -0,0 +1,134 @@
+"""
+SocketIO imported from socket module in Python 3.
+
+Copyright (c) 2001-2013 Python Software Foundation; All Rights Reserved.
+"""
+
+from socket import *
+import io
+import errno
+
+__all__ = ['SocketIO']
+
+EINTR = errno.EINTR
+_blocking_errnos = (errno.EAGAIN, errno.EWOULDBLOCK)
+
+class SocketIO(io.RawIOBase):
+
+ """Raw I/O implementation for stream sockets.
+
+ This class supports the makefile() method on sockets. It provides
+ the raw I/O interface on top of a socket object.
+ """
+
+ # One might wonder why not let FileIO do the job instead. There are two
+ # main reasons why FileIO is not adapted:
+ # - it wouldn't work under Windows (where you can't used read() and
+ # write() on a socket handle)
+ # - it wouldn't work with socket timeouts (FileIO would ignore the
+ # timeout and consider the socket non-blocking)
+
+ # XXX More docs
+
+ def __init__(self, sock, mode):
+ if mode not in ("r", "w", "rw", "rb", "wb", "rwb"):
+ raise ValueError("invalid mode: %r" % mode)
+ io.RawIOBase.__init__(self)
+ self._sock = sock
+ if "b" not in mode:
+ mode += "b"
+ self._mode = mode
+ self._reading = "r" in mode
+ self._writing = "w" in mode
+ self._timeout_occurred = False
+
+ def readinto(self, b):
+ """Read up to len(b) bytes into the writable buffer *b* and return
+ the number of bytes read. If the socket is non-blocking and no bytes
+ are available, None is returned.
+
+ If *b* is non-empty, a 0 return value indicates that the connection
+ was shutdown at the other end.
+ """
+ self._checkClosed()
+ self._checkReadable()
+ if self._timeout_occurred:
+ raise IOError("cannot read from timed out object")
+ while True:
+ try:
+ return self._sock.recv_into(b)
+ except timeout:
+ self._timeout_occurred = True
+ raise
+ except error as e:
+ n = e.args[0]
+ if n == EINTR:
+ continue
+ if n in _blocking_errnos:
+ return None
+ raise
+
+ def write(self, b):
+ """Write the given bytes or bytearray object *b* to the socket
+ and return the number of bytes written. This can be less than
+ len(b) if not all data could be written. If the socket is
+ non-blocking and no bytes could be written None is returned.
+ """
+ self._checkClosed()
+ self._checkWritable()
+ try:
+ return self._sock.send(b)
+ except error as e:
+ # XXX what about EINTR?
+ if e.args[0] in _blocking_errnos:
+ return None
+ raise
+
+ def readable(self):
+ """True if the SocketIO is open for reading.
+ """
+ if self.closed:
+ raise ValueError("I/O operation on closed socket.")
+ return self._reading
+
+ def writable(self):
+ """True if the SocketIO is open for writing.
+ """
+ if self.closed:
+ raise ValueError("I/O operation on closed socket.")
+ return self._writing
+
+ def seekable(self):
+ """True if the SocketIO is open for seeking.
+ """
+ if self.closed:
+ raise ValueError("I/O operation on closed socket.")
+ return super().seekable()
+
+ def fileno(self):
+ """Return the file descriptor of the underlying socket.
+ """
+ self._checkClosed()
+ return self._sock.fileno()
+
+ @property
+ def name(self):
+ if not self.closed:
+ return self.fileno()
+ else:
+ return -1
+
+ @property
+ def mode(self):
+ return self._mode
+
+ def close(self):
+ """Close the SocketIO object. This doesn't close the underlying
+ socket, except if all references to it have disappeared.
+ """
+ if self.closed:
+ return
+ io.RawIOBase.close(self)
+ self._sock._decref_socketios()
+ self._sock = None
+
diff --git a/gluon/contrib/pymysql/charset.py b/gluon/contrib/pymysql/charset.py
index 507fd07de..968376cfa 100644
--- a/gluon/contrib/pymysql/charset.py
+++ b/gluon/contrib/pymysql/charset.py
@@ -5,11 +5,28 @@
91:2
}
-class Charset:
+
+class Charset(object):
def __init__(self, id, name, collation, is_default):
self.id, self.name, self.collation = id, name, collation
self.is_default = is_default == 'Yes'
+ def __repr__(self):
+ return "Charset(id=%s, name=%r, collation=%r)" % (
+ self.id, self.name, self.collation)
+
+ @property
+ def encoding(self):
+ name = self.name
+ if name == 'utf8mb4':
+ return 'utf8'
+ return name
+
+ @property
+ def is_binary(self):
+ return self.id == 63
+
+
class Charsets:
def __init__(self):
self._by_id = {}
@@ -21,6 +38,7 @@ def by_id(self, id):
return self._by_id[id]
def by_name(self, name):
+ name = name.lower()
for c in self._by_id.values():
if c.name == name and c.is_default:
return c
@@ -92,13 +110,11 @@ def by_name(self, name):
_charsets.add(Charset(53, 'macroman', 'macroman_bin', ''))
_charsets.add(Charset(54, 'utf16', 'utf16_general_ci', 'Yes'))
_charsets.add(Charset(55, 'utf16', 'utf16_bin', ''))
-_charsets.add(Charset(56, 'utf16le', 'utf16le_general_ci', 'Yes'))
_charsets.add(Charset(57, 'cp1256', 'cp1256_general_ci', 'Yes'))
_charsets.add(Charset(58, 'cp1257', 'cp1257_bin', ''))
_charsets.add(Charset(59, 'cp1257', 'cp1257_general_ci', 'Yes'))
_charsets.add(Charset(60, 'utf32', 'utf32_general_ci', 'Yes'))
_charsets.add(Charset(61, 'utf32', 'utf32_bin', ''))
-_charsets.add(Charset(62, 'utf16le', 'utf16le_bin', ''))
_charsets.add(Charset(63, 'binary', 'binary', 'Yes'))
_charsets.add(Charset(64, 'armscii8', 'armscii8_bin', ''))
_charsets.add(Charset(65, 'ascii', 'ascii_bin', ''))
@@ -155,10 +171,6 @@ def by_name(self, name):
_charsets.add(Charset(118, 'utf16', 'utf16_esperanto_ci', ''))
_charsets.add(Charset(119, 'utf16', 'utf16_hungarian_ci', ''))
_charsets.add(Charset(120, 'utf16', 'utf16_sinhala_ci', ''))
-_charsets.add(Charset(121, 'utf16', 'utf16_german2_ci', ''))
-_charsets.add(Charset(122, 'utf16', 'utf16_croatian_ci', ''))
-_charsets.add(Charset(123, 'utf16', 'utf16_unicode_520_ci', ''))
-_charsets.add(Charset(124, 'utf16', 'utf16_vietnamese_ci', ''))
_charsets.add(Charset(128, 'ucs2', 'ucs2_unicode_ci', ''))
_charsets.add(Charset(129, 'ucs2', 'ucs2_icelandic_ci', ''))
_charsets.add(Charset(130, 'ucs2', 'ucs2_latvian_ci', ''))
@@ -179,10 +191,6 @@ def by_name(self, name):
_charsets.add(Charset(145, 'ucs2', 'ucs2_esperanto_ci', ''))
_charsets.add(Charset(146, 'ucs2', 'ucs2_hungarian_ci', ''))
_charsets.add(Charset(147, 'ucs2', 'ucs2_sinhala_ci', ''))
-_charsets.add(Charset(148, 'ucs2', 'ucs2_german2_ci', ''))
-_charsets.add(Charset(149, 'ucs2', 'ucs2_croatian_ci', ''))
-_charsets.add(Charset(150, 'ucs2', 'ucs2_unicode_520_ci', ''))
-_charsets.add(Charset(151, 'ucs2', 'ucs2_vietnamese_ci', ''))
_charsets.add(Charset(159, 'ucs2', 'ucs2_general_mysql500_ci', ''))
_charsets.add(Charset(160, 'utf32', 'utf32_unicode_ci', ''))
_charsets.add(Charset(161, 'utf32', 'utf32_icelandic_ci', ''))
@@ -204,10 +212,6 @@ def by_name(self, name):
_charsets.add(Charset(177, 'utf32', 'utf32_esperanto_ci', ''))
_charsets.add(Charset(178, 'utf32', 'utf32_hungarian_ci', ''))
_charsets.add(Charset(179, 'utf32', 'utf32_sinhala_ci', ''))
-_charsets.add(Charset(180, 'utf32', 'utf32_german2_ci', ''))
-_charsets.add(Charset(181, 'utf32', 'utf32_croatian_ci', ''))
-_charsets.add(Charset(182, 'utf32', 'utf32_unicode_520_ci', ''))
-_charsets.add(Charset(183, 'utf32', 'utf32_vietnamese_ci', ''))
_charsets.add(Charset(192, 'utf8', 'utf8_unicode_ci', ''))
_charsets.add(Charset(193, 'utf8', 'utf8_icelandic_ci', ''))
_charsets.add(Charset(194, 'utf8', 'utf8_latvian_ci', ''))
@@ -228,10 +232,6 @@ def by_name(self, name):
_charsets.add(Charset(209, 'utf8', 'utf8_esperanto_ci', ''))
_charsets.add(Charset(210, 'utf8', 'utf8_hungarian_ci', ''))
_charsets.add(Charset(211, 'utf8', 'utf8_sinhala_ci', ''))
-_charsets.add(Charset(212, 'utf8', 'utf8_german2_ci', ''))
-_charsets.add(Charset(213, 'utf8', 'utf8_croatian_ci', ''))
-_charsets.add(Charset(214, 'utf8', 'utf8_unicode_520_ci', ''))
-_charsets.add(Charset(215, 'utf8', 'utf8_vietnamese_ci', ''))
_charsets.add(Charset(223, 'utf8', 'utf8_general_mysql500_ci', ''))
_charsets.add(Charset(224, 'utf8mb4', 'utf8mb4_unicode_ci', ''))
_charsets.add(Charset(225, 'utf8mb4', 'utf8mb4_icelandic_ci', ''))
@@ -258,9 +258,13 @@ def by_name(self, name):
_charsets.add(Charset(246, 'utf8mb4', 'utf8mb4_unicode_520_ci', ''))
_charsets.add(Charset(247, 'utf8mb4', 'utf8mb4_vietnamese_ci', ''))
-def charset_by_name(name):
- return _charsets.by_name(name)
-def charset_by_id(id):
- return _charsets.by_id(id)
+charset_by_name = _charsets.by_name
+charset_by_id = _charsets.by_id
+
+def charset_to_encoding(name):
+ """Convert MySQL's charset name to Python's codec name"""
+ if name == 'utf8mb4':
+ return 'utf8'
+ return name
diff --git a/gluon/contrib/pymysql/connections.py b/gluon/contrib/pymysql/connections.py
index e4f91330a..d5e39a1cb 100644
--- a/gluon/contrib/pymysql/connections.py
+++ b/gluon/contrib/pymysql/connections.py
@@ -1,91 +1,141 @@
# Python implementation of the MySQL client-server protocol
-# http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol
+# http://dev.mysql.com/doc/internals/en/client-server-protocol.html
+# Error codes:
+# http://dev.mysql.com/doc/refman/5.5/en/error-messages-client.html
+from __future__ import print_function
+from ._compat import PY2, range_type, text_type, str_type, JYTHON, IRONPYTHON
+
+import errno
+from functools import partial
+import hashlib
+import io
+import os
+import socket
+import struct
+import sys
+import traceback
+import warnings
-try:
- import hashlib
- sha_new = lambda *args, **kwargs: hashlib.new("sha1", *args, **kwargs)
-except ImportError:
- import sha
- sha_new = sha.new
+from .charset import MBLENGTH, charset_by_name, charset_by_id
+from .constants import CLIENT, COMMAND, FIELD_TYPE, SERVER_STATUS
+from .converters import escape_item, escape_string, through, conversions as _conv
+from .cursors import Cursor
+from .optionfile import Parser
+from .util import byte2int, int2byte
+from . import err
-import socket
try:
import ssl
SSL_ENABLED = True
except ImportError:
+ ssl = None
SSL_ENABLED = False
-import struct
-import sys
-import os
-import ConfigParser
-
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-
try:
import getpass
DEFAULT_USER = getpass.getuser()
-except ImportError:
+ del getpass
+except (ImportError, KeyError):
+ # KeyError occurs when there's no entry in OS database for a current user.
DEFAULT_USER = None
-from charset import MBLENGTH, charset_by_name, charset_by_id
-from cursors import Cursor
-from constants import FIELD_TYPE, FLAG
-from constants import SERVER_STATUS
-from constants.CLIENT import *
-from constants.COMMAND import *
-from util import join_bytes, byte2int, int2byte
-from converters import escape_item, encoders, decoders
-from err import raise_mysql_exception, Warning, Error, \
- InterfaceError, DataError, DatabaseError, OperationalError, \
- IntegrityError, InternalError, NotSupportedError, ProgrammingError
DEBUG = False
+_py_version = sys.version_info[:2]
+
+
+# socket.makefile() in Python 2 is not usable because very inefficient and
+# bad behavior about timeout.
+# XXX: ._socketio doesn't work under IronPython.
+if _py_version == (2, 7) and not IRONPYTHON:
+ # read method of file-like returned by sock.makefile() is very slow.
+ # So we copy io-based one from Python 3.
+ from ._socketio import SocketIO
+
+ def _makefile(sock, mode):
+ return io.BufferedReader(SocketIO(sock, mode))
+elif _py_version == (2, 6):
+ # Python 2.6 doesn't have fast io module.
+ # So we make original one.
+ class SockFile(object):
+ def __init__(self, sock):
+ self._sock = sock
+
+ def read(self, n):
+ read = self._sock.recv(n)
+ if len(read) == n:
+ return read
+ while True:
+ data = self._sock.recv(n-len(read))
+ if not data:
+ return read
+ read += data
+ if len(read) == n:
+ return read
+
+ def _makefile(sock, mode):
+ assert mode == 'rb'
+ return SockFile(sock)
+else:
+ # socket.makefile in Python 3 is nice.
+ def _makefile(sock, mode):
+ return sock.makefile(mode)
+
+
+TEXT_TYPES = set([
+ FIELD_TYPE.BIT,
+ FIELD_TYPE.BLOB,
+ FIELD_TYPE.LONG_BLOB,
+ FIELD_TYPE.MEDIUM_BLOB,
+ FIELD_TYPE.STRING,
+ FIELD_TYPE.TINY_BLOB,
+ FIELD_TYPE.VAR_STRING,
+ FIELD_TYPE.VARCHAR,
+ FIELD_TYPE.GEOMETRY])
+
+sha_new = partial(hashlib.new, 'sha1')
+
NULL_COLUMN = 251
UNSIGNED_CHAR_COLUMN = 251
UNSIGNED_SHORT_COLUMN = 252
UNSIGNED_INT24_COLUMN = 253
UNSIGNED_INT64_COLUMN = 254
-UNSIGNED_CHAR_LENGTH = 1
-UNSIGNED_SHORT_LENGTH = 2
-UNSIGNED_INT24_LENGTH = 3
-UNSIGNED_INT64_LENGTH = 8
DEFAULT_CHARSET = 'latin1'
+MAX_PACKET_LEN = 2**24-1
-def dump_packet(data):
-
+
+def dump_packet(data): # pragma: no cover
def is_ascii(data):
- if byte2int(data) >= 65 and byte2int(data) <= 122: #data.isalnum():
+ if 65 <= byte2int(data) <= 122:
+ if isinstance(data, int):
+ return chr(data)
return data
return '.'
-
+
try:
- print "packet length %d" % len(data)
- print "method call[1]: %s" % sys._getframe(1).f_code.co_name
- print "method call[2]: %s" % sys._getframe(2).f_code.co_name
- print "method call[3]: %s" % sys._getframe(3).f_code.co_name
- print "method call[4]: %s" % sys._getframe(4).f_code.co_name
- print "method call[5]: %s" % sys._getframe(5).f_code.co_name
- print "-" * 88
- except ValueError: pass
- dump_data = [data[i:i+16] for i in xrange(len(data)) if i%16 == 0]
+ print("packet length:", len(data))
+ for i in range(1, 6):
+ f = sys._getframe(i)
+ print("call[%d]: %s (line %d)" % (i, f.f_code.co_name, f.f_lineno))
+ print("-" * 66)
+ except ValueError:
+ pass
+ dump_data = [data[i:i+16] for i in range_type(0, min(len(data), 256), 16)]
for d in dump_data:
- print ' '.join(map(lambda x:"%02X" % byte2int(x), d)) + \
- ' ' * (16 - len(d)) + ' ' * 2 + \
- ' '.join(map(lambda x:"%s" % is_ascii(x), d))
- print "-" * 88
- print ""
+ print(' '.join(map(lambda x: "{:02X}".format(byte2int(x)), d)) +
+ ' ' * (16 - len(d)) + ' ' * 2 +
+ ''.join(map(lambda x: "{}".format(is_ascii(x)), d)))
+ print("-" * 66)
+ print()
+
def _scramble(password, message):
- if password == None or len(password) == 0:
- return int2byte(0)
- if DEBUG: print 'password=' + password
+ if not password:
+ return b''
+ if DEBUG: print('password=' + str(password))
stage1 = sha_new(password).digest()
stage2 = sha_new(stage1).digest()
s = sha_new()
@@ -94,11 +144,12 @@ def _scramble(password, message):
result = s.digest()
return _my_crypt(result, stage1)
+
def _my_crypt(message1, message2):
length = len(message1)
- result = struct.pack('B', length)
- for i in xrange(length):
- x = (struct.unpack('B', message1[i:i+1])[0] ^ \
+ result = b''
+ for i in range_type(length):
+ x = (struct.unpack('B', message1[i:i+1])[0] ^
struct.unpack('B', message2[i:i+1])[0])
result += struct.pack('B', x)
return result
@@ -106,17 +157,19 @@ def _my_crypt(message1, message2):
# old_passwords support ported from libmysql/password.c
SCRAMBLE_LENGTH_323 = 8
+
class RandStruct_323(object):
def __init__(self, seed1, seed2):
- self.max_value = 0x3FFFFFFFL
+ self.max_value = 0x3FFFFFFF
self.seed1 = seed1 % self.max_value
self.seed2 = seed2 % self.max_value
def my_rnd(self):
- self.seed1 = (self.seed1 * 3L + self.seed2) % self.max_value
- self.seed2 = (self.seed1 + self.seed2 + 33L) % self.max_value
+ self.seed1 = (self.seed1 * 3 + self.seed2) % self.max_value
+ self.seed2 = (self.seed1 + self.seed2 + 33) % self.max_value
return float(self.seed1) / float(self.max_value)
+
def _scramble_323(password, message):
hash_pass = _hash_password_323(password)
hash_message = _hash_password_323(message[:SCRAMBLE_LENGTH_323])
@@ -125,286 +178,273 @@ def _scramble_323(password, message):
rand_st = RandStruct_323(hash_pass_n[0] ^ hash_message_n[0],
hash_pass_n[1] ^ hash_message_n[1])
- outbuf = StringIO.StringIO()
- for _ in xrange(min(SCRAMBLE_LENGTH_323, len(message))):
+ outbuf = io.BytesIO()
+ for _ in range_type(min(SCRAMBLE_LENGTH_323, len(message))):
outbuf.write(int2byte(int(rand_st.my_rnd() * 31) + 64))
extra = int2byte(int(rand_st.my_rnd() * 31))
out = outbuf.getvalue()
- outbuf = StringIO.StringIO()
+ outbuf = io.BytesIO()
for c in out:
outbuf.write(int2byte(byte2int(c) ^ byte2int(extra)))
return outbuf.getvalue()
-def _hash_password_323(password):
- nr = 1345345333L
- add = 7L
- nr2 = 0x12345671L
-
- for c in [byte2int(x) for x in password if x not in (' ', '\t')]:
- nr^= (((nr & 63)+add)*c)+ (nr << 8) & 0xFFFFFFFF
- nr2= (nr2 + ((nr2 << 8) ^ nr)) & 0xFFFFFFFF
- add= (add + c) & 0xFFFFFFFF
-
- r1 = nr & ((1L << 31) - 1L) # kill sign bits
- r2 = nr2 & ((1L << 31) - 1L)
- # pack
+def _hash_password_323(password):
+ nr = 1345345333
+ add = 7
+ nr2 = 0x12345671
+
+ # x in py3 is numbers, p27 is chars
+ for c in [byte2int(x) for x in password if x not in (' ', '\t', 32, 9)]:
+ nr ^= (((nr & 63) + add) * c) + (nr << 8) & 0xFFFFFFFF
+ nr2 = (nr2 + ((nr2 << 8) ^ nr)) & 0xFFFFFFFF
+ add = (add + c) & 0xFFFFFFFF
+
+ r1 = nr & ((1 << 31) - 1) # kill sign bits
+ r2 = nr2 & ((1 << 31) - 1)
return struct.pack(">LL", r1, r2)
-def pack_int24(n):
- return struct.pack('BBB', n&0xFF, (n>>8)&0xFF, (n>>16)&0xFF)
-
-def unpack_uint16(n):
- return struct.unpack(' len(self._data):
+ raise Exception('Invalid advance amount (%s) for cursor. '
+ 'Position=%s' % (length, new_position))
+ self._position = new_position
+
+ def rewind(self, position=0):
+ """Set the position of the data buffer cursor to 'position'."""
+ if position < 0 or position > len(self._data):
+ raise Exception("Invalid position to rewind cursor to: %s." % position)
+ self._position = position
+
+ def get_bytes(self, position, length=1):
+ """Get 'length' bytes starting at 'position'.
+
+ Position is start of payload (first four packet header bytes are not
+ included) starting at index '0'.
+
+ No error checking is done. If requesting outside end of buffer
+ an empty string (or string shorter than 'length') may be returned!
+ """
+ return self._data[position:(position+length)]
- def advance(self, length):
- """Advance the cursor in data buffer 'length' bytes."""
- new_position = self.__position + length
- if new_position < 0 or new_position > len(self.__data):
- raise Exception('Invalid advance amount (%s) for cursor. '
- 'Position=%s' % (length, new_position))
- self.__position = new_position
-
- def rewind(self, position=0):
- """Set the position of the data buffer cursor to 'position'."""
- if position < 0 or position > len(self.__data):
- raise Exception("Invalid position to rewind cursor to: %s." % position)
- self.__position = position
-
- def peek(self, size):
- """Look at the first 'size' bytes in packet without moving cursor."""
- result = self.__data[self.__position:(self.__position+size)]
- if len(result) != size:
- error = ('Result length not requested length:\n'
- 'Expected=%s. Actual=%s. Position: %s. Data Length: %s'
- % (size, len(result), self.__position, len(self.__data)))
- if DEBUG:
- print error
- self.dump()
- raise AssertionError(error)
- return result
+ if PY2:
+ def read_uint8(self):
+ result = ord(self._data[self._position])
+ self._position += 1
+ return result
+ else:
+ def read_uint8(self):
+ result = self._data[self._position]
+ self._position += 1
+ return result
+
+ def read_uint16(self):
+ result = struct.unpack_from('= 7
- No error checking is done. If requesting outside end of buffer
- an empty string (or string shorter than 'length') may be returned!
- """
- return self.__data[position:(position+length)]
+ def is_eof_packet(self):
+ # http://dev.mysql.com/doc/internals/en/generic-response-packets.html#packet-EOF_Packet
+ # Caution: \xFE may be LengthEncodedInteger.
+ # If \xFE is LengthEncodedInteger header, 8bytes followed.
+ return self._data[0:1] == b'\xfe' and len(self._data) < 9
- def read_length_coded_binary(self):
- """Read a 'Length Coded Binary' number from the data buffer.
+ def is_auth_switch_request(self):
+ # http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
+ return self._data[0:1] == b'\xfe'
- Length coded numbers can be anywhere from 1 to 9 bytes depending
- on the value of the first byte.
- """
- c = byte2int(self.read(1))
- if c == NULL_COLUMN:
- return None
- if c < UNSIGNED_CHAR_COLUMN:
- return c
- elif c == UNSIGNED_SHORT_COLUMN:
- return unpack_uint16(self.read(UNSIGNED_SHORT_LENGTH))
- elif c == UNSIGNED_INT24_COLUMN:
- return unpack_int24(self.read(UNSIGNED_INT24_LENGTH))
- elif c == UNSIGNED_INT64_COLUMN:
- # TODO: what was 'longlong'? confirm it wasn't used?
- return unpack_int64(self.read(UNSIGNED_INT64_LENGTH))
-
- def read_length_coded_string(self):
- """Read a 'Length Coded String' from the data buffer.
-
- A 'Length Coded String' consists first of a length coded
- (unsigned, positive) integer represented in 1-9 bytes followed by
- that many bytes of binary data. (For example "cat" would be "3cat".)
- """
- length = self.read_length_coded_binary()
- if length is None:
- return None
- return self.read(length)
+ def is_resultset_packet(self):
+ field_count = ord(self._data[0:1])
+ return 1 <= field_count <= 250
- def is_ok_packet(self):
- return byte2int(self.get_bytes(0)) == 0
+ def is_load_local_packet(self):
+ return self._data[0:1] == b'\xfb'
- def is_eof_packet(self):
- return byte2int(self.get_bytes(0)) == 254 # 'fe'
+ def is_error_packet(self):
+ return self._data[0:1] == b'\xff'
- def is_resultset_packet(self):
- field_count = byte2int(self.get_bytes(0))
- return field_count >= 1 and field_count <= 250
+ def check_error(self):
+ if self.is_error_packet():
+ self.rewind()
+ self.advance(1) # field_count == error (we already know that)
+ errno = self.read_uint16()
+ if DEBUG: print("errno =", errno)
+ err.raise_mysql_exception(self._data)
- def is_error_packet(self):
- return byte2int(self.get_bytes(0)) == 255
-
- def check_error(self):
- if self.is_error_packet():
- self.rewind()
- self.advance(1) # field_count == error (we already know that)
- errno = unpack_uint16(self.read(2))
- if DEBUG: print "errno = %d" % errno
- raise_mysql_exception(self.__data)
-
- def dump(self):
- dump_packet(self.__data)
+ def dump(self):
+ dump_packet(self._data)
class FieldDescriptorPacket(MysqlPacket):
- """A MysqlPacket that represents a specific column's metadata in the result.
+ """A MysqlPacket that represents a specific column's metadata in the result.
- Parsing is automatically done and the results are exported via public
- attributes on the class such as: db, table_name, name, length, type_code.
- """
+ Parsing is automatically done and the results are exported via public
+ attributes on the class such as: db, table_name, name, length, type_code.
+ """
- def __init__(self, *args):
- MysqlPacket.__init__(self, *args)
- self.__parse_field_descriptor()
+ def __init__(self, data, encoding):
+ MysqlPacket.__init__(self, data, encoding)
+ self._parse_field_descriptor(encoding)
- def __parse_field_descriptor(self):
- """Parse the 'Field Descriptor' (Metadata) packet.
+ def _parse_field_descriptor(self, encoding):
+ """Parse the 'Field Descriptor' (Metadata) packet.
- This is compatible with MySQL 4.1+ (not compatible with MySQL 4.0).
- """
- self.catalog = self.read_length_coded_string()
- self.db = self.read_length_coded_string()
- self.table_name = self.read_length_coded_string()
- self.org_table = self.read_length_coded_string()
- self.name = self.read_length_coded_string().decode(self.connection.charset)
- self.org_name = self.read_length_coded_string()
- self.advance(1) # non-null filler
- self.charsetnr = struct.unpack(' 2:
use_unicode = True
+ if db is not None and database is None:
+ database = db
+ if passwd is not None and not password:
+ password = passwd
+
if compress or named_pipe:
- raise NotImplementedError, "compress and named_pipe arguments are not supported"
+ raise NotImplementedError("compress and named_pipe arguments are not supported")
- if ssl and (ssl.has_key('capath') or ssl.has_key('cipher')):
- raise NotImplementedError, 'ssl options capath and cipher are not supported'
+ if local_infile:
+ client_flag |= CLIENT.LOCAL_FILES
self.ssl = False
if ssl:
if not SSL_ENABLED:
- raise NotImplementedError, "ssl module not found"
+ raise NotImplementedError("ssl module not found")
self.ssl = True
- client_flag |= SSL
- for k in ('key', 'cert', 'ca'):
- v = None
- if ssl.has_key(k):
- v = ssl[k]
- setattr(self, k, v)
+ client_flag |= CLIENT.SSL
+ self.ctx = self._create_ssl_ctx(ssl)
if read_default_group and not read_default_file:
if sys.platform.startswith("win"):
@@ -530,29 +615,37 @@ def __init__(self, host="localhost", user=None, passwd="",
if not read_default_group:
read_default_group = "client"
- cfg = ConfigParser.RawConfigParser()
+ cfg = Parser()
cfg.read(os.path.expanduser(read_default_file))
- def _config(key, default):
+ def _config(key, arg):
+ if arg:
+ return arg
try:
- return cfg.get(read_default_group,key)
- except:
- return default
+ return cfg.get(read_default_group, key)
+ except Exception:
+ return arg
- user = _config("user",user)
- passwd = _config("password",passwd)
+ user = _config("user", user)
+ password = _config("password", password)
host = _config("host", host)
- db = _config("db",db)
- unix_socket = _config("socket",unix_socket)
+ database = _config("database", database)
+ unix_socket = _config("socket", unix_socket)
port = int(_config("port", port))
charset = _config("default-character-set", charset)
- self.host = host
- self.port = port
+ self.host = host or "localhost"
+ self.port = port or 3306
self.user = user or DEFAULT_USER
- self.password = passwd
- self.db = db
+ self.password = password or ""
+ self.db = database
self.unix_socket = unix_socket
+ if read_timeout is not None and read_timeout <= 0:
+ raise ValueError("read_timeout should be >= 0")
+ self._read_timeout = read_timeout
+ if write_timeout is not None and write_timeout <= 0:
+ raise ValueError("write_timeout should be >= 0")
+ self._write_timeout = write_timeout
if charset:
self.charset = charset
self.use_unicode = True
@@ -563,102 +656,167 @@ def _config(key, default):
if use_unicode is not None:
self.use_unicode = use_unicode
- client_flag |= CAPABILITIES
- client_flag |= MULTI_STATEMENTS
+ self.encoding = charset_by_name(self.charset).encoding
+
+ client_flag |= CLIENT.CAPABILITIES
if self.db:
- client_flag |= CONNECT_WITH_DB
+ client_flag |= CLIENT.CONNECT_WITH_DB
self.client_flag = client_flag
self.cursorclass = cursorclass
self.connect_timeout = connect_timeout
- self._connect()
-
self._result = None
self._affected_rows = 0
self.host_info = "Not connected"
- self.messages = []
- self.set_charset(charset)
- self.encoders = encoders
- self.decoders = conv
-
- self.autocommit(False)
-
- if sql_mode is not None:
- c = self.cursor()
- c.execute("SET sql_mode=%s", (sql_mode,))
-
- self.commit()
+ #: specified autocommit mode. None means use server default.
+ self.autocommit_mode = autocommit
+
+ if conv is None:
+ conv = _conv
+ # Need for MySQLdb compatibility.
+ self.encoders = dict([(k, v) for (k, v) in conv.items() if type(k) is not int])
+ self.decoders = dict([(k, v) for (k, v) in conv.items() if type(k) is int])
+ self.sql_mode = sql_mode
+ self.init_command = init_command
+ self.max_allowed_packet = max_allowed_packet
+ self._auth_plugin_map = auth_plugin_map
+ if defer_connect:
+ self._sock = None
+ else:
+ self.connect()
+
+ def _create_ssl_ctx(self, sslp):
+ if isinstance(sslp, ssl.SSLContext):
+ return sslp
+ ca = sslp.get('ca')
+ capath = sslp.get('capath')
+ hasnoca = ca is None and capath is None
+ ctx = ssl.create_default_context(cafile=ca, capath=capath)
+ ctx.check_hostname = not hasnoca and sslp.get('check_hostname', True)
+ ctx.verify_mode = ssl.CERT_NONE if hasnoca else ssl.CERT_REQUIRED
+ if 'cert' in sslp:
+ ctx.load_cert_chain(sslp['cert'], keyfile=sslp.get('key'))
+ if 'cipher' in sslp:
+ ctx.set_ciphers(sslp['cipher'])
+ ctx.options |= ssl.OP_NO_SSLv2
+ ctx.options |= ssl.OP_NO_SSLv3
+ return ctx
- if init_command is not None:
- c = self.cursor()
- c.execute(init_command)
+ def close(self):
+ """Send the quit message and close the socket"""
+ if self._sock is None:
+ raise err.Error("Already closed")
+ send_data = struct.pack('= 5:
+ self.client_flag |= CLIENT.MULTI_RESULTS
if self.user is None:
- raise ValueError, "Did not specify a username"
+ raise ValueError("Did not specify a username")
charset_id = charset_by_name(self.charset).id
- self.user = self.user.encode(self.charset)
-
- data_init = struct.pack('=5.0)
+ data += authresp + b'\0'
+
+ if self.db and self.server_capabilities & CLIENT.CONNECT_WITH_DB:
+ if isinstance(self.db, text_type):
+ self.db = self.db.encode(self.encoding)
+ data += self.db + b'\0'
+
+ if self.server_capabilities & CLIENT.PLUGIN_AUTH:
+ name = self._auth_plugin_name
+ if isinstance(name, text_type):
+ name = name.encode('ascii')
+ data += name + b'\0'
+
+ self.write_packet(data)
+ auth_packet = self._read_packet()
+
+ # if authentication method isn't accepted the first byte
+ # will have the octet 254
+ if auth_packet.is_auth_switch_request():
+ # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
+ auth_packet.read_uint8() # 0xfe packet identifier
+ plugin_name = auth_packet.read_string()
+ if self.server_capabilities & CLIENT.PLUGIN_AUTH and plugin_name is not None:
+ auth_packet = self._process_auth(plugin_name, auth_packet)
+ else:
+ # send legacy handshake
+ data = _scramble_323(self.password.encode('latin1'), self.salt) + b'\0'
+ self.write_packet(data)
+ auth_packet = self._read_packet()
+
+ def _process_auth(self, plugin_name, auth_packet):
+ plugin_class = self._auth_plugin_map.get(plugin_name)
+ if not plugin_class:
+ plugin_class = self._auth_plugin_map.get(plugin_name.decode('ascii'))
+ if plugin_class:
+ try:
+ handler = plugin_class(self)
+ return handler.authenticate(auth_packet)
+ except AttributeError:
+ if plugin_name != b'dialog':
+ raise err.OperationalError(2059, "Authentication plugin '%s'" \
+ " not loaded: - %r missing authenticate method" % (plugin_name, plugin_class))
+ except TypeError:
+ raise err.OperationalError(2059, "Authentication plugin '%s'" \
+ " not loaded: - %r cannot be constructed with connection object" % (plugin_name, plugin_class))
+ else:
+ handler = None
+ if plugin_name == b"mysql_native_password":
+ # https://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41
+ data = _scramble(self.password.encode('latin1'), auth_packet.read_all()) + b'\0'
+ elif plugin_name == b"mysql_old_password":
+ # https://dev.mysql.com/doc/internals/en/old-password-authentication.html
+ data = _scramble_323(self.password.encode('latin1'), auth_packet.read_all()) + b'\0'
+ elif plugin_name == b"mysql_clear_password":
+ # https://dev.mysql.com/doc/internals/en/clear-text-authentication.html
+ data = self.password.encode('latin1') + b'\0'
+ elif plugin_name == b"dialog":
+ pkt = auth_packet
+ while True:
+ flag = pkt.read_uint8()
+ echo = (flag & 0x06) == 0x02
+ last = (flag & 0x01) == 0x01
+ prompt = pkt.read_all()
+
+ if prompt == b"Password: ":
+ self.write_packet(self.password.encode('latin1') + b'\0')
+ elif handler:
+ resp = 'no response - TypeError within plugin.prompt method'
+ try:
+ resp = handler.prompt(echo, prompt)
+ self.write_packet(resp + b'\0')
+ except AttributeError:
+ raise err.OperationalError(2059, "Authentication plugin '%s'" \
+ " not loaded: - %r missing prompt method" % (plugin_name, handler))
+ except TypeError:
+ raise err.OperationalError(2061, "Authentication plugin '%s'" \
+ " %r didn't respond with string. Returned '%r' to prompt %r" % (plugin_name, handler, resp, prompt))
+ else:
+ raise err.OperationalError(2059, "Authentication plugin '%s' (%r) not configured" % (plugin_name, handler))
+ pkt = self._read_packet()
+ pkt.check_error()
+ if pkt.is_ok_packet() or last:
+ break
+ return pkt
+ else:
+ raise err.OperationalError(2059, "Authentication plugin '%s' not configured" % plugin_name)
+ self.write_packet(data)
+ pkt = self._read_packet()
+ pkt.check_error()
+ return pkt
# _mysql support
def thread_id(self):
@@ -877,64 +1204,87 @@ def get_proto_info(self):
def _get_server_information(self):
i = 0
- packet = MysqlPacket(self)
+ packet = self._read_packet()
data = packet.get_all_data()
if DEBUG: dump_packet(data)
- #packet_len = byte2int(data[i:i+1])
- #i += 4
self.protocol_version = byte2int(data[i:i+1])
-
i += 1
- server_end = data.find(int2byte(0), i)
- # TODO: is this the correct charset? should it be default_charset?
- self.server_version = data[i:server_end].decode(self.charset)
+ server_end = data.find(b'\0', i)
+ self.server_version = data[i:server_end].decode('latin1')
i = server_end + 1
- self.server_thread_id = struct.unpack('= i + 1:
- i += 1
- self.server_capabilities = struct.unpack('= i+12-1:
- rest_salt = data[i:i+12]
- self.salt += rest_salt
+ self.salt = data[i:i+8]
+ i += 9 # 8 + 1(filler)
+
+ self.server_capabilities = struct.unpack('= i + 6:
+ lang, stat, cap_h, salt_len = struct.unpack('= i + salt_len:
+ # salt_len includes auth_plugin_data_part_1 and filler
+ self.salt += data[i:i+salt_len]
+ i += salt_len
+
+ i+=1
+ # AUTH PLUGIN NAME may appear here.
+ if self.server_capabilities & CLIENT.PLUGIN_AUTH and len(data) >= i:
+ # Due to Bug#59453 the auth-plugin-name is missing the terminating
+ # NUL-char in versions prior to 5.5.10 and 5.6.2.
+ # ref: https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
+ # didn't use version checks as mariadb is corrected and reports
+ # earlier than those two.
+ server_end = data.find(b'\0', i)
+ if server_end < 0: # pragma: no cover - very specific upstream bug
+ # not found \0 and last field so take it all
+ self._auth_plugin_name = data[i:].decode('latin1')
+ else:
+ self._auth_plugin_name = data[i:server_end].decode('latin1')
def get_server_info(self):
return self.server_version
- Warning = Warning
- Error = Error
- InterfaceError = InterfaceError
- DatabaseError = DatabaseError
- DataError = DataError
- OperationalError = OperationalError
- IntegrityError = IntegrityError
- InternalError = InternalError
- ProgrammingError = ProgrammingError
- NotSupportedError = NotSupportedError
-
-# TODO: move OK and EOF packet parsing/logic into a proper subclass
-# of MysqlPacket like has been done with FieldDescriptorPacket.
+ Warning = err.Warning
+ Error = err.Error
+ InterfaceError = err.InterfaceError
+ DatabaseError = err.DatabaseError
+ DataError = err.DataError
+ OperationalError = err.OperationalError
+ IntegrityError = err.IntegrityError
+ InternalError = err.InternalError
+ ProgrammingError = err.ProgrammingError
+ NotSupportedError = err.NotSupportedError
+
+
class MySQLResult(object):
def __init__(self, connection):
- from weakref import proxy
- self.connection = proxy(connection)
+ """
+ :type connection: Connection
+ """
+ self.connection = connection
self.affected_rows = None
self.insert_id = None
- self.server_status = 0
+ self.server_status = None
self.warning_count = 0
self.message = None
self.field_count = 0
@@ -948,122 +1298,202 @@ def __del__(self):
self._finish_unbuffered_query()
def read(self):
- self.first_packet = self.connection.read_packet()
+ try:
+ first_packet = self.connection._read_packet()
- # TODO: use classes for different packet types?
- if self.first_packet.is_ok_packet():
- self._read_ok_packet()
- else:
- self._read_result_packet()
+ if first_packet.is_ok_packet():
+ self._read_ok_packet(first_packet)
+ elif first_packet.is_load_local_packet():
+ self._read_load_local_packet(first_packet)
+ else:
+ self._read_result_packet(first_packet)
+ finally:
+ self.connection = None
def init_unbuffered_query(self):
self.unbuffered_active = True
- self.first_packet = self.connection.read_packet()
+ first_packet = self.connection._read_packet()
- if self.first_packet.is_ok_packet():
- self._read_ok_packet()
+ if first_packet.is_ok_packet():
+ self._read_ok_packet(first_packet)
self.unbuffered_active = False
+ self.connection = None
+ elif first_packet.is_load_local_packet():
+ self._read_load_local_packet(first_packet)
+ self.unbuffered_active = False
+ self.connection = None
else:
- self.field_count = byte2int(self.first_packet.read(1))
+ self.field_count = first_packet.read_length_encoded_integer()
self._get_descriptions()
-
+
# Apparently, MySQLdb picks this number because it's the maximum
# value of a 64bit unsigned integer. Since we're emulating MySQLdb,
# we set it to this instead of None, which would be preferred.
self.affected_rows = 18446744073709551615
- def _read_ok_packet(self):
- ok_packet = OKPacketWrapper(self.first_packet)
+ def _read_ok_packet(self, first_packet):
+ ok_packet = OKPacketWrapper(first_packet)
self.affected_rows = ok_packet.affected_rows
self.insert_id = ok_packet.insert_id
self.server_status = ok_packet.server_status
self.warning_count = ok_packet.warning_count
self.message = ok_packet.message
+ self.has_next = ok_packet.has_next
+
+ def _read_load_local_packet(self, first_packet):
+ load_packet = LoadLocalPacketWrapper(first_packet)
+ sender = LoadLocalFile(load_packet.filename, self.connection)
+ try:
+ sender.send_data()
+ except:
+ self.connection._read_packet() # skip ok packet
+ raise
+
+ ok_packet = self.connection._read_packet()
+ if not ok_packet.is_ok_packet(): # pragma: no cover - upstream induced protocol error
+ raise err.OperationalError(2014, "Commands Out of Sync")
+ self._read_ok_packet(ok_packet)
def _check_packet_is_eof(self, packet):
- if packet.is_eof_packet():
- eof_packet = EOFPacketWrapper(packet)
- self.warning_count = eof_packet.warning_count
- self.has_next = eof_packet.has_next
- return True
- return False
-
- def _read_result_packet(self):
- self.field_count = byte2int(self.first_packet.read(1))
+ if not packet.is_eof_packet():
+ return False
+ #TODO: Support CLIENT.DEPRECATE_EOF
+ # 1) Add DEPRECATE_EOF to CAPABILITIES
+ # 2) Mask CAPABILITIES with server_capabilities
+ # 3) if server_capabilities & CLIENT.DEPRECATE_EOF: use OKPacketWrapper instead of EOFPacketWrapper
+ wp = EOFPacketWrapper(packet)
+ self.warning_count = wp.warning_count
+ self.has_next = wp.has_next
+ return True
+
+ def _read_result_packet(self, first_packet):
+ self.field_count = first_packet.read_length_encoded_integer()
self._get_descriptions()
self._read_rowdata_packet()
def _read_rowdata_packet_unbuffered(self):
# Check if in an active query
- if self.unbuffered_active == False: return
-
+ if not self.unbuffered_active:
+ return
+
# EOF
- packet = self.connection.read_packet()
+ packet = self.connection._read_packet()
if self._check_packet_is_eof(packet):
self.unbuffered_active = False
+ self.connection = None
self.rows = None
return
- row = []
- for field in self.fields:
- data = packet.read_length_coded_string()
- converted = None
- if field.type_code in self.connection.decoders:
- converter = self.connection.decoders[field.type_code]
- if DEBUG: print "DEBUG: field=%s, converter=%s" % (field, converter)
- if data != None:
- converted = converter(self.connection, field, data)
- row.append(converted)
-
+ row = self._read_row_from_packet(packet)
self.affected_rows = 1
- self.rows = tuple((row))
- if DEBUG: self.rows
+ self.rows = (row,) # rows should tuple of row for MySQL-python compatibility.
+ return row
def _finish_unbuffered_query(self):
# After much reading on the MySQL protocol, it appears that there is,
# in fact, no way to stop MySQL from sending all the data after
# executing a query, so we just spin, and wait for an EOF packet.
while self.unbuffered_active:
- packet = self.connection.read_packet()
+ packet = self.connection._read_packet()
if self._check_packet_is_eof(packet):
self.unbuffered_active = False
+ self.connection = None # release reference to kill cyclic reference.
- # TODO: implement this as an iteratable so that it is more
- # memory efficient and lower-latency to client...
def _read_rowdata_packet(self):
- """Read a rowdata packet for each data row in the result set."""
- rows = []
- while True:
- packet = self.connection.read_packet()
- if self._check_packet_is_eof(packet):
- break
+ """Read a rowdata packet for each data row in the result set."""
+ rows = []
+ while True:
+ packet = self.connection._read_packet()
+ if self._check_packet_is_eof(packet):
+ self.connection = None # release reference to kill cyclic reference.
+ break
+ rows.append(self._read_row_from_packet(packet))
+
+ self.affected_rows = len(rows)
+ self.rows = tuple(rows)
+ def _read_row_from_packet(self, packet):
row = []
- for field in self.fields:
- data = packet.read_length_coded_string()
- converted = None
- if field.type_code in self.connection.decoders:
- converter = self.connection.decoders[field.type_code]
- if DEBUG: print "DEBUG: field=%s, converter=%s" % (field, converter)
- if data != None:
- converted = converter(self.connection, field, data)
- row.append(converted)
-
- rows.append(tuple(row))
-
- self.affected_rows = len(rows)
- self.rows = tuple(rows)
- if DEBUG: self.rows
+ for encoding, converter in self.converters:
+ try:
+ data = packet.read_length_coded_string()
+ except IndexError:
+ # No more columns in this row
+ # See https://github.com/PyMySQL/PyMySQL/pull/434
+ break
+ if data is not None:
+ if encoding is not None:
+ data = data.decode(encoding)
+ if DEBUG: print("DEBUG: DATA = ", data)
+ if converter is not None:
+ data = converter(data)
+ row.append(data)
+ return tuple(row)
def _get_descriptions(self):
"""Read a column descriptor packet for each column in the result."""
self.fields = []
+ self.converters = []
+ use_unicode = self.connection.use_unicode
+ conn_encoding = self.connection.encoding
description = []
- for i in xrange(self.field_count):
- field = self.connection.read_packet(FieldDescriptorPacket)
+
+ for i in range_type(self.field_count):
+ field = self.connection._read_packet(FieldDescriptorPacket)
self.fields.append(field)
description.append(field.description())
-
- eof_packet = self.connection.read_packet()
+ field_type = field.type_code
+ if use_unicode:
+ if field_type == FIELD_TYPE.JSON:
+ # When SELECT from JSON column: charset = binary
+ # When SELECT CAST(... AS JSON): charset = connection encoding
+ # This behavior is different from TEXT / BLOB.
+ # We should decode result by connection encoding regardless charsetnr.
+ # See https://github.com/PyMySQL/PyMySQL/issues/488
+ encoding = conn_encoding # SELECT CAST(... AS JSON)
+ elif field_type in TEXT_TYPES:
+ if field.charsetnr == 63: # binary
+ # TEXTs with charset=binary means BINARY types.
+ encoding = None
+ else:
+ encoding = conn_encoding
+ else:
+ # Integers, Dates and Times, and other basic data is encoded in ascii
+ encoding = 'ascii'
+ else:
+ encoding = None
+ converter = self.connection.decoders.get(field_type)
+ if converter is through:
+ converter = None
+ if DEBUG: print("DEBUG: field={}, converter={}".format(field, converter))
+ self.converters.append((encoding, converter))
+
+ eof_packet = self.connection._read_packet()
assert eof_packet.is_eof_packet(), 'Protocol error, expecting EOF'
self.description = tuple(description)
+
+
+class LoadLocalFile(object):
+ def __init__(self, filename, connection):
+ self.filename = filename
+ self.connection = connection
+
+ def send_data(self):
+ """Send data packets from the local file to the server"""
+ if not self.connection._sock:
+ raise err.InterfaceError("(0, '')")
+ conn = self.connection
+
+ try:
+ with open(self.filename, 'rb') as open_file:
+ packet_size = min(conn.max_allowed_packet, 16*1024) # 16KB is efficient enough
+ while True:
+ chunk = open_file.read(packet_size)
+ if not chunk:
+ break
+ conn.write_packet(chunk)
+ except IOError:
+ raise err.OperationalError(1017, "Can't find file '{0}'".format(self.filename))
+ finally:
+ # send the empty packet to signify we are done sending data
+ conn.write_packet(b'')
diff --git a/gluon/contrib/pymysql/constants/CLIENT.py b/gluon/contrib/pymysql/constants/CLIENT.py
index 9d11ea100..6625b6a74 100644
--- a/gluon/contrib/pymysql/constants/CLIENT.py
+++ b/gluon/contrib/pymysql/constants/CLIENT.py
@@ -1,4 +1,4 @@
-
+# https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
LONG_PASSWORD = 1
FOUND_ROWS = 1 << 1
LONG_FLAG = 1 << 2
@@ -12,9 +12,20 @@
INTERACTIVE = 1 << 10
SSL = 1 << 11
IGNORE_SIGPIPE = 1 << 12
-TRANSACTIONS = 1 << 13
+TRANSACTIONS = 1 << 13
SECURE_CONNECTION = 1 << 15
MULTI_STATEMENTS = 1 << 16
MULTI_RESULTS = 1 << 17
-CAPABILITIES = LONG_PASSWORD|LONG_FLAG|TRANSACTIONS| \
- PROTOCOL_41|SECURE_CONNECTION
+PS_MULTI_RESULTS = 1 << 18
+PLUGIN_AUTH = 1 << 19
+PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21
+CAPABILITIES = (
+ LONG_PASSWORD | LONG_FLAG | PROTOCOL_41 | TRANSACTIONS
+ | SECURE_CONNECTION | MULTI_STATEMENTS | MULTI_RESULTS
+ | PLUGIN_AUTH | PLUGIN_AUTH_LENENC_CLIENT_DATA)
+
+# Not done yet
+CONNECT_ATTRS = 1 << 20
+HANDLE_EXPIRED_PASSWORDS = 1 << 22
+SESSION_TRACK = 1 << 23
+DEPRECATE_EOF = 1 << 24
diff --git a/gluon/contrib/pymysql/constants/COMMAND.py b/gluon/contrib/pymysql/constants/COMMAND.py
index 4a757da25..1da275533 100644
--- a/gluon/contrib/pymysql/constants/COMMAND.py
+++ b/gluon/contrib/pymysql/constants/COMMAND.py
@@ -21,3 +21,13 @@
COM_TABLE_DUMP = 0x13
COM_CONNECT_OUT = 0x14
COM_REGISTER_SLAVE = 0x15
+COM_STMT_PREPARE = 0x16
+COM_STMT_EXECUTE = 0x17
+COM_STMT_SEND_LONG_DATA = 0x18
+COM_STMT_CLOSE = 0x19
+COM_STMT_RESET = 0x1a
+COM_SET_OPTION = 0x1b
+COM_STMT_FETCH = 0x1c
+COM_DAEMON = 0x1d
+COM_BINLOG_DUMP_GTID = 0x1e
+COM_END = 0x1f
diff --git a/gluon/contrib/pymysql/constants/CR.py b/gluon/contrib/pymysql/constants/CR.py
new file mode 100755
index 000000000..48ca956ec
--- /dev/null
+++ b/gluon/contrib/pymysql/constants/CR.py
@@ -0,0 +1,68 @@
+# flake8: noqa
+# errmsg.h
+CR_ERROR_FIRST = 2000
+CR_UNKNOWN_ERROR = 2000
+CR_SOCKET_CREATE_ERROR = 2001
+CR_CONNECTION_ERROR = 2002
+CR_CONN_HOST_ERROR = 2003
+CR_IPSOCK_ERROR = 2004
+CR_UNKNOWN_HOST = 2005
+CR_SERVER_GONE_ERROR = 2006
+CR_VERSION_ERROR = 2007
+CR_OUT_OF_MEMORY = 2008
+CR_WRONG_HOST_INFO = 2009
+CR_LOCALHOST_CONNECTION = 2010
+CR_TCP_CONNECTION = 2011
+CR_SERVER_HANDSHAKE_ERR = 2012
+CR_SERVER_LOST = 2013
+CR_COMMANDS_OUT_OF_SYNC = 2014
+CR_NAMEDPIPE_CONNECTION = 2015
+CR_NAMEDPIPEWAIT_ERROR = 2016
+CR_NAMEDPIPEOPEN_ERROR = 2017
+CR_NAMEDPIPESETSTATE_ERROR = 2018
+CR_CANT_READ_CHARSET = 2019
+CR_NET_PACKET_TOO_LARGE = 2020
+CR_EMBEDDED_CONNECTION = 2021
+CR_PROBE_SLAVE_STATUS = 2022
+CR_PROBE_SLAVE_HOSTS = 2023
+CR_PROBE_SLAVE_CONNECT = 2024
+CR_PROBE_MASTER_CONNECT = 2025
+CR_SSL_CONNECTION_ERROR = 2026
+CR_MALFORMED_PACKET = 2027
+CR_WRONG_LICENSE = 2028
+
+CR_NULL_POINTER = 2029
+CR_NO_PREPARE_STMT = 2030
+CR_PARAMS_NOT_BOUND = 2031
+CR_DATA_TRUNCATED = 2032
+CR_NO_PARAMETERS_EXISTS = 2033
+CR_INVALID_PARAMETER_NO = 2034
+CR_INVALID_BUFFER_USE = 2035
+CR_UNSUPPORTED_PARAM_TYPE = 2036
+
+CR_SHARED_MEMORY_CONNECTION = 2037
+CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR = 2038
+CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR = 2039
+CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = 2040
+CR_SHARED_MEMORY_CONNECT_MAP_ERROR = 2041
+CR_SHARED_MEMORY_FILE_MAP_ERROR = 2042
+CR_SHARED_MEMORY_MAP_ERROR = 2043
+CR_SHARED_MEMORY_EVENT_ERROR = 2044
+CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR = 2045
+CR_SHARED_MEMORY_CONNECT_SET_ERROR = 2046
+CR_CONN_UNKNOW_PROTOCOL = 2047
+CR_INVALID_CONN_HANDLE = 2048
+CR_SECURE_AUTH = 2049
+CR_FETCH_CANCELED = 2050
+CR_NO_DATA = 2051
+CR_NO_STMT_METADATA = 2052
+CR_NO_RESULT_SET = 2053
+CR_NOT_IMPLEMENTED = 2054
+CR_SERVER_LOST_EXTENDED = 2055
+CR_STMT_CLOSED = 2056
+CR_NEW_STMT_METADATA = 2057
+CR_ALREADY_CONNECTED = 2058
+CR_AUTH_PLUGIN_CANNOT_LOAD = 2059
+CR_DUPLICATE_CONNECTION_ATTR = 2060
+CR_AUTH_PLUGIN_ERR = 2061
+CR_ERROR_LAST = 2061
diff --git a/gluon/contrib/pymysql/constants/FIELD_TYPE.py b/gluon/contrib/pymysql/constants/FIELD_TYPE.py
index 3df7ff487..51bd5143b 100644
--- a/gluon/contrib/pymysql/constants/FIELD_TYPE.py
+++ b/gluon/contrib/pymysql/constants/FIELD_TYPE.py
@@ -17,6 +17,7 @@
NEWDATE = 14
VARCHAR = 15
BIT = 16
+JSON = 245
NEWDECIMAL = 246
ENUM = 247
SET = 248
diff --git a/gluon/contrib/pymysql/constants/SERVER_STATUS.py b/gluon/contrib/pymysql/constants/SERVER_STATUS.py
index 010f542d7..6f5d56630 100644
--- a/gluon/contrib/pymysql/constants/SERVER_STATUS.py
+++ b/gluon/contrib/pymysql/constants/SERVER_STATUS.py
@@ -9,4 +9,3 @@
SERVER_STATUS_DB_DROPPED = 256
SERVER_STATUS_NO_BACKSLASH_ESCAPES = 512
SERVER_STATUS_METADATA_CHANGED = 1024
-
diff --git a/gluon/contrib/pymysql/converters.py b/gluon/contrib/pymysql/converters.py
index 56e93f9dc..4c00bc45b 100644
--- a/gluon/contrib/pymysql/converters.py
+++ b/gluon/contrib/pymysql/converters.py
@@ -1,106 +1,162 @@
-import re
+from ._compat import PY2, text_type, long_type, JYTHON, IRONPYTHON, unichr
+
import datetime
+from decimal import Decimal
+import re
import time
-import sys
-from constants import FIELD_TYPE, FLAG
-from charset import charset_by_id
+from .constants import FIELD_TYPE, FLAG
+from .charset import charset_by_id, charset_to_encoding
-PYTHON3 = sys.version_info[0] > 2
-try:
- set
-except NameError:
- try:
- from sets import BaseSet as set
- except ImportError:
- from sets import Set as set
-
-ESCAPE_REGEX = re.compile(r"[\0\n\r\032\'\"\\]")
-ESCAPE_MAP = {'\0': '\\0', '\n': '\\n', '\r': '\\r', '\032': '\\Z',
- '\'': '\\\'', '"': '\\"', '\\': '\\\\'}
-
-def escape_item(val, charset):
- if type(val) in [tuple, list, set]:
- return escape_sequence(val, charset)
- if type(val) is dict:
- return escape_dict(val, charset)
- if PYTHON3 and hasattr(val, "decode") and not isinstance(val, unicode):
- # deal with py3k bytes
- val = val.decode(charset)
- encoder = encoders[type(val)]
- val = encoder(val)
- if type(val) in [str, int]:
- return val
- val = val.encode(charset)
+def escape_item(val, charset, mapping=None):
+ if mapping is None:
+ mapping = encoders
+ encoder = mapping.get(type(val))
+
+ # Fallback to default when no encoder found
+ if not encoder:
+ try:
+ encoder = mapping[text_type]
+ except KeyError:
+ raise TypeError("no default type converter defined")
+
+ if encoder in (escape_dict, escape_sequence):
+ val = encoder(val, charset, mapping)
+ else:
+ val = encoder(val, mapping)
return val
-def escape_dict(val, charset):
+def escape_dict(val, charset, mapping=None):
n = {}
for k, v in val.items():
- quoted = escape_item(v, charset)
+ quoted = escape_item(v, charset, mapping)
n[k] = quoted
return n
-def escape_sequence(val, charset):
+def escape_sequence(val, charset, mapping=None):
n = []
for item in val:
- quoted = escape_item(item, charset)
+ quoted = escape_item(item, charset, mapping)
n.append(quoted)
return "(" + ",".join(n) + ")"
-def escape_set(val, charset):
- val = map(lambda x: escape_item(x, charset), val)
- return ','.join(val)
+def escape_set(val, charset, mapping=None):
+ return ','.join([escape_item(x, charset, mapping) for x in val])
-def escape_bool(value):
+def escape_bool(value, mapping=None):
return str(int(value))
-def escape_object(value):
+def escape_object(value, mapping=None):
return str(value)
-def escape_int(value):
- return value
-
-escape_long = escape_object
+def escape_int(value, mapping=None):
+ return str(value)
-def escape_float(value):
+def escape_float(value, mapping=None):
return ('%.15g' % value)
-def escape_string(value):
- return ("'%s'" % ESCAPE_REGEX.sub(
- lambda match: ESCAPE_MAP.get(match.group(0)), value))
+_escape_table = [unichr(x) for x in range(128)]
+_escape_table[0] = u'\\0'
+_escape_table[ord('\\')] = u'\\\\'
+_escape_table[ord('\n')] = u'\\n'
+_escape_table[ord('\r')] = u'\\r'
+_escape_table[ord('\032')] = u'\\Z'
+_escape_table[ord('"')] = u'\\"'
+_escape_table[ord("'")] = u"\\'"
-def escape_unicode(value):
- return escape_string(value)
+def _escape_unicode(value, mapping=None):
+ """escapes *value* without adding quote.
-def escape_None(value):
+ Value should be unicode
+ """
+ return value.translate(_escape_table)
+
+if PY2:
+ def escape_string(value, mapping=None):
+ """escape_string escapes *value* but not surround it with quotes.
+
+ Value should be bytes or unicode.
+ """
+ if isinstance(value, unicode):
+ return _escape_unicode(value)
+ assert isinstance(value, (bytes, bytearray))
+ value = value.replace('\\', '\\\\')
+ value = value.replace('\0', '\\0')
+ value = value.replace('\n', '\\n')
+ value = value.replace('\r', '\\r')
+ value = value.replace('\032', '\\Z')
+ value = value.replace("'", "\\'")
+ value = value.replace('"', '\\"')
+ return value
+
+ def escape_bytes(value, mapping=None):
+ assert isinstance(value, (bytes, bytearray))
+ return b"_binary'%s'" % escape_string(value)
+else:
+ escape_string = _escape_unicode
+
+ # On Python ~3.5, str.decode('ascii', 'surrogateescape') is slow.
+ # (fixed in Python 3.6, http://bugs.python.org/issue24870)
+ # Workaround is str.decode('latin1') then translate 0x80-0xff into 0udc80-0udcff.
+ # We can escape special chars and surrogateescape at once.
+ _escape_bytes_table = _escape_table + [chr(i) for i in range(0xdc80, 0xdd00)]
+
+ def escape_bytes(value, mapping=None):
+ return "_binary'%s'" % value.decode('latin1').translate(_escape_bytes_table)
+
+
+def escape_unicode(value, mapping=None):
+ return u"'%s'" % _escape_unicode(value)
+
+def escape_str(value, mapping=None):
+ return "'%s'" % escape_string(str(value), mapping)
+
+def escape_None(value, mapping=None):
return 'NULL'
-def escape_timedelta(obj):
+def escape_timedelta(obj, mapping=None):
seconds = int(obj.seconds) % 60
minutes = int(obj.seconds // 60) % 60
hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24
- return escape_string('%02d:%02d:%02d' % (hours, minutes, seconds))
+ if obj.microseconds:
+ fmt = "'{0:02d}:{1:02d}:{2:02d}.{3:06d}'"
+ else:
+ fmt = "'{0:02d}:{1:02d}:{2:02d}'"
+ return fmt.format(hours, minutes, seconds, obj.microseconds)
-def escape_time(obj):
- s = "%02d:%02d:%02d" % (int(obj.hour), int(obj.minute),
- int(obj.second))
+def escape_time(obj, mapping=None):
if obj.microsecond:
- s += ".%f" % obj.microsecond
-
- return escape_string(s)
+ fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'"
+ else:
+ fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}'"
+ return fmt.format(obj)
-def escape_datetime(obj):
- return escape_string(obj.strftime("%Y-%m-%d %H:%M:%S"))
+def escape_datetime(obj, mapping=None):
+ if obj.microsecond:
+ fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'"
+ else:
+ fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}'"
+ return fmt.format(obj)
-def escape_date(obj):
- return escape_string(obj.strftime("%Y-%m-%d"))
+def escape_date(obj, mapping=None):
+ fmt = "'{0.year:04}-{0.month:02}-{0.day:02}'"
+ return fmt.format(obj)
-def escape_struct_time(obj):
+def escape_struct_time(obj, mapping=None):
return escape_datetime(datetime.datetime(*obj[:6]))
-def convert_datetime(connection, field, obj):
+def _convert_second_fraction(s):
+ if not s:
+ return 0
+ # Pad zeros to ensure the fraction length in microseconds
+ s = s.ljust(6, '0')
+ return int(s[:6])
+
+DATETIME_RE = re.compile(r"(\d{1,4})-(\d{1,2})-(\d{1,2})[T ](\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?")
+
+
+def convert_datetime(obj):
"""Returns a DATETIME or TIMESTAMP column value as a datetime object:
>>> datetime_or_None('2007-02-25 23:06:20')
@@ -116,22 +172,24 @@ def convert_datetime(connection, field, obj):
True
"""
- if not isinstance(obj, unicode):
- obj = obj.decode(connection.charset)
- if ' ' in obj:
- sep = ' '
- elif 'T' in obj:
- sep = 'T'
- else:
- return convert_date(connection, field, obj)
+ if not PY2 and isinstance(obj, (bytes, bytearray)):
+ obj = obj.decode('ascii')
+
+ m = DATETIME_RE.match(obj)
+ if not m:
+ return convert_date(obj)
try:
- ymd, hms = obj.split(sep, 1)
- return datetime.datetime(*[ int(x) for x in ymd.split('-')+hms.split(':') ])
+ groups = list(m.groups())
+ groups[-1] = _convert_second_fraction(groups[-1])
+ return datetime.datetime(*[ int(x) for x in groups ])
except ValueError:
- return convert_date(connection, field, obj)
+ return convert_date(obj)
+
+TIMEDELTA_RE = re.compile(r"(-)?(\d{1,3}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?")
-def convert_timedelta(connection, field, obj):
+
+def convert_timedelta(obj):
"""Returns a TIME column as a timedelta object:
>>> timedelta_or_None('25:06:17')
@@ -148,25 +206,33 @@ def convert_timedelta(connection, field, obj):
can accept values as (+|-)DD HH:MM:SS. The latter format will not
be parsed correctly by this function.
"""
+ if not PY2 and isinstance(obj, (bytes, bytearray)):
+ obj = obj.decode('ascii')
+
+ m = TIMEDELTA_RE.match(obj)
+ if not m:
+ return None
+
try:
- microseconds = 0
- if not isinstance(obj, unicode):
- obj = obj.decode(connection.charset)
- if "." in obj:
- (obj, tail) = obj.split('.')
- microseconds = int(tail)
- hours, minutes, seconds = obj.split(':')
+ groups = list(m.groups())
+ groups[-1] = _convert_second_fraction(groups[-1])
+ negate = -1 if groups[0] else 1
+ hours, minutes, seconds, microseconds = groups[1:]
+
tdelta = datetime.timedelta(
hours = int(hours),
minutes = int(minutes),
seconds = int(seconds),
- microseconds = microseconds
- )
+ microseconds = int(microseconds)
+ ) * negate
return tdelta
except ValueError:
return None
-def convert_time(connection, field, obj):
+TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?")
+
+
+def convert_time(obj):
"""Returns a TIME column as a time object:
>>> time_or_None('15:06:17')
@@ -188,18 +254,24 @@ def convert_time(connection, field, obj):
to be treated as time-of-day and not a time offset, then you can
use set this function as the converter for FIELD_TYPE.TIME.
"""
+ if not PY2 and isinstance(obj, (bytes, bytearray)):
+ obj = obj.decode('ascii')
+
+ m = TIME_RE.match(obj)
+ if not m:
+ return None
+
try:
- microseconds = 0
- if "." in obj:
- (obj, tail) = obj.split('.')
- microseconds = int(tail)
- hours, minutes, seconds = obj.split(':')
+ groups = list(m.groups())
+ groups[-1] = _convert_second_fraction(groups[-1])
+ hours, minutes, seconds, microseconds = groups
return datetime.time(hour=int(hours), minute=int(minutes),
- second=int(seconds), microsecond=microseconds)
+ second=int(seconds), microsecond=int(microseconds))
except ValueError:
return None
-def convert_date(connection, field, obj):
+
+def convert_date(obj):
"""Returns a DATE column as a date object:
>>> date_or_None('2007-02-26')
@@ -213,14 +285,15 @@ def convert_date(connection, field, obj):
True
"""
+ if not PY2 and isinstance(obj, (bytes, bytearray)):
+ obj = obj.decode('ascii')
try:
- if not isinstance(obj, unicode):
- obj = obj.decode(connection.charset)
return datetime.date(*[ int(x) for x in obj.split('-', 2) ])
except ValueError:
return None
-def convert_mysql_timestamp(connection, field, timestamp):
+
+def convert_mysql_timestamp(timestamp):
"""Convert a MySQL TIMESTAMP to a Timestamp object.
MySQL >= 4.1 returns TIMESTAMP in the same format as DATETIME:
@@ -241,11 +314,10 @@ def convert_mysql_timestamp(connection, field, timestamp):
True
"""
- if not isinstance(timestamp, unicode):
- timestamp = timestamp.decode(connection.charset)
-
+ if not PY2 and isinstance(timestamp, (bytes, bytearray)):
+ timestamp = timestamp.decode('ascii')
if timestamp[4] == '-':
- return convert_datetime(connection, field, timestamp)
+ return convert_datetime(timestamp)
timestamp += "0"*(14-len(timestamp)) # padding
year, month, day, hour, minute, second = \
int(timestamp[:4]), int(timestamp[4:6]), int(timestamp[6:8]), \
@@ -256,101 +328,92 @@ def convert_mysql_timestamp(connection, field, timestamp):
return None
def convert_set(s):
+ if isinstance(s, (bytes, bytearray)):
+ return set(s.split(b","))
return set(s.split(","))
-def convert_bit(connection, field, b):
- #b = "\x00" * (8 - len(b)) + b # pad w/ zeroes
- #return struct.unpack(">Q", b)[0]
- #
- # the snippet above is right, but MySQLdb doesn't process bits,
- # so we shouldn't either
- return b
+
+def through(x):
+ return x
+
+
+#def convert_bit(b):
+# b = "\x00" * (8 - len(b)) + b # pad w/ zeroes
+# return struct.unpack(">Q", b)[0]
+#
+# the snippet above is right, but MySQLdb doesn't process bits,
+# so we shouldn't either
+convert_bit = through
+
def convert_characters(connection, field, data):
field_charset = charset_by_id(field.charsetnr).name
+ encoding = charset_to_encoding(field_charset)
if field.flags & FLAG.SET:
- return convert_set(data.decode(field_charset))
+ return convert_set(data.decode(encoding))
if field.flags & FLAG.BINARY:
return data
if connection.use_unicode:
- data = data.decode(field_charset)
+ data = data.decode(encoding)
elif connection.charset != field_charset:
- data = data.decode(field_charset)
- data = data.encode(connection.charset)
+ data = data.decode(encoding)
+ data = data.encode(connection.encoding)
return data
-def convert_int(connection, field, data):
- return int(data)
-
-def convert_long(connection, field, data):
- return long(data)
-
-def convert_float(connection, field, data):
- return float(data)
-
encoders = {
- bool: escape_bool,
- int: escape_int,
- long: escape_long,
- float: escape_float,
- str: escape_string,
- unicode: escape_unicode,
- tuple: escape_sequence,
- list:escape_sequence,
- set:escape_sequence,
- dict:escape_dict,
- type(None):escape_None,
- datetime.date: escape_date,
- datetime.datetime : escape_datetime,
- datetime.timedelta : escape_timedelta,
- datetime.time : escape_time,
- time.struct_time : escape_struct_time,
- }
+ bool: escape_bool,
+ int: escape_int,
+ long_type: escape_int,
+ float: escape_float,
+ str: escape_str,
+ text_type: escape_unicode,
+ tuple: escape_sequence,
+ list: escape_sequence,
+ set: escape_sequence,
+ frozenset: escape_sequence,
+ dict: escape_dict,
+ bytearray: escape_bytes,
+ type(None): escape_None,
+ datetime.date: escape_date,
+ datetime.datetime: escape_datetime,
+ datetime.timedelta: escape_timedelta,
+ datetime.time: escape_time,
+ time.struct_time: escape_struct_time,
+ Decimal: escape_object,
+}
+
+if not PY2 or JYTHON or IRONPYTHON:
+ encoders[bytes] = escape_bytes
decoders = {
- FIELD_TYPE.BIT: convert_bit,
- FIELD_TYPE.TINY: convert_int,
- FIELD_TYPE.SHORT: convert_int,
- FIELD_TYPE.LONG: convert_long,
- FIELD_TYPE.FLOAT: convert_float,
- FIELD_TYPE.DOUBLE: convert_float,
- FIELD_TYPE.DECIMAL: convert_float,
- FIELD_TYPE.NEWDECIMAL: convert_float,
- FIELD_TYPE.LONGLONG: convert_long,
- FIELD_TYPE.INT24: convert_int,
- FIELD_TYPE.YEAR: convert_int,
- FIELD_TYPE.TIMESTAMP: convert_mysql_timestamp,
- FIELD_TYPE.DATETIME: convert_datetime,
- FIELD_TYPE.TIME: convert_timedelta,
- FIELD_TYPE.DATE: convert_date,
- FIELD_TYPE.SET: convert_set,
- FIELD_TYPE.BLOB: convert_characters,
- FIELD_TYPE.TINY_BLOB: convert_characters,
- FIELD_TYPE.MEDIUM_BLOB: convert_characters,
- FIELD_TYPE.LONG_BLOB: convert_characters,
- FIELD_TYPE.STRING: convert_characters,
- FIELD_TYPE.VAR_STRING: convert_characters,
- FIELD_TYPE.VARCHAR: convert_characters,
- #FIELD_TYPE.BLOB: str,
- #FIELD_TYPE.STRING: str,
- #FIELD_TYPE.VAR_STRING: str,
- #FIELD_TYPE.VARCHAR: str
- }
-conversions = decoders # for MySQLdb compatibility
-
-try:
- # python version > 2.3
- from decimal import Decimal
- def convert_decimal(connection, field, data):
- data = data.decode(connection.charset)
- return Decimal(data)
- decoders[FIELD_TYPE.DECIMAL] = convert_decimal
- decoders[FIELD_TYPE.NEWDECIMAL] = convert_decimal
-
- def escape_decimal(obj):
- return unicode(obj)
- encoders[Decimal] = escape_decimal
-
-except ImportError:
- pass
+ FIELD_TYPE.BIT: convert_bit,
+ FIELD_TYPE.TINY: int,
+ FIELD_TYPE.SHORT: int,
+ FIELD_TYPE.LONG: int,
+ FIELD_TYPE.FLOAT: float,
+ FIELD_TYPE.DOUBLE: float,
+ FIELD_TYPE.LONGLONG: int,
+ FIELD_TYPE.INT24: int,
+ FIELD_TYPE.YEAR: int,
+ FIELD_TYPE.TIMESTAMP: convert_mysql_timestamp,
+ FIELD_TYPE.DATETIME: convert_datetime,
+ FIELD_TYPE.TIME: convert_timedelta,
+ FIELD_TYPE.DATE: convert_date,
+ FIELD_TYPE.SET: convert_set,
+ FIELD_TYPE.BLOB: through,
+ FIELD_TYPE.TINY_BLOB: through,
+ FIELD_TYPE.MEDIUM_BLOB: through,
+ FIELD_TYPE.LONG_BLOB: through,
+ FIELD_TYPE.STRING: through,
+ FIELD_TYPE.VAR_STRING: through,
+ FIELD_TYPE.VARCHAR: through,
+ FIELD_TYPE.DECIMAL: Decimal,
+ FIELD_TYPE.NEWDECIMAL: Decimal,
+}
+
+
+# for MySQLdb compatibility
+conversions = encoders.copy()
+conversions.update(decoders)
+Thing2Literal = escape_str
diff --git a/gluon/contrib/pymysql/cursors.py b/gluon/contrib/pymysql/cursors.py
index f9775f569..c3e16ba12 100644
--- a/gluon/contrib/pymysql/cursors.py
+++ b/gluon/contrib/pymysql/cursors.py
@@ -1,67 +1,82 @@
# -*- coding: utf-8 -*-
-import struct
+from __future__ import print_function, absolute_import
+from functools import partial
import re
+import warnings
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
+from ._compat import range_type, text_type, PY2
+from . import err
-from err import Warning, Error, InterfaceError, DataError, \
- DatabaseError, OperationalError, IntegrityError, InternalError, \
- NotSupportedError, ProgrammingError
-insert_values = re.compile(r'\svalues\s*(\(.+\))', re.IGNORECASE)
+#: Regular expression for :meth:`Cursor.executemany`.
+#: executemany only suports simple bulk insert.
+#: You can use it to load large dataset.
+RE_INSERT_VALUES = re.compile(
+ r"\s*((?:INSERT|REPLACE)\s.+\sVALUES?\s+)" +
+ r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))" +
+ r"(\s*(?:ON DUPLICATE.*)?)\Z",
+ re.IGNORECASE | re.DOTALL)
+
class Cursor(object):
- '''
+ """
This is the object you use to interact with the database.
- '''
+ """
+
+ #: Max stetement size which :meth:`executemany` generates.
+ #:
+ #: Max size of allowed statement is max_allowed_packet - packet_header_size.
+ #: Default value of max_allowed_packet is 1048576.
+ max_stmt_length = 1024000
+
+ _defer_warnings = False
+
def __init__(self, connection):
- '''
+ """
Do not create an instance of a Cursor yourself. Call
connections.Connection.cursor().
- '''
- from weakref import proxy
- self.connection = proxy(connection)
+ """
+ self.connection = connection
self.description = None
self.rownumber = 0
self.rowcount = -1
self.arraysize = 1
self._executed = None
- self.messages = []
- self.errorhandler = connection.errorhandler
- self._has_next = None
- self._rows = ()
-
- def __del__(self):
- '''
- When this gets GC'd close it.
- '''
- self.close()
+ self._result = None
+ self._rows = None
+ self._warnings_handled = False
def close(self):
- '''
+ """
Closing a cursor just exhausts all remaining data.
- '''
- if not self.connection:
+ """
+ conn = self.connection
+ if conn is None:
return
try:
while self.nextset():
pass
- except:
- pass
+ finally:
+ self.connection = None
+
+ def __enter__(self):
+ return self
- self.connection = None
+ def __exit__(self, *exc_info):
+ del exc_info
+ self.close()
def _get_db(self):
if not self.connection:
- self.errorhandler(self, ProgrammingError, "cursor closed")
+ raise err.ProgrammingError("Cursor closed")
return self.connection
def _check_executed(self):
if not self._executed:
- self.errorhandler(self, ProgrammingError, "execute() first")
+ raise err.ProgrammingError("execute() first")
+
+ def _conv_row(self, row):
+ return row
def setinputsizes(self, *args):
"""Does nothing, required by DB API."""
@@ -69,69 +84,152 @@ def setinputsizes(self, *args):
def setoutputsizes(self, *args):
"""Does nothing, required by DB API."""
- def nextset(self):
- ''' Get the next query set '''
- if self._executed:
- self.fetchall()
- del self.messages[:]
-
- if not self._has_next:
+ def _nextset(self, unbuffered=False):
+ """Get the next query set"""
+ conn = self._get_db()
+ current_result = self._result
+ # for unbuffered queries warnings are only available once whole result has been read
+ if unbuffered:
+ self._show_warnings()
+ if current_result is None or current_result is not conn._result:
+ return None
+ if not current_result.has_next:
return None
- connection = self._get_db()
- connection.next_result()
+ conn.next_result(unbuffered=unbuffered)
self._do_get_result()
return True
- def execute(self, query, args=None):
- ''' Execute a query '''
- from sys import exc_info
+ def nextset(self):
+ return self._nextset(False)
+
+ def _ensure_bytes(self, x, encoding=None):
+ if isinstance(x, text_type):
+ x = x.encode(encoding)
+ elif isinstance(x, (tuple, list)):
+ x = type(x)(self._ensure_bytes(v, encoding=encoding) for v in x)
+ return x
+
+ def _escape_args(self, args, conn):
+ ensure_bytes = partial(self._ensure_bytes, encoding=conn.encoding)
+
+ if isinstance(args, (tuple, list)):
+ if PY2:
+ args = tuple(map(ensure_bytes, args))
+ return tuple(conn.literal(arg) for arg in args)
+ elif isinstance(args, dict):
+ if PY2:
+ args = dict((ensure_bytes(key), ensure_bytes(val)) for
+ (key, val) in args.items())
+ return dict((key, conn.literal(val)) for (key, val) in args.items())
+ else:
+ # If it's not a dictionary let's try escaping it anyways.
+ # Worst case it will throw a Value error
+ if PY2:
+ args = ensure_bytes(args)
+ return conn.escape(args)
+
+ def mogrify(self, query, args=None):
+ """
+ Returns the exact string that is sent to the database by calling the
+ execute() method.
+ This method follows the extension to the DB API 2.0 followed by Psycopg.
+ """
conn = self._get_db()
- charset = conn.charset
- del self.messages[:]
+ if PY2: # Use bytes on Python 2 always
+ query = self._ensure_bytes(query, encoding=conn.encoding)
- # TODO: make sure that conn.escape is correct
+ if args is not None:
+ query = query % self._escape_args(args, conn)
- if isinstance(query, unicode):
- query = query.encode(charset)
+ return query
- if args is not None:
- if isinstance(args, tuple) or isinstance(args, list):
- escaped_args = tuple(conn.escape(arg) for arg in args)
- elif isinstance(args, dict):
- escaped_args = dict((key, conn.escape(val)) for (key, val) in args.items())
- else:
- #If it's not a dictionary let's try escaping it anyways.
- #Worst case it will throw a Value error
- escaped_args = conn.escape(args)
+ def execute(self, query, args=None):
+ """Execute a query
- query = query % escaped_args
+ :param str query: Query to execute.
- result = 0
- try:
- result = self._query(query)
- except:
- exc, value, tb = exc_info()
- del tb
- self.messages.append((exc,value))
- self.errorhandler(self, exc, value)
+ :param args: parameters used with query. (optional)
+ :type args: tuple, list or dict
+
+ :return: Number of affected rows
+ :rtype: int
+
+ If args is a list or tuple, %s can be used as a placeholder in the query.
+ If args is a dict, %(name)s can be used as a placeholder in the query.
+ """
+ while self.nextset():
+ pass
+ query = self.mogrify(query, args)
+
+ result = self._query(query)
self._executed = query
return result
def executemany(self, query, args):
- ''' Run several data against one query '''
- del self.messages[:]
- #conn = self._get_db()
+ # type: (str, list) -> int
+ """Run several data against one query
+
+ :param query: query to execute on server
+ :param args: Sequence of sequences or mappings. It is used as parameter.
+ :return: Number of rows affected, if any.
+
+ This method improves performance on multiple-row INSERT and
+ REPLACE. Otherwise it is equivalent to looping over args with
+ execute().
+ """
if not args:
return
- #charset = conn.charset
- #if isinstance(query, unicode):
- # query = query.encode(charset)
- self.rowcount = sum([ self.execute(query, arg) for arg in args ])
+ m = RE_INSERT_VALUES.match(query)
+ if m:
+ q_prefix = m.group(1) % ()
+ q_values = m.group(2).rstrip()
+ q_postfix = m.group(3) or ''
+ assert q_values[0] == '(' and q_values[-1] == ')'
+ return self._do_execute_many(q_prefix, q_values, q_postfix, args,
+ self.max_stmt_length,
+ self._get_db().encoding)
+
+ self.rowcount = sum(self.execute(query, arg) for arg in args)
return self.rowcount
+ def _do_execute_many(self, prefix, values, postfix, args, max_stmt_length, encoding):
+ conn = self._get_db()
+ escape = self._escape_args
+ if isinstance(prefix, text_type):
+ prefix = prefix.encode(encoding)
+ if PY2 and isinstance(values, text_type):
+ values = values.encode(encoding)
+ if isinstance(postfix, text_type):
+ postfix = postfix.encode(encoding)
+ sql = bytearray(prefix)
+ args = iter(args)
+ v = values % escape(next(args), conn)
+ if isinstance(v, text_type):
+ if PY2:
+ v = v.encode(encoding)
+ else:
+ v = v.encode(encoding, 'surrogateescape')
+ sql += v
+ rows = 0
+ for arg in args:
+ v = values % escape(arg, conn)
+ if isinstance(v, text_type):
+ if PY2:
+ v = v.encode(encoding)
+ else:
+ v = v.encode(encoding, 'surrogateescape')
+ if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length:
+ rows += self.execute(sql + postfix)
+ sql = bytearray(prefix)
+ else:
+ sql += b','
+ sql += v
+ rows += self.execute(sql + postfix)
+ self.rowcount = rows
+ return rows
def callproc(self, procname, args=()):
"""Execute stored procedure procname with args
@@ -164,23 +262,18 @@ def callproc(self, procname, args=()):
conn = self._get_db()
for index, arg in enumerate(args):
q = "SET @_%s_%d=%s" % (procname, index, conn.escape(arg))
- if isinstance(q, unicode):
- q = q.encode(conn.charset)
self._query(q)
self.nextset()
q = "CALL %s(%s)" % (procname,
','.join(['@_%s_%d' % (procname, i)
- for i in range(len(args))]))
- if isinstance(q, unicode):
- q = q.encode(conn.charset)
+ for i in range_type(len(args))]))
self._query(q)
self._executed = q
-
return args
def fetchone(self):
- ''' Fetch the next row '''
+ """Fetch the next row"""
self._check_executed()
if self._rows is None or self.rownumber >= len(self._rows):
return None
@@ -189,20 +282,20 @@ def fetchone(self):
return result
def fetchmany(self, size=None):
- ''' Fetch several rows '''
+ """Fetch several rows"""
self._check_executed()
+ if self._rows is None:
+ return ()
end = self.rownumber + (size or self.arraysize)
result = self._rows[self.rownumber:end]
- if self._rows is None:
- return None
self.rownumber = min(end, len(self._rows))
return result
def fetchall(self):
- ''' Fetch all the rows '''
+ """Fetch all the rows"""
self._check_executed()
if self._rows is None:
- return None
+ return ()
if self.rownumber:
result = self._rows[self.rownumber:]
else:
@@ -217,11 +310,10 @@ def scroll(self, value, mode='relative'):
elif mode == 'absolute':
r = value
else:
- self.errorhandler(self, ProgrammingError,
- "unknown scroll mode %s" % mode)
+ raise err.ProgrammingError("unknown scroll mode %s" % mode)
- if r < 0 or r >= len(self._rows):
- self.errorhandler(self, IndexError, "out of range")
+ if not (0 <= r < len(self._rows)):
+ raise IndexError("out of range")
self.rownumber = r
def _query(self, q):
@@ -233,92 +325,112 @@ def _query(self, q):
def _do_get_result(self):
conn = self._get_db()
- self.rowcount = conn._result.affected_rows
self.rownumber = 0
- self.description = conn._result.description
- self.lastrowid = conn._result.insert_id
- self._rows = conn._result.rows
- self._has_next = conn._result.has_next
+ self._result = result = conn._result
+
+ self.rowcount = result.affected_rows
+ self.description = result.description
+ self.lastrowid = result.insert_id
+ self._rows = result.rows
+ self._warnings_handled = False
+
+ if not self._defer_warnings:
+ self._show_warnings()
+
+ def _show_warnings(self):
+ if self._warnings_handled:
+ return
+ self._warnings_handled = True
+ if self._result and (self._result.has_next or not self._result.warning_count):
+ return
+ ws = self._get_db().show_warnings()
+ if ws is None:
+ return
+ for w in ws:
+ msg = w[-1]
+ if PY2:
+ if isinstance(msg, unicode):
+ msg = msg.encode('utf-8', 'replace')
+ warnings.warn(err.Warning(*w[1:3]), stacklevel=4)
def __iter__(self):
return iter(self.fetchone, None)
- Warning = Warning
- Error = Error
- InterfaceError = InterfaceError
- DatabaseError = DatabaseError
- DataError = DataError
- OperationalError = OperationalError
- IntegrityError = IntegrityError
- InternalError = InternalError
- ProgrammingError = ProgrammingError
- NotSupportedError = NotSupportedError
-
-class DictCursor(Cursor):
- """A cursor which returns results as a dictionary"""
+ Warning = err.Warning
+ Error = err.Error
+ InterfaceError = err.InterfaceError
+ DatabaseError = err.DatabaseError
+ DataError = err.DataError
+ OperationalError = err.OperationalError
+ IntegrityError = err.IntegrityError
+ InternalError = err.InternalError
+ ProgrammingError = err.ProgrammingError
+ NotSupportedError = err.NotSupportedError
- def execute(self, query, args=None):
- result = super(DictCursor, self).execute(query, args)
+
+class DictCursorMixin(object):
+ # You can override this to use OrderedDict or other dict-like types.
+ dict_type = dict
+
+ def _do_get_result(self):
+ super(DictCursorMixin, self)._do_get_result()
+ fields = []
if self.description:
- self._fields = [ field[0] for field in self.description ]
- return result
+ for f in self._result.fields:
+ name = f.name
+ if name in fields:
+ name = f.table_name + '.' + name
+ fields.append(name)
+ self._fields = fields
- def fetchone(self):
- ''' Fetch the next row '''
- self._check_executed()
- if self._rows is None or self.rownumber >= len(self._rows):
- return None
- result = dict(zip(self._fields, self._rows[self.rownumber]))
- self.rownumber += 1
- return result
+ if fields and self._rows:
+ self._rows = [self._conv_row(r) for r in self._rows]
- def fetchmany(self, size=None):
- ''' Fetch several rows '''
- self._check_executed()
- if self._rows is None:
+ def _conv_row(self, row):
+ if row is None:
return None
- end = self.rownumber + (size or self.arraysize)
- result = [ dict(zip(self._fields, r)) for r in self._rows[self.rownumber:end] ]
- self.rownumber = min(end, len(self._rows))
- return tuple(result)
+ return self.dict_type(zip(self._fields, row))
+
+
+class DictCursor(DictCursorMixin, Cursor):
+ """A cursor which returns results as a dictionary"""
- def fetchall(self):
- ''' Fetch all the rows '''
- self._check_executed()
- if self._rows is None:
- return None
- if self.rownumber:
- result = [ dict(zip(self._fields, r)) for r in self._rows[self.rownumber:] ]
- else:
- result = [ dict(zip(self._fields, r)) for r in self._rows ]
- self.rownumber = len(self._rows)
- return tuple(result)
class SSCursor(Cursor):
"""
Unbuffered Cursor, mainly useful for queries that return a lot of data,
or for connections to remote servers over a slow network.
-
+
Instead of copying every row of data into a buffer, this will fetch
rows as needed. The upside of this, is the client uses much less memory,
and rows are returned much faster when traveling over a slow network,
or if the result set is very big.
-
+
There are limitations, though. The MySQL protocol doesn't support
returning the total number of rows, so the only way to tell how many rows
there are is to iterate over every row returned. Also, it currently isn't
possible to scroll backwards, as only the current row is held in memory.
"""
-
+
+ _defer_warnings = True
+
+ def _conv_row(self, row):
+ return row
+
def close(self):
- conn = self._get_db()
- conn._result._finish_unbuffered_query()
-
+ conn = self.connection
+ if conn is None:
+ return
+
+ if self._result is not None and self._result is conn._result:
+ self._result._finish_unbuffered_query()
+
try:
- if self._has_next:
- while self.nextset(): pass
- except: pass
+ while self.nextset():
+ pass
+ finally:
+ self.connection = None
def _query(self, q):
conn = self._get_db()
@@ -326,38 +438,31 @@ def _query(self, q):
conn.query(q, unbuffered=True)
self._do_get_result()
return self.rowcount
-
+
+ def nextset(self):
+ return self._nextset(unbuffered=True)
+
def read_next(self):
- """ Read next row """
-
- conn = self._get_db()
- conn._result._read_rowdata_packet_unbuffered()
- return conn._result.rows
-
+ """Read next row"""
+ return self._conv_row(self._result._read_rowdata_packet_unbuffered())
+
def fetchone(self):
- """ Fetch next row """
-
+ """Fetch next row"""
self._check_executed()
row = self.read_next()
if row is None:
+ self._show_warnings()
return None
self.rownumber += 1
return row
-
+
def fetchall(self):
"""
Fetch all, as per MySQLdb. Pretty useless for large queries, as
it is buffered. See fetchall_unbuffered(), if you want an unbuffered
generator version of this method.
"""
-
- rows = []
- while True:
- row = self.fetchone()
- if row is None:
- break
- rows.append(row)
- return tuple(rows)
+ return list(self.fetchall_unbuffered())
def fetchall_unbuffered(self):
"""
@@ -365,46 +470,50 @@ def fetchall_unbuffered(self):
however, it doesn't make sense to return everything in a list, as that
would use ridiculous memory for large result sets.
"""
-
- row = self.fetchone()
- while row is not None:
- yield row
- row = self.fetchone()
-
+ return iter(self.fetchone, None)
+
+ def __iter__(self):
+ return self.fetchall_unbuffered()
+
def fetchmany(self, size=None):
- """ Fetch many """
-
+ """Fetch many"""
self._check_executed()
if size is None:
size = self.arraysize
-
+
rows = []
- for i in range(0, size):
+ for i in range_type(size):
row = self.read_next()
if row is None:
+ self._show_warnings()
break
rows.append(row)
self.rownumber += 1
- return tuple(rows)
-
+ return rows
+
def scroll(self, value, mode='relative'):
self._check_executed()
- if not mode == 'relative' and not mode == 'absolute':
- self.errorhandler(self, ProgrammingError,
- "unknown scroll mode %s" % mode)
-
+
if mode == 'relative':
if value < 0:
- self.errorhandler(self, NotSupportedError,
- "Backwards scrolling not supported by this cursor")
-
- for i in range(0, value): self.read_next()
+ raise err.NotSupportedError(
+ "Backwards scrolling not supported by this cursor")
+
+ for _ in range_type(value):
+ self.read_next()
self.rownumber += value
- else:
+ elif mode == 'absolute':
if value < self.rownumber:
- self.errorhandler(self, NotSupportedError,
+ raise err.NotSupportedError(
"Backwards scrolling not supported by this cursor")
-
+
end = value - self.rownumber
- for i in range(0, end): self.read_next()
+ for _ in range_type(end):
+ self.read_next()
self.rownumber = value
+ else:
+ raise err.ProgrammingError("unknown scroll mode %s" % mode)
+
+
+class SSDictCursor(DictCursorMixin, SSCursor):
+ """An unbuffered cursor, which returns results as a dictionary"""
diff --git a/gluon/contrib/pymysql/err.py b/gluon/contrib/pymysql/err.py
index b4322c633..24862632c 100644
--- a/gluon/contrib/pymysql/err.py
+++ b/gluon/contrib/pymysql/err.py
@@ -1,57 +1,39 @@
import struct
+from .constants import ER
-try:
- StandardError, Warning
-except ImportError:
- try:
- from exceptions import StandardError, Warning
- except ImportError:
- import sys
- e = sys.modules['exceptions']
- StandardError = e.StandardError
- Warning = e.Warning
-
-from constants import ER
-import sys
-
-class MySQLError(StandardError):
-
+
+class MySQLError(Exception):
"""Exception related to operation with MySQL."""
class Warning(Warning, MySQLError):
-
"""Exception raised for important warnings like data truncations
while inserting, etc."""
-class Error(MySQLError):
+class Error(MySQLError):
"""Exception that is the base class of all other error exceptions
(not Warning)."""
class InterfaceError(Error):
-
"""Exception raised for errors that are related to the database
interface rather than the database itself."""
class DatabaseError(Error):
-
"""Exception raised for errors that are related to the
database."""
class DataError(DatabaseError):
-
"""Exception raised for errors that are due to problems with the
processed data like division by zero, numeric value out of range,
etc."""
class OperationalError(DatabaseError):
-
"""Exception raised for errors that are related to the database's
operation and not necessarily under the control of the programmer,
e.g. an unexpected disconnect occurs, the data source name is not
@@ -60,28 +42,24 @@ class OperationalError(DatabaseError):
class IntegrityError(DatabaseError):
-
"""Exception raised when the relational integrity of the database
is affected, e.g. a foreign key check fails, duplicate key,
etc."""
class InternalError(DatabaseError):
-
"""Exception raised when the database encounters an internal
error, e.g. the cursor is not valid anymore, the transaction is
out of sync, etc."""
class ProgrammingError(DatabaseError):
-
"""Exception raised for programming errors, e.g. table not found
or already exists, syntax error in the SQL statement, wrong number
of parameters specified, etc."""
class NotSupportedError(DatabaseError):
-
"""Exception raised in case a method or database API was used
which is not supported by the database, e.g. requesting a
.rollback() on a connection that does not support transaction or
@@ -90,10 +68,12 @@ class NotSupportedError(DatabaseError):
error_map = {}
+
def _map_error(exc, *errors):
for error in errors:
error_map[error] = exc
+
_map_error(ProgrammingError, ER.DB_CREATE_EXISTS, ER.SYNTAX_ERROR,
ER.PARSE_ERROR, ER.NO_SUCH_TABLE, ER.WRONG_DB_NAME,
ER.WRONG_TABLE_NAME, ER.FIELD_SPECIFIED_TWICE,
@@ -104,44 +84,24 @@ def _map_error(exc, *errors):
ER.DATA_TOO_LONG, ER.DATETIME_FUNCTION_OVERFLOW)
_map_error(IntegrityError, ER.DUP_ENTRY, ER.NO_REFERENCED_ROW,
ER.NO_REFERENCED_ROW_2, ER.ROW_IS_REFERENCED, ER.ROW_IS_REFERENCED_2,
- ER.CANNOT_ADD_FOREIGN)
+ ER.CANNOT_ADD_FOREIGN, ER.BAD_NULL_ERROR)
_map_error(NotSupportedError, ER.WARNING_NOT_COMPLETE_ROLLBACK,
ER.NOT_SUPPORTED_YET, ER.FEATURE_DISABLED, ER.UNKNOWN_STORAGE_ENGINE)
-_map_error(OperationalError, ER.DBACCESS_DENIED_ERROR, ER.ACCESS_DENIED_ERROR,
- ER.TABLEACCESS_DENIED_ERROR, ER.COLUMNACCESS_DENIED_ERROR)
+_map_error(OperationalError, ER.DBACCESS_DENIED_ERROR, ER.ACCESS_DENIED_ERROR,
+ ER.CON_COUNT_ERROR, ER.TABLEACCESS_DENIED_ERROR,
+ ER.COLUMNACCESS_DENIED_ERROR)
+
del _map_error, ER
-
-def _get_error_info(data):
+
+def raise_mysql_exception(data):
errno = struct.unpack('= 2 and value[0] == value[-1] == quote:
+ return value[1:-1]
+ return value
+
+ def get(self, section, option):
+ value = configparser.RawConfigParser.get(self, section, option)
+ return self.__remove_quotes(value)
diff --git a/gluon/contrib/pymysql/tests/__init__.py b/gluon/contrib/pymysql/tests/__init__.py
index 8ad5f8aad..a9f5a4bfe 100644
--- a/gluon/contrib/pymysql/tests/__init__.py
+++ b/gluon/contrib/pymysql/tests/__init__.py
@@ -1,13 +1,18 @@
-from pymysql.tests.test_issues import *
-from pymysql.tests.test_example import *
-from pymysql.tests.test_basic import *
+# Sorted by alphabetical order
from pymysql.tests.test_DictCursor import *
+from pymysql.tests.test_SSCursor import *
+from pymysql.tests.test_basic import *
+from pymysql.tests.test_connection import *
+from pymysql.tests.test_converters import *
+from pymysql.tests.test_cursor import *
+from pymysql.tests.test_err import *
+from pymysql.tests.test_issues import *
+from pymysql.tests.test_load_local import *
+from pymysql.tests.test_nextset import *
+from pymysql.tests.test_optionfile import *
-import sys
-if sys.version_info[0] == 2:
- # MySQLdb tests were designed for Python 3
- from pymysql.tests.thirdparty import *
+from pymysql.tests.thirdparty import *
if __name__ == "__main__":
- import unittest
- unittest.main()
+ import unittest2
+ unittest2.main()
diff --git a/gluon/contrib/pymysql/tests/base.py b/gluon/contrib/pymysql/tests/base.py
index ce11b6b26..740157b15 100644
--- a/gluon/contrib/pymysql/tests/base.py
+++ b/gluon/contrib/pymysql/tests/base.py
@@ -1,20 +1,86 @@
+import gc
+import json
+import os
+import re
+import warnings
+
+import unittest2
+
import pymysql
-import unittest
+from .._compat import CPYTHON
+
+
+class PyMySQLTestCase(unittest2.TestCase):
+ # You can specify your test environment creating a file named
+ # "databases.json" or editing the `databases` variable below.
+ fname = os.path.join(os.path.dirname(__file__), "databases.json")
+ if os.path.exists(fname):
+ with open(fname) as f:
+ databases = json.load(f)
+ else:
+ databases = [
+ {"host":"localhost","user":"root",
+ "passwd":"","db":"test_pymysql", "use_unicode": True, 'local_infile': True},
+ {"host":"localhost","user":"root","passwd":"","db":"test_pymysql2"}]
-class PyMySQLTestCase(unittest.TestCase):
- # Edit this to suit your test environment.
- databases = [
- {"host":"localhost","user":"root",
- "passwd":"","db":"test_pymysql", "use_unicode": True},
- {"host":"localhost","user":"root","passwd":"","db":"test_pymysql2"}]
+ def mysql_server_is(self, conn, version_tuple):
+ """Return True if the given connection is on the version given or
+ greater.
+
+ e.g.::
+
+ if self.mysql_server_is(conn, (5, 6, 4)):
+ # do something for MySQL 5.6.4 and above
+ """
+ server_version = conn.get_server_info()
+ server_version_tuple = tuple(
+ (int(dig) if dig is not None else 0)
+ for dig in
+ re.match(r'(\d+)\.(\d+)\.(\d+)', server_version).group(1, 2, 3)
+ )
+ return server_version_tuple >= version_tuple
def setUp(self):
self.connections = []
-
for params in self.databases:
self.connections.append(pymysql.connect(**params))
+ self.addCleanup(self._teardown_connections)
- def tearDown(self):
+ def _teardown_connections(self):
for connection in self.connections:
connection.close()
+ def safe_create_table(self, connection, tablename, ddl, cleanup=True):
+ """create a table.
+
+ Ensures any existing version of that table is first dropped.
+
+ Also adds a cleanup rule to drop the table after the test
+ completes.
+ """
+ cursor = connection.cursor()
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ cursor.execute("drop table if exists `%s`" % (tablename,))
+ cursor.execute(ddl)
+ cursor.close()
+ if cleanup:
+ self.addCleanup(self.drop_table, connection, tablename)
+
+ def drop_table(self, connection, tablename):
+ cursor = connection.cursor()
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ cursor.execute("drop table if exists `%s`" % (tablename,))
+ cursor.close()
+
+ def safe_gc_collect(self):
+ """Ensure cycles are collected via gc.
+
+ Runs additional times on non-CPython platforms.
+
+ """
+ gc.collect()
+ if not CPYTHON:
+ gc.collect()
diff --git a/gluon/contrib/pymysql/tests/data/load_local_data.txt b/gluon/contrib/pymysql/tests/data/load_local_data.txt
new file mode 100755
index 000000000..f9f99c8fa
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/data/load_local_data.txt
@@ -0,0 +1,22749 @@
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+71,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
diff --git a/gluon/contrib/pymysql/tests/data/load_local_warn_data.txt b/gluon/contrib/pymysql/tests/data/load_local_warn_data.txt
new file mode 100755
index 000000000..2bda5a96f
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/data/load_local_warn_data.txt
@@ -0,0 +1,50 @@
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
+5,6,
+7,8,
+1,2,
+3,4,
diff --git a/gluon/contrib/pymysql/tests/test_DictCursor.py b/gluon/contrib/pymysql/tests/test_DictCursor.py
index 0166214a3..9a0d638b2 100755
--- a/gluon/contrib/pymysql/tests/test_DictCursor.py
+++ b/gluon/contrib/pymysql/tests/test_DictCursor.py
@@ -2,54 +2,117 @@
import pymysql.cursors
import datetime
+import warnings
+
class TestDictCursor(base.PyMySQLTestCase):
+ bob = {'name': 'bob', 'age': 21, 'DOB': datetime.datetime(1990, 2, 6, 23, 4, 56)}
+ jim = {'name': 'jim', 'age': 56, 'DOB': datetime.datetime(1955, 5, 9, 13, 12, 45)}
+ fred = {'name': 'fred', 'age': 100, 'DOB': datetime.datetime(1911, 9, 12, 1, 1, 1)}
+
+ cursor_type = pymysql.cursors.DictCursor
+
+ def setUp(self):
+ super(TestDictCursor, self).setUp()
+ self.conn = conn = self.connections[0]
+ c = conn.cursor(self.cursor_type)
- def test_DictCursor(self):
- #all assert test compare to the structure as would come out from MySQLdb
- conn = self.connections[0]
- c = conn.cursor(pymysql.cursors.DictCursor)
# create a table ane some data to query
- c.execute("""CREATE TABLE dictcursor (name char(20), age int , DOB datetime)""")
- data = (("bob",21,"1990-02-06 23:04:56"),
- ("jim",56,"1955-05-09 13:12:45"),
- ("fred",100,"1911-09-12 01:01:01"))
- bob = {'name':'bob','age':21,'DOB':datetime.datetime(1990, 02, 6, 23, 04, 56)}
- jim = {'name':'jim','age':56,'DOB':datetime.datetime(1955, 05, 9, 13, 12, 45)}
- fred = {'name':'fred','age':100,'DOB':datetime.datetime(1911, 9, 12, 1, 1, 1)}
- try:
- c.executemany("insert into dictcursor values (%s,%s,%s)", data)
- # try an update which should return no rows
- c.execute("update dictcursor set age=20 where name='bob'")
- bob['age'] = 20
- # pull back the single row dict for bob and check
- c.execute("SELECT * from dictcursor where name='bob'")
- r = c.fetchone()
- self.assertEqual(bob,r,"fetchone via DictCursor failed")
- # same again, but via fetchall => tuple)
- c.execute("SELECT * from dictcursor where name='bob'")
- r = c.fetchall()
- self.assertEqual((bob,),r,"fetch a 1 row result via fetchall failed via DictCursor")
- # same test again but iterate over the
- c.execute("SELECT * from dictcursor where name='bob'")
- for r in c:
- self.assertEqual(bob, r,"fetch a 1 row result via iteration failed via DictCursor")
- # get all 3 row via fetchall
- c.execute("SELECT * from dictcursor")
- r = c.fetchall()
- self.assertEqual((bob,jim,fred), r, "fetchall failed via DictCursor")
- #same test again but do a list comprehension
- c.execute("SELECT * from dictcursor")
- r = [x for x in c]
- self.assertEqual([bob,jim,fred], r, "list comprehension failed via DictCursor")
- # get all 2 row via fetchmany
- c.execute("SELECT * from dictcursor")
- r = c.fetchmany(2)
- self.assertEqual((bob,jim), r, "fetchmany failed via DictCursor")
- finally:
- c.execute("drop table dictcursor")
-
-__all__ = ["TestDictCursor"]
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists dictcursor")
+ # include in filterwarnings since for unbuffered dict cursor warning for lack of table
+ # will only be propagated at start of next execute() call
+ c.execute("""CREATE TABLE dictcursor (name char(20), age int , DOB datetime)""")
+ data = [("bob", 21, "1990-02-06 23:04:56"),
+ ("jim", 56, "1955-05-09 13:12:45"),
+ ("fred", 100, "1911-09-12 01:01:01")]
+ c.executemany("insert into dictcursor values (%s,%s,%s)", data)
+
+ def tearDown(self):
+ c = self.conn.cursor()
+ c.execute("drop table dictcursor")
+ super(TestDictCursor, self).tearDown()
+
+ def _ensure_cursor_expired(self, cursor):
+ pass
+
+ def test_DictCursor(self):
+ bob, jim, fred = self.bob.copy(), self.jim.copy(), self.fred.copy()
+ #all assert test compare to the structure as would come out from MySQLdb
+ conn = self.conn
+ c = conn.cursor(self.cursor_type)
+
+ # try an update which should return no rows
+ c.execute("update dictcursor set age=20 where name='bob'")
+ bob['age'] = 20
+ # pull back the single row dict for bob and check
+ c.execute("SELECT * from dictcursor where name='bob'")
+ r = c.fetchone()
+ self.assertEqual(bob, r, "fetchone via DictCursor failed")
+ self._ensure_cursor_expired(c)
+
+ # same again, but via fetchall => tuple)
+ c.execute("SELECT * from dictcursor where name='bob'")
+ r = c.fetchall()
+ self.assertEqual([bob], r, "fetch a 1 row result via fetchall failed via DictCursor")
+ # same test again but iterate over the
+ c.execute("SELECT * from dictcursor where name='bob'")
+ for r in c:
+ self.assertEqual(bob, r, "fetch a 1 row result via iteration failed via DictCursor")
+ # get all 3 row via fetchall
+ c.execute("SELECT * from dictcursor")
+ r = c.fetchall()
+ self.assertEqual([bob,jim,fred], r, "fetchall failed via DictCursor")
+ #same test again but do a list comprehension
+ c.execute("SELECT * from dictcursor")
+ r = list(c)
+ self.assertEqual([bob,jim,fred], r, "DictCursor should be iterable")
+ # get all 2 row via fetchmany
+ c.execute("SELECT * from dictcursor")
+ r = c.fetchmany(2)
+ self.assertEqual([bob, jim], r, "fetchmany failed via DictCursor")
+ self._ensure_cursor_expired(c)
+
+ def test_custom_dict(self):
+ class MyDict(dict): pass
+
+ class MyDictCursor(self.cursor_type):
+ dict_type = MyDict
+
+ keys = ['name', 'age', 'DOB']
+ bob = MyDict([(k, self.bob[k]) for k in keys])
+ jim = MyDict([(k, self.jim[k]) for k in keys])
+ fred = MyDict([(k, self.fred[k]) for k in keys])
+
+ cur = self.conn.cursor(MyDictCursor)
+ cur.execute("SELECT * FROM dictcursor WHERE name='bob'")
+ r = cur.fetchone()
+ self.assertEqual(bob, r, "fetchone() returns MyDictCursor")
+ self._ensure_cursor_expired(cur)
+
+ cur.execute("SELECT * FROM dictcursor")
+ r = cur.fetchall()
+ self.assertEqual([bob, jim, fred], r,
+ "fetchall failed via MyDictCursor")
+
+ cur.execute("SELECT * FROM dictcursor")
+ r = list(cur)
+ self.assertEqual([bob, jim, fred], r,
+ "list failed via MyDictCursor")
+
+ cur.execute("SELECT * FROM dictcursor")
+ r = cur.fetchmany(2)
+ self.assertEqual([bob, jim], r,
+ "list failed via MyDictCursor")
+ self._ensure_cursor_expired(cur)
+
+
+class TestSSDictCursor(TestDictCursor):
+ cursor_type = pymysql.cursors.SSDictCursor
+
+ def _ensure_cursor_expired(self, cursor):
+ list(cursor.fetchall_unbuffered())
if __name__ == "__main__":
import unittest
diff --git a/gluon/contrib/pymysql/tests/test_SSCursor.py b/gluon/contrib/pymysql/tests/test_SSCursor.py
index a0e3caf34..e6d6cf530 100755
--- a/gluon/contrib/pymysql/tests/test_SSCursor.py
+++ b/gluon/contrib/pymysql/tests/test_SSCursor.py
@@ -3,7 +3,7 @@
try:
from pymysql.tests import base
import pymysql.cursors
-except:
+except Exception:
# For local testing from top-level directory, without installing
sys.path.append('../pymysql')
from pymysql.tests import base
@@ -12,7 +12,7 @@
class TestSSCursor(base.PyMySQLTestCase):
def test_SSCursor(self):
affected_rows = 18446744073709551615
-
+
conn = self.connections[0]
data = [
('America', '', 'America/Jamaica'),
@@ -25,22 +25,23 @@ def test_SSCursor(self):
('America', '', 'America/Costa_Rica'),
('America', '', 'America/Denver'),
('America', '', 'America/Detroit'),]
-
+
try:
cursor = conn.cursor(pymysql.cursors.SSCursor)
-
+
# Create table
cursor.execute(('CREATE TABLE tz_data ('
'region VARCHAR(64),'
'zone VARCHAR(64),'
'name VARCHAR(64))'))
-
+
+ conn.begin()
# Test INSERT
for i in data:
cursor.execute('INSERT INTO tz_data VALUES (%s, %s, %s)', i)
self.assertEqual(conn.affected_rows(), 1, 'affected_rows does not match')
conn.commit()
-
+
# Test fetchone()
iter = 0
cursor.execute('SELECT * FROM tz_data')
@@ -49,46 +50,55 @@ def test_SSCursor(self):
if row is None:
break
iter += 1
-
+
# Test cursor.rowcount
self.assertEqual(cursor.rowcount, affected_rows,
'cursor.rowcount != %s' % (str(affected_rows)))
-
+
# Test cursor.rownumber
self.assertEqual(cursor.rownumber, iter,
'cursor.rowcount != %s' % (str(iter)))
-
+
# Test row came out the same as it went in
self.assertEqual((row in data), True,
'Row not found in source data')
-
+
# Test fetchall
cursor.execute('SELECT * FROM tz_data')
self.assertEqual(len(cursor.fetchall()), len(data),
'fetchall failed. Number of rows does not match')
-
+
# Test fetchmany
cursor.execute('SELECT * FROM tz_data')
self.assertEqual(len(cursor.fetchmany(2)), 2,
'fetchmany failed. Number of rows does not match')
-
+
# So MySQLdb won't throw "Commands out of sync"
while True:
res = cursor.fetchone()
if res is None:
break
-
+
# Test update, affected_rows()
cursor.execute('UPDATE tz_data SET zone = %s', ['Foo'])
conn.commit()
self.assertEqual(cursor.rowcount, len(data),
'Update failed. affected_rows != %s' % (str(len(data))))
-
+
# Test executemany
cursor.executemany('INSERT INTO tz_data VALUES (%s, %s, %s)', data)
self.assertEqual(cursor.rowcount, len(data),
'executemany failed. cursor.rowcount != %s' % (str(len(data))))
-
+
+ # Test multiple datasets
+ cursor.execute('SELECT 1; SELECT 2; SELECT 3')
+ self.assertListEqual(list(cursor), [(1, )])
+ self.assertTrue(cursor.nextset())
+ self.assertListEqual(list(cursor), [(2, )])
+ self.assertTrue(cursor.nextset())
+ self.assertListEqual(list(cursor), [(3, )])
+ self.assertFalse(cursor.nextset())
+
finally:
cursor.execute('DROP TABLE tz_data')
cursor.close()
diff --git a/gluon/contrib/pymysql/tests/test_basic.py b/gluon/contrib/pymysql/tests/test_basic.py
index fb7a30f8e..ed9c4f613 100644
--- a/gluon/contrib/pymysql/tests/test_basic.py
+++ b/gluon/contrib/pymysql/tests/test_basic.py
@@ -1,8 +1,19 @@
-from pymysql.tests import base
+# coding: utf-8
+import datetime
+import json
+import time
+import warnings
+
+from unittest2 import SkipTest
+
from pymysql import util
+import pymysql.cursors
+from pymysql.tests import base
+from pymysql.err import ProgrammingError
+
+
+__all__ = ["TestConversion", "TestCursor", "TestBulkInserts"]
-import time
-import datetime
class TestConversion(base.PyMySQLTestCase):
def test_datatypes(self):
@@ -12,16 +23,13 @@ def test_datatypes(self):
c.execute("create table test_datatypes (b bit, i int, l bigint, f real, s varchar(32), u varchar(32), bb blob, d date, dt datetime, ts timestamp, td time, t time, st datetime)")
try:
# insert values
- v = (True, -3, 123456789012, 5.7, "hello'\" world", u"Espa\xc3\xb1ol", "binary\x00data".encode(conn.charset), datetime.date(1988,2,2), datetime.datetime.now(), datetime.timedelta(5,6), datetime.time(16,32), time.localtime())
+
+ v = (True, -3, 123456789012, 5.7, "hello'\" world", u"Espa\xc3\xb1ol", "binary\x00data".encode(conn.charset), datetime.date(1988,2,2), datetime.datetime(2014, 5, 15, 7, 45, 57), datetime.timedelta(5,6), datetime.time(16,32), time.localtime())
c.execute("insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", v)
c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes")
r = c.fetchone()
self.assertEqual(util.int2byte(1), r[0])
- self.assertEqual(v[1:8], r[1:8])
- # mysql throws away microseconds so we need to check datetimes
- # specially. additionally times are turned into timedeltas.
- self.assertEqual(datetime.datetime(*v[8].timetuple()[:6]), r[8])
- self.assertEqual(v[9], r[9]) # just timedeltas
+ self.assertEqual(v[1:10], r[1:10])
self.assertEqual(datetime.timedelta(0, 60 * (v[10].hour * 60 + v[10].minute)), r[10])
self.assertEqual(datetime.datetime(*v[-1][:6]), r[-1])
@@ -35,11 +43,15 @@ def test_datatypes(self):
c.execute("delete from test_datatypes")
- # check sequence type
- c.execute("insert into test_datatypes (i, l) values (2,4), (6,8), (10,12)")
- c.execute("select l from test_datatypes where i in %s order by i", ((2,6),))
- r = c.fetchall()
- self.assertEqual(((4,),(8,)), r)
+ # check sequences type
+ for seq_type in (tuple, list, set, frozenset):
+ c.execute("insert into test_datatypes (i, l) values (2,4), (6,8), (10,12)")
+ seq = seq_type([2,6])
+ c.execute("select l from test_datatypes where i in %s order by i", (seq,))
+ r = c.fetchall()
+ self.assertEqual(((4,),(8,)), r)
+ c.execute("delete from test_datatypes")
+
finally:
c.execute("drop table test_datatypes")
@@ -79,20 +91,18 @@ def test_integer(self):
finally:
c.execute("drop table test_dict")
-
- def test_big_blob(self):
- """ test tons of data """
+ def test_blob(self):
+ """test binary data"""
+ data = bytes(bytearray(range(256)) * 4)
conn = self.connections[0]
- c = conn.cursor()
- c.execute("create table test_big_blob (b blob)")
- try:
- data = "pymysql" * 1024
- c.execute("insert into test_big_blob (b) values (%s)", (data,))
- c.execute("select b from test_big_blob")
- self.assertEqual(data.encode(conn.charset), c.fetchone()[0])
- finally:
- c.execute("drop table test_big_blob")
-
+ self.safe_create_table(
+ conn, "test_blob", "create table test_blob (b blob)")
+
+ with conn.cursor() as c:
+ c.execute("insert into test_blob (b) values (%s)", (data,))
+ c.execute("select b from test_blob")
+ self.assertEqual(data, c.fetchone()[0])
+
def test_untyped(self):
""" test conversion of null, empty string """
conn = self.connections[0]
@@ -101,17 +111,40 @@ def test_untyped(self):
self.assertEqual((None,u''), c.fetchone())
c.execute("select '',null")
self.assertEqual((u'',None), c.fetchone())
-
- def test_datetime(self):
- """ test conversion of null, empty string """
+
+ def test_timedelta(self):
+ """ test timedelta conversion """
conn = self.connections[0]
c = conn.cursor()
- c.execute("select time('12:30'), time('23:12:59'), time('23:12:59.05100')")
+ c.execute("select time('12:30'), time('23:12:59'), time('23:12:59.05100'), time('-12:30'), time('-23:12:59'), time('-23:12:59.05100'), time('-00:30')")
self.assertEqual((datetime.timedelta(0, 45000),
datetime.timedelta(0, 83579),
- datetime.timedelta(0, 83579, 51000)),
+ datetime.timedelta(0, 83579, 51000),
+ -datetime.timedelta(0, 45000),
+ -datetime.timedelta(0, 83579),
+ -datetime.timedelta(0, 83579, 51000),
+ -datetime.timedelta(0, 1800)),
c.fetchone())
+ def test_datetime_microseconds(self):
+ """ test datetime conversion w microseconds"""
+
+ conn = self.connections[0]
+ if not self.mysql_server_is(conn, (5, 6, 4)):
+ raise SkipTest("target backend does not support microseconds")
+ c = conn.cursor()
+ dt = datetime.datetime(2013, 11, 12, 9, 9, 9, 123450)
+ c.execute("create table test_datetime (id int, ts datetime(6))")
+ try:
+ c.execute(
+ "insert into test_datetime values (%s, %s)",
+ (1, dt)
+ )
+ c.execute("select ts from test_datetime")
+ self.assertEqual((dt,), c.fetchone())
+ finally:
+ c.execute("drop table test_datetime")
+
class TestCursor(base.PyMySQLTestCase):
# this test case does not work quite right yet, however,
@@ -185,7 +218,7 @@ def test_aggregates(self):
c = conn.cursor()
try:
c.execute('create table test_aggregates (i integer)')
- for i in xrange(0, 10):
+ for i in range(0, 10):
c.execute('insert into test_aggregates (i) values (%s)', (i,))
c.execute('select sum(i) from test_aggregates')
r, = c.fetchone()
@@ -197,17 +230,150 @@ def test_single_tuple(self):
""" test a single tuple """
conn = self.connections[0]
c = conn.cursor()
- try:
- c.execute("create table mystuff (id integer primary key)")
- c.execute("insert into mystuff (id) values (1)")
- c.execute("insert into mystuff (id) values (2)")
- c.execute("select id from mystuff where id in %s", ((1,),))
- self.assertEqual([(1,)], list(c.fetchall()))
- finally:
- c.execute("drop table mystuff")
+ self.safe_create_table(
+ conn, 'mystuff',
+ "create table mystuff (id integer primary key)")
+ c.execute("insert into mystuff (id) values (1)")
+ c.execute("insert into mystuff (id) values (2)")
+ c.execute("select id from mystuff where id in %s", ((1,),))
+ self.assertEqual([(1,)], list(c.fetchall()))
+ c.close()
+
+ def test_json(self):
+ args = self.databases[0].copy()
+ args["charset"] = "utf8mb4"
+ conn = pymysql.connect(**args)
+ if not self.mysql_server_is(conn, (5, 7, 0)):
+ raise SkipTest("JSON type is not supported on MySQL <= 5.6")
+
+ self.safe_create_table(conn, "test_json", """\
+create table test_json (
+ id int not null,
+ json JSON not null,
+ primary key (id)
+);""")
+ cur = conn.cursor()
+
+ json_str = u'{"hello": "こんにちは"}'
+ cur.execute("INSERT INTO test_json (id, `json`) values (42, %s)", (json_str,))
+ cur.execute("SELECT `json` from `test_json` WHERE `id`=42")
+ res = cur.fetchone()[0]
+ self.assertEqual(json.loads(res), json.loads(json_str))
+
+ cur.execute("SELECT CAST(%s AS JSON) AS x", (json_str,))
+ res = cur.fetchone()[0]
+ self.assertEqual(json.loads(res), json.loads(json_str))
+
+
+class TestBulkInserts(base.PyMySQLTestCase):
+
+ cursor_type = pymysql.cursors.DictCursor
+
+ def setUp(self):
+ super(TestBulkInserts, self).setUp()
+ self.conn = conn = self.connections[0]
+ c = conn.cursor(self.cursor_type)
-__all__ = ["TestConversion","TestCursor"]
+ # create a table ane some data to query
+ self.safe_create_table(conn, 'bulkinsert', """\
+CREATE TABLE bulkinsert
+(
+id int(11),
+name char(20),
+age int,
+height int,
+PRIMARY KEY (id)
+)
+""")
+
+ def _verify_records(self, data):
+ conn = self.connections[0]
+ cursor = conn.cursor()
+ cursor.execute("SELECT id, name, age, height from bulkinsert")
+ result = cursor.fetchall()
+ self.assertEqual(sorted(data), sorted(result))
+
+ def test_bulk_insert(self):
+ conn = self.connections[0]
+ cursor = conn.cursor()
+
+ data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)]
+ cursor.executemany("insert into bulkinsert (id, name, age, height) "
+ "values (%s,%s,%s,%s)", data)
+ self.assertEqual(
+ cursor._last_executed, bytearray(
+ b"insert into bulkinsert (id, name, age, height) values "
+ b"(0,'bob',21,123),(1,'jim',56,45),(2,'fred',100,180)"))
+ cursor.execute('commit')
+ self._verify_records(data)
+
+ def test_bulk_insert_multiline_statement(self):
+ conn = self.connections[0]
+ cursor = conn.cursor()
+ data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)]
+ cursor.executemany("""insert
+into bulkinsert (id, name,
+age, height)
+values (%s,
+%s , %s,
+%s )
+ """, data)
+ self.assertEqual(cursor._last_executed.strip(), bytearray(b"""insert
+into bulkinsert (id, name,
+age, height)
+values (0,
+'bob' , 21,
+123 ),(1,
+'jim' , 56,
+45 ),(2,
+'fred' , 100,
+180 )"""))
+ cursor.execute('commit')
+ self._verify_records(data)
+
+ def test_bulk_insert_single_record(self):
+ conn = self.connections[0]
+ cursor = conn.cursor()
+ data = [(0, "bob", 21, 123)]
+ cursor.executemany("insert into bulkinsert (id, name, age, height) "
+ "values (%s,%s,%s,%s)", data)
+ cursor.execute('commit')
+ self._verify_records(data)
+
+ def test_issue_288(self):
+ """executemany should work with "insert ... on update" """
+ conn = self.connections[0]
+ cursor = conn.cursor()
+ data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)]
+ cursor.executemany("""insert
+into bulkinsert (id, name,
+age, height)
+values (%s,
+%s , %s,
+%s ) on duplicate key update
+age = values(age)
+ """, data)
+ self.assertEqual(cursor._last_executed.strip(), bytearray(b"""insert
+into bulkinsert (id, name,
+age, height)
+values (0,
+'bob' , 21,
+123 ),(1,
+'jim' , 56,
+45 ),(2,
+'fred' , 100,
+180 ) on duplicate key update
+age = values(age)"""))
+ cursor.execute('commit')
+ self._verify_records(data)
-if __name__ == "__main__":
- import unittest
- unittest.main()
+ def test_warnings(self):
+ con = self.connections[0]
+ cur = con.cursor()
+ with warnings.catch_warnings(record=True) as ws:
+ warnings.simplefilter("always")
+ cur.execute("drop table if exists no_exists_table")
+ self.assertEqual(len(ws), 1)
+ self.assertEqual(ws[0].category, pymysql.Warning)
+ if u"no_exists_table" not in str(ws[0].message):
+ self.fail("'no_exists_table' not in %s" % (str(ws[0].message),))
diff --git a/gluon/contrib/pymysql/tests/test_connection.py b/gluon/contrib/pymysql/tests/test_connection.py
new file mode 100755
index 000000000..518b6fe74
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/test_connection.py
@@ -0,0 +1,576 @@
+import datetime
+import sys
+import time
+import unittest2
+import pymysql
+from pymysql.tests import base
+from pymysql._compat import text_type
+
+
+class TempUser:
+ def __init__(self, c, user, db, auth=None, authdata=None, password=None):
+ self._c = c
+ self._user = user
+ self._db = db
+ create = "CREATE USER " + user
+ if password is not None:
+ create += " IDENTIFIED BY '%s'" % password
+ elif auth is not None:
+ create += " IDENTIFIED WITH %s" % auth
+ if authdata is not None:
+ create += " AS '%s'" % authdata
+ try:
+ c.execute(create)
+ self._created = True
+ except pymysql.err.InternalError:
+ # already exists - TODO need to check the same plugin applies
+ self._created = False
+ try:
+ c.execute("GRANT SELECT ON %s.* TO %s" % (db, user))
+ self._grant = True
+ except pymysql.err.InternalError:
+ self._grant = False
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self._grant:
+ self._c.execute("REVOKE SELECT ON %s.* FROM %s" % (self._db, self._user))
+ if self._created:
+ self._c.execute("DROP USER %s" % self._user)
+
+
+class TestAuthentication(base.PyMySQLTestCase):
+
+ socket_auth = False
+ socket_found = False
+ two_questions_found = False
+ three_attempts_found = False
+ pam_found = False
+ mysql_old_password_found = False
+ sha256_password_found = False
+
+ import os
+ osuser = os.environ.get('USER')
+
+ # socket auth requires the current user and for the connection to be a socket
+ # rest do grants @localhost due to incomplete logic - TODO change to @% then
+ db = base.PyMySQLTestCase.databases[0].copy()
+
+ socket_auth = db.get('unix_socket') is not None \
+ and db.get('host') in ('localhost', '127.0.0.1')
+
+ cur = pymysql.connect(**db).cursor()
+ del db['user']
+ cur.execute("SHOW PLUGINS")
+ for r in cur:
+ if (r[1], r[2]) != (u'ACTIVE', u'AUTHENTICATION'):
+ continue
+ if r[3] == u'auth_socket.so':
+ socket_plugin_name = r[0]
+ socket_found = True
+ elif r[3] == u'dialog_examples.so':
+ if r[0] == 'two_questions':
+ two_questions_found = True
+ elif r[0] == 'three_attempts':
+ three_attempts_found = True
+ elif r[0] == u'pam':
+ pam_found = True
+ pam_plugin_name = r[3].split('.')[0]
+ if pam_plugin_name == 'auth_pam':
+ pam_plugin_name = 'pam'
+ # MySQL: authentication_pam
+ # https://dev.mysql.com/doc/refman/5.5/en/pam-authentication-plugin.html
+
+ # MariaDB: pam
+ # https://mariadb.com/kb/en/mariadb/pam-authentication-plugin/
+
+ # Names differ but functionality is close
+ elif r[0] == u'mysql_old_password':
+ mysql_old_password_found = True
+ elif r[0] == u'sha256_password':
+ sha256_password_found = True
+ #else:
+ # print("plugin: %r" % r[0])
+
+ def test_plugin(self):
+ # Bit of an assumption that the current user is a native password
+ self.assertEqual('mysql_native_password', self.connections[0]._auth_plugin_name)
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipIf(socket_found, "socket plugin already installed")
+ def testSocketAuthInstallPlugin(self):
+ # needs plugin. lets install it.
+ cur = self.connections[0].cursor()
+ try:
+ cur.execute("install plugin auth_socket soname 'auth_socket.so'")
+ TestAuthentication.socket_found = True
+ self.socket_plugin_name = 'auth_socket'
+ self.realtestSocketAuth()
+ except pymysql.err.InternalError:
+ try:
+ cur.execute("install soname 'auth_socket'")
+ TestAuthentication.socket_found = True
+ self.socket_plugin_name = 'unix_socket'
+ self.realtestSocketAuth()
+ except pymysql.err.InternalError:
+ TestAuthentication.socket_found = False
+ raise unittest2.SkipTest('we couldn\'t install the socket plugin')
+ finally:
+ if TestAuthentication.socket_found:
+ cur.execute("uninstall plugin %s" % self.socket_plugin_name)
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipUnless(socket_found, "no socket plugin")
+ def testSocketAuth(self):
+ self.realtestSocketAuth()
+
+ def realtestSocketAuth(self):
+ with TempUser(self.connections[0].cursor(), TestAuthentication.osuser + '@localhost',
+ self.databases[0]['db'], self.socket_plugin_name) as u:
+ c = pymysql.connect(user=TestAuthentication.osuser, **self.db)
+
+ class Dialog(object):
+ fail=False
+
+ def __init__(self, con):
+ self.fail=TestAuthentication.Dialog.fail
+ pass
+
+ def prompt(self, echo, prompt):
+ if self.fail:
+ self.fail=False
+ return b'bad guess at a password'
+ return self.m.get(prompt)
+
+ class DialogHandler(object):
+
+ def __init__(self, con):
+ self.con=con
+
+ def authenticate(self, pkt):
+ while True:
+ flag = pkt.read_uint8()
+ echo = (flag & 0x06) == 0x02
+ last = (flag & 0x01) == 0x01
+ prompt = pkt.read_all()
+
+ if prompt == b'Password, please:':
+ self.con.write_packet(b'stillnotverysecret\0')
+ else:
+ self.con.write_packet(b'no idea what to do with this prompt\0')
+ pkt = self.con._read_packet()
+ pkt.check_error()
+ if pkt.is_ok_packet() or last:
+ break
+ return pkt
+
+ class DefectiveHandler(object):
+ def __init__(self, con):
+ self.con=con
+
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipIf(two_questions_found, "two_questions plugin already installed")
+ def testDialogAuthTwoQuestionsInstallPlugin(self):
+ # needs plugin. lets install it.
+ cur = self.connections[0].cursor()
+ try:
+ cur.execute("install plugin two_questions soname 'dialog_examples.so'")
+ TestAuthentication.two_questions_found = True
+ self.realTestDialogAuthTwoQuestions()
+ except pymysql.err.InternalError:
+ raise unittest2.SkipTest('we couldn\'t install the two_questions plugin')
+ finally:
+ if TestAuthentication.two_questions_found:
+ cur.execute("uninstall plugin two_questions")
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipUnless(two_questions_found, "no two questions auth plugin")
+ def testDialogAuthTwoQuestions(self):
+ self.realTestDialogAuthTwoQuestions()
+
+ def realTestDialogAuthTwoQuestions(self):
+ TestAuthentication.Dialog.fail=False
+ TestAuthentication.Dialog.m = {b'Password, please:': b'notverysecret',
+ b'Are you sure ?': b'yes, of course'}
+ with TempUser(self.connections[0].cursor(), 'pymysql_2q@localhost',
+ self.databases[0]['db'], 'two_questions', 'notverysecret') as u:
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user='pymysql_2q', **self.db)
+ pymysql.connect(user='pymysql_2q', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipIf(three_attempts_found, "three_attempts plugin already installed")
+ def testDialogAuthThreeAttemptsQuestionsInstallPlugin(self):
+ # needs plugin. lets install it.
+ cur = self.connections[0].cursor()
+ try:
+ cur.execute("install plugin three_attempts soname 'dialog_examples.so'")
+ TestAuthentication.three_attempts_found = True
+ self.realTestDialogAuthThreeAttempts()
+ except pymysql.err.InternalError:
+ raise unittest2.SkipTest('we couldn\'t install the three_attempts plugin')
+ finally:
+ if TestAuthentication.three_attempts_found:
+ cur.execute("uninstall plugin three_attempts")
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipUnless(three_attempts_found, "no three attempts plugin")
+ def testDialogAuthThreeAttempts(self):
+ self.realTestDialogAuthThreeAttempts()
+
+ def realTestDialogAuthThreeAttempts(self):
+ TestAuthentication.Dialog.m = {b'Password, please:': b'stillnotverysecret'}
+ TestAuthentication.Dialog.fail=True # fail just once. We've got three attempts after all
+ with TempUser(self.connections[0].cursor(), 'pymysql_3a@localhost',
+ self.databases[0]['db'], 'three_attempts', 'stillnotverysecret') as u:
+ pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
+ pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.DialogHandler}, **self.db)
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': object}, **self.db)
+
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.DefectiveHandler}, **self.db)
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user='pymysql_3a', auth_plugin_map={b'notdialogplugin': TestAuthentication.Dialog}, **self.db)
+ TestAuthentication.Dialog.m = {b'Password, please:': b'I do not know'}
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
+ TestAuthentication.Dialog.m = {b'Password, please:': None}
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db)
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipIf(pam_found, "pam plugin already installed")
+ @unittest2.skipIf(os.environ.get('PASSWORD') is None, "PASSWORD env var required")
+ @unittest2.skipIf(os.environ.get('PAMSERVICE') is None, "PAMSERVICE env var required")
+ def testPamAuthInstallPlugin(self):
+ # needs plugin. lets install it.
+ cur = self.connections[0].cursor()
+ try:
+ cur.execute("install plugin pam soname 'auth_pam.so'")
+ TestAuthentication.pam_found = True
+ self.realTestPamAuth()
+ except pymysql.err.InternalError:
+ raise unittest2.SkipTest('we couldn\'t install the auth_pam plugin')
+ finally:
+ if TestAuthentication.pam_found:
+ cur.execute("uninstall plugin pam")
+
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipUnless(pam_found, "no pam plugin")
+ @unittest2.skipIf(os.environ.get('PASSWORD') is None, "PASSWORD env var required")
+ @unittest2.skipIf(os.environ.get('PAMSERVICE') is None, "PAMSERVICE env var required")
+ def testPamAuth(self):
+ self.realTestPamAuth()
+
+ def realTestPamAuth(self):
+ db = self.db.copy()
+ import os
+ db['password'] = os.environ.get('PASSWORD')
+ cur = self.connections[0].cursor()
+ try:
+ cur.execute('show grants for ' + TestAuthentication.osuser + '@localhost')
+ grants = cur.fetchone()[0]
+ cur.execute('drop user ' + TestAuthentication.osuser + '@localhost')
+ except pymysql.OperationalError as e:
+ # assuming the user doesn't exist which is ok too
+ self.assertEqual(1045, e.args[0])
+ grants = None
+ with TempUser(cur, TestAuthentication.osuser + '@localhost',
+ self.databases[0]['db'], 'pam', os.environ.get('PAMSERVICE')) as u:
+ try:
+ c = pymysql.connect(user=TestAuthentication.osuser, **db)
+ db['password'] = 'very bad guess at password'
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user=TestAuthentication.osuser,
+ auth_plugin_map={b'mysql_cleartext_password': TestAuthentication.DefectiveHandler},
+ **self.db)
+ except pymysql.OperationalError as e:
+ self.assertEqual(1045, e.args[0])
+ # we had 'bad guess at password' work with pam. Well at least we get a permission denied here
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user=TestAuthentication.osuser,
+ auth_plugin_map={b'mysql_cleartext_password': TestAuthentication.DefectiveHandler},
+ **self.db)
+ if grants:
+ # recreate the user
+ cur.execute(grants)
+
+ # select old_password("crummy p\tassword");
+ #| old_password("crummy p\tassword") |
+ #| 2a01785203b08770 |
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipUnless(mysql_old_password_found, "no mysql_old_password plugin")
+ def testMySQLOldPasswordAuth(self):
+ if self.mysql_server_is(self.connections[0], (5, 7, 0)):
+ raise unittest2.SkipTest('Old passwords aren\'t supported in 5.7')
+ # pymysql.err.OperationalError: (1045, "Access denied for user 'old_pass_user'@'localhost' (using password: YES)")
+ # from login in MySQL-5.6
+ if self.mysql_server_is(self.connections[0], (5, 6, 0)):
+ raise unittest2.SkipTest('Old passwords don\'t authenticate in 5.6')
+ db = self.db.copy()
+ db['password'] = "crummy p\tassword"
+ with self.connections[0] as c:
+ # deprecated in 5.6
+ if sys.version_info[0:2] >= (3,2) and self.mysql_server_is(self.connections[0], (5, 6, 0)):
+ with self.assertWarns(pymysql.err.Warning) as cm:
+ c.execute("SELECT OLD_PASSWORD('%s')" % db['password'])
+ else:
+ c.execute("SELECT OLD_PASSWORD('%s')" % db['password'])
+ v = c.fetchone()[0]
+ self.assertEqual(v, '2a01785203b08770')
+ # only works in MariaDB and MySQL-5.6 - can't separate out by version
+ #if self.mysql_server_is(self.connections[0], (5, 5, 0)):
+ # with TempUser(c, 'old_pass_user@localhost',
+ # self.databases[0]['db'], 'mysql_old_password', '2a01785203b08770') as u:
+ # cur = pymysql.connect(user='old_pass_user', **db).cursor()
+ # cur.execute("SELECT VERSION()")
+ c.execute("SELECT @@secure_auth")
+ secure_auth_setting = c.fetchone()[0]
+ c.execute('set old_passwords=1')
+ # pymysql.err.Warning: 'pre-4.1 password hash' is deprecated and will be removed in a future release. Please use post-4.1 password hash instead
+ if sys.version_info[0:2] >= (3,2) and self.mysql_server_is(self.connections[0], (5, 6, 0)):
+ with self.assertWarns(pymysql.err.Warning) as cm:
+ c.execute('set global secure_auth=0')
+ else:
+ c.execute('set global secure_auth=0')
+ with TempUser(c, 'old_pass_user@localhost',
+ self.databases[0]['db'], password=db['password']) as u:
+ cur = pymysql.connect(user='old_pass_user', **db).cursor()
+ cur.execute("SELECT VERSION()")
+ c.execute('set global secure_auth=%r' % secure_auth_setting)
+
+ @unittest2.skipUnless(socket_auth, "connection to unix_socket required")
+ @unittest2.skipUnless(sha256_password_found, "no sha256 password authentication plugin found")
+ def testAuthSHA256(self):
+ c = self.connections[0].cursor()
+ with TempUser(c, 'pymysql_sha256@localhost',
+ self.databases[0]['db'], 'sha256_password') as u:
+ if self.mysql_server_is(self.connections[0], (5, 7, 0)):
+ c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' ='Sh@256Pa33'")
+ else:
+ c.execute('SET old_passwords = 2')
+ c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' = PASSWORD('Sh@256Pa33')")
+ db = self.db.copy()
+ db['password'] = "Sh@256Pa33"
+ # not implemented yet so thows error
+ with self.assertRaises(pymysql.err.OperationalError):
+ pymysql.connect(user='pymysql_256', **db)
+
+class TestConnection(base.PyMySQLTestCase):
+
+ def test_utf8mb4(self):
+ """This test requires MySQL >= 5.5"""
+ arg = self.databases[0].copy()
+ arg['charset'] = 'utf8mb4'
+ conn = pymysql.connect(**arg)
+
+ def test_largedata(self):
+ """Large query and response (>=16MB)"""
+ cur = self.connections[0].cursor()
+ cur.execute("SELECT @@max_allowed_packet")
+ if cur.fetchone()[0] < 16*1024*1024 + 10:
+ print("Set max_allowed_packet to bigger than 17MB")
+ return
+ t = 'a' * (16*1024*1024)
+ cur.execute("SELECT '" + t + "'")
+ assert cur.fetchone()[0] == t
+
+ def test_autocommit(self):
+ con = self.connections[0]
+ self.assertFalse(con.get_autocommit())
+
+ cur = con.cursor()
+ cur.execute("SET AUTOCOMMIT=1")
+ self.assertTrue(con.get_autocommit())
+
+ con.autocommit(False)
+ self.assertFalse(con.get_autocommit())
+ cur.execute("SELECT @@AUTOCOMMIT")
+ self.assertEqual(cur.fetchone()[0], 0)
+
+ def test_select_db(self):
+ con = self.connections[0]
+ current_db = self.databases[0]['db']
+ other_db = self.databases[1]['db']
+
+ cur = con.cursor()
+ cur.execute('SELECT database()')
+ self.assertEqual(cur.fetchone()[0], current_db)
+
+ con.select_db(other_db)
+ cur.execute('SELECT database()')
+ self.assertEqual(cur.fetchone()[0], other_db)
+
+ def test_connection_gone_away(self):
+ """
+ http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
+ http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html#error_cr_server_gone_error
+ """
+ con = self.connections[0]
+ cur = con.cursor()
+ cur.execute("SET wait_timeout=1")
+ time.sleep(2)
+ with self.assertRaises(pymysql.OperationalError) as cm:
+ cur.execute("SELECT 1+1")
+ # error occures while reading, not writing because of socket buffer.
+ #self.assertEqual(cm.exception.args[0], 2006)
+ self.assertIn(cm.exception.args[0], (2006, 2013))
+
+ def test_init_command(self):
+ conn = pymysql.connect(
+ init_command='SELECT "bar"; SELECT "baz"',
+ **self.databases[0]
+ )
+ c = conn.cursor()
+ c.execute('select "foobar";')
+ self.assertEqual(('foobar',), c.fetchone())
+ conn.close()
+ with self.assertRaises(pymysql.err.Error):
+ conn.ping(reconnect=False)
+
+ def test_read_default_group(self):
+ conn = pymysql.connect(
+ read_default_group='client',
+ **self.databases[0]
+ )
+ self.assertTrue(conn.open)
+
+ def test_context(self):
+ with self.assertRaises(ValueError):
+ c = pymysql.connect(**self.databases[0])
+ with c as cur:
+ cur.execute('create table test ( a int )')
+ c.begin()
+ cur.execute('insert into test values ((1))')
+ raise ValueError('pseudo abort')
+ c.commit()
+ c = pymysql.connect(**self.databases[0])
+ with c as cur:
+ cur.execute('select count(*) from test')
+ self.assertEqual(0, cur.fetchone()[0])
+ cur.execute('insert into test values ((1))')
+ with c as cur:
+ cur.execute('select count(*) from test')
+ self.assertEqual(1,cur.fetchone()[0])
+ cur.execute('drop table test')
+
+ def test_set_charset(self):
+ c = pymysql.connect(**self.databases[0])
+ c.set_charset('utf8')
+ # TODO validate setting here
+
+ def test_defer_connect(self):
+ import socket
+ for db in self.databases:
+ d = db.copy()
+ try:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.connect(d['unix_socket'])
+ except KeyError:
+ sock = socket.create_connection(
+ (d.get('host', 'localhost'), d.get('port', 3306)))
+ for k in ['unix_socket', 'host', 'port']:
+ try:
+ del d[k]
+ except KeyError:
+ pass
+
+ c = pymysql.connect(defer_connect=True, **d)
+ self.assertFalse(c.open)
+ c.connect(sock)
+ c.close()
+ sock.close()
+
+ @unittest2.skipUnless(sys.version_info[0:2] >= (3,2), "required py-3.2")
+ def test_no_delay_warning(self):
+ current_db = self.databases[0].copy()
+ current_db['no_delay'] = True
+ with self.assertWarns(DeprecationWarning) as cm:
+ conn = pymysql.connect(**current_db)
+
+
+# A custom type and function to escape it
+class Foo(object):
+ value = "bar"
+
+
+def escape_foo(x, d):
+ return x.value
+
+
+class TestEscape(base.PyMySQLTestCase):
+ def test_escape_string(self):
+ con = self.connections[0]
+ cur = con.cursor()
+
+ self.assertEqual(con.escape("foo'bar"), "'foo\\'bar'")
+ # added NO_AUTO_CREATE_USER as not including it in 5.7 generates warnings
+ cur.execute("SET sql_mode='NO_BACKSLASH_ESCAPES,NO_AUTO_CREATE_USER'")
+ self.assertEqual(con.escape("foo'bar"), "'foo''bar'")
+
+ def test_escape_builtin_encoders(self):
+ con = self.connections[0]
+ cur = con.cursor()
+
+ val = datetime.datetime(2012, 3, 4, 5, 6)
+ self.assertEqual(con.escape(val, con.encoders), "'2012-03-04 05:06:00'")
+
+ def test_escape_custom_object(self):
+ con = self.connections[0]
+ cur = con.cursor()
+
+ mapping = {Foo: escape_foo}
+ self.assertEqual(con.escape(Foo(), mapping), "bar")
+
+ def test_escape_fallback_encoder(self):
+ con = self.connections[0]
+ cur = con.cursor()
+
+ class Custom(str):
+ pass
+
+ mapping = {text_type: pymysql.escape_string}
+ self.assertEqual(con.escape(Custom('foobar'), mapping), "'foobar'")
+
+ def test_escape_no_default(self):
+ con = self.connections[0]
+ cur = con.cursor()
+
+ self.assertRaises(TypeError, con.escape, 42, {})
+
+ def test_escape_dict_value(self):
+ con = self.connections[0]
+ cur = con.cursor()
+
+ mapping = con.encoders.copy()
+ mapping[Foo] = escape_foo
+ self.assertEqual(con.escape({'foo': Foo()}, mapping), {'foo': "bar"})
+
+ def test_escape_list_item(self):
+ con = self.connections[0]
+ cur = con.cursor()
+
+ mapping = con.encoders.copy()
+ mapping[Foo] = escape_foo
+ self.assertEqual(con.escape([Foo()], mapping), "(bar)")
+
+ def test_previous_cursor_not_closed(self):
+ con = self.connections[0]
+ cur1 = con.cursor()
+ cur1.execute("SELECT 1; SELECT 2")
+ cur2 = con.cursor()
+ cur2.execute("SELECT 3")
+ self.assertEqual(cur2.fetchone()[0], 3)
+
+ def test_commit_during_multi_result(self):
+ con = self.connections[0]
+ cur = con.cursor()
+ cur.execute("SELECT 1; SELECT 2")
+ con.commit()
+ cur.execute("SELECT 3")
+ self.assertEqual(cur.fetchone()[0], 3)
diff --git a/gluon/contrib/pymysql/tests/test_converters.py b/gluon/contrib/pymysql/tests/test_converters.py
new file mode 100755
index 000000000..b7b5a9846
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/test_converters.py
@@ -0,0 +1,67 @@
+import datetime
+from unittest import TestCase
+
+from pymysql._compat import PY2
+from pymysql import converters
+
+
+__all__ = ["TestConverter"]
+
+
+class TestConverter(TestCase):
+
+ def test_escape_string(self):
+ self.assertEqual(
+ converters.escape_string(u"foo\nbar"),
+ u"foo\\nbar"
+ )
+
+ if PY2:
+ def test_escape_string_bytes(self):
+ self.assertEqual(
+ converters.escape_string(b"foo\nbar"),
+ b"foo\\nbar"
+ )
+
+ def test_convert_datetime(self):
+ expected = datetime.datetime(2007, 2, 24, 23, 6, 20)
+ dt = converters.convert_datetime('2007-02-24 23:06:20')
+ self.assertEqual(dt, expected)
+
+ def test_convert_datetime_with_fsp(self):
+ expected = datetime.datetime(2007, 2, 24, 23, 6, 20, 511581)
+ dt = converters.convert_datetime('2007-02-24 23:06:20.511581')
+ self.assertEqual(dt, expected)
+
+ def _test_convert_timedelta(self, with_negate=False, with_fsp=False):
+ d = {'hours': 789, 'minutes': 12, 'seconds': 34}
+ s = '%(hours)s:%(minutes)s:%(seconds)s' % d
+ if with_fsp:
+ d['microseconds'] = 511581
+ s += '.%(microseconds)s' % d
+
+ expected = datetime.timedelta(**d)
+ if with_negate:
+ expected = -expected
+ s = '-' + s
+
+ tdelta = converters.convert_timedelta(s)
+ self.assertEqual(tdelta, expected)
+
+ def test_convert_timedelta(self):
+ self._test_convert_timedelta(with_negate=False, with_fsp=False)
+ self._test_convert_timedelta(with_negate=True, with_fsp=False)
+
+ def test_convert_timedelta_with_fsp(self):
+ self._test_convert_timedelta(with_negate=False, with_fsp=True)
+ self._test_convert_timedelta(with_negate=False, with_fsp=True)
+
+ def test_convert_time(self):
+ expected = datetime.time(23, 6, 20)
+ time_obj = converters.convert_time('23:06:20')
+ self.assertEqual(time_obj, expected)
+
+ def test_convert_time_with_fsp(self):
+ expected = datetime.time(23, 6, 20, 511581)
+ time_obj = converters.convert_time('23:06:20.511581')
+ self.assertEqual(time_obj, expected)
diff --git a/gluon/contrib/pymysql/tests/test_cursor.py b/gluon/contrib/pymysql/tests/test_cursor.py
new file mode 100755
index 000000000..431ef4dd9
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/test_cursor.py
@@ -0,0 +1,104 @@
+import warnings
+
+from pymysql.tests import base
+import pymysql.cursors
+
+class CursorTest(base.PyMySQLTestCase):
+ def setUp(self):
+ super(CursorTest, self).setUp()
+
+ conn = self.connections[0]
+ self.safe_create_table(
+ conn,
+ "test", "create table test (data varchar(10))",
+ )
+ cursor = conn.cursor()
+ cursor.execute(
+ "insert into test (data) values "
+ "('row1'), ('row2'), ('row3'), ('row4'), ('row5')")
+ cursor.close()
+ self.test_connection = pymysql.connect(**self.databases[0])
+ self.addCleanup(self.test_connection.close)
+
+ def test_cleanup_rows_unbuffered(self):
+ conn = self.test_connection
+ cursor = conn.cursor(pymysql.cursors.SSCursor)
+
+ cursor.execute("select * from test as t1, test as t2")
+ for counter, row in enumerate(cursor):
+ if counter > 10:
+ break
+
+ del cursor
+ self.safe_gc_collect()
+
+ c2 = conn.cursor()
+
+ c2.execute("select 1")
+ self.assertEqual(c2.fetchone(), (1,))
+ self.assertIsNone(c2.fetchone())
+
+ def test_cleanup_rows_buffered(self):
+ conn = self.test_connection
+ cursor = conn.cursor(pymysql.cursors.Cursor)
+
+ cursor.execute("select * from test as t1, test as t2")
+ for counter, row in enumerate(cursor):
+ if counter > 10:
+ break
+
+ del cursor
+ self.safe_gc_collect()
+
+ c2 = conn.cursor()
+
+ c2.execute("select 1")
+
+ self.assertEqual(
+ c2.fetchone(), (1,)
+ )
+ self.assertIsNone(c2.fetchone())
+
+ def test_executemany(self):
+ conn = self.test_connection
+ cursor = conn.cursor(pymysql.cursors.Cursor)
+
+ m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%s, %s)")
+ self.assertIsNotNone(m, 'error parse %s')
+ self.assertEqual(m.group(3), '', 'group 3 not blank, bug in RE_INSERT_VALUES?')
+
+ m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)")
+ self.assertIsNotNone(m, 'error parse %(name)s')
+ self.assertEqual(m.group(3), '', 'group 3 not blank, bug in RE_INSERT_VALUES?')
+
+ m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)")
+ self.assertIsNotNone(m, 'error parse %(id_name)s')
+ self.assertEqual(m.group(3), '', 'group 3 not blank, bug in RE_INSERT_VALUES?')
+
+ m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update")
+ self.assertIsNotNone(m, 'error parse %(id_name)s')
+ self.assertEqual(m.group(3), ' ON duplicate update', 'group 3 not ON duplicate update, bug in RE_INSERT_VALUES?')
+
+ # cursor._executed must bee "insert into test (data) values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)"
+ # list args
+ data = range(10)
+ cursor.executemany("insert into test (data) values (%s)", data)
+ self.assertTrue(cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %s not in one query')
+
+ # dict args
+ data_dict = [{'data': i} for i in range(10)]
+ cursor.executemany("insert into test (data) values (%(data)s)", data_dict)
+ self.assertTrue(cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %(data)s not in one query')
+
+ # %% in column set
+ cursor.execute("""\
+ CREATE TABLE percent_test (
+ `A%` INTEGER,
+ `B%` INTEGER)""")
+ try:
+ q = "INSERT INTO percent_test (`A%%`, `B%%`) VALUES (%s, %s)"
+ self.assertIsNotNone(pymysql.cursors.RE_INSERT_VALUES.match(q))
+ cursor.executemany(q, [(3, 4), (5, 6)])
+ self.assertTrue(cursor._executed.endswith(b"(3, 4),(5, 6)"), "executemany with %% not in one query")
+ finally:
+ cursor.execute("DROP TABLE IF EXISTS percent_test")
diff --git a/gluon/contrib/pymysql/tests/test_err.py b/gluon/contrib/pymysql/tests/test_err.py
new file mode 100755
index 000000000..3468d1b10
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/test_err.py
@@ -0,0 +1,21 @@
+import unittest2
+
+from pymysql import err
+
+
+__all__ = ["TestRaiseException"]
+
+
+class TestRaiseException(unittest2.TestCase):
+
+ def test_raise_mysql_exception(self):
+ data = b"\xff\x15\x04Access denied"
+ with self.assertRaises(err.OperationalError) as cm:
+ err.raise_mysql_exception(data)
+ self.assertEqual(cm.exception.args, (1045, 'Access denied'))
+
+ def test_raise_mysql_exception_client_protocol_41(self):
+ data = b"\xff\x15\x04#28000Access denied"
+ with self.assertRaises(err.OperationalError) as cm:
+ err.raise_mysql_exception(data)
+ self.assertEqual(cm.exception.args, (1045, 'Access denied'))
diff --git a/gluon/contrib/pymysql/tests/test_issues.py b/gluon/contrib/pymysql/tests/test_issues.py
index 6f7fc3d1b..fe3e29845 100644
--- a/gluon/contrib/pymysql/tests/test_issues.py
+++ b/gluon/contrib/pymysql/tests/test_issues.py
@@ -1,8 +1,13 @@
+import datetime
+import time
+import warnings
+import sys
+
import pymysql
+from pymysql import cursors
+from pymysql._compat import text_type
from pymysql.tests import base
-import unittest
-
-import sys
+import unittest2
try:
import imp
@@ -10,17 +15,17 @@
except AttributeError:
pass
-import datetime
-# backwards compatibility:
-if not hasattr(unittest, "skip"):
- unittest.skip = lambda message: lambda f: f
+__all__ = ["TestOldIssues", "TestNewIssues", "TestGitHubIssues"]
class TestOldIssues(base.PyMySQLTestCase):
def test_issue_3(self):
""" undefined methods datetime_or_None, date_or_None """
conn = self.connections[0]
c = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue3")
c.execute("create table issue3 (d date, t time, dt datetime, ts timestamp)")
try:
c.execute("insert into issue3 (d, t, dt, ts) values (%s,%s,%s,%s)", (None, None, None, None))
@@ -39,6 +44,9 @@ def test_issue_4(self):
""" can't retrieve TIMESTAMP fields """
conn = self.connections[0]
c = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue4")
c.execute("create table issue4 (ts timestamp)")
try:
c.execute("insert into issue4 (ts) values (now())")
@@ -55,7 +63,10 @@ def test_issue_5(self):
def test_issue_6(self):
""" exception: TypeError: ord() expected a character, but string of length 0 found """
- conn = pymysql.connect(host="localhost",user="root",passwd="",db="mysql")
+ # ToDo: this test requires access to db 'mysql'.
+ kwargs = self.databases[0].copy()
+ kwargs['db'] = "mysql"
+ conn = pymysql.connect(**kwargs)
c = conn.cursor()
c.execute("select * from user")
conn.close()
@@ -64,8 +75,11 @@ def test_issue_8(self):
""" Primary Key and Index error when selecting data """
conn = self.connections[0]
c = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists test")
c.execute("""CREATE TABLE `test` (`station` int(10) NOT NULL DEFAULT '0', `dh`
-datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `echeance` int(1) NOT NULL
+datetime NOT NULL DEFAULT '2015-01-01 00:00:00', `echeance` int(1) NOT NULL
DEFAULT '0', `me` double DEFAULT NULL, `mo` double DEFAULT NULL, PRIMARY
KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""")
try:
@@ -82,18 +96,13 @@ def test_issue_9(self):
except DeprecationWarning:
self.fail()
- def test_issue_10(self):
- """ Allocate a variable to return when the exception handler is permissive """
- conn = self.connections[0]
- conn.errorhandler = lambda cursor, errorclass, errorvalue: None
- cur = conn.cursor()
- cur.execute( "create table t( n int )" )
- cur.execute( "create table t( n int )" )
-
def test_issue_13(self):
""" can't handle large result fields """
conn = self.connections[0]
cur = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ cur.execute("drop table if exists issue13")
try:
cur.execute("create table issue13 (t text)")
# ticket says 18k
@@ -106,18 +115,13 @@ def test_issue_13(self):
finally:
cur.execute("drop table issue13")
- def test_issue_14(self):
- """ typo in converters.py """
- self.assertEqual('1', pymysql.converters.escape_item(1, "utf8"))
- self.assertEqual('1', pymysql.converters.escape_item(1L, "utf8"))
-
- self.assertEqual('1', pymysql.converters.escape_object(1))
- self.assertEqual('1', pymysql.converters.escape_object(1L))
-
def test_issue_15(self):
""" query should be expanded before perform character encoding """
conn = self.connections[0]
c = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue15")
c.execute("create table issue15 (t varchar(32))")
try:
c.execute("insert into issue15 (t) values (%s)", (u'\xe4\xf6\xfc',))
@@ -130,6 +134,9 @@ def test_issue_16(self):
""" Patch for string and tuple escaping """
conn = self.connections[0]
c = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue16")
c.execute("create table issue16 (name varchar(32) primary key, email varchar(32))")
try:
c.execute("insert into issue16 (name, email) values ('pete', 'floydophone')")
@@ -138,20 +145,24 @@ def test_issue_16(self):
finally:
c.execute("drop table issue16")
- @unittest.skip("test_issue_17() requires a custom, legacy MySQL configuration and will not be run.")
+ @unittest2.skip("test_issue_17() requires a custom, legacy MySQL configuration and will not be run.")
def test_issue_17(self):
- """ could not connect mysql use passwod """
+ """could not connect mysql use passwod"""
conn = self.connections[0]
host = self.databases[0]["host"]
db = self.databases[0]["db"]
c = conn.cursor()
+
# grant access to a table to a user with a password
try:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue17")
c.execute("create table issue17 (x varchar(32) primary key)")
c.execute("insert into issue17 (x) values ('hello, world!')")
c.execute("grant all privileges on %s.issue17 to 'issue17user'@'%%' identified by '1234'" % db)
conn.commit()
-
+
conn2 = pymysql.connect(host=host, user="issue17user", passwd="1234", db=db)
c2 = conn2.cursor()
c2.execute("select x from issue17")
@@ -159,71 +170,71 @@ def test_issue_17(self):
finally:
c.execute("drop table issue17")
-def _uni(s, e):
- # hack for py3
- if sys.version_info[0] > 2:
- return unicode(bytes(s, sys.getdefaultencoding()), e)
- else:
- return unicode(s, e)
-
class TestNewIssues(base.PyMySQLTestCase):
def test_issue_34(self):
try:
pymysql.connect(host="localhost", port=1237, user="root")
self.fail()
- except pymysql.OperationalError, e:
+ except pymysql.OperationalError as e:
self.assertEqual(2003, e.args[0])
- except:
+ except Exception:
self.fail()
def test_issue_33(self):
- conn = pymysql.connect(host="localhost", user="root", db=self.databases[0]["db"], charset="utf8")
+ conn = pymysql.connect(charset="utf8", **self.databases[0])
+ self.safe_create_table(conn, u'hei\xdfe',
+ u'create table hei\xdfe (name varchar(32))')
c = conn.cursor()
- try:
- c.execute(_uni("create table hei\xc3\x9fe (name varchar(32))", "utf8"))
- c.execute(_uni("insert into hei\xc3\x9fe (name) values ('Pi\xc3\xb1ata')", "utf8"))
- c.execute(_uni("select name from hei\xc3\x9fe", "utf8"))
- self.assertEqual(_uni("Pi\xc3\xb1ata","utf8"), c.fetchone()[0])
- finally:
- c.execute(_uni("drop table hei\xc3\x9fe", "utf8"))
+ c.execute(u"insert into hei\xdfe (name) values ('Pi\xdfata')")
+ c.execute(u"select name from hei\xdfe")
+ self.assertEqual(u"Pi\xdfata", c.fetchone()[0])
- @unittest.skip("This test requires manual intervention")
+ @unittest2.skip("This test requires manual intervention")
def test_issue_35(self):
conn = self.connections[0]
c = conn.cursor()
- print "sudo killall -9 mysqld within the next 10 seconds"
+ print("sudo killall -9 mysqld within the next 10 seconds")
try:
c.execute("select sleep(10)")
self.fail()
- except pymysql.OperationalError, e:
+ except pymysql.OperationalError as e:
self.assertEqual(2013, e.args[0])
def test_issue_36(self):
- conn = self.connections[0]
+ # connection 0 is super user, connection 1 isn't
+ conn = self.connections[1]
c = conn.cursor()
- # kill connections[0]
c.execute("show processlist")
kill_id = None
- for id,user,host,db,command,time,state,info in c.fetchall():
+ for row in c.fetchall():
+ id = row[0]
+ info = row[7]
if info == "show processlist":
kill_id = id
break
+ self.assertEqual(kill_id, conn.thread_id())
# now nuke the connection
- conn.kill(kill_id)
+ self.connections[0].kill(kill_id)
# make sure this connection has broken
try:
c.execute("show tables")
self.fail()
- except:
+ except Exception:
pass
+ c.close()
+ conn.close()
+
# check the process list from the other connection
try:
- c = self.connections[1].cursor()
+ # Wait since Travis-CI sometimes fail this test.
+ time.sleep(0.1)
+
+ c = self.connections[0].cursor()
c.execute("show processlist")
ids = [row[0] for row in c.fetchall()]
self.assertFalse(kill_id in ids)
finally:
- del self.connections[0]
+ del self.connections[1]
def test_issue_37(self):
conn = self.connections[0]
@@ -237,8 +248,11 @@ def test_issue_38(self):
conn = self.connections[0]
c = conn.cursor()
datum = "a" * 1024 * 1023 # reduced size for most default mysql installs
-
+
try:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue38")
c.execute("create table issue38 (id integer, data mediumblob)")
c.execute("insert into issue38 values (1, %s)", (datum,))
finally:
@@ -247,8 +261,11 @@ def test_issue_38(self):
def disabled_test_issue_54(self):
conn = self.connections[0]
c = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue54")
big_sql = "select * from issue54 where "
- big_sql += " and ".join("%d=%d" % (i,i) for i in xrange(0, 100000))
+ big_sql += " and ".join("%d=%d" % (i,i) for i in range(0, 100000))
try:
c.execute("create table issue54 (id integer primary key)")
@@ -260,10 +277,14 @@ def disabled_test_issue_54(self):
class TestGitHubIssues(base.PyMySQLTestCase):
def test_issue_66(self):
+ """ 'Connection' object has no attribute 'insert_id' """
conn = self.connections[0]
c = conn.cursor()
self.assertEqual(0, conn.insert_id())
try:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists issue66")
c.execute("create table issue66 (id integer primary key auto_increment, x integer)")
c.execute("insert into issue66 (x) values (1)")
c.execute("insert into issue66 (x) values (1)")
@@ -271,8 +292,224 @@ def test_issue_66(self):
finally:
c.execute("drop table issue66")
-__all__ = ["TestOldIssues", "TestNewIssues", "TestGitHubIssues"]
+ def test_issue_79(self):
+ """ Duplicate field overwrites the previous one in the result of DictCursor """
+ conn = self.connections[0]
+ c = conn.cursor(pymysql.cursors.DictCursor)
+
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ c.execute("drop table if exists a")
+ c.execute("drop table if exists b")
+ c.execute("""CREATE TABLE a (id int, value int)""")
+ c.execute("""CREATE TABLE b (id int, value int)""")
+
+ a=(1,11)
+ b=(1,22)
+ try:
+ c.execute("insert into a values (%s, %s)", a)
+ c.execute("insert into b values (%s, %s)", b)
+
+ c.execute("SELECT * FROM a inner join b on a.id = b.id")
+ r = c.fetchall()[0]
+ self.assertEqual(r['id'], 1)
+ self.assertEqual(r['value'], 11)
+ self.assertEqual(r['b.value'], 22)
+ finally:
+ c.execute("drop table a")
+ c.execute("drop table b")
+
+ def test_issue_95(self):
+ """ Leftover trailing OK packet for "CALL my_sp" queries """
+ conn = self.connections[0]
+ cur = conn.cursor()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ cur.execute("DROP PROCEDURE IF EXISTS `foo`")
+ cur.execute("""CREATE PROCEDURE `foo` ()
+ BEGIN
+ SELECT 1;
+ END""")
+ try:
+ cur.execute("""CALL foo()""")
+ cur.execute("""SELECT 1""")
+ self.assertEqual(cur.fetchone()[0], 1)
+ finally:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ cur.execute("DROP PROCEDURE IF EXISTS `foo`")
+
+ def test_issue_114(self):
+ """ autocommit is not set after reconnecting with ping() """
+ conn = pymysql.connect(charset="utf8", **self.databases[0])
+ conn.autocommit(False)
+ c = conn.cursor()
+ c.execute("""select @@autocommit;""")
+ self.assertFalse(c.fetchone()[0])
+ conn.close()
+ conn.ping()
+ c.execute("""select @@autocommit;""")
+ self.assertFalse(c.fetchone()[0])
+ conn.close()
+
+ # Ensure autocommit() is still working
+ conn = pymysql.connect(charset="utf8", **self.databases[0])
+ c = conn.cursor()
+ c.execute("""select @@autocommit;""")
+ self.assertFalse(c.fetchone()[0])
+ conn.close()
+ conn.ping()
+ conn.autocommit(True)
+ c.execute("""select @@autocommit;""")
+ self.assertTrue(c.fetchone()[0])
+ conn.close()
+
+ def test_issue_175(self):
+ """ The number of fields returned by server is read in wrong way """
+ conn = self.connections[0]
+ cur = conn.cursor()
+ for length in (200, 300):
+ columns = ', '.join('c{0} integer'.format(i) for i in range(length))
+ sql = 'create table test_field_count ({0})'.format(columns)
+ try:
+ cur.execute(sql)
+ cur.execute('select * from test_field_count')
+ assert len(cur.description) == length
+ finally:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+ cur.execute('drop table if exists test_field_count')
+
+ def test_issue_321(self):
+ """ Test iterable as query argument. """
+ conn = pymysql.connect(charset="utf8", **self.databases[0])
+ self.safe_create_table(
+ conn, "issue321",
+ "create table issue321 (value_1 varchar(1), value_2 varchar(1))")
+
+ sql_insert = "insert into issue321 (value_1, value_2) values (%s, %s)"
+ sql_dict_insert = ("insert into issue321 (value_1, value_2) "
+ "values (%(value_1)s, %(value_2)s)")
+ sql_select = ("select * from issue321 where "
+ "value_1 in %s and value_2=%s")
+ data = [
+ [(u"a", ), u"\u0430"],
+ [[u"b"], u"\u0430"],
+ {"value_1": [[u"c"]], "value_2": u"\u0430"}
+ ]
+ cur = conn.cursor()
+ self.assertEqual(cur.execute(sql_insert, data[0]), 1)
+ self.assertEqual(cur.execute(sql_insert, data[1]), 1)
+ self.assertEqual(cur.execute(sql_dict_insert, data[2]), 1)
+ self.assertEqual(
+ cur.execute(sql_select, [(u"a", u"b", u"c"), u"\u0430"]), 3)
+ self.assertEqual(cur.fetchone(), (u"a", u"\u0430"))
+ self.assertEqual(cur.fetchone(), (u"b", u"\u0430"))
+ self.assertEqual(cur.fetchone(), (u"c", u"\u0430"))
+
+ def test_issue_364(self):
+ """ Test mixed unicode/binary arguments in executemany. """
+ conn = pymysql.connect(charset="utf8", **self.databases[0])
+ self.safe_create_table(
+ conn, "issue364",
+ "create table issue364 (value_1 binary(3), value_2 varchar(3)) "
+ "engine=InnoDB default charset=utf8")
+
+ sql = "insert into issue364 (value_1, value_2) values (%s, %s)"
+ usql = u"insert into issue364 (value_1, value_2) values (%s, %s)"
+ values = [pymysql.Binary(b"\x00\xff\x00"), u"\xe4\xf6\xfc"]
+
+ # test single insert and select
+ cur = conn.cursor()
+ cur.execute(sql, args=values)
+ cur.execute("select * from issue364")
+ self.assertEqual(cur.fetchone(), tuple(values))
+
+ # test single insert unicode query
+ cur.execute(usql, args=values)
+
+ # test multi insert and select
+ cur.executemany(sql, args=(values, values, values))
+ cur.execute("select * from issue364")
+ for row in cur.fetchall():
+ self.assertEqual(row, tuple(values))
+
+ # test multi insert with unicode query
+ cur.executemany(usql, args=(values, values, values))
+
+ def test_issue_363(self):
+ """ Test binary / geometry types. """
+ conn = pymysql.connect(charset="utf8", **self.databases[0])
+ self.safe_create_table(
+ conn, "issue363",
+ "CREATE TABLE issue363 ( "
+ "id INTEGER PRIMARY KEY, geom LINESTRING NOT NULL, "
+ "SPATIAL KEY geom (geom)) "
+ "ENGINE=MyISAM default charset=utf8")
+
+ cur = conn.cursor()
+ query = ("INSERT INTO issue363 (id, geom) VALUES"
+ "(1998, GeomFromText('LINESTRING(1.1 1.1,2.2 2.2)'))")
+ # From MySQL 5.7, ST_GeomFromText is added and GeomFromText is deprecated.
+ if self.mysql_server_is(conn, (5, 7, 0)):
+ with self.assertWarns(pymysql.err.Warning) as cm:
+ cur.execute(query)
+ else:
+ cur.execute(query)
+
+ # select WKT
+ query = "SELECT AsText(geom) FROM issue363"
+ if self.mysql_server_is(conn, (5, 7, 0)):
+ with self.assertWarns(pymysql.err.Warning) as cm:
+ cur.execute(query)
+ else:
+ cur.execute(query)
+ row = cur.fetchone()
+ self.assertEqual(row, ("LINESTRING(1.1 1.1,2.2 2.2)", ))
+
+ # select WKB
+ query = "SELECT AsBinary(geom) FROM issue363"
+ if self.mysql_server_is(conn, (5, 7, 0)):
+ with self.assertWarns(pymysql.err.Warning) as cm:
+ cur.execute(query)
+ else:
+ cur.execute(query)
+ row = cur.fetchone()
+ self.assertEqual(row,
+ (b"\x01\x02\x00\x00\x00\x02\x00\x00\x00"
+ b"\x9a\x99\x99\x99\x99\x99\xf1?"
+ b"\x9a\x99\x99\x99\x99\x99\xf1?"
+ b"\x9a\x99\x99\x99\x99\x99\x01@"
+ b"\x9a\x99\x99\x99\x99\x99\x01@", ))
+
+ # select internal binary
+ cur.execute("SELECT geom FROM issue363")
+ row = cur.fetchone()
+ # don't assert the exact internal binary value, as it could
+ # vary across implementations
+ self.assertTrue(isinstance(row[0], bytes))
+
+ def test_issue_491(self):
+ """ Test warning propagation """
+ conn = pymysql.connect(charset="utf8", **self.databases[0])
+
+ with warnings.catch_warnings():
+ # Ignore all warnings other than pymysql generated ones
+ warnings.simplefilter("ignore")
+ warnings.simplefilter("error", category=pymysql.Warning)
-if __name__ == "__main__":
- import unittest
- unittest.main()
+ # verify for both buffered and unbuffered cursor types
+ for cursor_class in (cursors.Cursor, cursors.SSCursor):
+ c = conn.cursor(cursor_class)
+ try:
+ c.execute("SELECT CAST('124b' AS SIGNED)")
+ c.fetchall()
+ except pymysql.Warning as e:
+ # Warnings should have errorcode and string message, just like exceptions
+ self.assertEqual(len(e.args), 2)
+ self.assertEqual(e.args[0], 1292)
+ self.assertTrue(isinstance(e.args[1], text_type))
+ else:
+ self.fail("Should raise Warning")
+ finally:
+ c.close()
diff --git a/gluon/contrib/pymysql/tests/test_load_local.py b/gluon/contrib/pymysql/tests/test_load_local.py
new file mode 100755
index 000000000..85fd94ea5
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/test_load_local.py
@@ -0,0 +1,93 @@
+from pymysql import cursors, OperationalError, Warning
+from pymysql.tests import base
+
+import os
+import warnings
+
+__all__ = ["TestLoadLocal"]
+
+
+class TestLoadLocal(base.PyMySQLTestCase):
+ def test_no_file(self):
+ """Test load local infile when the file does not exist"""
+ conn = self.connections[0]
+ c = conn.cursor()
+ c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)")
+ try:
+ self.assertRaises(
+ OperationalError,
+ c.execute,
+ ("LOAD DATA LOCAL INFILE 'no_data.txt' INTO TABLE "
+ "test_load_local fields terminated by ','")
+ )
+ finally:
+ c.execute("DROP TABLE test_load_local")
+ c.close()
+
+ def test_load_file(self):
+ """Test load local infile with a valid file"""
+ conn = self.connections[0]
+ c = conn.cursor()
+ c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)")
+ filename = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ 'data',
+ 'load_local_data.txt')
+ try:
+ c.execute(
+ ("LOAD DATA LOCAL INFILE '{0}' INTO TABLE " +
+ "test_load_local FIELDS TERMINATED BY ','").format(filename)
+ )
+ c.execute("SELECT COUNT(*) FROM test_load_local")
+ self.assertEqual(22749, c.fetchone()[0])
+ finally:
+ c.execute("DROP TABLE test_load_local")
+
+ def test_unbuffered_load_file(self):
+ """Test unbuffered load local infile with a valid file"""
+ conn = self.connections[0]
+ c = conn.cursor(cursors.SSCursor)
+ c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)")
+ filename = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ 'data',
+ 'load_local_data.txt')
+ try:
+ c.execute(
+ ("LOAD DATA LOCAL INFILE '{0}' INTO TABLE " +
+ "test_load_local FIELDS TERMINATED BY ','").format(filename)
+ )
+ c.execute("SELECT COUNT(*) FROM test_load_local")
+ self.assertEqual(22749, c.fetchone()[0])
+ finally:
+ c.close()
+ conn.close()
+ conn.connect()
+ c = conn.cursor()
+ c.execute("DROP TABLE test_load_local")
+
+ def test_load_warnings(self):
+ """Test load local infile produces the appropriate warnings"""
+ conn = self.connections[0]
+ c = conn.cursor()
+ c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)")
+ filename = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ 'data',
+ 'load_local_warn_data.txt')
+ try:
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always')
+ c.execute(
+ ("LOAD DATA LOCAL INFILE '{0}' INTO TABLE " +
+ "test_load_local FIELDS TERMINATED BY ','").format(filename)
+ )
+ self.assertEqual(w[0].category, Warning)
+ expected_message = "Incorrect integer value"
+ if expected_message not in str(w[-1].message):
+ self.fail("%r not in %r" % (expected_message, w[-1].message))
+ finally:
+ c.execute("DROP TABLE test_load_local")
+ c.close()
+
+
+if __name__ == "__main__":
+ import unittest
+ unittest.main()
diff --git a/gluon/contrib/pymysql/tests/test_nextset.py b/gluon/contrib/pymysql/tests/test_nextset.py
new file mode 100755
index 000000000..cdb6754f5
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/test_nextset.py
@@ -0,0 +1,68 @@
+import unittest2
+
+from pymysql.tests import base
+from pymysql import util
+
+
+class TestNextset(base.PyMySQLTestCase):
+
+ def setUp(self):
+ super(TestNextset, self).setUp()
+ self.con = self.connections[0]
+
+ def test_nextset(self):
+ cur = self.con.cursor()
+ cur.execute("SELECT 1; SELECT 2;")
+ self.assertEqual([(1,)], list(cur))
+
+ r = cur.nextset()
+ self.assertTrue(r)
+
+ self.assertEqual([(2,)], list(cur))
+ self.assertIsNone(cur.nextset())
+
+ def test_skip_nextset(self):
+ cur = self.con.cursor()
+ cur.execute("SELECT 1; SELECT 2;")
+ self.assertEqual([(1,)], list(cur))
+
+ cur.execute("SELECT 42")
+ self.assertEqual([(42,)], list(cur))
+
+ def test_ok_and_next(self):
+ cur = self.con.cursor()
+ cur.execute("SELECT 1; commit; SELECT 2;")
+ self.assertEqual([(1,)], list(cur))
+ self.assertTrue(cur.nextset())
+ self.assertTrue(cur.nextset())
+ self.assertEqual([(2,)], list(cur))
+ self.assertFalse(bool(cur.nextset()))
+
+ @unittest2.expectedFailure
+ def test_multi_cursor(self):
+ cur1 = self.con.cursor()
+ cur2 = self.con.cursor()
+
+ cur1.execute("SELECT 1; SELECT 2;")
+ cur2.execute("SELECT 42")
+
+ self.assertEqual([(1,)], list(cur1))
+ self.assertEqual([(42,)], list(cur2))
+
+ r = cur1.nextset()
+ self.assertTrue(r)
+
+ self.assertEqual([(2,)], list(cur1))
+ self.assertIsNone(cur1.nextset())
+
+ def test_multi_statement_warnings(self):
+ cursor = self.con.cursor()
+
+ try:
+ cursor.execute('DROP TABLE IF EXISTS a; '
+ 'DROP TABLE IF EXISTS b;')
+ except TypeError:
+ self.fail()
+
+ #TODO: How about SSCursor and nextset?
+ # It's very hard to implement correctly...
diff --git a/gluon/contrib/pymysql/tests/test_optionfile.py b/gluon/contrib/pymysql/tests/test_optionfile.py
new file mode 100755
index 000000000..abc63ea1c
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/test_optionfile.py
@@ -0,0 +1,32 @@
+from pymysql.optionfile import Parser
+from unittest import TestCase
+from pymysql._compat import PY2
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+
+__all__ = ['TestParser']
+
+
+_cfg_file = (r"""
+[default]
+string = foo
+quoted = "bar"
+single_quoted = 'foobar'
+""")
+
+
+class TestParser(TestCase):
+
+ def test_string(self):
+ parser = Parser()
+ if PY2:
+ parser.readfp(StringIO(_cfg_file))
+ else:
+ parser.read_file(StringIO(_cfg_file))
+ self.assertEqual(parser.get("default", "string"), "foo")
+ self.assertEqual(parser.get("default", "quoted"), "bar")
+ self.assertEqual(parser.get("default", "single_quoted"), "foobar")
diff --git a/gluon/contrib/pymysql/tests/thirdparty/__init__.py b/gluon/contrib/pymysql/tests/thirdparty/__init__.py
new file mode 100755
index 000000000..6d59e1127
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/thirdparty/__init__.py
@@ -0,0 +1,8 @@
+from .test_MySQLdb import *
+
+if __name__ == "__main__":
+ try:
+ import unittest2 as unittest
+ except ImportError:
+ import unittest
+ unittest.main()
diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/__init__.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/__init__.py
new file mode 100755
index 000000000..e4237c69a
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/__init__.py
@@ -0,0 +1,7 @@
+from .test_MySQLdb_capabilities import test_MySQLdb as test_capabilities
+from .test_MySQLdb_nonstandard import *
+from .test_MySQLdb_dbapi20 import test_MySQLdb as test_dbapi2
+
+if __name__ == "__main__":
+ import unittest
+ unittest.main()
diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py
new file mode 100755
index 000000000..e4aae2064
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py
@@ -0,0 +1,298 @@
+#!/usr/bin/env python -O
+""" Script to test database capabilities and the DB-API interface
+ for functionality and memory leaks.
+
+ Adapted from a script by M-A Lemburg.
+
+"""
+import sys
+from time import time
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+PY2 = sys.version_info[0] == 2
+
+class DatabaseTest(unittest.TestCase):
+
+ db_module = None
+ connect_args = ()
+ connect_kwargs = dict(use_unicode=True, charset="utf8")
+ create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8"
+ rows = 10
+ debug = False
+
+ def setUp(self):
+ db = self.db_module.connect(*self.connect_args, **self.connect_kwargs)
+ self.connection = db
+ self.cursor = db.cursor()
+ self.BLOBText = ''.join([chr(i) for i in range(256)] * 100);
+ if PY2:
+ self.BLOBUText = unicode().join(unichr(i) for i in range(16834))
+ else:
+ self.BLOBUText = "".join(chr(i) for i in range(16834))
+ data = bytearray(range(256)) * 16
+ self.BLOBBinary = self.db_module.Binary(data)
+
+ leak_test = True
+
+ def tearDown(self):
+ if self.leak_test:
+ import gc
+ del self.cursor
+ orphans = gc.collect()
+ self.assertFalse(orphans, "%d orphaned objects found after deleting cursor" % orphans)
+
+ del self.connection
+ orphans = gc.collect()
+ self.assertFalse(orphans, "%d orphaned objects found after deleting connection" % orphans)
+
+ def table_exists(self, name):
+ try:
+ self.cursor.execute('select * from %s where 1=0' % name)
+ except Exception:
+ return False
+ else:
+ return True
+
+ def quote_identifier(self, ident):
+ return '"%s"' % ident
+
+ def new_table_name(self):
+ i = id(self.cursor)
+ while True:
+ name = self.quote_identifier('tb%08x' % i)
+ if not self.table_exists(name):
+ return name
+ i = i + 1
+
+ def create_table(self, columndefs):
+
+ """ Create a table using a list of column definitions given in
+ columndefs.
+
+ generator must be a function taking arguments (row_number,
+ col_number) returning a suitable data object for insertion
+ into the table.
+
+ """
+ self.table = self.new_table_name()
+ self.cursor.execute('CREATE TABLE %s (%s) %s' %
+ (self.table,
+ ',\n'.join(columndefs),
+ self.create_table_extra))
+
+ def check_data_integrity(self, columndefs, generator):
+ # insert
+ self.create_table(columndefs)
+ insert_statement = ('INSERT INTO %s VALUES (%s)' %
+ (self.table,
+ ','.join(['%s'] * len(columndefs))))
+ data = [ [ generator(i,j) for j in range(len(columndefs)) ]
+ for i in range(self.rows) ]
+ if self.debug:
+ print(data)
+ self.cursor.executemany(insert_statement, data)
+ self.connection.commit()
+ # verify
+ self.cursor.execute('select * from %s' % self.table)
+ l = self.cursor.fetchall()
+ if self.debug:
+ print(l)
+ self.assertEqual(len(l), self.rows)
+ try:
+ for i in range(self.rows):
+ for j in range(len(columndefs)):
+ self.assertEqual(l[i][j], generator(i,j))
+ finally:
+ if not self.debug:
+ self.cursor.execute('drop table %s' % (self.table))
+
+ def test_transactions(self):
+ columndefs = ( 'col1 INT', 'col2 VARCHAR(255)')
+ def generator(row, col):
+ if col == 0: return row
+ else: return ('%i' % (row%10))*255
+ self.create_table(columndefs)
+ insert_statement = ('INSERT INTO %s VALUES (%s)' %
+ (self.table,
+ ','.join(['%s'] * len(columndefs))))
+ data = [ [ generator(i,j) for j in range(len(columndefs)) ]
+ for i in range(self.rows) ]
+ self.cursor.executemany(insert_statement, data)
+ # verify
+ self.connection.commit()
+ self.cursor.execute('select * from %s' % self.table)
+ l = self.cursor.fetchall()
+ self.assertEqual(len(l), self.rows)
+ for i in range(self.rows):
+ for j in range(len(columndefs)):
+ self.assertEqual(l[i][j], generator(i,j))
+ delete_statement = 'delete from %s where col1=%%s' % self.table
+ self.cursor.execute(delete_statement, (0,))
+ self.cursor.execute('select col1 from %s where col1=%s' % \
+ (self.table, 0))
+ l = self.cursor.fetchall()
+ self.assertFalse(l, "DELETE didn't work")
+ self.connection.rollback()
+ self.cursor.execute('select col1 from %s where col1=%s' % \
+ (self.table, 0))
+ l = self.cursor.fetchall()
+ self.assertTrue(len(l) == 1, "ROLLBACK didn't work")
+ self.cursor.execute('drop table %s' % (self.table))
+
+ def test_truncation(self):
+ columndefs = ( 'col1 INT', 'col2 VARCHAR(255)')
+ def generator(row, col):
+ if col == 0: return row
+ else: return ('%i' % (row%10))*((255-self.rows//2)+row)
+ self.create_table(columndefs)
+ insert_statement = ('INSERT INTO %s VALUES (%s)' %
+ (self.table,
+ ','.join(['%s'] * len(columndefs))))
+
+ try:
+ self.cursor.execute(insert_statement, (0, '0'*256))
+ except Warning:
+ if self.debug: print(self.cursor.messages)
+ except self.connection.DataError:
+ pass
+ else:
+ self.fail("Over-long column did not generate warnings/exception with single insert")
+
+ self.connection.rollback()
+
+ try:
+ for i in range(self.rows):
+ data = []
+ for j in range(len(columndefs)):
+ data.append(generator(i,j))
+ self.cursor.execute(insert_statement,tuple(data))
+ except Warning:
+ if self.debug: print(self.cursor.messages)
+ except self.connection.DataError:
+ pass
+ else:
+ self.fail("Over-long columns did not generate warnings/exception with execute()")
+
+ self.connection.rollback()
+
+ try:
+ data = [ [ generator(i,j) for j in range(len(columndefs)) ]
+ for i in range(self.rows) ]
+ self.cursor.executemany(insert_statement, data)
+ except Warning:
+ if self.debug: print(self.cursor.messages)
+ except self.connection.DataError:
+ pass
+ else:
+ self.fail("Over-long columns did not generate warnings/exception with executemany()")
+
+ self.connection.rollback()
+ self.cursor.execute('drop table %s' % (self.table))
+
+ def test_CHAR(self):
+ # Character data
+ def generator(row,col):
+ return ('%i' % ((row+col) % 10)) * 255
+ self.check_data_integrity(
+ ('col1 char(255)','col2 char(255)'),
+ generator)
+
+ def test_INT(self):
+ # Number data
+ def generator(row,col):
+ return row*row
+ self.check_data_integrity(
+ ('col1 INT',),
+ generator)
+
+ def test_DECIMAL(self):
+ # DECIMAL
+ def generator(row,col):
+ from decimal import Decimal
+ return Decimal("%d.%02d" % (row, col))
+ self.check_data_integrity(
+ ('col1 DECIMAL(5,2)',),
+ generator)
+
+ def test_DATE(self):
+ ticks = time()
+ def generator(row,col):
+ return self.db_module.DateFromTicks(ticks+row*86400-col*1313)
+ self.check_data_integrity(
+ ('col1 DATE',),
+ generator)
+
+ def test_TIME(self):
+ ticks = time()
+ def generator(row,col):
+ return self.db_module.TimeFromTicks(ticks+row*86400-col*1313)
+ self.check_data_integrity(
+ ('col1 TIME',),
+ generator)
+
+ def test_DATETIME(self):
+ ticks = time()
+ def generator(row,col):
+ return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313)
+ self.check_data_integrity(
+ ('col1 DATETIME',),
+ generator)
+
+ def test_TIMESTAMP(self):
+ ticks = time()
+ def generator(row,col):
+ return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313)
+ self.check_data_integrity(
+ ('col1 TIMESTAMP',),
+ generator)
+
+ def test_fractional_TIMESTAMP(self):
+ ticks = time()
+ def generator(row,col):
+ return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313+row*0.7*col/3.0)
+ self.check_data_integrity(
+ ('col1 TIMESTAMP',),
+ generator)
+
+ def test_LONG(self):
+ def generator(row,col):
+ if col == 0:
+ return row
+ else:
+ return self.BLOBUText # 'BLOB Text ' * 1024
+ self.check_data_integrity(
+ ('col1 INT', 'col2 LONG'),
+ generator)
+
+ def test_TEXT(self):
+ def generator(row,col):
+ if col == 0:
+ return row
+ else:
+ return self.BLOBUText[:5192] # 'BLOB Text ' * 1024
+ self.check_data_integrity(
+ ('col1 INT', 'col2 TEXT'),
+ generator)
+
+ def test_LONG_BYTE(self):
+ def generator(row,col):
+ if col == 0:
+ return row
+ else:
+ return self.BLOBBinary # 'BLOB\000Binary ' * 1024
+ self.check_data_integrity(
+ ('col1 INT','col2 LONG BYTE'),
+ generator)
+
+ def test_BLOB(self):
+ def generator(row,col):
+ if col == 0:
+ return row
+ else:
+ return self.BLOBBinary # 'BLOB\000Binary ' * 1024
+ self.check_data_integrity(
+ ('col1 INT','col2 BLOB'),
+ generator)
diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py
new file mode 100755
index 000000000..e86652483
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py
@@ -0,0 +1,856 @@
+#!/usr/bin/env python
+''' Python DB API 2.0 driver compliance unit test suite.
+
+ This software is Public Domain and may be used without restrictions.
+
+ "Now we have booze and barflies entering the discussion, plus rumours of
+ DBAs on drugs... and I won't tell you what flashes through my mind each
+ time I read the subject line with 'Anal Compliance' in it. All around
+ this is turning out to be a thoroughly unwholesome unit test."
+
+ -- Ian Bicking
+'''
+
+__rcs_id__ = '$Id$'
+__version__ = '$Revision$'[11:-2]
+__author__ = 'Stuart Bishop '
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+import time
+
+# $Log$
+# Revision 1.1.2.1 2006/02/25 03:44:32 adustman
+# Generic DB-API unit test module
+#
+# Revision 1.10 2003/10/09 03:14:14 zenzen
+# Add test for DB API 2.0 optional extension, where database exceptions
+# are exposed as attributes on the Connection object.
+#
+# Revision 1.9 2003/08/13 01:16:36 zenzen
+# Minor tweak from Stefan Fleiter
+#
+# Revision 1.8 2003/04/10 00:13:25 zenzen
+# Changes, as per suggestions by M.-A. Lemburg
+# - Add a table prefix, to ensure namespace collisions can always be avoided
+#
+# Revision 1.7 2003/02/26 23:33:37 zenzen
+# Break out DDL into helper functions, as per request by David Rushby
+#
+# Revision 1.6 2003/02/21 03:04:33 zenzen
+# Stuff from Henrik Ekelund:
+# added test_None
+# added test_nextset & hooks
+#
+# Revision 1.5 2003/02/17 22:08:43 zenzen
+# Implement suggestions and code from Henrik Eklund - test that cursor.arraysize
+# defaults to 1 & generic cursor.callproc test added
+#
+# Revision 1.4 2003/02/15 00:16:33 zenzen
+# Changes, as per suggestions and bug reports by M.-A. Lemburg,
+# Matthew T. Kromer, Federico Di Gregorio and Daniel Dittmar
+# - Class renamed
+# - Now a subclass of TestCase, to avoid requiring the driver stub
+# to use multiple inheritance
+# - Reversed the polarity of buggy test in test_description
+# - Test exception heirarchy correctly
+# - self.populate is now self._populate(), so if a driver stub
+# overrides self.ddl1 this change propogates
+# - VARCHAR columns now have a width, which will hopefully make the
+# DDL even more portible (this will be reversed if it causes more problems)
+# - cursor.rowcount being checked after various execute and fetchXXX methods
+# - Check for fetchall and fetchmany returning empty lists after results
+# are exhausted (already checking for empty lists if select retrieved
+# nothing
+# - Fix bugs in test_setoutputsize_basic and test_setinputsizes
+#
+
+class DatabaseAPI20Test(unittest.TestCase):
+ ''' Test a database self.driver for DB API 2.0 compatibility.
+ This implementation tests Gadfly, but the TestCase
+ is structured so that other self.drivers can subclass this
+ test case to ensure compiliance with the DB-API. It is
+ expected that this TestCase may be expanded in the future
+ if ambiguities or edge conditions are discovered.
+
+ The 'Optional Extensions' are not yet being tested.
+
+ self.drivers should subclass this test, overriding setUp, tearDown,
+ self.driver, connect_args and connect_kw_args. Class specification
+ should be as follows:
+
+ import dbapi20
+ class mytest(dbapi20.DatabaseAPI20Test):
+ [...]
+
+ Don't 'import DatabaseAPI20Test from dbapi20', or you will
+ confuse the unit tester - just 'import dbapi20'.
+ '''
+
+ # The self.driver module. This should be the module where the 'connect'
+ # method is to be found
+ driver = None
+ connect_args = () # List of arguments to pass to connect
+ connect_kw_args = {} # Keyword arguments for connect
+ table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables
+
+ ddl1 = 'create table %sbooze (name varchar(20))' % table_prefix
+ ddl2 = 'create table %sbarflys (name varchar(20))' % table_prefix
+ xddl1 = 'drop table %sbooze' % table_prefix
+ xddl2 = 'drop table %sbarflys' % table_prefix
+
+ lowerfunc = 'lower' # Name of stored procedure to convert string->lowercase
+
+ # Some drivers may need to override these helpers, for example adding
+ # a 'commit' after the execute.
+ def executeDDL1(self,cursor):
+ cursor.execute(self.ddl1)
+
+ def executeDDL2(self,cursor):
+ cursor.execute(self.ddl2)
+
+ def setUp(self):
+ ''' self.drivers should override this method to perform required setup
+ if any is necessary, such as creating the database.
+ '''
+ pass
+
+ def tearDown(self):
+ ''' self.drivers should override this method to perform required cleanup
+ if any is necessary, such as deleting the test database.
+ The default drops the tables that may be created.
+ '''
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ for ddl in (self.xddl1,self.xddl2):
+ try:
+ cur.execute(ddl)
+ con.commit()
+ except self.driver.Error:
+ # Assume table didn't exist. Other tests will check if
+ # execute is busted.
+ pass
+ finally:
+ con.close()
+
+ def _connect(self):
+ try:
+ return self.driver.connect(
+ *self.connect_args,**self.connect_kw_args
+ )
+ except AttributeError:
+ self.fail("No connect method found in self.driver module")
+
+ def test_connect(self):
+ con = self._connect()
+ con.close()
+
+ def test_apilevel(self):
+ try:
+ # Must exist
+ apilevel = self.driver.apilevel
+ # Must equal 2.0
+ self.assertEqual(apilevel,'2.0')
+ except AttributeError:
+ self.fail("Driver doesn't define apilevel")
+
+ def test_threadsafety(self):
+ try:
+ # Must exist
+ threadsafety = self.driver.threadsafety
+ # Must be a valid value
+ self.assertTrue(threadsafety in (0,1,2,3))
+ except AttributeError:
+ self.fail("Driver doesn't define threadsafety")
+
+ def test_paramstyle(self):
+ try:
+ # Must exist
+ paramstyle = self.driver.paramstyle
+ # Must be a valid value
+ self.assertTrue(paramstyle in (
+ 'qmark','numeric','named','format','pyformat'
+ ))
+ except AttributeError:
+ self.fail("Driver doesn't define paramstyle")
+
+ def test_Exceptions(self):
+ # Make sure required exceptions exist, and are in the
+ # defined heirarchy.
+ self.assertTrue(issubclass(self.driver.Warning,Exception))
+ self.assertTrue(issubclass(self.driver.Error,Exception))
+ self.assertTrue(
+ issubclass(self.driver.InterfaceError,self.driver.Error)
+ )
+ self.assertTrue(
+ issubclass(self.driver.DatabaseError,self.driver.Error)
+ )
+ self.assertTrue(
+ issubclass(self.driver.OperationalError,self.driver.Error)
+ )
+ self.assertTrue(
+ issubclass(self.driver.IntegrityError,self.driver.Error)
+ )
+ self.assertTrue(
+ issubclass(self.driver.InternalError,self.driver.Error)
+ )
+ self.assertTrue(
+ issubclass(self.driver.ProgrammingError,self.driver.Error)
+ )
+ self.assertTrue(
+ issubclass(self.driver.NotSupportedError,self.driver.Error)
+ )
+
+ def test_ExceptionsAsConnectionAttributes(self):
+ # OPTIONAL EXTENSION
+ # Test for the optional DB API 2.0 extension, where the exceptions
+ # are exposed as attributes on the Connection object
+ # I figure this optional extension will be implemented by any
+ # driver author who is using this test suite, so it is enabled
+ # by default.
+ con = self._connect()
+ drv = self.driver
+ self.assertTrue(con.Warning is drv.Warning)
+ self.assertTrue(con.Error is drv.Error)
+ self.assertTrue(con.InterfaceError is drv.InterfaceError)
+ self.assertTrue(con.DatabaseError is drv.DatabaseError)
+ self.assertTrue(con.OperationalError is drv.OperationalError)
+ self.assertTrue(con.IntegrityError is drv.IntegrityError)
+ self.assertTrue(con.InternalError is drv.InternalError)
+ self.assertTrue(con.ProgrammingError is drv.ProgrammingError)
+ self.assertTrue(con.NotSupportedError is drv.NotSupportedError)
+
+
+ def test_commit(self):
+ con = self._connect()
+ try:
+ # Commit must work, even if it doesn't do anything
+ con.commit()
+ finally:
+ con.close()
+
+ def test_rollback(self):
+ con = self._connect()
+ # If rollback is defined, it should either work or throw
+ # the documented exception
+ if hasattr(con,'rollback'):
+ try:
+ con.rollback()
+ except self.driver.NotSupportedError:
+ pass
+
+ def test_cursor(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ finally:
+ con.close()
+
+ def test_cursor_isolation(self):
+ con = self._connect()
+ try:
+ # Make sure cursors created from the same connection have
+ # the documented transaction isolation level
+ cur1 = con.cursor()
+ cur2 = con.cursor()
+ self.executeDDL1(cur1)
+ cur1.execute("insert into %sbooze values ('Victoria Bitter')" % (
+ self.table_prefix
+ ))
+ cur2.execute("select name from %sbooze" % self.table_prefix)
+ booze = cur2.fetchall()
+ self.assertEqual(len(booze),1)
+ self.assertEqual(len(booze[0]),1)
+ self.assertEqual(booze[0][0],'Victoria Bitter')
+ finally:
+ con.close()
+
+ def test_description(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self.executeDDL1(cur)
+ self.assertEqual(cur.description,None,
+ 'cursor.description should be none after executing a '
+ 'statement that can return no rows (such as DDL)'
+ )
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ self.assertEqual(len(cur.description),1,
+ 'cursor.description describes too many columns'
+ )
+ self.assertEqual(len(cur.description[0]),7,
+ 'cursor.description[x] tuples must have 7 elements'
+ )
+ self.assertEqual(cur.description[0][0].lower(),'name',
+ 'cursor.description[x][0] must return column name'
+ )
+ self.assertEqual(cur.description[0][1],self.driver.STRING,
+ 'cursor.description[x][1] must return column type. Got %r'
+ % cur.description[0][1]
+ )
+
+ # Make sure self.description gets reset
+ self.executeDDL2(cur)
+ self.assertEqual(cur.description,None,
+ 'cursor.description not being set to None when executing '
+ 'no-result statements (eg. DDL)'
+ )
+ finally:
+ con.close()
+
+ def test_rowcount(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self.executeDDL1(cur)
+ self.assertEqual(cur.rowcount,-1,
+ 'cursor.rowcount should be -1 after executing no-result '
+ 'statements'
+ )
+ cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
+ self.table_prefix
+ ))
+ self.assertTrue(cur.rowcount in (-1,1),
+ 'cursor.rowcount should == number or rows inserted, or '
+ 'set to -1 after executing an insert statement'
+ )
+ cur.execute("select name from %sbooze" % self.table_prefix)
+ self.assertTrue(cur.rowcount in (-1,1),
+ 'cursor.rowcount should == number of rows returned, or '
+ 'set to -1 after executing a select statement'
+ )
+ self.executeDDL2(cur)
+ self.assertEqual(cur.rowcount,-1,
+ 'cursor.rowcount not being reset to -1 after executing '
+ 'no-result statements'
+ )
+ finally:
+ con.close()
+
+ lower_func = 'lower'
+ def test_callproc(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ if self.lower_func and hasattr(cur,'callproc'):
+ r = cur.callproc(self.lower_func,('FOO',))
+ self.assertEqual(len(r),1)
+ self.assertEqual(r[0],'FOO')
+ r = cur.fetchall()
+ self.assertEqual(len(r),1,'callproc produced no result set')
+ self.assertEqual(len(r[0]),1,
+ 'callproc produced invalid result set'
+ )
+ self.assertEqual(r[0][0],'foo',
+ 'callproc produced invalid results'
+ )
+ finally:
+ con.close()
+
+ def test_close(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ finally:
+ con.close()
+
+ # cursor.execute should raise an Error if called after connection
+ # closed
+ self.assertRaises(self.driver.Error,self.executeDDL1,cur)
+
+ # connection.commit should raise an Error if called after connection'
+ # closed.'
+ self.assertRaises(self.driver.Error,con.commit)
+
+ # connection.close should raise an Error if called more than once
+ self.assertRaises(self.driver.Error,con.close)
+
+ def test_execute(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self._paraminsert(cur)
+ finally:
+ con.close()
+
+ def _paraminsert(self,cur):
+ self.executeDDL1(cur)
+ cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
+ self.table_prefix
+ ))
+ self.assertTrue(cur.rowcount in (-1,1))
+
+ if self.driver.paramstyle == 'qmark':
+ cur.execute(
+ 'insert into %sbooze values (?)' % self.table_prefix,
+ ("Cooper's",)
+ )
+ elif self.driver.paramstyle == 'numeric':
+ cur.execute(
+ 'insert into %sbooze values (:1)' % self.table_prefix,
+ ("Cooper's",)
+ )
+ elif self.driver.paramstyle == 'named':
+ cur.execute(
+ 'insert into %sbooze values (:beer)' % self.table_prefix,
+ {'beer':"Cooper's"}
+ )
+ elif self.driver.paramstyle == 'format':
+ cur.execute(
+ 'insert into %sbooze values (%%s)' % self.table_prefix,
+ ("Cooper's",)
+ )
+ elif self.driver.paramstyle == 'pyformat':
+ cur.execute(
+ 'insert into %sbooze values (%%(beer)s)' % self.table_prefix,
+ {'beer':"Cooper's"}
+ )
+ else:
+ self.fail('Invalid paramstyle')
+ self.assertTrue(cur.rowcount in (-1,1))
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ res = cur.fetchall()
+ self.assertEqual(len(res),2,'cursor.fetchall returned too few rows')
+ beers = [res[0][0],res[1][0]]
+ beers.sort()
+ self.assertEqual(beers[0],"Cooper's",
+ 'cursor.fetchall retrieved incorrect data, or data inserted '
+ 'incorrectly'
+ )
+ self.assertEqual(beers[1],"Victoria Bitter",
+ 'cursor.fetchall retrieved incorrect data, or data inserted '
+ 'incorrectly'
+ )
+
+ def test_executemany(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self.executeDDL1(cur)
+ largs = [ ("Cooper's",) , ("Boag's",) ]
+ margs = [ {'beer': "Cooper's"}, {'beer': "Boag's"} ]
+ if self.driver.paramstyle == 'qmark':
+ cur.executemany(
+ 'insert into %sbooze values (?)' % self.table_prefix,
+ largs
+ )
+ elif self.driver.paramstyle == 'numeric':
+ cur.executemany(
+ 'insert into %sbooze values (:1)' % self.table_prefix,
+ largs
+ )
+ elif self.driver.paramstyle == 'named':
+ cur.executemany(
+ 'insert into %sbooze values (:beer)' % self.table_prefix,
+ margs
+ )
+ elif self.driver.paramstyle == 'format':
+ cur.executemany(
+ 'insert into %sbooze values (%%s)' % self.table_prefix,
+ largs
+ )
+ elif self.driver.paramstyle == 'pyformat':
+ cur.executemany(
+ 'insert into %sbooze values (%%(beer)s)' % (
+ self.table_prefix
+ ),
+ margs
+ )
+ else:
+ self.fail('Unknown paramstyle')
+ self.assertTrue(cur.rowcount in (-1,2),
+ 'insert using cursor.executemany set cursor.rowcount to '
+ 'incorrect value %r' % cur.rowcount
+ )
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ res = cur.fetchall()
+ self.assertEqual(len(res),2,
+ 'cursor.fetchall retrieved incorrect number of rows'
+ )
+ beers = [res[0][0],res[1][0]]
+ beers.sort()
+ self.assertEqual(beers[0],"Boag's",'incorrect data retrieved')
+ self.assertEqual(beers[1],"Cooper's",'incorrect data retrieved')
+ finally:
+ con.close()
+
+ def test_fetchone(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+
+ # cursor.fetchone should raise an Error if called before
+ # executing a select-type query
+ self.assertRaises(self.driver.Error,cur.fetchone)
+
+ # cursor.fetchone should raise an Error if called after
+ # executing a query that cannnot return rows
+ self.executeDDL1(cur)
+ self.assertRaises(self.driver.Error,cur.fetchone)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ self.assertEqual(cur.fetchone(),None,
+ 'cursor.fetchone should return None if a query retrieves '
+ 'no rows'
+ )
+ self.assertTrue(cur.rowcount in (-1,0))
+
+ # cursor.fetchone should raise an Error if called after
+ # executing a query that cannnot return rows
+ cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
+ self.table_prefix
+ ))
+ self.assertRaises(self.driver.Error,cur.fetchone)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ r = cur.fetchone()
+ self.assertEqual(len(r),1,
+ 'cursor.fetchone should have retrieved a single row'
+ )
+ self.assertEqual(r[0],'Victoria Bitter',
+ 'cursor.fetchone retrieved incorrect data'
+ )
+ self.assertEqual(cur.fetchone(),None,
+ 'cursor.fetchone should return None if no more rows available'
+ )
+ self.assertTrue(cur.rowcount in (-1,1))
+ finally:
+ con.close()
+
+ samples = [
+ 'Carlton Cold',
+ 'Carlton Draft',
+ 'Mountain Goat',
+ 'Redback',
+ 'Victoria Bitter',
+ 'XXXX'
+ ]
+
+ def _populate(self):
+ ''' Return a list of sql commands to setup the DB for the fetch
+ tests.
+ '''
+ populate = [
+ "insert into %sbooze values ('%s')" % (self.table_prefix,s)
+ for s in self.samples
+ ]
+ return populate
+
+ def test_fetchmany(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+
+ # cursor.fetchmany should raise an Error if called without
+ #issuing a query
+ self.assertRaises(self.driver.Error,cur.fetchmany,4)
+
+ self.executeDDL1(cur)
+ for sql in self._populate():
+ cur.execute(sql)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ r = cur.fetchmany()
+ self.assertEqual(len(r),1,
+ 'cursor.fetchmany retrieved incorrect number of rows, '
+ 'default of arraysize is one.'
+ )
+ cur.arraysize=10
+ r = cur.fetchmany(3) # Should get 3 rows
+ self.assertEqual(len(r),3,
+ 'cursor.fetchmany retrieved incorrect number of rows'
+ )
+ r = cur.fetchmany(4) # Should get 2 more
+ self.assertEqual(len(r),2,
+ 'cursor.fetchmany retrieved incorrect number of rows'
+ )
+ r = cur.fetchmany(4) # Should be an empty sequence
+ self.assertEqual(len(r),0,
+ 'cursor.fetchmany should return an empty sequence after '
+ 'results are exhausted'
+ )
+ self.assertTrue(cur.rowcount in (-1,6))
+
+ # Same as above, using cursor.arraysize
+ cur.arraysize=4
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ r = cur.fetchmany() # Should get 4 rows
+ self.assertEqual(len(r),4,
+ 'cursor.arraysize not being honoured by fetchmany'
+ )
+ r = cur.fetchmany() # Should get 2 more
+ self.assertEqual(len(r),2)
+ r = cur.fetchmany() # Should be an empty sequence
+ self.assertEqual(len(r),0)
+ self.assertTrue(cur.rowcount in (-1,6))
+
+ cur.arraysize=6
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ rows = cur.fetchmany() # Should get all rows
+ self.assertTrue(cur.rowcount in (-1,6))
+ self.assertEqual(len(rows),6)
+ self.assertEqual(len(rows),6)
+ rows = [r[0] for r in rows]
+ rows.sort()
+
+ # Make sure we get the right data back out
+ for i in range(0,6):
+ self.assertEqual(rows[i],self.samples[i],
+ 'incorrect data retrieved by cursor.fetchmany'
+ )
+
+ rows = cur.fetchmany() # Should return an empty list
+ self.assertEqual(len(rows),0,
+ 'cursor.fetchmany should return an empty sequence if '
+ 'called after the whole result set has been fetched'
+ )
+ self.assertTrue(cur.rowcount in (-1,6))
+
+ self.executeDDL2(cur)
+ cur.execute('select name from %sbarflys' % self.table_prefix)
+ r = cur.fetchmany() # Should get empty sequence
+ self.assertEqual(len(r),0,
+ 'cursor.fetchmany should return an empty sequence if '
+ 'query retrieved no rows'
+ )
+ self.assertTrue(cur.rowcount in (-1,0))
+
+ finally:
+ con.close()
+
+ def test_fetchall(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ # cursor.fetchall should raise an Error if called
+ # without executing a query that may return rows (such
+ # as a select)
+ self.assertRaises(self.driver.Error, cur.fetchall)
+
+ self.executeDDL1(cur)
+ for sql in self._populate():
+ cur.execute(sql)
+
+ # cursor.fetchall should raise an Error if called
+ # after executing a a statement that cannot return rows
+ self.assertRaises(self.driver.Error,cur.fetchall)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ rows = cur.fetchall()
+ self.assertTrue(cur.rowcount in (-1,len(self.samples)))
+ self.assertEqual(len(rows),len(self.samples),
+ 'cursor.fetchall did not retrieve all rows'
+ )
+ rows = [r[0] for r in rows]
+ rows.sort()
+ for i in range(0,len(self.samples)):
+ self.assertEqual(rows[i],self.samples[i],
+ 'cursor.fetchall retrieved incorrect rows'
+ )
+ rows = cur.fetchall()
+ self.assertEqual(
+ len(rows),0,
+ 'cursor.fetchall should return an empty list if called '
+ 'after the whole result set has been fetched'
+ )
+ self.assertTrue(cur.rowcount in (-1,len(self.samples)))
+
+ self.executeDDL2(cur)
+ cur.execute('select name from %sbarflys' % self.table_prefix)
+ rows = cur.fetchall()
+ self.assertTrue(cur.rowcount in (-1,0))
+ self.assertEqual(len(rows),0,
+ 'cursor.fetchall should return an empty list if '
+ 'a select query returns no rows'
+ )
+
+ finally:
+ con.close()
+
+ def test_mixedfetch(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self.executeDDL1(cur)
+ for sql in self._populate():
+ cur.execute(sql)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ rows1 = cur.fetchone()
+ rows23 = cur.fetchmany(2)
+ rows4 = cur.fetchone()
+ rows56 = cur.fetchall()
+ self.assertTrue(cur.rowcount in (-1,6))
+ self.assertEqual(len(rows23),2,
+ 'fetchmany returned incorrect number of rows'
+ )
+ self.assertEqual(len(rows56),2,
+ 'fetchall returned incorrect number of rows'
+ )
+
+ rows = [rows1[0]]
+ rows.extend([rows23[0][0],rows23[1][0]])
+ rows.append(rows4[0])
+ rows.extend([rows56[0][0],rows56[1][0]])
+ rows.sort()
+ for i in range(0,len(self.samples)):
+ self.assertEqual(rows[i],self.samples[i],
+ 'incorrect data retrieved or inserted'
+ )
+ finally:
+ con.close()
+
+ def help_nextset_setUp(self,cur):
+ ''' Should create a procedure called deleteme
+ that returns two result sets, first the
+ number of rows in booze then "name from booze"
+ '''
+ raise NotImplementedError('Helper not implemented')
+ #sql="""
+ # create procedure deleteme as
+ # begin
+ # select count(*) from booze
+ # select name from booze
+ # end
+ #"""
+ #cur.execute(sql)
+
+ def help_nextset_tearDown(self,cur):
+ 'If cleaning up is needed after nextSetTest'
+ raise NotImplementedError('Helper not implemented')
+ #cur.execute("drop procedure deleteme")
+
+ def test_nextset(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ if not hasattr(cur,'nextset'):
+ return
+
+ try:
+ self.executeDDL1(cur)
+ sql=self._populate()
+ for sql in self._populate():
+ cur.execute(sql)
+
+ self.help_nextset_setUp(cur)
+
+ cur.callproc('deleteme')
+ numberofrows=cur.fetchone()
+ assert numberofrows[0]== len(self.samples)
+ assert cur.nextset()
+ names=cur.fetchall()
+ assert len(names) == len(self.samples)
+ s=cur.nextset()
+ assert s == None,'No more return sets, should return None'
+ finally:
+ self.help_nextset_tearDown(cur)
+
+ finally:
+ con.close()
+
+ def test_nextset(self):
+ raise NotImplementedError('Drivers need to override this test')
+
+ def test_arraysize(self):
+ # Not much here - rest of the tests for this are in test_fetchmany
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self.assertTrue(hasattr(cur,'arraysize'),
+ 'cursor.arraysize must be defined'
+ )
+ finally:
+ con.close()
+
+ def test_setinputsizes(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ cur.setinputsizes( (25,) )
+ self._paraminsert(cur) # Make sure cursor still works
+ finally:
+ con.close()
+
+ def test_setoutputsize_basic(self):
+ # Basic test is to make sure setoutputsize doesn't blow up
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ cur.setoutputsize(1000)
+ cur.setoutputsize(2000,0)
+ self._paraminsert(cur) # Make sure the cursor still works
+ finally:
+ con.close()
+
+ def test_setoutputsize(self):
+ # Real test for setoutputsize is driver dependant
+ raise NotImplementedError('Driver need to override this test')
+
+ def test_None(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self.executeDDL1(cur)
+ cur.execute('insert into %sbooze values (NULL)' % self.table_prefix)
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ r = cur.fetchall()
+ self.assertEqual(len(r),1)
+ self.assertEqual(len(r[0]),1)
+ self.assertEqual(r[0][0],None,'NULL value not returned as None')
+ finally:
+ con.close()
+
+ def test_Date(self):
+ d1 = self.driver.Date(2002,12,25)
+ d2 = self.driver.DateFromTicks(time.mktime((2002,12,25,0,0,0,0,0,0)))
+ # Can we assume this? API doesn't specify, but it seems implied
+ # self.assertEqual(str(d1),str(d2))
+
+ def test_Time(self):
+ t1 = self.driver.Time(13,45,30)
+ t2 = self.driver.TimeFromTicks(time.mktime((2001,1,1,13,45,30,0,0,0)))
+ # Can we assume this? API doesn't specify, but it seems implied
+ # self.assertEqual(str(t1),str(t2))
+
+ def test_Timestamp(self):
+ t1 = self.driver.Timestamp(2002,12,25,13,45,30)
+ t2 = self.driver.TimestampFromTicks(
+ time.mktime((2002,12,25,13,45,30,0,0,0))
+ )
+ # Can we assume this? API doesn't specify, but it seems implied
+ # self.assertEqual(str(t1),str(t2))
+
+ def test_Binary(self):
+ b = self.driver.Binary(b'Something')
+ b = self.driver.Binary(b'')
+
+ def test_STRING(self):
+ self.assertTrue(hasattr(self.driver,'STRING'),
+ 'module.STRING must be defined'
+ )
+
+ def test_BINARY(self):
+ self.assertTrue(hasattr(self.driver,'BINARY'),
+ 'module.BINARY must be defined.'
+ )
+
+ def test_NUMBER(self):
+ self.assertTrue(hasattr(self.driver,'NUMBER'),
+ 'module.NUMBER must be defined.'
+ )
+
+ def test_DATETIME(self):
+ self.assertTrue(hasattr(self.driver,'DATETIME'),
+ 'module.DATETIME must be defined.'
+ )
+
+ def test_ROWID(self):
+ self.assertTrue(hasattr(self.driver,'ROWID'),
+ 'module.ROWID must be defined.'
+ )
diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py
new file mode 100755
index 000000000..9cacbbd34
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+from . import capabilities
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+import pymysql
+from pymysql.tests import base
+import warnings
+
+warnings.filterwarnings('error')
+
+class test_MySQLdb(capabilities.DatabaseTest):
+
+ db_module = pymysql
+ connect_args = ()
+ connect_kwargs = base.PyMySQLTestCase.databases[0].copy()
+ connect_kwargs.update(dict(read_default_file='~/.my.cnf',
+ use_unicode=True,
+ charset='utf8', sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL"))
+
+ create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8"
+ leak_test = False
+
+ def quote_identifier(self, ident):
+ return "`%s`" % ident
+
+ def test_TIME(self):
+ from datetime import timedelta
+ def generator(row,col):
+ return timedelta(0, row*8000)
+ self.check_data_integrity(
+ ('col1 TIME',),
+ generator)
+
+ def test_TINYINT(self):
+ # Number data
+ def generator(row,col):
+ v = (row*row) % 256
+ if v > 127:
+ v = v-256
+ return v
+ self.check_data_integrity(
+ ('col1 TINYINT',),
+ generator)
+
+ def test_stored_procedures(self):
+ db = self.connection
+ c = self.cursor
+ try:
+ self.create_table(('pos INT', 'tree CHAR(20)'))
+ c.executemany("INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table,
+ list(enumerate('ash birch cedar larch pine'.split())))
+ db.commit()
+
+ c.execute("""
+ CREATE PROCEDURE test_sp(IN t VARCHAR(255))
+ BEGIN
+ SELECT pos FROM %s WHERE tree = t;
+ END
+ """ % self.table)
+ db.commit()
+
+ c.callproc('test_sp', ('larch',))
+ rows = c.fetchall()
+ self.assertEqual(len(rows), 1)
+ self.assertEqual(rows[0][0], 3)
+ c.nextset()
+ finally:
+ c.execute("DROP PROCEDURE IF EXISTS test_sp")
+ c.execute('drop table %s' % (self.table))
+
+ def test_small_CHAR(self):
+ # Character data
+ def generator(row,col):
+ i = ((row+1)*(col+1)+62)%256
+ if i == 62: return ''
+ if i == 63: return None
+ return chr(i)
+ self.check_data_integrity(
+ ('col1 char(1)','col2 char(1)'),
+ generator)
+
+ def test_bug_2671682(self):
+ from pymysql.constants import ER
+ try:
+ self.cursor.execute("describe some_non_existent_table");
+ except self.connection.ProgrammingError as msg:
+ self.assertEqual(msg.args[0], ER.NO_SUCH_TABLE)
+
+ def test_ping(self):
+ self.connection.ping()
+
+ def test_literal_int(self):
+ self.assertTrue("2" == self.connection.literal(2))
+
+ def test_literal_float(self):
+ self.assertTrue("3.1415" == self.connection.literal(3.1415))
+
+ def test_literal_string(self):
+ self.assertTrue("'foo'" == self.connection.literal("foo"))
+
+
+if __name__ == '__main__':
+ if test_MySQLdb.leak_test:
+ import gc
+ gc.enable()
+ gc.set_debug(gc.DEBUG_LEAK)
+ unittest.main()
diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py
new file mode 100755
index 000000000..ca430fd0e
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python
+from . import dbapi20
+import pymysql
+from pymysql.tests import base
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+
+class test_MySQLdb(dbapi20.DatabaseAPI20Test):
+ driver = pymysql
+ connect_args = ()
+ connect_kw_args = base.PyMySQLTestCase.databases[0].copy()
+ connect_kw_args.update(dict(read_default_file='~/.my.cnf',
+ charset='utf8',
+ sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL"))
+
+ def test_setoutputsize(self): pass
+ def test_setoutputsize_basic(self): pass
+ def test_nextset(self): pass
+
+ """The tests on fetchone and fetchall and rowcount bogusly
+ test for an exception if the statement cannot return a
+ result set. MySQL always returns a result set; it's just that
+ some things return empty result sets."""
+
+ def test_fetchall(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ # cursor.fetchall should raise an Error if called
+ # without executing a query that may return rows (such
+ # as a select)
+ self.assertRaises(self.driver.Error, cur.fetchall)
+
+ self.executeDDL1(cur)
+ for sql in self._populate():
+ cur.execute(sql)
+
+ # cursor.fetchall should raise an Error if called
+ # after executing a a statement that cannot return rows
+## self.assertRaises(self.driver.Error,cur.fetchall)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ rows = cur.fetchall()
+ self.assertTrue(cur.rowcount in (-1,len(self.samples)))
+ self.assertEqual(len(rows),len(self.samples),
+ 'cursor.fetchall did not retrieve all rows'
+ )
+ rows = [r[0] for r in rows]
+ rows.sort()
+ for i in range(0,len(self.samples)):
+ self.assertEqual(rows[i],self.samples[i],
+ 'cursor.fetchall retrieved incorrect rows'
+ )
+ rows = cur.fetchall()
+ self.assertEqual(
+ len(rows),0,
+ 'cursor.fetchall should return an empty list if called '
+ 'after the whole result set has been fetched'
+ )
+ self.assertTrue(cur.rowcount in (-1,len(self.samples)))
+
+ self.executeDDL2(cur)
+ cur.execute('select name from %sbarflys' % self.table_prefix)
+ rows = cur.fetchall()
+ self.assertTrue(cur.rowcount in (-1,0))
+ self.assertEqual(len(rows),0,
+ 'cursor.fetchall should return an empty list if '
+ 'a select query returns no rows'
+ )
+
+ finally:
+ con.close()
+
+ def test_fetchone(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+
+ # cursor.fetchone should raise an Error if called before
+ # executing a select-type query
+ self.assertRaises(self.driver.Error,cur.fetchone)
+
+ # cursor.fetchone should raise an Error if called after
+ # executing a query that cannnot return rows
+ self.executeDDL1(cur)
+## self.assertRaises(self.driver.Error,cur.fetchone)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ self.assertEqual(cur.fetchone(),None,
+ 'cursor.fetchone should return None if a query retrieves '
+ 'no rows'
+ )
+ self.assertTrue(cur.rowcount in (-1,0))
+
+ # cursor.fetchone should raise an Error if called after
+ # executing a query that cannnot return rows
+ cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
+ self.table_prefix
+ ))
+## self.assertRaises(self.driver.Error,cur.fetchone)
+
+ cur.execute('select name from %sbooze' % self.table_prefix)
+ r = cur.fetchone()
+ self.assertEqual(len(r),1,
+ 'cursor.fetchone should have retrieved a single row'
+ )
+ self.assertEqual(r[0],'Victoria Bitter',
+ 'cursor.fetchone retrieved incorrect data'
+ )
+## self.assertEqual(cur.fetchone(),None,
+## 'cursor.fetchone should return None if no more rows available'
+## )
+ self.assertTrue(cur.rowcount in (-1,1))
+ finally:
+ con.close()
+
+ # Same complaint as for fetchall and fetchone
+ def test_rowcount(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ self.executeDDL1(cur)
+## self.assertEqual(cur.rowcount,-1,
+## 'cursor.rowcount should be -1 after executing no-result '
+## 'statements'
+## )
+ cur.execute("insert into %sbooze values ('Victoria Bitter')" % (
+ self.table_prefix
+ ))
+## self.assertTrue(cur.rowcount in (-1,1),
+## 'cursor.rowcount should == number or rows inserted, or '
+## 'set to -1 after executing an insert statement'
+## )
+ cur.execute("select name from %sbooze" % self.table_prefix)
+ self.assertTrue(cur.rowcount in (-1,1),
+ 'cursor.rowcount should == number of rows returned, or '
+ 'set to -1 after executing a select statement'
+ )
+ self.executeDDL2(cur)
+## self.assertEqual(cur.rowcount,-1,
+## 'cursor.rowcount not being reset to -1 after executing '
+## 'no-result statements'
+## )
+ finally:
+ con.close()
+
+ def test_callproc(self):
+ pass # performed in test_MySQL_capabilities
+
+ def help_nextset_setUp(self,cur):
+ ''' Should create a procedure called deleteme
+ that returns two result sets, first the
+ number of rows in booze then "name from booze"
+ '''
+ sql="""
+ create procedure deleteme()
+ begin
+ select count(*) from %(tp)sbooze;
+ select name from %(tp)sbooze;
+ end
+ """ % dict(tp=self.table_prefix)
+ cur.execute(sql)
+
+ def help_nextset_tearDown(self,cur):
+ 'If cleaning up is needed after nextSetTest'
+ cur.execute("drop procedure deleteme")
+
+ def test_nextset(self):
+ from warnings import warn
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ if not hasattr(cur,'nextset'):
+ return
+
+ try:
+ self.executeDDL1(cur)
+ sql=self._populate()
+ for sql in self._populate():
+ cur.execute(sql)
+
+ self.help_nextset_setUp(cur)
+
+ cur.callproc('deleteme')
+ numberofrows=cur.fetchone()
+ assert numberofrows[0]== len(self.samples)
+ assert cur.nextset()
+ names=cur.fetchall()
+ assert len(names) == len(self.samples)
+ s=cur.nextset()
+ if s:
+ empty = cur.fetchall()
+ self.assertEqual(len(empty), 0,
+ "non-empty result set after other result sets")
+ #warn("Incompatibility: MySQL returns an empty result set for the CALL itself",
+ # Warning)
+ #assert s == None,'No more return sets, should return None'
+ finally:
+ self.help_nextset_tearDown(cur)
+
+ finally:
+ con.close()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py
new file mode 100755
index 000000000..17fc2cde5
--- /dev/null
+++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py
@@ -0,0 +1,101 @@
+import sys
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+import pymysql
+_mysql = pymysql
+from pymysql.constants import FIELD_TYPE
+from pymysql.tests import base
+from pymysql._compat import PY2, long_type
+
+if not PY2:
+ basestring = str
+
+
+class TestDBAPISet(unittest.TestCase):
+ def test_set_equality(self):
+ self.assertTrue(pymysql.STRING == pymysql.STRING)
+
+ def test_set_inequality(self):
+ self.assertTrue(pymysql.STRING != pymysql.NUMBER)
+
+ def test_set_equality_membership(self):
+ self.assertTrue(FIELD_TYPE.VAR_STRING == pymysql.STRING)
+
+ def test_set_inequality_membership(self):
+ self.assertTrue(FIELD_TYPE.DATE != pymysql.STRING)
+
+
+class CoreModule(unittest.TestCase):
+ """Core _mysql module features."""
+
+ def test_NULL(self):
+ """Should have a NULL constant."""
+ self.assertEqual(_mysql.NULL, 'NULL')
+
+ def test_version(self):
+ """Version information sanity."""
+ self.assertTrue(isinstance(_mysql.__version__, basestring))
+
+ self.assertTrue(isinstance(_mysql.version_info, tuple))
+ self.assertEqual(len(_mysql.version_info), 5)
+
+ def test_client_info(self):
+ self.assertTrue(isinstance(_mysql.get_client_info(), basestring))
+
+ def test_thread_safe(self):
+ self.assertTrue(isinstance(_mysql.thread_safe(), int))
+
+
+class CoreAPI(unittest.TestCase):
+ """Test _mysql interaction internals."""
+
+ def setUp(self):
+ kwargs = base.PyMySQLTestCase.databases[0].copy()
+ kwargs["read_default_file"] = "~/.my.cnf"
+ self.conn = _mysql.connect(**kwargs)
+
+ def tearDown(self):
+ self.conn.close()
+
+ def test_thread_id(self):
+ tid = self.conn.thread_id()
+ self.assertTrue(isinstance(tid, (int, long_type)),
+ "thread_id didn't return an integral value.")
+
+ self.assertRaises(TypeError, self.conn.thread_id, ('evil',),
+ "thread_id shouldn't accept arguments.")
+
+ def test_affected_rows(self):
+ self.assertEqual(self.conn.affected_rows(), 0,
+ "Should return 0 before we do anything.")
+
+
+ #def test_debug(self):
+ ## FIXME Only actually tests if you lack SUPER
+ #self.assertRaises(pymysql.OperationalError,
+ #self.conn.dump_debug_info)
+
+ def test_charset_name(self):
+ self.assertTrue(isinstance(self.conn.character_set_name(), basestring),
+ "Should return a string.")
+
+ def test_host_info(self):
+ assert isinstance(self.conn.get_host_info(), basestring), "should return a string"
+
+ def test_proto_info(self):
+ self.assertTrue(isinstance(self.conn.get_proto_info(), int),
+ "Should return an int.")
+
+ def test_server_info(self):
+ if sys.version_info[0] == 2:
+ self.assertTrue(isinstance(self.conn.get_server_info(), basestring),
+ "Should return an str.")
+ else:
+ self.assertTrue(isinstance(self.conn.get_server_info(), basestring),
+ "Should return an str.")
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/gluon/contrib/pymysql/times.py b/gluon/contrib/pymysql/times.py
index c47db09eb..4497dacf6 100644
--- a/gluon/contrib/pymysql/times.py
+++ b/gluon/contrib/pymysql/times.py
@@ -1,16 +1,20 @@
from time import localtime
from datetime import date, datetime, time, timedelta
+
Date = date
Time = time
TimeDelta = timedelta
Timestamp = datetime
+
def DateFromTicks(ticks):
return date(*localtime(ticks)[:3])
+
def TimeFromTicks(ticks):
return time(*localtime(ticks)[3:6])
+
def TimestampFromTicks(ticks):
return datetime(*localtime(ticks)[:6])
diff --git a/gluon/contrib/pymysql/util.py b/gluon/contrib/pymysql/util.py
index cc622e57b..3e82ac7b5 100644
--- a/gluon/contrib/pymysql/util.py
+++ b/gluon/contrib/pymysql/util.py
@@ -1,14 +1,17 @@
import struct
+
def byte2int(b):
if isinstance(b, int):
return b
else:
return struct.unpack("!B", b)[0]
+
def int2byte(i):
return struct.pack("!B", i)
+
def join_bytes(bs):
if len(bs) == 0:
return ""
diff --git a/gluon/contrib/simplejsonrpc.py b/gluon/contrib/simplejsonrpc.py
index 2a4547f7a..f60dc4c75 100644
--- a/gluon/contrib/simplejsonrpc.py
+++ b/gluon/contrib/simplejsonrpc.py
@@ -17,12 +17,17 @@
__license__ = "LGPL 3.0"
__version__ = "0.05"
+import sys
+PY2 = sys.version_info[0] == 2
import urllib
-from xmlrpclib import Transport, SafeTransport
-from cStringIO import StringIO
+if PY2:
+ from xmlrpclib import Transport, SafeTransport
+ from cStringIO import StringIO
+else:
+ from xmlrpc.client import Transport, SafeTransport
+ from io import StringIO
import random
-import sys
import json
diff --git a/gluon/globals.py b/gluon/globals.py
index 41ac708e8..29fb3f0d3 100644
--- a/gluon/globals.py
+++ b/gluon/globals.py
@@ -13,7 +13,7 @@
- Session
"""
-from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT
+from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT, long
from gluon.storage import Storage, List
from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE
from gluon.contenttype import contenttype
@@ -439,13 +439,11 @@ def render(self, *a, **b):
from gluon._compat import StringIO
(obody, oview) = (self.body, self.view)
(self.body, self.view) = (StringIO(), view)
- run_view_in(self._view_environment)
- page = self.body.getvalue()
+ page = run_view_in(self._view_environment)
self.body.close()
(self.body, self.view) = (obody, oview)
else:
- run_view_in(self._view_environment)
- page = self.body.getvalue()
+ page = run_view_in(self._view_environment)
return page
def include_meta(self):
diff --git a/gluon/html.py b/gluon/html.py
index da8e2d6ec..ceef7bd29 100644
--- a/gluon/html.py
+++ b/gluon/html.py
@@ -20,7 +20,7 @@
import base64
from gluon import sanitizer, decoder
import itertools
-from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool
+from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type
from gluon.utils import local_html_escape
import marshal
@@ -122,7 +122,7 @@ def xmlescape(data, quote=True):
if hasattr(data, 'xml') and callable(data.xml):
return to_bytes(data.xml())
- if not(isinstance(data, basestring)):
+ if not(isinstance(data, (text_type, bytes))):
# i.e., integers
data=str(data)
data = to_bytes(data, 'utf8', 'xmlcharrefreplace')
@@ -1040,7 +1040,7 @@ def elements(self, *args, **kargs):
hello
>>> a.elements('a[u:v=$]')[0].xml()
'hello'
- >>> a=FORM( INPUT(_type='text'), SELECT(range(1)), TEXTAREA() )
+ >>> a=FORM( INPUT(_type='text'), SELECT(list(range(1))), TEXTAREA() )
>>> for c in a.elements('input, select, textarea'): c['_disabled'] = 'disabled'
>>> a.xml()
''
diff --git a/gluon/main.py b/gluon/main.py
index 971c48a7d..a830d7a84 100644
--- a/gluon/main.py
+++ b/gluon/main.py
@@ -180,8 +180,8 @@ def serve_controller(request, response, session):
if isinstance(page, dict):
response._vars = page
response._view_environment.update(page)
- run_view_in(response._view_environment)
- page = response.body.getvalue()
+ page = run_view_in(response._view_environment)
+
# logic to garbage collect after exec, not always, once every 100 requests
global requests
requests = ('requests' in globals()) and (requests + 1) % 100 or 0
diff --git a/gluon/packages/dal b/gluon/packages/dal
index 77e0d3f38..12bc6d974 160000
--- a/gluon/packages/dal
+++ b/gluon/packages/dal
@@ -1 +1 @@
-Subproject commit 77e0d3f386e326a2016a7e2139900ce72d7ddada
+Subproject commit 12bc6d97402acce462c1193f57bbba4afde7c3c3
diff --git a/gluon/restricted.py b/gluon/restricted.py
index c67bcbf4e..84c46b36e 100644
--- a/gluon/restricted.py
+++ b/gluon/restricted.py
@@ -199,10 +199,10 @@ def __str__(self):
def compile2(code, layer):
- return compile(code.rstrip(), layer, 'exec')
+ return compile(code, layer, 'exec')
-def restricted(code, environment=None, layer='Unknown'):
+def restricted(ccode, environment=None, layer='Unknown'):
"""
Runs code in environment and returns the output. If an exception occurs
in code it raises a RestrictedError containing the traceback. Layer is
@@ -213,10 +213,6 @@ def restricted(code, environment=None, layer='Unknown'):
environment['__file__'] = layer
environment['__name__'] = '__restricted__'
try:
- if isinstance(code, types.CodeType):
- ccode = code
- else:
- ccode = compile2(code, layer)
exec(ccode, environment)
except HTTP:
raise
@@ -231,7 +227,7 @@ def restricted(code, environment=None, layer='Unknown'):
sys.excepthook(etype, evalue, tb)
del tb
output = "%s %s" % (etype, evalue)
- raise RestrictedError(layer, code, output, environment)
+ raise RestrictedError(layer, ccode, output, environment)
def snapshot(info=None, context=5, code=None, environment=None):
diff --git a/gluon/scheduler.py b/gluon/scheduler.py
index 11dccd168..e758c3792 100644
--- a/gluon/scheduler.py
+++ b/gluon/scheduler.py
@@ -477,7 +477,8 @@ def write(self, data):
# Get controller-specific subdirectory if task.app is of
# form 'app/controller'
(a, c, f) = parse_path_info(task.app)
- _env = env(a=a, c=c, import_models=True)
+ _env = env(a=a, c=c, import_models=True,
+ extra_request={'is_scheduler': True})
logging.getLogger().setLevel(level)
f = task.function
functions = current._scheduler.tasks
@@ -1499,6 +1500,8 @@ def queue_task(self, function, pargs=[], pvars={}, **kwargs):
kwargs.update(start_time=start_time, next_run_time=next_run_time)
except:
pass
+ if 'start_time' in kwargs and 'next_run_time' not in kwargs:
+ kwargs.update(next_run_time=kwargs['start_time'])
rtn = self.db.scheduler_task.validate_and_insert(**kwargs)
if not rtn.errors:
rtn.uuid = tuuid
diff --git a/gluon/shell.py b/gluon/shell.py
index 95359c3a5..9d93f1a6d 100644
--- a/gluon/shell.py
+++ b/gluon/shell.py
@@ -15,6 +15,7 @@
import os
import sys
import code
+import copy
import logging
import types
import re
@@ -167,6 +168,8 @@ def check_credentials(request, other_application='admin'):
sys.stderr.write(e.traceback + '\n')
sys.exit(1)
+ response._view_environment = copy.copy(environment)
+
environment['__name__'] = '__main__'
return environment
diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py
index 9ddc34d68..e4291cae0 100644
--- a/gluon/sqlhtml.py
+++ b/gluon/sqlhtml.py
@@ -19,7 +19,7 @@
import re
import os
-from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native
+from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native, to_unicode
from gluon.http import HTTP, redirect
from gluon.html import XmlComponent, truncate_string
from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG
@@ -657,7 +657,8 @@ class AutocompleteWidget(object):
def __init__(self, request, field, id_field=None, db=None,
orderby=None, limitby=(0, 10), distinct=False,
keyword='_autocomplete_%(tablename)s_%(fieldname)s',
- min_length=2, help_fields=None, help_string=None, at_beginning = True):
+ min_length=2, help_fields=None, help_string=None,
+ at_beginning=True, default_var='ac'):
self.help_fields = help_fields or []
self.help_string = help_string
@@ -680,7 +681,9 @@ def __init__(self, request, field, id_field=None, db=None,
else:
self.is_reference = False
if hasattr(request, 'application'):
- self.url = URL(args=request.args)
+ urlvars = request.vars
+ urlvars[default_var] = 1
+ self.url = URL(args=request.args, vars=urlvars)
self.callback()
else:
self.url = request
@@ -688,35 +691,55 @@ def __init__(self, request, field, id_field=None, db=None,
def callback(self):
if self.keyword in self.request.vars:
field = self.fields[0]
+ kword = self.request.vars[self.keyword]
if isinstance(field, Field.Virtual):
records = []
table_rows = self.db(self.db[field.tablename]).select(orderby=self.orderby)
count = 0
for row in table_rows:
if self.at_beginning:
- if row[field.name].lower().startswith(self.request.vars[self.keyword]):
+ if row[field.name].lower().startswith(kword):
count += 1
records.append(row)
else:
- if self.request.vars[self.keyword] in row[field.name].lower():
+ if kword in row[field.name].lower():
count += 1
records.append(row)
if count == 10:
break
- rows = Rows(self.db, records, table_rows.colnames, compact=table_rows.compact)
+ rows = Rows(self.db, records, table_rows.colnames,
+ compact=table_rows.compact)
elif settings and settings.global_settings.web2py_runtime_gae:
- rows = self.db(field.__ge__(self.request.vars[self.keyword]) & field.__lt__(self.request.vars[self.keyword] + u'\ufffd')).select(orderby=self.orderby, limitby=self.limitby, *(self.fields+self.help_fields))
+ rows = self.db(field.__ge__(kword) &
+ field.__lt__(kword + u'\ufffd')
+ ).select(orderby=self.orderby,
+ limitby=self.limitby,
+ *(self.fields + self.help_fields))
elif self.at_beginning:
- rows = self.db(field.like(self.request.vars[self.keyword] + '%', case_sensitive=False)).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields))
+ rows = self.db(field.like(kword + '%', case_sensitive=False)
+ ).select(orderby=self.orderby,
+ limitby=self.limitby,
+ distinct=self.distinct,
+ *(self.fields + self.help_fields))
else:
- rows = self.db(field.contains(self.request.vars[self.keyword], case_sensitive=False)).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields))
+ rows = self.db(field.contains(kword, case_sensitive=False)
+ ).select(orderby=self.orderby,
+ limitby=self.limitby,
+ distinct=self.distinct,
+ *(self.fields + self.help_fields))
if rows:
if self.is_reference:
id_field = self.fields[1]
if self.help_fields:
- options = [OPTION(
- self.help_string % dict([(h.name, s[h.name]) for h in self.fields[:1] + self.help_fields]),
- _value=s[id_field.name], _selected=(k == 0)) for k, s in enumerate(rows)]
+ options = [
+ OPTION(
+ self.help_string % dict(
+ [(h.name, s[h.name]) for h
+ in self.fields[:1] + self.help_fields]),
+ _value=s[id_field.name],
+ _selected=(k == 0))
+ for k, s in enumerate(rows)
+ ]
else:
options = [OPTION(
s[field.name], _value=s[id_field.name],
@@ -763,28 +786,100 @@ def __call__(self, field, value, **attributes):
record = self.db(
self.fields[1] == value).select(self.fields[0]).first()
attr['value'] = record and record[self.fields[0].name]
- attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
+ attr['_onblur'] = "jQuery('#%(div_id)s').delay(500).fadeOut('slow');" % \
dict(div_id=div_id, u='F' + self.keyword)
- attr['_onkeyup'] = "jQuery('#%(key3)s').val('');var e=event.which?event.which:event.keyCode; function %(u)s(){jQuery('#%(id)s').val(jQuery('#%(key)s :selected').text());jQuery('#%(key3)s').val(jQuery('#%(key)s').val())}; if(e==39) %(u)s(); else if(e==40) {if(jQuery('#%(key)s option:selected').next().length)jQuery('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); %(u)s();} else if(e==38) {if(jQuery('#%(key)s option:selected').prev().length)jQuery('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); %(u)s();} else if(jQuery('#%(id)s').val().length>=%(min_length)s) jQuery.get('%(url)s?%(key)s='+encodeURIComponent(jQuery('#%(id)s').val()),function(data){if(data=='')jQuery('#%(key3)s').val('');else{jQuery('#%(id)s').next('.error').hide();jQuery('#%(div_id)s').html(data).show().focus();jQuery('#%(div_id)s select').css('width',jQuery('#%(id)s').css('width'));jQuery('#%(key3)s').val(jQuery('#%(key)s').val());jQuery('#%(key)s').change(%(u)s);jQuery('#%(key)s').click(%(u)s);};}); else jQuery('#%(div_id)s').fadeOut('slow');" % \
+ js = """
+ (function($) {
+ function doit(e_) {
+ $('#%(key3)s').val('');
+ var e=e_.which?e_.which:e_.keyCode;
+ function %(u)s(){
+ $('#%(id)s').val($('#%(key)s :selected').text());
+ $('#%(key3)s').val($('#%(key)s').val())
+ };
+ if(e==39) %(u)s();
+ else if(e==40) {
+ if($('#%(key)s option:selected').next().length)
+ $('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected');
+ %(u)s();
+ }
+ else if(e==38) {
+ if($('#%(key)s option:selected').prev().length)
+ $('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected');
+ %(u)s();
+ }
+ else if($('#%(id)s').val().length>=%(min_length)s)
+ $.get('%(url)s&%(key)s='+encodeURIComponent($('#%(id)s').val()),
+ function(data){
+ if(data=='')$('#%(key3)s').val('');
+ else{
+ $('#%(id)s').next('.error').hide();
+ $('#%(div_id)s').html(data).show().focus();
+ $('#%(div_id)s select').css('width',$('#%(id)s').css('width'));
+ $('#%(key3)s').val($('#%(key)s').val());
+ $('#%(key)s').change(%(u)s).click(%(u)s);
+ };
+ });
+ else $('#%(div_id)s').fadeOut('slow');
+ }
+ var tmr = null;
+ $("#%(id)s").on('keyup focus',function(e) {
+ if (tmr) clearTimeout(tmr);
+ if($('#%(id)s').val().length>=%(min_length)s) {
+ tmr = setTimeout(function() { tmr = null; doit(e); }, 300);
+ }
+ });
+ })(jQuery)""".replace('\n', '').replace(' ' * 4, '') % \
dict(url=self.url, min_length=self.min_length,
key=self.keyword, id=attr['_id'], key2=key2, key3=key3,
name=name, div_id=div_id, u='F' + self.keyword)
- if self.min_length == 0:
- attr['_onfocus'] = attr['_onkeyup']
return CAT(INPUT(**attr),
INPUT(_type='hidden', _id=key3, _value=value,
_name=name, requires=field.requires),
+ SCRIPT(js),
DIV(_id=div_id, _style='position:absolute;'))
else:
attr['_name'] = field.name
- attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
+ attr['_onblur'] = "jQuery('#%(div_id)s').delay(500).fadeOut('slow');" % \
dict(div_id=div_id, u='F' + self.keyword)
- attr['_onkeyup'] = "var e=event.which?event.which:event.keyCode; function %(u)s(){jQuery('#%(id)s').val(jQuery('#%(key)s').val())}; if(e==39) %(u)s(); else if(e==40) {if(jQuery('#%(key)s option:selected').next().length)jQuery('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); %(u)s();} else if(e==38) {if(jQuery('#%(key)s option:selected').prev().length)jQuery('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); %(u)s();} else if(jQuery('#%(id)s').val().length>=%(min_length)s) jQuery.get('%(url)s?%(key)s='+encodeURIComponent(jQuery('#%(id)s').val()),function(data){jQuery('#%(id)s').next('.error').hide();jQuery('#%(div_id)s').html(data).show().focus();jQuery('#%(div_id)s select').css('width',jQuery('#%(id)s').css('width'));jQuery('#%(key)s').change(%(u)s);jQuery('#%(key)s').click(%(u)s);}); else jQuery('#%(div_id)s').fadeOut('slow');" % \
+ js = """
+ (function($) {
+ function doit(e_) {
+ var e=e_.which?e_.which:e_.keyCode;
+ function %(u)s(){
+ $('#%(id)s').val($('#%(key)s').val())
+ };
+ if(e==39) %(u)s();
+ else if(e==40) {
+ if($('#%(key)s option:selected').next().length)
+ $('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected');
+ %(u)s();
+ } else if(e==38) {
+ if($('#%(key)s option:selected').prev().length)
+ $('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected');
+ %(u)s();
+ } else if($('#%(id)s').val().length>=%(min_length)s)
+ $.get('%(url)s&%(key)s='+encodeURIComponent($('#%(id)s').val()),
+ function(data){
+ $('#%(id)s').next('.error').hide();
+ $('#%(div_id)s').html(data).show().focus();
+ $('#%(div_id)s select').css('width',$('#%(id)s').css('width'));
+ $('#%(key)s').change(%(u)s).click(%(u)s);
+ }
+ );
+ else $('#%(div_id)s').fadeOut('slow');
+ }
+ var tmr = null;
+ $("#%(id)s").on('keyup focus',function(e) {
+ if (tmr) clearTimeout(tmr);
+ if($('#%(id)s').val().length>=%(min_length)s) {
+ tmr = setTimeout(function() { tmr = null; doit(e); }, 300);
+ }
+ });
+ })(jQuery)""".replace('\n', '').replace(' ' * 4, '') % \
dict(url=self.url, min_length=self.min_length,
key=self.keyword, id=attr['_id'], div_id=div_id, u='F' + self.keyword)
- if self.min_length == 0:
- attr['_onfocus'] = attr['_onkeyup']
- return CAT(INPUT(**attr),
+ return CAT(INPUT(**attr), SCRIPT(js),
DIV(_id=div_id, _style='position:absolute;'))
@@ -875,7 +970,7 @@ def formstyle_bootstrap(form, fields):
controls.add_class('span4')
if isinstance(label, LABEL):
- label['_class'] = add_class(label.get('_class'),'control-label')
+ label['_class'] = add_class(label.get('_class'), 'control-label')
if _submit:
# submit button has unwrapped label and controls, different class
@@ -925,8 +1020,11 @@ def formstyle_bootstrap3_stacked(form, fields):
for e in controls.elements("input"):
e.add_class('form-control')
+ elif isinstance(controls, CAT) and isinstance(controls[0], INPUT):
+ controls[0].add_class('form-control')
+
if isinstance(label, LABEL):
- label['_class'] = add_class(label.get('_class'),'control-label')
+ label['_class'] = add_class(label.get('_class'), 'control-label')
parent.append(DIV(label, _controls, _class='form-group', _id=id))
return parent
@@ -975,8 +1073,10 @@ def _inner(form, fields):
elif isinstance(controls, UL):
for e in controls.elements("input"):
e.add_class('form-control')
+ elif isinstance(controls, CAT) and isinstance(controls[0], INPUT):
+ controls[0].add_class('form-control')
if isinstance(label, LABEL):
- label['_class'] = add_class(label.get('_class'),'control-label %s' % label_col_class)
+ label['_class'] = add_class(label.get('_class'), 'control-label %s' % label_col_class)
parent.append(DIV(label, _controls, _class='form-group', _id=id))
return parent
diff --git a/gluon/template.py b/gluon/template.py
index 2afc29abe..41da7f2d6 100644
--- a/gluon/template.py
+++ b/gluon/template.py
@@ -426,7 +426,7 @@ def _get_file_text(self, filename):
# Allow Views to include other views dynamically
context = self.context
- if current and not "response" in context:
+ if current and "response" not in context:
context["response"] = getattr(current, 'response', None)
# Get the filename; filename looks like ``"template.html"``.
@@ -779,10 +779,10 @@ def parse_template(filename,
# First, if we have a str try to open the file
if isinstance(filename, str):
+ fname = os.path.join(path, filename)
try:
- fp = open(os.path.join(path, filename), 'rb')
- text = fp.read()
- fp.close()
+ with open(fname, 'rb') as fp:
+ text = fp.read()
except IOError:
raise RestrictedError(filename, '', 'Unable to find the file')
else:
@@ -890,7 +890,7 @@ def render(content="hello world",
Response = DummyResponse
# Add it to the context so we can use it.
- if not 'NOESCAPE' in context:
+ if 'NOESCAPE' not in context:
context['NOESCAPE'] = NOESCAPE
if isinstance(content, unicodeT):
@@ -936,8 +936,3 @@ def render(content="hello world",
if old_response_body is not None:
context['response'].body = old_response_body
return text
-
-
-if __name__ == '__main__':
- import doctest
- doctest.testmod()
diff --git a/gluon/tests/test_appadmin.py b/gluon/tests/test_appadmin.py
index 9d7e04937..4b502c126 100644
--- a/gluon/tests/test_appadmin.py
+++ b/gluon/tests/test_appadmin.py
@@ -10,12 +10,13 @@
import unittest
-from gluon.compileapp import run_controller_in, run_view_in
+from gluon.compileapp import run_controller_in, run_view_in, compile_application, remove_compiled_application
from gluon.languages import translator
from gluon.storage import Storage, List
from gluon import fileutils
from gluon.dal import DAL, Field, Table
from gluon.http import HTTP
+from gluon.fileutils import open_file
DEFAULT_URI = os.getenv('DB', 'sqlite:memory')
@@ -30,6 +31,7 @@ class TestAppAdmin(unittest.TestCase):
def setUp(self):
from gluon.globals import Request, Response, Session, current
from gluon.html import A, DIV, FORM, MENU, TABLE, TR, INPUT, URL, XML
+ from gluon.html import ASSIGNJS
from gluon.validators import IS_NOT_EMPTY
from gluon.compileapp import LOAD
from gluon.http import HTTP, redirect
@@ -76,16 +78,31 @@ def run_function(self):
def run_view(self):
return run_view_in(self.env)
- def test_index(self):
+ def run_view_file_stream(self):
+ view_path = os.path.join(self.env['request'].folder, 'views', 'appadmin.html')
+ self.env['response'].view = open_file(view_path, 'r')
+ return run_view_in(self.env)
+
+ def _test_index(self):
result = self.run_function()
self.assertTrue('db' in result['databases'])
self.env.update(result)
try:
self.run_view()
+ self.run_view_file_stream()
except Exception as e:
print(e.message)
self.fail('Could not make the view')
+ def test_index(self):
+ self._test_index()
+
+ def test_index_compiled(self):
+ appname_path = os.path.join(os.getcwd(), 'applications', 'welcome')
+ compile_application(appname_path)
+ self._test_index()
+ remove_compiled_application(appname_path)
+
def test_select(self):
request = self.env['request']
request.args = List(['db'])
diff --git a/gluon/tests/test_html.py b/gluon/tests/test_html.py
index d7a75517c..e091d1cef 100644
--- a/gluon/tests/test_html.py
+++ b/gluon/tests/test_html.py
@@ -253,6 +253,7 @@ def test_DIV(self):
# test .get('attrib')
self.assertEqual(DIV('Test
', _class="class_test").get('_class'), 'class_test')
+ self.assertEqual(DIV(b'a').xml(), b'a
')
def test_CAT(self):
# Empty CAT()
diff --git a/gluon/tools.py b/gluon/tools.py
index e0838f9fd..eb4a61f92 100644
--- a/gluon/tools.py
+++ b/gluon/tools.py
@@ -12,9 +12,11 @@
import base64
from functools import reduce
-from gluon._compat import pickle, thread, urllib2, Cookie, StringIO, configparser, MIMEBase, MIMEMultipart, \
- MIMEText, Encoders, Charset, long, urllib_quote, iteritems, to_bytes, to_native, add_charset, \
- charset_QP, basestring, unicodeT, to_unicode
+from gluon._compat import pickle, thread, urllib2, Cookie, StringIO
+from gluon._compat import configparser, MIMEBase, MIMEMultipart, MIMEText
+from gluon._compat import Encoders, Charset, long, urllib_quote, iteritems
+from gluon._compat import to_bytes, to_native, add_charset
+from gluon._compat import charset_QP, basestring, unicodeT, to_unicode
import datetime
import logging
import sys
@@ -40,8 +42,9 @@
from gluon.fileutils import read_file, check_credentials
from gluon import *
from gluon.contrib.autolinks import expand_one
-from gluon.contrib.markmin.markmin2html import \
- replace_at_urls, replace_autolinks, replace_components
+from gluon.contrib.markmin.markmin2html import replace_at_urls
+from gluon.contrib.markmin.markmin2html import replace_autolinks
+from gluon.contrib.markmin.markmin2html import replace_components
from pydal.objects import Row, Set, Query
import gluon.serializers as serializers
@@ -1578,7 +1581,6 @@ class Auth(object):
logged_out='Logged out',
registration_successful='Registration successful',
invalid_email='Invalid email',
- unable_send_email='Unable to send email',
invalid_login='Invalid login',
invalid_user='Invalid user',
invalid_password='Invalid password',
@@ -3633,7 +3635,7 @@ def reset_password_deprecated(self,
message=self.messages.retrieve_password % dict(password=password)):
session.flash = self.messages.email_sent
else:
- session.flash = self.messages.unable_to_send_email
+ session.flash = self.messages.unable_send_email
self.log_event(log, user)
callback(onaccept, form)
if not next:
@@ -3930,7 +3932,7 @@ def request_reset_password(self,
if self.email_reset_password(user):
session.flash = self.messages.email_sent
else:
- session.flash = self.messages.unable_to_send_email
+ session.flash = self.messages.unable_send_email
self.log_event(log, user)
callback(onaccept, form)
if not next:
diff --git a/gluon/utils.py b/gluon/utils.py
index a9e9f842c..a7ffdeb68 100644
--- a/gluon/utils.py
+++ b/gluon/utils.py
@@ -18,24 +18,24 @@
import time
import os
import re
-import sys
import logging
import socket
import base64
import zlib
+import hashlib
+import binascii
+import hmac
+from hashlib import md5, sha1, sha224, sha256, sha384, sha512
from gluon._compat import basestring, pickle, PY2, xrange, to_bytes, to_native
_struct_2_long_long = struct.Struct('=QQ')
-import hashlib, binascii
-from hashlib import md5, sha1, sha224, sha256, sha384, sha512
-
try:
from Crypto.Cipher import AES
+ HAVE_AES = True
except ImportError:
- import gluon.contrib.aes as AES
-
-import hmac
+ import gluon.contrib.pyaes as PYAES
+ HAVE_AES = False
if hasattr(hashlib, "pbkdf2_hmac"):
def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None):
@@ -66,11 +66,35 @@ def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None):
def AES_new(key, IV=None):
- """ Returns an AES cipher object and random IV if None specified """
+ """Return an AES cipher object and random IV if None specified."""
if IV is None:
IV = fast_urandom16()
+ if HAVE_AES:
+ return AES.new(key, AES.MODE_CBC, IV), IV
+ else:
+ return PYAES.AESModeOfOperationCBC(key, iv=IV), IV
- return AES.new(key, AES.MODE_CBC, IV), IV
+
+def AES_enc(cipher, data):
+ """Encrypt data with the cipher."""
+ if HAVE_AES:
+ return cipher.encrypt(data)
+ else:
+ encrypter = PYAES.Encrypter(cipher)
+ enc = encrypter.feed(data)
+ enc += encrypter.feed()
+ return enc
+
+
+def AES_dec(cipher, data):
+ """Decrypt data with the cipher."""
+ if HAVE_AES:
+ return cipher.decrypt(data)
+ else:
+ decrypter = PYAES.Decrypter(cipher)
+ dec = decrypter.feed(data)
+ dec += decrypter.feed()
+ return dec
def compare(a, b):
@@ -79,20 +103,17 @@ def compare(a, b):
return hmac.compare_digest(a, b)
result = len(a) ^ len(b)
for i in xrange(len(b)):
- result |= ord(a[i%len(a)]) ^ ord(b[i])
+ result |= ord(a[i % len(a)]) ^ ord(b[i])
return result == 0
def md5_hash(text):
- """ Generates a md5 hash with the given text """
+ """Generate an md5 hash with the given text."""
return md5(to_bytes(text)).hexdigest()
def simple_hash(text, key='', salt='', digest_alg='md5'):
- """
- Generates hash with the given text using the specified
- digest hashing algorithm
- """
+ """Generate hash with the given text using the specified digest algorithm."""
text = to_bytes(text)
key = to_bytes(key)
salt = to_bytes(salt)
@@ -114,9 +135,7 @@ def simple_hash(text, key='', salt='', digest_alg='md5'):
def get_digest(value):
- """
- Returns a hashlib digest algorithm from a string
- """
+ """Return a hashlib digest algorithm from a string."""
if not isinstance(value, str):
return value
value = value.lower()
@@ -165,11 +184,11 @@ def pad(s, n=32):
def unpad(s, n=32):
padlen = s[-1]
- if isinstance(padlen,str):
- padlen = ord(padlen) # python2
- if (padlen < 1) | (padlen > n): # avoid short-circuit
+ if isinstance(padlen, str):
+ padlen = ord(padlen) # python2
+ if (padlen < 1) | (padlen > n): # avoid short-circuit
# return garbage to minimize side channels
- return bytes(bytearray(len(s)*[0]))
+ return bytes(bytearray(len(s) * [0]))
return s[:-padlen]
@@ -181,7 +200,7 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None):
if not hash_key:
hash_key = hashlib.sha256(encryption_key).digest()
cipher, IV = AES_new(pad(encryption_key)[:32])
- encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump)))
+ encrypted_data = base64.urlsafe_b64encode(IV + AES_enc(cipher, pad(dump)))
signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data, hashlib.sha256).hexdigest())
return b'hmac256:' + signature + b':' + encrypted_data
@@ -192,7 +211,7 @@ def secure_loads(data, encryption_key, hash_key=None, compression_level=None):
return secure_loads_deprecated(data, encryption_key, hash_key, compression_level)
if components != 2:
return None
- version,signature,encrypted_data = data.split(b':', 2)
+ version, signature, encrypted_data = data.split(b':', 2)
if version != b'hmac256':
return None
encryption_key = to_bytes(encryption_key)
@@ -205,7 +224,7 @@ def secure_loads(data, encryption_key, hash_key=None, compression_level=None):
IV, encrypted_data = encrypted_data[:16], encrypted_data[16:]
cipher, _ = AES_new(pad(encryption_key)[:32], IV=IV)
try:
- data = unpad(cipher.decrypt(encrypted_data))
+ data = unpad(AES_dec(cipher, encrypted_data))
if compression_level:
data = zlib.decompress(data)
return pickle.loads(data)
@@ -226,7 +245,7 @@ def secure_dumps_deprecated(data, encryption_key, hash_key=None, compression_lev
dump = zlib.compress(dump, compression_level)
key = __pad_deprecated(encryption_key)[:32]
cipher, IV = AES_new(key)
- encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump)))
+ encrypted_data = base64.urlsafe_b64encode(IV + AES_enc(cipher, pad(dump)))
signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data, hashlib.md5).hexdigest())
return signature + b':' + encrypted_data
@@ -248,7 +267,7 @@ def secure_loads_deprecated(data, encryption_key, hash_key=None, compression_lev
IV, encrypted_data = encrypted_data[:16], encrypted_data[16:]
cipher, _ = AES_new(key, IV=IV)
try:
- data = cipher.decrypt(encrypted_data)
+ data = AES_dec(cipher, encrypted_data)
data = data.rstrip(b' ')
if compression_level:
data = zlib.decompress(data)
@@ -285,9 +304,9 @@ def initialize_urandom():
frandom = open('/dev/urandom', 'wb')
try:
if PY2:
- frandom.write(''.join(chr(t) for t in ctokens)) # python 2
+ frandom.write(''.join(chr(t) for t in ctokens))
else:
- frandom.write(bytes([]).join(bytes([t]) for t in ctokens)) # python 3
+ frandom.write(bytes([]).join(bytes([t]) for t in ctokens))
finally:
frandom.close()
except IOError:
@@ -300,9 +319,9 @@ def initialize_urandom():
your system does not provide a cryptographically secure entropy source.
This is not specific to web2py; consider deploying on a different operating system.""")
if PY2:
- packed = ''.join(chr(x) for x in ctokens) # python 2
+ packed = ''.join(chr(x) for x in ctokens)
else:
- packed = bytes([]).join(bytes([x]) for x in ctokens) # python 3
+ packed = bytes([]).join(bytes([x]) for x in ctokens)
unpacked_ctokens = _struct_2_long_long.unpack(packed)
return unpacked_ctokens, have_urandom
UNPACKED_CTOKENS, HAVE_URANDOM = initialize_urandom()
@@ -392,7 +411,7 @@ def is_loopback_ip_address(ip=None, addrinfo=None):
Determines whether the address appears to be a loopback address.
This assumes that the IP is valid.
"""
- if addrinfo: # see socket.getaddrinfo() for layout of addrinfo tuple
+ if addrinfo: # see socket.getaddrinfo() for layout of addrinfo tuple
if addrinfo[0] == socket.AF_INET or addrinfo[0] == socket.AF_INET6:
ip = addrinfo[4]
if not isinstance(ip, basestring):
@@ -433,11 +452,10 @@ def local_html_escape(data, quote=False):
import html
if isinstance(data, str):
return html.escape(data, quote=quote)
- data = data.replace(b"&", b"&") # Must be done first!
+ data = data.replace(b"&", b"&") # Must be done first!
data = data.replace(b"<", b"<")
data = data.replace(b">", b">")
if quote:
data = data.replace(b'"', b""")
data = data.replace(b'\'', b"'")
return data
-
diff --git a/gluon/widget.py b/gluon/widget.py
index af3b76892..131bfc3f6 100644
--- a/gluon/widget.py
+++ b/gluon/widget.py
@@ -55,8 +55,11 @@ def run_system_tests(options):
"""
import subprocess
major_version = sys.version_info[0]
- minor_version = sys.version_info[1]
call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests']
+ if major_version == 2:
+ sys.stderr.write("Python 2.7\n")
+ else:
+ sys.stderr.write("Experimental Python 3.x.\n")
if options.with_coverage:
has_coverage = False
coverage_exec = 'coverage2' if major_version == 2 else 'coverage3'
@@ -70,14 +73,12 @@ def run_system_tests(options):
coverage_config_file)
call_args = [coverage_exec, 'run', '--rcfile=%s' %
coverage_config, '-m', 'unittest', '-v', 'gluon.tests']
- if major_version == 2:
- sys.stderr.write("Python 2.7\n")
- else:
- sys.stderr.write("Experimental Python 3.x.\n")
if has_coverage:
ret = subprocess.call(call_args)
else:
ret = 256
+ else:
+ ret = subprocess.call(call_args)
sys.exit(ret and 1)
diff --git a/handlers/wsgihandler.py b/handlers/wsgihandler.py
index 39b66d6c1..e4f19c9c3 100644
--- a/handlers/wsgihandler.py
+++ b/handlers/wsgihandler.py
@@ -7,22 +7,16 @@
License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
-This is a WSGI handler for Apache
-Requires apache+mod_wsgi.
-
-In httpd.conf put something like:
-
- LoadModule wsgi_module modules/mod_wsgi.so
- WSGIScriptAlias / /path/to/wsgihandler.py
-
+This is a WSGI handler
"""
+import sys
+import os
+
# change these parameters as required
LOGGING = False
SOFTCRON = False
-import sys
-import os
path = os.path.dirname(os.path.abspath(__file__))
os.chdir(path)
@@ -32,8 +26,6 @@
sys.path = [path] + [p for p in sys.path if not p == path]
-sys.stdout = sys.stderr
-
import gluon.main
if LOGGING:
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 904545b4f..000000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-pycrypto
diff --git a/scripts/sessions2trash.py b/scripts/sessions2trash.py
index 65758c876..f338b453b 100755
--- a/scripts/sessions2trash.py
+++ b/scripts/sessions2trash.py
@@ -30,18 +30,14 @@ def delete_sessions():
from __future__ import with_statement
-import sys
-import os
-sys.path.append(os.path.join(*__file__.split(os.sep)[:-2] or ['.']))
-
from gluon import current
from gluon.storage import Storage
+from gluon._compat import pickle
from optparse import OptionParser
-import cPickle
import datetime
-import os
import stat
import time
+import os
EXPIRATION_MINUTES = 60
SLEEP_MINUTES = 5
@@ -86,12 +82,12 @@ def trash(self):
status = 'trashed'
if self.verbose > 1:
- print 'key: %s' % str(item)
- print 'expiration: %s seconds' % self.expiration
- print 'last visit: %s' % str(last_visit)
- print 'age: %s seconds' % age
- print 'status: %s' % status
- print ''
+ print('key: %s' % str(item))
+ print('expiration: %s seconds' % self.expiration)
+ print('last visit: %s' % str(last_visit))
+ print('age: %s seconds' % age)
+ print('status: %s' % status)
+ print('')
elif self.verbose > 0:
print('%s %s' % (str(item), status))
@@ -145,7 +141,7 @@ def delete(self):
def get(self):
session = Storage()
- session.update(cPickle.loads(self.row.session_data))
+ session.update(pickle.loads(self.row.session_data))
return session
def last_visit_default(self):
@@ -155,7 +151,7 @@ def last_visit_default(self):
try:
return datetime.datetime.strptime(self.row.modified_datetime, '%Y-%m-%d %H:%M:%S.%f')
except:
- print 'failed to retrieve last modified time (value: %s)' % self.row.modified_datetime
+ print('failed to retrieve last modified time (value: %s)' % self.row.modified_datetime)
def __str__(self):
return self.row.unique_key
@@ -248,7 +244,7 @@ def main():
break
else:
if options.verbose:
- print 'Sleeping %s seconds' % (options.sleep)
+ print('Sleeping %s seconds' % (options.sleep))
time.sleep(options.sleep)
if __name__ == '__main__':