PageRenderTime 60ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/pandas/io/tests/test_gbq.py

https://github.com/ajcr/pandas
Python | 474 lines | 452 code | 18 blank | 4 comment | 3 complexity | 3104a727c788bb1afd82cbf401c62d0d MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0
  1. import ast
  2. import nose
  3. import os
  4. import shutil
  5. import subprocess
  6. import numpy as np
  7. import pandas.io.gbq as gbq
  8. import pandas.util.testing as tm
  9. from pandas.core.frame import DataFrame
  10. from pandas.util.testing import with_connectivity_check
  11. from pandas.compat import u
  12. from pandas import NaT
  13. try:
  14. import bq
  15. import bigquery_client
  16. import gflags as flags
  17. except ImportError:
  18. raise nose.SkipTest
  19. ####################################################################################
  20. # Fake Google BigQuery Client
  21. class FakeClient:
  22. def __init__(self):
  23. self.apiclient = FakeApiClient()
  24. def GetTableSchema(self,table_dict):
  25. retval = {'fields': [
  26. {'type': 'STRING', 'name': 'corpus', 'mode': 'NULLABLE'},
  27. {'type': 'INTEGER', 'name': 'corpus_date', 'mode': 'NULLABLE'},
  28. {'type': 'STRING', 'name': 'word', 'mode': 'NULLABLE'},
  29. {'type': 'INTEGER', 'name': 'word_count', 'mode': 'NULLABLE'}
  30. ]}
  31. return retval
  32. # Fake Google BigQuery API Client
  33. class FakeApiClient:
  34. def __init__(self):
  35. self._fakejobs = FakeJobs()
  36. def jobs(self):
  37. return self._fakejobs
  38. class FakeJobs:
  39. def __init__(self):
  40. self._fakequeryresults = FakeResults()
  41. def getQueryResults(self, job_id=None, project_id=None,
  42. max_results=None, timeout_ms=None, **kwargs):
  43. return self._fakequeryresults
  44. class FakeResults:
  45. def execute(self):
  46. return {'rows': [ {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'brave'}, {'v': '3'}]},
  47. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'attended'}, {'v': '1'}]},
  48. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'treason'}, {'v': '1'}]},
  49. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'islanders'}, {'v': '1'}]},
  50. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'heed'}, {'v': '3'}]},
  51. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'alehouse'}, {'v': '1'}]},
  52. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'corrigible'}, {'v': '1'}]},
  53. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'brawl'}, {'v': '2'}]},
  54. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': "'"}, {'v': '17'}]},
  55. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'troubled'}, {'v': '1'}]}
  56. ],
  57. 'kind': 'bigquery#tableDataList',
  58. 'etag': '"4PTsVxg68bQkQs1RJ1Ndewqkgg4/hoRHzb4qfhJAIa2mEewC-jhs9Bg"',
  59. 'totalRows': '10',
  60. 'jobComplete' : True}
  61. ####################################################################################
  62. class TestGbq(tm.TestCase):
  63. def setUp(self):
  64. with open(self.fake_job_path, 'r') as fin:
  65. self.fake_job = ast.literal_eval(fin.read())
  66. self.test_data_small = [{'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'brave'}, {'v': '3'}]},
  67. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'attended'}, {'v': '1'}]},
  68. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'treason'}, {'v': '1'}]},
  69. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'islanders'}, {'v': '1'}]},
  70. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'heed'}, {'v': '3'}]},
  71. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'alehouse'}, {'v': '1'}]},
  72. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'corrigible'}, {'v': '1'}]},
  73. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'brawl'}, {'v': '2'}]},
  74. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': "'"}, {'v': '17'}]},
  75. {'f': [{'v': 'othello'}, {'v': '1603'}, {'v': 'troubled'},
  76. {'v': '1'}]}]
  77. self.correct_data_small = np.array(
  78. [('othello', 1603, 'brave', 3),
  79. ('othello', 1603, 'attended', 1),
  80. ('othello', 1603, 'treason', 1),
  81. ('othello', 1603, 'islanders', 1),
  82. ('othello', 1603, 'heed', 3),
  83. ('othello', 1603, 'alehouse', 1),
  84. ('othello', 1603, 'corrigible', 1),
  85. ('othello', 1603, 'brawl', 2),
  86. ('othello', 1603, "'", 17),
  87. ('othello', 1603, 'troubled', 1)
  88. ],
  89. dtype=[('corpus', 'S16'),
  90. ('corpus_date', '<i8'),
  91. ('word', 'S16'),
  92. ('word_count', '<i8')]
  93. )
  94. self.correct_test_datatype = DataFrame(
  95. {'VALID_STRING' : ['PI'],
  96. 'EMPTY_STRING' : [""],
  97. 'NULL_STRING' : [None],
  98. 'VALID_INTEGER' : [3],
  99. 'NULL_INTEGER' : [np.nan],
  100. 'VALID_FLOAT' : [3.141592653589793],
  101. 'NULL_FLOAT' : [np.nan],
  102. 'UNIX_EPOCH' : [np.datetime64('1970-01-01T00:00:00.000000Z')],
  103. 'VALID_TIMESTAMP' : [np.datetime64('2004-09-15T05:00:00.000000Z')],
  104. 'NULL_TIMESTAMP' :[NaT],
  105. 'TRUE_BOOLEAN' : [True],
  106. 'FALSE_BOOLEAN' : [False],
  107. 'NULL_BOOLEAN' : [None]
  108. }
  109. )[['VALID_STRING',
  110. 'EMPTY_STRING',
  111. 'NULL_STRING',
  112. 'VALID_INTEGER',
  113. 'NULL_INTEGER',
  114. 'VALID_FLOAT',
  115. 'NULL_FLOAT',
  116. 'UNIX_EPOCH',
  117. 'VALID_TIMESTAMP',
  118. 'NULL_TIMESTAMP',
  119. 'TRUE_BOOLEAN',
  120. 'FALSE_BOOLEAN',
  121. 'NULL_BOOLEAN']]
  122. @classmethod
  123. def setUpClass(cls):
  124. # Integration tests require a valid bigquery token
  125. # be present in the user's home directory. This
  126. # can be generated with 'bq init' in the command line
  127. super(TestGbq, cls).setUpClass()
  128. cls.dirpath = tm.get_data_path()
  129. home = os.path.expanduser("~")
  130. cls.bq_token = os.path.join(home, '.bigquery.v2.token')
  131. cls.fake_job_path = os.path.join(cls.dirpath, 'gbq_fake_job.txt')
  132. # If we're using a valid token, make a test dataset
  133. # Note, dataset functionality is beyond the scope
  134. # of the module under test, so we rely on the command
  135. # line utility for this.
  136. if os.path.exists(cls.bq_token):
  137. subprocess.call(['bq','mk', '-d', 'pandas_testing_dataset'])
  138. @classmethod
  139. def tearDownClass(cls):
  140. super(TestGbq, cls).tearDownClass()
  141. # If we're using a valid token, remove the test dataset
  142. # created.
  143. if os.path.exists(cls.bq_token):
  144. subprocess.call(['bq', 'rm', '-r', '-f', '-d', 'pandas_testing_dataset'])
  145. @with_connectivity_check
  146. def test_valid_authentication(self):
  147. # If the user has a token file, they should recieve a client from gbq._authenticate
  148. if not os.path.exists(self.bq_token):
  149. raise nose.SkipTest('Skipped because authentication information is not available.')
  150. self.assertTrue(gbq._authenticate is not None, 'Authentication To GBQ Failed')
  151. @with_connectivity_check
  152. def test_malformed_query(self):
  153. # If the user has a connection file, performing an invalid query should raise an error
  154. if not os.path.exists(self.bq_token):
  155. raise nose.SkipTest('Skipped because authentication information is not available.')
  156. else:
  157. self.assertRaises(bigquery_client.BigqueryInvalidQueryError,
  158. gbq.read_gbq, "SELCET * FORM [publicdata:samples.shakespeare]")
  159. def test_type_conversion(self):
  160. # All BigQuery Types should be cast into appropriate numpy types
  161. sample_input = [('1.095292800E9', 'TIMESTAMP'),
  162. ('false', 'BOOLEAN'),
  163. ('2', 'INTEGER'),
  164. ('3.14159', 'FLOAT'),
  165. ('Hello World', 'STRING')]
  166. actual_output = [gbq._parse_entry(result[0],result[1]) for result in sample_input]
  167. sample_output = [np.datetime64('2004-09-16T00:00:00.000000Z'),
  168. np.bool(False),
  169. np.int('2'),
  170. np.float('3.14159'),
  171. u('Hello World')]
  172. self.assertEqual(actual_output, sample_output, 'A format conversion failed')
  173. @with_connectivity_check
  174. def test_unicode_string_conversion(self):
  175. # Strings from BigQuery Should be converted to UTF-8 properly
  176. if not os.path.exists(self.bq_token):
  177. raise nose.SkipTest('Skipped because authentication information is not available.')
  178. correct_test_datatype = DataFrame(
  179. {'UNICODE_STRING' : [u("\xe9\xfc")]}
  180. )
  181. query = """SELECT '\xc3\xa9\xc3\xbc' as UNICODE_STRING"""
  182. client = gbq._authenticate()
  183. a = gbq.read_gbq(query)
  184. tm.assert_frame_equal(a, correct_test_datatype)
  185. def test_data_small(self):
  186. # Parsing a fixed page of data should return the proper fixed np.array()
  187. result_frame = gbq._parse_page(self.test_data_small,
  188. ['corpus','corpus_date','word','word_count'],
  189. ['STRING','INTEGER','STRING','INTEGER'],
  190. [object,np.dtype(int),object,np.dtype(int)]
  191. )
  192. tm.assert_frame_equal(DataFrame(result_frame), DataFrame(self.correct_data_small),
  193. 'An element in the result DataFrame didn\'t match the sample set')
  194. def test_index_column(self):
  195. # A user should be able to specify an index column for return
  196. result_frame = gbq._parse_data(FakeClient(), self.fake_job, index_col='word')
  197. correct_frame = DataFrame(self.correct_data_small)
  198. correct_frame.set_index('word', inplace=True)
  199. self.assertTrue(result_frame.index.name == correct_frame.index.name)
  200. def test_column_order(self):
  201. # A User should be able to specify the order in which columns are returned in the dataframe
  202. col_order = ['corpus_date', 'word_count', 'corpus', 'word']
  203. result_frame = gbq._parse_data(FakeClient(), self.fake_job, col_order=col_order)
  204. tm.assert_index_equal(result_frame.columns, DataFrame(self.correct_data_small)[col_order].columns)
  205. def test_column_order_plus_index(self):
  206. # A User should be able to specify an index and the order of THE REMAINING columns.. they should be notified
  207. # if they screw up
  208. col_order = ['corpus_date', 'word', 'corpus']
  209. result_frame = gbq._parse_data(FakeClient(), self.fake_job, index_col='word_count', col_order=col_order)
  210. correct_frame_small = DataFrame(self.correct_data_small)
  211. correct_frame_small.set_index('word_count',inplace=True)
  212. correct_frame_small = DataFrame(correct_frame_small)[col_order]
  213. tm.assert_index_equal(result_frame.columns, correct_frame_small.columns)
  214. @with_connectivity_check
  215. def test_download_dataset_larger_than_100k_rows(self):
  216. # Test for known BigQuery bug in datasets larger than 100k rows
  217. # http://stackoverflow.com/questions/19145587/bq-py-not-paging-results
  218. if not os.path.exists(self.bq_token):
  219. raise nose.SkipTest('Skipped because authentication information is not available.')
  220. client = gbq._authenticate()
  221. a = gbq.read_gbq("SELECT id, FROM [publicdata:samples.wikipedia] LIMIT 100005")
  222. self.assertTrue(len(a) == 100005)
  223. @with_connectivity_check
  224. def test_download_all_data_types(self):
  225. # Test that all available data types from BigQuery (as of now)
  226. # are handled properly
  227. if not os.path.exists(self.bq_token):
  228. raise nose.SkipTest('Skipped because authentication information is not available.')
  229. query = """SELECT "PI" as VALID_STRING,
  230. "" as EMPTY_STRING,
  231. STRING(NULL) as NULL_STRING,
  232. INTEGER(3) as VALID_INTEGER,
  233. INTEGER(NULL) as NULL_INTEGER,
  234. PI() as VALID_FLOAT,
  235. FLOAT(NULL) as NULL_FLOAT,
  236. TIMESTAMP("1970-01-01 00:00:00") as UNIX_EPOCH,
  237. TIMESTAMP("2004-09-15 05:00:00") as VALID_TIMESTAMP,
  238. TIMESTAMP(NULL) as NULL_TIMESTAMP,
  239. BOOLEAN(TRUE) as TRUE_BOOLEAN,
  240. BOOLEAN(FALSE) as FALSE_BOOLEAN,
  241. BOOLEAN(NULL) as NULL_BOOLEAN"""
  242. client = gbq._authenticate()
  243. a = gbq.read_gbq(query, col_order = ['VALID_STRING',
  244. 'EMPTY_STRING',
  245. 'NULL_STRING',
  246. 'VALID_INTEGER',
  247. 'NULL_INTEGER',
  248. 'VALID_FLOAT',
  249. 'NULL_FLOAT',
  250. 'UNIX_EPOCH',
  251. 'VALID_TIMESTAMP',
  252. 'NULL_TIMESTAMP',
  253. 'TRUE_BOOLEAN',
  254. 'FALSE_BOOLEAN',
  255. 'NULL_BOOLEAN'])
  256. tm.assert_frame_equal(a, self.correct_test_datatype)
  257. @with_connectivity_check
  258. def test_table_exists(self):
  259. # Given a table name in the format {dataset}.{tablename}, if a table exists,
  260. # the GetTableReference should accurately indicate this.
  261. # This could possibly change in future implementations of bq,
  262. # but it is the simplest way to provide users with appropriate
  263. # error messages regarding schemas.
  264. if not os.path.exists(self.bq_token):
  265. raise nose.SkipTest('Skipped because authentication information is not available.')
  266. client = gbq._authenticate()
  267. table_reference = client.GetTableReference("publicdata:samples.shakespeare")
  268. self.assertTrue(client.TableExists(table_reference))
  269. @with_connectivity_check
  270. def test_table__not_exists(self):
  271. # Test the inverse of `test_table_exists`
  272. if not os.path.exists(self.bq_token):
  273. raise nose.SkipTest('Skipped because authentication information is not available.')
  274. client = gbq._authenticate()
  275. table_reference = client.GetTableReference("publicdata:samples.does_not_exist")
  276. self.assertFalse(client.TableExists(table_reference))
  277. @with_connectivity_check
  278. def test_upload_new_table_schema_error(self):
  279. # Attempting to upload to a non-existent table without a schema should fail
  280. if not os.path.exists(self.bq_token):
  281. raise nose.SkipTest('Skipped because authentication information is not available.')
  282. df = DataFrame(self.correct_data_small)
  283. with self.assertRaises(gbq.SchemaMissing):
  284. gbq.to_gbq(df, 'pandas_testing_dataset.test_database', schema=None, col_order=None, if_exists='fail')
  285. @with_connectivity_check
  286. def test_upload_replace_schema_error(self):
  287. # Attempting to replace an existing table without specifying a schema should fail
  288. if not os.path.exists(self.bq_token):
  289. raise nose.SkipTest('Skipped because authentication information is not available.')
  290. df = DataFrame(self.correct_data_small)
  291. with self.assertRaises(gbq.SchemaMissing):
  292. gbq.to_gbq(df, 'pandas_testing_dataset.test_database', schema=None, col_order=None, if_exists='replace')
  293. @with_connectivity_check
  294. def test_upload_public_data_error(self):
  295. # Attempting to upload to a public, read-only, dataset should fail
  296. if not os.path.exists(self.bq_token):
  297. raise nose.SkipTest('Skipped because authentication information is not available.')
  298. array = [['TESTING_GBQ', 999999999, 'hi', 0, True, 9999999999, '00.000.00.000', 1, 'hola',
  299. 99999999, False, False, 1, 'Jedi', 11210]]
  300. df = DataFrame(array)
  301. with self.assertRaises(bigquery_client.BigqueryServiceError):
  302. gbq.to_gbq(df, 'publicdata:samples.wikipedia', schema=None, col_order=None, if_exists='append')
  303. @with_connectivity_check
  304. def test_upload_new_table(self):
  305. # Attempting to upload to a new table with valid data and a valid schema should succeed
  306. if not os.path.exists(self.bq_token):
  307. raise nose.SkipTest('Skipped because authentication information is not available.')
  308. schema = ['STRING', 'INTEGER', 'STRING', 'INTEGER', 'BOOLEAN',
  309. 'INTEGER', 'STRING', 'INTEGER',
  310. 'STRING', 'INTEGER', 'BOOLEAN', 'BOOLEAN',
  311. 'INTEGER', 'STRING', 'INTEGER']
  312. array = [['TESTING_GBQ', 999999999, 'hi', 0, True, 9999999999, '00.000.00.000', 1, 'hola',
  313. 99999999, False, False, 1, 'Jedi', 11210]]
  314. df = DataFrame(array, columns=['title','id','language','wp_namespace','is_redirect','revision_id',
  315. 'contributor_ip','contributor_id','contributor_username','timestamp',
  316. 'is_minor','is_bot','reversion_id','comment','num_characters'])
  317. gbq.to_gbq(df, 'pandas_testing_dataset.test_data2', schema=schema, col_order=None, if_exists='append')
  318. a = gbq.read_gbq("SELECT * FROM pandas_testing_dataset.test_data2")
  319. self.assertTrue((a == df).all().all())
  320. @with_connectivity_check
  321. def test_upload_bad_data_table(self):
  322. # Attempting to upload data that does not match schema should fail
  323. if not os.path.exists(self.bq_token):
  324. raise nose.SkipTest('Skipped because authentication information is not available.')
  325. schema = ['STRING', 'INTEGER', 'STRING', 'INTEGER', 'BOOLEAN',
  326. 'INTEGER', 'STRING', 'INTEGER',
  327. 'STRING', 'INTEGER', 'BOOLEAN', 'BOOLEAN',
  328. 'INTEGER', 'STRING', 'INTEGER']
  329. array = [['TESTING_GBQ\',', False, 'hi', 0, True, 'STRING IN INTEGER', '00.000.00.000', 1, 'hola',
  330. 99999999, -100, 1000, 1, 'Jedi', 11210]]
  331. df = DataFrame(array, columns=['title','id','language','wp_namespace','is_redirect','revision_id',
  332. 'contributor_ip','contributor_id','contributor_username','timestamp',
  333. 'is_minor','is_bot','reversion_id','comment','num_characters'])
  334. with self.assertRaises(bigquery_client.BigqueryServiceError):
  335. gbq.to_gbq(df, 'pandas_testing_dataset.test_data1', schema=schema, col_order=None, if_exists='append')
  336. @with_connectivity_check
  337. def test_invalid_column_name_schema(self):
  338. # Specifying a schema that contains an invalid column name should fail
  339. if not os.path.exists(self.bq_token):
  340. raise nose.SkipTest('Skipped because authentication information is not available.')
  341. schema = ['INCORRECT']
  342. df = DataFrame([[1]],columns=['fake'])
  343. with self.assertRaises(gbq.InvalidSchema):
  344. gbq.to_gbq(df, 'pandas_testing_dataset.test_data', schema=schema, col_order=None, if_exists='append')
  345. @with_connectivity_check
  346. def test_invalid_number_of_columns_schema(self):
  347. # Specifying a schema that does not have same shape as dataframe should fail
  348. if not os.path.exists(self.bq_token):
  349. raise nose.SkipTest('Skipped because authentication information is not available.')
  350. schema = ['INTEGER']
  351. df = DataFrame([[1, 'STRING']],columns=['fake','fake'])
  352. with self.assertRaises(gbq.InvalidSchema):
  353. gbq.to_gbq(df, 'pandas_testing_dataset.test_data4', schema=schema, col_order=None, if_exists='append')
  354. @with_connectivity_check
  355. def test_upload_fail_if_exists(self):
  356. # Attempting to upload to a new table with valid data and a valid schema should succeed
  357. if not os.path.exists(self.bq_token):
  358. raise nose.SkipTest('Skipped because authentication information is not available.')
  359. schema = ['STRING', 'INTEGER', 'STRING', 'INTEGER', 'BOOLEAN',
  360. 'INTEGER', 'STRING', 'INTEGER',
  361. 'STRING', 'INTEGER', 'BOOLEAN', 'BOOLEAN',
  362. 'INTEGER', 'STRING', 'INTEGER']
  363. array = [['TESTING_GBQ', 999999999, 'hi', 0, True, 9999999999, '00.000.00.000', 1, 'hola',
  364. 99999999, False, False, 1, 'Jedi', 11210]]
  365. df = DataFrame(array, columns=['title','id','language','wp_namespace','is_redirect','revision_id',
  366. 'contributor_ip','contributor_id','contributor_username','timestamp',
  367. 'is_minor','is_bot','reversion_id','comment','num_characters'])
  368. gbq.to_gbq(df, 'pandas_testing_dataset.test_data3', schema=schema, col_order=None, if_exists='fail')
  369. with self.assertRaises(gbq.TableExistsFail):
  370. gbq.to_gbq(df, 'pandas_testing_dataset.test_data3', schema=schema, col_order=None, if_exists='fail')
  371. @with_connectivity_check
  372. def test_upload_replace(self):
  373. # Attempting to overwrite an existing table with valid data and a valid schema should succeed
  374. if not os.path.exists(self.bq_token):
  375. raise nose.SkipTest('Skipped because authentication information is not available.')
  376. schema = ['STRING', 'INTEGER', 'STRING', 'INTEGER', 'BOOLEAN',
  377. 'INTEGER', 'STRING', 'INTEGER',
  378. 'STRING', 'INTEGER', 'BOOLEAN', 'BOOLEAN',
  379. 'INTEGER', 'STRING', 'INTEGER']
  380. # Setup an existing table
  381. array1 = [['', 1, '', 1, False, 1, '00.111.00.111', 1, 'hola',
  382. 1, True, True, 1, 'Sith', 1]]
  383. df1 = DataFrame(array1, columns=['title','id','language','wp_namespace','is_redirect','revision_id',
  384. 'contributor_ip','contributor_id','contributor_username','timestamp',
  385. 'is_minor','is_bot','reversion_id','comment','num_characters'])
  386. gbq.to_gbq(df1, 'pandas_testing_dataset.test_data5', schema=schema, col_order=None, if_exists='fail')
  387. array2 = [['TESTING_GBQ', 999999999, 'hi', 0, True, 9999999999, '00.000.00.000', 1, 'hola',
  388. 99999999, False, False, 1, 'Jedi', 11210]]
  389. # Overwrite the existing table with different data
  390. df2 = DataFrame(array2, columns=['title','id','language','wp_namespace','is_redirect','revision_id',
  391. 'contributor_ip','contributor_id','contributor_username','timestamp',
  392. 'is_minor','is_bot','reversion_id','comment','num_characters'])
  393. gbq.to_gbq(df2, 'pandas_testing_dataset.test_data5', schema=schema, col_order=None, if_exists='replace')
  394. # Read the table and confirm the new data is all that is there
  395. a = gbq.read_gbq("SELECT * FROM pandas_testing_dataset.test_data5")
  396. self.assertTrue((a == df2).all().all())
  397. if __name__ == '__main__':
  398. nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
  399. exit=False)