reflection.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. # dialects/mysql/reflection.py
  2. # Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. # mypy: ignore-errors
  8. import re
  9. from .enumerated import ENUM
  10. from .enumerated import SET
  11. from .types import DATETIME
  12. from .types import TIME
  13. from .types import TIMESTAMP
  14. from ... import log
  15. from ... import types as sqltypes
  16. from ... import util
  17. class ReflectedState:
  18. """Stores raw information about a SHOW CREATE TABLE statement."""
  19. def __init__(self):
  20. self.columns = []
  21. self.table_options = {}
  22. self.table_name = None
  23. self.keys = []
  24. self.fk_constraints = []
  25. self.ck_constraints = []
  26. @log.class_logger
  27. class MySQLTableDefinitionParser:
  28. """Parses the results of a SHOW CREATE TABLE statement."""
  29. def __init__(self, dialect, preparer):
  30. self.dialect = dialect
  31. self.preparer = preparer
  32. self._prep_regexes()
  33. def parse(self, show_create, charset):
  34. state = ReflectedState()
  35. state.charset = charset
  36. for line in re.split(r"\r?\n", show_create):
  37. if line.startswith(" " + self.preparer.initial_quote):
  38. self._parse_column(line, state)
  39. # a regular table options line
  40. elif line.startswith(") "):
  41. self._parse_table_options(line, state)
  42. # an ANSI-mode table options line
  43. elif line == ")":
  44. pass
  45. elif line.startswith("CREATE "):
  46. self._parse_table_name(line, state)
  47. elif "PARTITION" in line:
  48. self._parse_partition_options(line, state)
  49. # Not present in real reflection, but may be if
  50. # loading from a file.
  51. elif not line:
  52. pass
  53. else:
  54. type_, spec = self._parse_constraints(line)
  55. if type_ is None:
  56. util.warn("Unknown schema content: %r" % line)
  57. elif type_ == "key":
  58. state.keys.append(spec)
  59. elif type_ == "fk_constraint":
  60. state.fk_constraints.append(spec)
  61. elif type_ == "ck_constraint":
  62. state.ck_constraints.append(spec)
  63. else:
  64. pass
  65. return state
  66. def _check_view(self, sql: str) -> bool:
  67. return bool(self._re_is_view.match(sql))
  68. def _parse_constraints(self, line):
  69. """Parse a KEY or CONSTRAINT line.
  70. :param line: A line of SHOW CREATE TABLE output
  71. """
  72. # KEY
  73. m = self._re_key.match(line)
  74. if m:
  75. spec = m.groupdict()
  76. # convert columns into name, length pairs
  77. # NOTE: we may want to consider SHOW INDEX as the
  78. # format of indexes in MySQL becomes more complex
  79. spec["columns"] = self._parse_keyexprs(spec["columns"])
  80. if spec["version_sql"]:
  81. m2 = self._re_key_version_sql.match(spec["version_sql"])
  82. if m2 and m2.groupdict()["parser"]:
  83. spec["parser"] = m2.groupdict()["parser"]
  84. if spec["parser"]:
  85. spec["parser"] = self.preparer.unformat_identifiers(
  86. spec["parser"]
  87. )[0]
  88. return "key", spec
  89. # FOREIGN KEY CONSTRAINT
  90. m = self._re_fk_constraint.match(line)
  91. if m:
  92. spec = m.groupdict()
  93. spec["table"] = self.preparer.unformat_identifiers(spec["table"])
  94. spec["local"] = [c[0] for c in self._parse_keyexprs(spec["local"])]
  95. spec["foreign"] = [
  96. c[0] for c in self._parse_keyexprs(spec["foreign"])
  97. ]
  98. return "fk_constraint", spec
  99. # CHECK constraint
  100. m = self._re_ck_constraint.match(line)
  101. if m:
  102. spec = m.groupdict()
  103. return "ck_constraint", spec
  104. # PARTITION and SUBPARTITION
  105. m = self._re_partition.match(line)
  106. if m:
  107. # Punt!
  108. return "partition", line
  109. # No match.
  110. return (None, line)
  111. def _parse_table_name(self, line, state):
  112. """Extract the table name.
  113. :param line: The first line of SHOW CREATE TABLE
  114. """
  115. regex, cleanup = self._pr_name
  116. m = regex.match(line)
  117. if m:
  118. state.table_name = cleanup(m.group("name"))
  119. def _parse_table_options(self, line, state):
  120. """Build a dictionary of all reflected table-level options.
  121. :param line: The final line of SHOW CREATE TABLE output.
  122. """
  123. options = {}
  124. if line and line != ")":
  125. rest_of_line = line
  126. for regex, cleanup in self._pr_options:
  127. m = regex.search(rest_of_line)
  128. if not m:
  129. continue
  130. directive, value = m.group("directive"), m.group("val")
  131. if cleanup:
  132. value = cleanup(value)
  133. options[directive.lower()] = value
  134. rest_of_line = regex.sub("", rest_of_line)
  135. for nope in ("auto_increment", "data directory", "index directory"):
  136. options.pop(nope, None)
  137. for opt, val in options.items():
  138. state.table_options["%s_%s" % (self.dialect.name, opt)] = val
  139. def _parse_partition_options(self, line, state):
  140. options = {}
  141. new_line = line[:]
  142. while new_line.startswith("(") or new_line.startswith(" "):
  143. new_line = new_line[1:]
  144. for regex, cleanup in self._pr_options:
  145. m = regex.search(new_line)
  146. if not m or "PARTITION" not in regex.pattern:
  147. continue
  148. directive = m.group("directive")
  149. directive = directive.lower()
  150. is_subpartition = directive == "subpartition"
  151. if directive == "partition" or is_subpartition:
  152. new_line = new_line.replace(") */", "")
  153. new_line = new_line.replace(",", "")
  154. if is_subpartition and new_line.endswith(")"):
  155. new_line = new_line[:-1]
  156. if self.dialect.name == "mariadb" and new_line.endswith(")"):
  157. if (
  158. "MAXVALUE" in new_line
  159. or "MINVALUE" in new_line
  160. or "ENGINE" in new_line
  161. ):
  162. # final line of MariaDB partition endswith ")"
  163. new_line = new_line[:-1]
  164. defs = "%s_%s_definitions" % (self.dialect.name, directive)
  165. options[defs] = new_line
  166. else:
  167. directive = directive.replace(" ", "_")
  168. value = m.group("val")
  169. if cleanup:
  170. value = cleanup(value)
  171. options[directive] = value
  172. break
  173. for opt, val in options.items():
  174. part_def = "%s_partition_definitions" % (self.dialect.name)
  175. subpart_def = "%s_subpartition_definitions" % (self.dialect.name)
  176. if opt == part_def or opt == subpart_def:
  177. # builds a string of definitions
  178. if opt not in state.table_options:
  179. state.table_options[opt] = val
  180. else:
  181. state.table_options[opt] = "%s, %s" % (
  182. state.table_options[opt],
  183. val,
  184. )
  185. else:
  186. state.table_options["%s_%s" % (self.dialect.name, opt)] = val
  187. def _parse_column(self, line, state):
  188. """Extract column details.
  189. Falls back to a 'minimal support' variant if full parse fails.
  190. :param line: Any column-bearing line from SHOW CREATE TABLE
  191. """
  192. spec = None
  193. m = self._re_column.match(line)
  194. if m:
  195. spec = m.groupdict()
  196. spec["full"] = True
  197. else:
  198. m = self._re_column_loose.match(line)
  199. if m:
  200. spec = m.groupdict()
  201. spec["full"] = False
  202. if not spec:
  203. util.warn("Unknown column definition %r" % line)
  204. return
  205. if not spec["full"]:
  206. util.warn("Incomplete reflection of column definition %r" % line)
  207. name, type_, args = spec["name"], spec["coltype"], spec["arg"]
  208. try:
  209. col_type = self.dialect.ischema_names[type_]
  210. except KeyError:
  211. util.warn(
  212. "Did not recognize type '%s' of column '%s'" % (type_, name)
  213. )
  214. col_type = sqltypes.NullType
  215. # Column type positional arguments eg. varchar(32)
  216. if args is None or args == "":
  217. type_args = []
  218. elif args[0] == "'" and args[-1] == "'":
  219. type_args = self._re_csv_str.findall(args)
  220. else:
  221. type_args = [int(v) for v in self._re_csv_int.findall(args)]
  222. # Column type keyword options
  223. type_kw = {}
  224. if issubclass(col_type, (DATETIME, TIME, TIMESTAMP)):
  225. if type_args:
  226. type_kw["fsp"] = type_args.pop(0)
  227. for kw in ("unsigned", "zerofill"):
  228. if spec.get(kw, False):
  229. type_kw[kw] = True
  230. for kw in ("charset", "collate"):
  231. if spec.get(kw, False):
  232. type_kw[kw] = spec[kw]
  233. if issubclass(col_type, (ENUM, SET)):
  234. type_args = _strip_values(type_args)
  235. if issubclass(col_type, SET) and "" in type_args:
  236. type_kw["retrieve_as_bitwise"] = True
  237. type_instance = col_type(*type_args, **type_kw)
  238. col_kw = {}
  239. # NOT NULL
  240. col_kw["nullable"] = True
  241. # this can be "NULL" in the case of TIMESTAMP
  242. if spec.get("notnull", False) == "NOT NULL":
  243. col_kw["nullable"] = False
  244. # AUTO_INCREMENT
  245. if spec.get("autoincr", False):
  246. col_kw["autoincrement"] = True
  247. elif issubclass(col_type, sqltypes.Integer):
  248. col_kw["autoincrement"] = False
  249. # DEFAULT
  250. default = spec.get("default", None)
  251. if default == "NULL":
  252. # eliminates the need to deal with this later.
  253. default = None
  254. comment = spec.get("comment", None)
  255. if comment is not None:
  256. comment = cleanup_text(comment)
  257. sqltext = spec.get("generated")
  258. if sqltext is not None:
  259. computed = dict(sqltext=sqltext)
  260. persisted = spec.get("persistence")
  261. if persisted is not None:
  262. computed["persisted"] = persisted == "STORED"
  263. col_kw["computed"] = computed
  264. col_d = dict(
  265. name=name, type=type_instance, default=default, comment=comment
  266. )
  267. col_d.update(col_kw)
  268. state.columns.append(col_d)
  269. def _describe_to_create(self, table_name, columns):
  270. """Re-format DESCRIBE output as a SHOW CREATE TABLE string.
  271. DESCRIBE is a much simpler reflection and is sufficient for
  272. reflecting views for runtime use. This method formats DDL
  273. for columns only- keys are omitted.
  274. :param columns: A sequence of DESCRIBE or SHOW COLUMNS 6-tuples.
  275. SHOW FULL COLUMNS FROM rows must be rearranged for use with
  276. this function.
  277. """
  278. buffer = []
  279. for row in columns:
  280. (name, col_type, nullable, default, extra) = (
  281. row[i] for i in (0, 1, 2, 4, 5)
  282. )
  283. line = [" "]
  284. line.append(self.preparer.quote_identifier(name))
  285. line.append(col_type)
  286. if not nullable:
  287. line.append("NOT NULL")
  288. if default:
  289. if "auto_increment" in default:
  290. pass
  291. elif col_type.startswith("timestamp") and default.startswith(
  292. "C"
  293. ):
  294. line.append("DEFAULT")
  295. line.append(default)
  296. elif default == "NULL":
  297. line.append("DEFAULT")
  298. line.append(default)
  299. else:
  300. line.append("DEFAULT")
  301. line.append("'%s'" % default.replace("'", "''"))
  302. if extra:
  303. line.append(extra)
  304. buffer.append(" ".join(line))
  305. return "".join(
  306. [
  307. (
  308. "CREATE TABLE %s (\n"
  309. % self.preparer.quote_identifier(table_name)
  310. ),
  311. ",\n".join(buffer),
  312. "\n) ",
  313. ]
  314. )
  315. def _parse_keyexprs(self, identifiers):
  316. """Unpack '"col"(2),"col" ASC'-ish strings into components."""
  317. return [
  318. (colname, int(length) if length else None, modifiers)
  319. for colname, length, modifiers in self._re_keyexprs.findall(
  320. identifiers
  321. )
  322. ]
  323. def _prep_regexes(self):
  324. """Pre-compile regular expressions."""
  325. self._re_columns = []
  326. self._pr_options = []
  327. _final = self.preparer.final_quote
  328. quotes = dict(
  329. zip(
  330. ("iq", "fq", "esc_fq"),
  331. [
  332. re.escape(s)
  333. for s in (
  334. self.preparer.initial_quote,
  335. _final,
  336. self.preparer._escape_identifier(_final),
  337. )
  338. ],
  339. )
  340. )
  341. self._pr_name = _pr_compile(
  342. r"^CREATE (?:\w+ +)?TABLE +"
  343. r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +\($" % quotes,
  344. self.preparer._unescape_identifier,
  345. )
  346. self._re_is_view = _re_compile(r"^CREATE(?! TABLE)(\s.*)?\sVIEW")
  347. # `col`,`col2`(32),`col3`(15) DESC
  348. #
  349. self._re_keyexprs = _re_compile(
  350. r"(?:"
  351. r"(?:%(iq)s((?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)"
  352. r"(?:\((\d+)\))?(?: +(ASC|DESC))?(?=\,|$))+" % quotes
  353. )
  354. # 'foo' or 'foo','bar' or 'fo,o','ba''a''r'
  355. self._re_csv_str = _re_compile(r"\x27(?:\x27\x27|[^\x27])*\x27")
  356. # 123 or 123,456
  357. self._re_csv_int = _re_compile(r"\d+")
  358. # `colname` <type> [type opts]
  359. # (NOT NULL | NULL)
  360. # DEFAULT ('value' | CURRENT_TIMESTAMP...)
  361. # COMMENT 'comment'
  362. # COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT)
  363. # STORAGE (DISK|MEMORY)
  364. self._re_column = _re_compile(
  365. r" "
  366. r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
  367. r"(?P<coltype>\w+)"
  368. r"(?:\((?P<arg>(?:\d+|\d+,\d+|"
  369. r"(?:'(?:''|[^'])*',?)+))\))?"
  370. r"(?: +(?P<unsigned>UNSIGNED))?"
  371. r"(?: +(?P<zerofill>ZEROFILL))?"
  372. r"(?: +CHARACTER SET +(?P<charset>[\w_]+))?"
  373. r"(?: +COLLATE +(?P<collate>[\w_]+))?"
  374. r"(?: +(?P<notnull>(?:NOT )?NULL))?"
  375. r"(?: +DEFAULT +(?P<default>"
  376. r"(?:NULL|'(?:''|[^'])*'|[\-\w\.\(\)]+"
  377. r"(?: +ON UPDATE [\-\w\.\(\)]+)?)"
  378. r"))?"
  379. r"(?: +(?:GENERATED ALWAYS)? ?AS +(?P<generated>\("
  380. r".*\))? ?(?P<persistence>VIRTUAL|STORED)?)?"
  381. r"(?: +(?P<autoincr>AUTO_INCREMENT))?"
  382. r"(?: +COMMENT +'(?P<comment>(?:''|[^'])*)')?"
  383. r"(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?"
  384. r"(?: +STORAGE +(?P<storage>\w+))?"
  385. r"(?: +(?P<extra>.*))?"
  386. r",?$" % quotes
  387. )
  388. # Fallback, try to parse as little as possible
  389. self._re_column_loose = _re_compile(
  390. r" "
  391. r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
  392. r"(?P<coltype>\w+)"
  393. r"(?:\((?P<arg>(?:\d+|\d+,\d+|\x27(?:\x27\x27|[^\x27])+\x27))\))?"
  394. r".*?(?P<notnull>(?:NOT )NULL)?" % quotes
  395. )
  396. # (PRIMARY|UNIQUE|FULLTEXT|SPATIAL) INDEX `name` (USING (BTREE|HASH))?
  397. # (`col` (ASC|DESC)?, `col` (ASC|DESC)?)
  398. # KEY_BLOCK_SIZE size | WITH PARSER name /*!50100 WITH PARSER name */
  399. self._re_key = _re_compile(
  400. r" "
  401. r"(?:(?P<type>\S+) )?KEY"
  402. r"(?: +%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)?"
  403. r"(?: +USING +(?P<using_pre>\S+))?"
  404. r" +\((?P<columns>.+?)\)"
  405. r"(?: +USING +(?P<using_post>\S+))?"
  406. r"(?: +KEY_BLOCK_SIZE *[ =]? *(?P<keyblock>\S+))?"
  407. r"(?: +WITH PARSER +(?P<parser>\S+))?"
  408. r"(?: +COMMENT +(?P<comment>(\x27\x27|\x27([^\x27])*?\x27)+))?"
  409. r"(?: +/\*(?P<version_sql>.+)\*/ *)?"
  410. r",?$" % quotes
  411. )
  412. # https://forums.mysql.com/read.php?20,567102,567111#msg-567111
  413. # It means if the MySQL version >= \d+, execute what's in the comment
  414. self._re_key_version_sql = _re_compile(
  415. r"\!\d+ " r"(?: *WITH PARSER +(?P<parser>\S+) *)?"
  416. )
  417. # CONSTRAINT `name` FOREIGN KEY (`local_col`)
  418. # REFERENCES `remote` (`remote_col`)
  419. # MATCH FULL | MATCH PARTIAL | MATCH SIMPLE
  420. # ON DELETE CASCADE ON UPDATE RESTRICT
  421. #
  422. # unique constraints come back as KEYs
  423. kw = quotes.copy()
  424. kw["on"] = "RESTRICT|CASCADE|SET NULL|NO ACTION"
  425. self._re_fk_constraint = _re_compile(
  426. r" "
  427. r"CONSTRAINT +"
  428. r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
  429. r"FOREIGN KEY +"
  430. r"\((?P<local>[^\)]+?)\) REFERENCES +"
  431. r"(?P<table>%(iq)s[^%(fq)s]+%(fq)s"
  432. r"(?:\.%(iq)s[^%(fq)s]+%(fq)s)?) +"
  433. r"\((?P<foreign>(?:%(iq)s[^%(fq)s]+%(fq)s(?: *, *)?)+)\)"
  434. r"(?: +(?P<match>MATCH \w+))?"
  435. r"(?: +ON DELETE (?P<ondelete>%(on)s))?"
  436. r"(?: +ON UPDATE (?P<onupdate>%(on)s))?" % kw
  437. )
  438. # CONSTRAINT `CONSTRAINT_1` CHECK (`x` > 5)'
  439. # testing on MariaDB 10.2 shows that the CHECK constraint
  440. # is returned on a line by itself, so to match without worrying
  441. # about parenthesis in the expression we go to the end of the line
  442. self._re_ck_constraint = _re_compile(
  443. r" "
  444. r"CONSTRAINT +"
  445. r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
  446. r"CHECK +"
  447. r"\((?P<sqltext>.+)\),?" % kw
  448. )
  449. # PARTITION
  450. #
  451. # punt!
  452. self._re_partition = _re_compile(r"(?:.*)(?:SUB)?PARTITION(?:.*)")
  453. # Table-level options (COLLATE, ENGINE, etc.)
  454. # Do the string options first, since they have quoted
  455. # strings we need to get rid of.
  456. for option in _options_of_type_string:
  457. self._add_option_string(option)
  458. for option in (
  459. "ENGINE",
  460. "TYPE",
  461. "AUTO_INCREMENT",
  462. "AVG_ROW_LENGTH",
  463. "CHARACTER SET",
  464. "DEFAULT CHARSET",
  465. "CHECKSUM",
  466. "COLLATE",
  467. "DELAY_KEY_WRITE",
  468. "INSERT_METHOD",
  469. "MAX_ROWS",
  470. "MIN_ROWS",
  471. "PACK_KEYS",
  472. "ROW_FORMAT",
  473. "KEY_BLOCK_SIZE",
  474. "STATS_SAMPLE_PAGES",
  475. ):
  476. self._add_option_word(option)
  477. for option in (
  478. "PARTITION BY",
  479. "SUBPARTITION BY",
  480. "PARTITIONS",
  481. "SUBPARTITIONS",
  482. "PARTITION",
  483. "SUBPARTITION",
  484. ):
  485. self._add_partition_option_word(option)
  486. self._add_option_regex("UNION", r"\([^\)]+\)")
  487. self._add_option_regex("TABLESPACE", r".*? STORAGE DISK")
  488. self._add_option_regex(
  489. "RAID_TYPE",
  490. r"\w+\s+RAID_CHUNKS\s*\=\s*\w+RAID_CHUNKSIZE\s*=\s*\w+",
  491. )
  492. _optional_equals = r"(?:\s*(?:=\s*)|\s+)"
  493. def _add_option_string(self, directive):
  494. regex = r"(?P<directive>%s)%s" r"'(?P<val>(?:[^']|'')*?)'(?!')" % (
  495. re.escape(directive),
  496. self._optional_equals,
  497. )
  498. self._pr_options.append(_pr_compile(regex, cleanup_text))
  499. def _add_option_word(self, directive):
  500. regex = r"(?P<directive>%s)%s" r"(?P<val>\w+)" % (
  501. re.escape(directive),
  502. self._optional_equals,
  503. )
  504. self._pr_options.append(_pr_compile(regex))
  505. def _add_partition_option_word(self, directive):
  506. if directive == "PARTITION BY" or directive == "SUBPARTITION BY":
  507. regex = r"(?<!\S)(?P<directive>%s)%s" r"(?P<val>\w+.*)" % (
  508. re.escape(directive),
  509. self._optional_equals,
  510. )
  511. elif directive == "SUBPARTITIONS" or directive == "PARTITIONS":
  512. regex = r"(?<!\S)(?P<directive>%s)%s" r"(?P<val>\d+)" % (
  513. re.escape(directive),
  514. self._optional_equals,
  515. )
  516. else:
  517. regex = r"(?<!\S)(?P<directive>%s)(?!\S)" % (re.escape(directive),)
  518. self._pr_options.append(_pr_compile(regex))
  519. def _add_option_regex(self, directive, regex):
  520. regex = r"(?P<directive>%s)%s" r"(?P<val>%s)" % (
  521. re.escape(directive),
  522. self._optional_equals,
  523. regex,
  524. )
  525. self._pr_options.append(_pr_compile(regex))
  526. _options_of_type_string = (
  527. "COMMENT",
  528. "DATA DIRECTORY",
  529. "INDEX DIRECTORY",
  530. "PASSWORD",
  531. "CONNECTION",
  532. )
  533. def _pr_compile(regex, cleanup=None):
  534. """Prepare a 2-tuple of compiled regex and callable."""
  535. return (_re_compile(regex), cleanup)
  536. def _re_compile(regex):
  537. """Compile a string to regex, I and UNICODE."""
  538. return re.compile(regex, re.I | re.UNICODE)
  539. def _strip_values(values):
  540. "Strip reflected values quotes"
  541. strip_values = []
  542. for a in values:
  543. if a[0:1] == '"' or a[0:1] == "'":
  544. # strip enclosing quotes and unquote interior
  545. a = a[1:-1].replace(a[0] * 2, a[0])
  546. strip_values.append(a)
  547. return strip_values
  548. def cleanup_text(raw_text: str) -> str:
  549. if "\\" in raw_text:
  550. raw_text = re.sub(
  551. _control_char_regexp, lambda s: _control_char_map[s[0]], raw_text
  552. )
  553. return raw_text.replace("''", "'")
  554. _control_char_map = {
  555. "\\\\": "\\",
  556. "\\0": "\0",
  557. "\\a": "\a",
  558. "\\b": "\b",
  559. "\\t": "\t",
  560. "\\n": "\n",
  561. "\\v": "\v",
  562. "\\f": "\f",
  563. "\\r": "\r",
  564. # '\\e':'\e',
  565. }
  566. _control_char_regexp = re.compile(
  567. "|".join(re.escape(k) for k in _control_char_map)
  568. )