Skip to content

Commit 74394cd

Browse files
committed
Add _restful_mysql module and a mysql cmdline tool
1 parent 90f7d61 commit 74394cd

18 files changed

Lines changed: 2375 additions & 4 deletions

dev_server/cloudsql.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright (C) 2012-2013 SINA, All rights reserved.
4+
5+
"""Command line client for SAE MySQL Service. """
6+
7+
import sys
8+
import os
9+
import logging
10+
import optparse
11+
12+
import sae._restful_mysql
13+
import sae._restful_mysql._mysql_exceptions
14+
sys.modules['_mysql_exceptions'] = sae._restful_mysql._mysql_exceptions
15+
16+
from grizzled import db
17+
from grizzled.db import mysql
18+
import prettytable
19+
import sqlcmd
20+
from sqlcmd import config
21+
22+
logging.basicConfig(level=logging.WARNING)
23+
sqlcmd.log = logging.getLogger('cloudsql')
24+
25+
sqlcmd.DEFAULT_CONFIG_DIR = os.path.expanduser('~/.saecloud')
26+
sqlcmd.RC_FILE = os.path.join(sqlcmd.DEFAULT_CONFIG_DIR, 'cloudsql.config')
27+
sqlcmd.HISTORY_FILE_FORMAT = os.path.join(sqlcmd.DEFAULT_CONFIG_DIR, '%s.hist')
28+
sqlcmd.INTRO = 'SAE MySQL Client\n\nType "help" or "?" for help.\n'
29+
30+
DEFAULT_ENCODING = 'utf-8'
31+
USAGE = '%prog [options] database_name'
32+
33+
DEFAULT_SAE_MYSQL_HOST = 'w.rdc.sae.sina.com.cn'
34+
DEFAULT_SAE_MYSQL_PORT = 3307
35+
DEFAULT_SAE_MYSQL_DB_PREFIX = 'app_'
36+
37+
class CloudSqlDriver(mysql.MySQLDriver):
38+
"""Grizzled DB Driver for Cloud SAE MySQL Service."""
39+
40+
NAME = 'cloudsql'
41+
42+
def get_import(self):
43+
return sae._restful_mysql
44+
45+
def get_display_name(self):
46+
return 'Cloud SQL'
47+
48+
def do_connect(self, host, port, user, password, database):
49+
# Fix grizzled's mysql driver which omit the port argument when connecting.
50+
dbi = self.get_import()
51+
port = port and int(port) or 3306
52+
return dbi.connect(host=host, user=user, passwd=password, db=database, port=port)
53+
54+
class CloudSqlCmd(sqlcmd.SQLCmd):
55+
"""The SQLCmd command interpreter for Cloud SQL."""
56+
57+
sqlcmd.SQLCmd.MAIN_PROMPT = 'mysql> '
58+
sqlcmd.SQLCmd.CONTINUATION_PROMPT = ' -> '
59+
60+
sqlcmd.SQLCmd.NO_SEMI_NEEDED.update(
61+
['about', 'desc', 'describe', 'echo', 'exit', 'h', 'hist',
62+
'history', 'load', 'run', 'r', 'redo', 'set', 'show', 'var', 'vars'])
63+
64+
for method in ['do_dot_connect', 'do_dot_desc', 'do_begin']:
65+
delattr(sqlcmd.SQLCmd, method)
66+
67+
for cmd in ['show', 'describe', 'echo', 'load', 'run', 'exit', 'h',
68+
'hist', 'history', 'var', 'vars', 'about']:
69+
method = 'do_dot_' + cmd
70+
setattr(sqlcmd.SQLCmd, method.replace('dot_', ''), getattr(
71+
sqlcmd.SQLCmd, method))
72+
delattr(sqlcmd.SQLCmd, method)
73+
method = 'complete_dot_' + cmd
74+
if hasattr(sqlcmd.SQLCmd, method):
75+
setattr(sqlcmd.SQLCmd, method.replace('dot_', ''), getattr(
76+
sqlcmd.SQLCmd, method))
77+
delattr(sqlcmd.SQLCmd, method)
78+
79+
def do_redo(self, args):
80+
# XXX: Fix global name 'do_r' is not defined problem in sqlcmd
81+
self.do_r(args)
82+
83+
def _SQLCmd__set_setting(self, varname, value):
84+
# XXX: Fix bool object has no lower attribute in sqlcmd
85+
return sqlcmd.SQLCmd._SQLCmd__set_setting(self, varname, str(value))
86+
87+
def do_desc(self, args):
88+
self.do_describe(args, cmd='.desc')
89+
complete_desc = sqlcmd.SQLCmd.complete_dot_desc
90+
91+
def do_load(self, args):
92+
self.do_run(args)
93+
94+
def __init__(self, *args, **kwargs):
95+
sqlcmd.SQLCmd.__init__(self, *args, **kwargs)
96+
self.prompt = sqlcmd.SQLCmd.MAIN_PROMPT
97+
self.output_encoding = DEFAULT_ENCODING
98+
99+
def set_output_encoding(self, encoding):
100+
self.output_encoding = encoding
101+
102+
def _build_table(self, cursor):
103+
"""Builds an output PrettyTable from the results in the given cursor."""
104+
if not cursor.description:
105+
return None
106+
107+
column_names = [column[0] for column in cursor.description]
108+
table = prettytable.PrettyTable(column_names)
109+
rows = cursor.fetchall()
110+
if not rows:
111+
return table
112+
for i, col in enumerate(rows[0]):
113+
table.align[column_names[i]] = isinstance(col, basestring) and 'l' or 'r'
114+
for row in rows: table.add_row(row)
115+
return table
116+
117+
def _SQLCmd__handle_select(self, args, cursor, command='select'):
118+
"""Overrides SQLCmd.__handle_select to display output with prettytable."""
119+
self._SQLCmd__exec_SQL(cursor, command, args)
120+
table = self._build_table(cursor)
121+
if table:
122+
output = table.get_string()
123+
if isinstance(output, unicode):
124+
print output.encode(self.output_encoding)
125+
else:
126+
print output
127+
128+
def _create_config_dir():
129+
"""Creates the sqlcmd config directory if necessary."""
130+
directory = sqlcmd.DEFAULT_CONFIG_DIR
131+
if not os.access(directory, os.R_OK | os.W_OK | os.X_OK):
132+
old_umask = os.umask(077)
133+
os.makedirs(sqlcmd.DEFAULT_CONFIG_DIR)
134+
os.umask(old_umask)
135+
136+
def main(argv):
137+
parser = optparse.OptionParser(usage=USAGE)
138+
parser.add_option('-u', '--username', dest='username',
139+
help='MySQL username to use when connecting to the server.')
140+
parser.add_option('-p', '--password', dest='password',
141+
help='MySQL password to use when connecting to the server.')
142+
parser.add_option('-e', '--output_encoding', dest='output_encoding',
143+
default=DEFAULT_ENCODING,
144+
help='Output encoding. Defaults to %s.' % DEFAULT_ENCODING)
145+
146+
(options, args) = parser.parse_args(argv[1:])
147+
148+
if len(args) != 1:
149+
parser.print_help(sys.stderr)
150+
return 1
151+
152+
if not options.username or not options.password:
153+
print >>sys.stderr, 'Error: username or password is missing.\n'
154+
return 1
155+
156+
if args[0].startswith(DEFAULT_SAE_MYSQL_DB_PREFIX):
157+
database_name = args[0]
158+
else:
159+
database_name = DEFAULT_SAE_MYSQL_DB_PREFIX + args[0]
160+
instance_alias = database_name
161+
162+
_create_config_dir()
163+
164+
db.add_driver(CloudSqlDriver.NAME, CloudSqlDriver)
165+
sql_cmd_config = config.SQLCmdConfig(None)
166+
sql_cmd_config.add('__cloudsql__', instance_alias,
167+
DEFAULT_SAE_MYSQL_HOST , DEFAULT_SAE_MYSQL_PORT, database_name,
168+
CloudSqlDriver.NAME, options.username, options.password)
169+
sql_cmd = CloudSqlCmd(sql_cmd_config)
170+
sql_cmd.set_output_encoding(options.output_encoding)
171+
sql_cmd.set_database(instance_alias)
172+
sql_cmd.cmdloop()
173+
174+
return 0
175+
176+
if __name__ == '__main__':
177+
sys.exit(main(sys.argv))
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""MySQLdb - A DB API v2.0 compatible interface to MySQL.
2+
3+
This package is a wrapper around _mysql, which mostly implements the
4+
MySQL C API.
5+
6+
connect() -- connects to server
7+
8+
See the C API specification and the MySQL documentation for more info
9+
on other items.
10+
11+
For information on how MySQLdb handles type conversion, see the
12+
MySQLdb.converters module.
13+
14+
"""
15+
16+
__revision__ = """$Revision: 603 $"""[11:-2]
17+
from release import __version__, version_info, __author__
18+
19+
import _mysql
20+
21+
if version_info != _mysql.version_info:
22+
raise ImportError("this is MySQLdb version %s, but _mysql is version %r" %
23+
(version_info, _mysql.version_info))
24+
25+
threadsafety = 1
26+
apilevel = "2.0"
27+
paramstyle = "format"
28+
29+
from _mysql import *
30+
from constants import FIELD_TYPE
31+
from times import Date, Time, Timestamp, \
32+
DateFromTicks, TimeFromTicks, TimestampFromTicks
33+
34+
try:
35+
frozenset
36+
except NameError:
37+
from sets import ImmutableSet as frozenset
38+
39+
class DBAPISet(frozenset):
40+
41+
"""A special type of set for which A == x is true if A is a
42+
DBAPISet and x is a member of that set."""
43+
44+
def __eq__(self, other):
45+
if isinstance(other, DBAPISet):
46+
return not self.difference(other)
47+
return other in self
48+
49+
50+
STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING,
51+
FIELD_TYPE.VAR_STRING])
52+
BINARY = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB,
53+
FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB])
54+
NUMBER = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT,
55+
FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG,
56+
FIELD_TYPE.TINY, FIELD_TYPE.YEAR])
57+
DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE])
58+
TIME = DBAPISet([FIELD_TYPE.TIME])
59+
TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME])
60+
DATETIME = TIMESTAMP
61+
ROWID = DBAPISet()
62+
63+
def test_DBAPISet_set_equality():
64+
assert STRING == STRING
65+
66+
def test_DBAPISet_set_inequality():
67+
assert STRING != NUMBER
68+
69+
def test_DBAPISet_set_equality_membership():
70+
assert FIELD_TYPE.VAR_STRING == STRING
71+
72+
def test_DBAPISet_set_inequality_membership():
73+
assert FIELD_TYPE.DATE != STRING
74+
75+
def Binary(x):
76+
return str(x)
77+
78+
def Connect(*args, **kwargs):
79+
"""Factory function for connections.Connection."""
80+
from connections import Connection
81+
return Connection(*args, **kwargs)
82+
83+
connect = Connection = Connect
84+
85+
__all__ = [ 'BINARY', 'Binary', 'Connect', 'Connection', 'DATE',
86+
'Date', 'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks',
87+
'TimestampFromTicks', 'DataError', 'DatabaseError', 'Error',
88+
'FIELD_TYPE', 'IntegrityError', 'InterfaceError', 'InternalError',
89+
'MySQLError', 'NULL', 'NUMBER', 'NotSupportedError', 'DBAPISet',
90+
'OperationalError', 'ProgrammingError', 'ROWID', 'STRING', 'TIME',
91+
'TIMESTAMP', 'Warning', 'apilevel', 'connect', 'connections',
92+
'constants', 'converters', 'cursors', 'debug', 'escape', 'escape_dict',
93+
'escape_sequence', 'escape_string', 'get_client_info',
94+
'paramstyle', 'string_literal', 'threadsafety', 'version_info']
95+
96+
97+
98+

0 commit comments

Comments
 (0)