singlestoredb 0.9.1__tar.gz → 0.9.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of singlestoredb might be problematic. Click here for more details.

Files changed (107) hide show
  1. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/PKG-INFO +2 -1
  2. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/setup.cfg +4 -1
  3. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/setup.py +21 -13
  4. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/__init__.py +1 -1
  5. singlestoredb-0.9.3/singlestoredb/fusion/__init__.py +11 -0
  6. singlestoredb-0.9.3/singlestoredb/fusion/handler.py +555 -0
  7. singlestoredb-0.9.3/singlestoredb/fusion/handlers/workspace.py +361 -0
  8. singlestoredb-0.9.3/singlestoredb/fusion/registry.py +167 -0
  9. singlestoredb-0.9.3/singlestoredb/fusion/result.py +120 -0
  10. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/http/connection.py +69 -13
  11. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/workspace.py +1 -1
  12. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/connection.py +10 -4
  13. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/constants/FIELD_TYPE.py +1 -0
  14. singlestoredb-0.9.3/singlestoredb/tests/test_fusion.py +83 -0
  15. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_management.py +1 -1
  16. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_results.py +6 -6
  17. singlestoredb-0.9.3/singlestoredb/utils/__init__.py +0 -0
  18. singlestoredb-0.9.3/singlestoredb/utils/mogrify.py +151 -0
  19. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb.egg-info/PKG-INFO +2 -1
  20. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb.egg-info/SOURCES.txt +8 -0
  21. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb.egg-info/requires.txt +1 -0
  22. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/LICENSE +0 -0
  23. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/README.md +0 -0
  24. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/accel.c +0 -0
  25. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/alchemy/__init__.py +0 -0
  26. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/auth.py +0 -0
  27. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/config.py +0 -0
  28. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/connection.py +0 -0
  29. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/converters.py +0 -0
  30. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/exceptions.py +0 -0
  31. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/__init__.py +0 -0
  32. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/decorator.py +0 -0
  33. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/dtypes.py +0 -0
  34. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/ext/__init__.py +0 -0
  35. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/ext/asgi.py +0 -0
  36. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/ext/json.py +0 -0
  37. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/ext/rowdat_1.py +0 -0
  38. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/functions/signature.py +0 -0
  39. {singlestoredb-0.9.1/singlestoredb/mysql/constants → singlestoredb-0.9.3/singlestoredb/fusion/handlers}/__init__.py +0 -0
  40. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/http/__init__.py +0 -0
  41. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/__init__.py +0 -0
  42. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/billing_usage.py +0 -0
  43. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/cluster.py +0 -0
  44. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/manager.py +0 -0
  45. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/organization.py +0 -0
  46. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/region.py +0 -0
  47. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/management/utils.py +0 -0
  48. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/__init__.py +0 -0
  49. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/_auth.py +0 -0
  50. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/charset.py +0 -0
  51. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/constants/CLIENT.py +0 -0
  52. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/constants/COMMAND.py +0 -0
  53. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/constants/CR.py +0 -0
  54. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/constants/ER.py +0 -0
  55. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/constants/FLAG.py +0 -0
  56. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/constants/SERVER_STATUS.py +0 -0
  57. {singlestoredb-0.9.1/singlestoredb/tests → singlestoredb-0.9.3/singlestoredb/mysql/constants}/__init__.py +0 -0
  58. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/converters.py +0 -0
  59. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/cursors.py +0 -0
  60. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/err.py +0 -0
  61. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/optionfile.py +0 -0
  62. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/protocol.py +0 -0
  63. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/__init__.py +0 -0
  64. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/base.py +0 -0
  65. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/conftest.py +0 -0
  66. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_DictCursor.py +0 -0
  67. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_SSCursor.py +0 -0
  68. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_basic.py +0 -0
  69. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_connection.py +0 -0
  70. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_converters.py +0 -0
  71. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_cursor.py +0 -0
  72. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_err.py +0 -0
  73. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_issues.py +0 -0
  74. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_load_local.py +0 -0
  75. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_nextset.py +0 -0
  76. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/test_optionfile.py +0 -0
  77. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/thirdparty/__init__.py +0 -0
  78. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +0 -0
  79. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +0 -0
  80. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +0 -0
  81. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +0 -0
  82. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +0 -0
  83. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +0 -0
  84. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/mysql/times.py +0 -0
  85. {singlestoredb-0.9.1/singlestoredb/utils → singlestoredb-0.9.3/singlestoredb/tests}/__init__.py +0 -0
  86. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/empty.sql +0 -0
  87. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/local_infile.csv +0 -0
  88. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test.sql +0 -0
  89. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test2.sql +0 -0
  90. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_basics.py +0 -0
  91. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_config.py +0 -0
  92. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_connection.py +0 -0
  93. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_dbapi.py +0 -0
  94. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_exceptions.py +0 -0
  95. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_http.py +0 -0
  96. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_types.py +0 -0
  97. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_udf.py +0 -0
  98. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/test_xdict.py +0 -0
  99. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/tests/utils.py +0 -0
  100. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/types.py +0 -0
  101. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/utils/config.py +0 -0
  102. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/utils/convert_rows.py +0 -0
  103. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/utils/debug.py +0 -0
  104. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/utils/results.py +0 -0
  105. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb/utils/xdict.py +0 -0
  106. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb.egg-info/dependency_links.txt +0 -0
  107. {singlestoredb-0.9.1 → singlestoredb-0.9.3}/singlestoredb.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: singlestoredb
3
- Version: 0.9.1
3
+ Version: 0.9.3
4
4
  Summary: Interface to the SingleStoreDB database and workspace management APIs
5
5
  Home-page: https://github.com/singlestore-labs/singlestoredb-python
6
6
  Author: SingleStore
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: PyJWT
18
18
  Requires-Dist: build
19
+ Requires-Dist: parsimonious
19
20
  Requires-Dist: requests
20
21
  Requires-Dist: sqlparams
21
22
  Requires-Dist: wheel
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = singlestoredb
3
- version = 0.9.1
3
+ version = 0.9.3
4
4
  description = Interface to the SingleStoreDB database and workspace management APIs
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -21,6 +21,7 @@ packages = find:
21
21
  install_requires =
22
22
  PyJWT
23
23
  build
24
+ parsimonious
24
25
  requests
25
26
  sqlparams
26
27
  wheel
@@ -73,6 +74,8 @@ max-complexity = 30
73
74
  max-line-length = 90
74
75
  per-file-ignores =
75
76
  singlestoredb/__init__.py:F401
77
+ singlestoredb/fusion/__init__.py:F401
78
+ singlestoredb/fusion/grammar.py:E501
76
79
  singlestoredb/http/__init__.py:F401
77
80
  singlestoredb/management/__init__.py:F401
78
81
  singlestoredb/mysql/__init__.py:F401
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
  """SingleStoreDB package installer."""
3
+ import os
3
4
  import platform
4
5
  from typing import Tuple
5
6
 
@@ -11,6 +12,8 @@ from wheel.bdist_wheel import bdist_wheel
11
12
  py_limited_api = '0x03080000'
12
13
  # py_limited_api = False
13
14
 
15
+ build_extension = bool(int(os.environ.get('SINGLESTOREDB_BUILD_EXTENSION', '1')))
16
+
14
17
  universal2_flags = ['-arch', 'x86_64', '-arch', 'arm64'] \
15
18
  if (
16
19
  platform.platform().startswith('mac') and
@@ -33,16 +36,21 @@ class bdist_wheel_abi3(bdist_wheel):
33
36
  return python, abi, plat
34
37
 
35
38
 
36
- setup(
37
- ext_modules=[
38
- Extension(
39
- '_singlestoredb_accel',
40
- sources=['accel.c'],
41
- define_macros=[('Py_LIMITED_API', py_limited_api)] if py_limited_api else [],
42
- py_limited_api=bool(py_limited_api),
43
- extra_compile_args=universal2_flags,
44
- extra_link_args=universal2_flags,
45
- ),
46
- ],
47
- cmdclass={'bdist_wheel': bdist_wheel_abi3 if py_limited_api else bdist_wheel},
48
- )
39
+ if build_extension:
40
+ setup(
41
+ ext_modules=[
42
+ Extension(
43
+ '_singlestoredb_accel',
44
+ sources=['accel.c'],
45
+ define_macros=[
46
+ ('Py_LIMITED_API', py_limited_api),
47
+ ] if py_limited_api else [],
48
+ py_limited_api=bool(py_limited_api),
49
+ extra_compile_args=universal2_flags,
50
+ extra_link_args=universal2_flags,
51
+ ),
52
+ ],
53
+ cmdclass={'bdist_wheel': bdist_wheel_abi3 if py_limited_api else bdist_wheel},
54
+ )
55
+ else:
56
+ setup()
@@ -13,7 +13,7 @@ Examples
13
13
 
14
14
  """
15
15
 
16
- __version__ = '0.9.1'
16
+ __version__ = '0.9.3'
17
17
 
18
18
  from .alchemy import create_engine
19
19
  from .config import options, get_option, set_option, describe_option
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ import importlib
3
+ import os
4
+
5
+ from .registry import execute
6
+ from .registry import get_handler
7
+
8
+ # Load all files in handlers directory
9
+ for f in os.listdir(os.path.join(os.path.dirname(__file__), 'handlers')):
10
+ if f.endswith('.py') and not f.startswith('_'):
11
+ importlib.import_module(f'singlestoredb.fusion.handlers.{f[:-3]}')
@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env python3
2
+ import abc
3
+ import functools
4
+ import re
5
+ import textwrap
6
+ from typing import Any
7
+ from typing import Callable
8
+ from typing import Dict
9
+ from typing import Iterable
10
+ from typing import List
11
+ from typing import Optional
12
+ from typing import Tuple
13
+
14
+ from parsimonious import Grammar
15
+ from parsimonious import ParseError
16
+ from parsimonious.nodes import Node
17
+ from parsimonious.nodes import NodeVisitor
18
+
19
+ from . import result
20
+ from ..connection import Connection
21
+
22
+ CORE_GRAMMAR = r'''
23
+ ws = ~r"(\s*(/\*.*\*/)*\s*)*"
24
+ qs = ~r"\"([^\"]*)\"|'([^\']*)'|`([^\`]*)`|(\S+)"
25
+ number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i
26
+ integer = ~r"-?\d+"
27
+ comma = ws "," ws
28
+ open_paren = ws "(" ws
29
+ close_paren = ws ")" ws
30
+ '''
31
+
32
+
33
+ def get_keywords(grammar: str) -> Tuple[str, ...]:
34
+ """Return all all-caps words from the beginning of the line."""
35
+ m = re.match(r'^\s*([A-Z0-9_]+(\s+|$|;))+', grammar)
36
+ if not m:
37
+ return tuple()
38
+ return tuple(re.split(r'\s+', m.group(0).replace(';', '').strip()))
39
+
40
+
41
+ def process_optional(m: Any) -> str:
42
+ """Create options or groups of options."""
43
+ sql = m.group(1).strip()
44
+ if '|' in sql:
45
+ return f'( {sql} )*'
46
+ return f'( {sql} )?'
47
+
48
+
49
+ def process_alternates(m: Any) -> str:
50
+ """Make alternates mandatory groups."""
51
+ sql = m.group(1).strip()
52
+ if '|' in sql:
53
+ return f'( {sql} )'
54
+ raise ValueError(f'alternates must contain "|": {sql}')
55
+
56
+
57
+ def process_repeats(m: Any) -> str:
58
+ """Add repeated patterns."""
59
+ sql = m.group(1).strip()
60
+ return f'open_paren? {sql} ws ( comma {sql} ws )* close_paren?'
61
+
62
+
63
+ def lower_and_regex(m: Any) -> str:
64
+ """Lowercase and convert literal to regex."""
65
+ sql = m.group(1)
66
+ return f'~"{sql.lower()}"i'
67
+
68
+
69
+ def split_unions(grammar: str) -> str:
70
+ """
71
+ Convert grammar in the form '[ x ] [ y ]' to '[ x | y ]'.
72
+
73
+ Parameters
74
+ ----------
75
+ grammar : str
76
+ SQL grammar
77
+
78
+ Returns
79
+ -------
80
+ str
81
+
82
+ """
83
+ in_alternate = False
84
+ out = []
85
+ for c in grammar:
86
+ if c == '{':
87
+ in_alternate = True
88
+ out.append(c)
89
+ elif c == '}':
90
+ in_alternate = False
91
+ out.append(c)
92
+ elif not in_alternate and c == '|':
93
+ out.append(']')
94
+ out.append(' ')
95
+ out.append('[')
96
+ else:
97
+ out.append(c)
98
+ return ''.join(out)
99
+
100
+
101
+ def expand_rules(rules: Dict[str, str], m: Any) -> str:
102
+ """
103
+ Return expanded grammar syntax for given rule.
104
+
105
+ Parameters
106
+ ----------
107
+ ops : Dict[str, str]
108
+ Dictionary of rules in grammar
109
+
110
+ Returns
111
+ -------
112
+ str
113
+
114
+ """
115
+ txt = m.group(1)
116
+ if txt in rules:
117
+ return f' {rules[txt]} '
118
+ return f' <{txt}> '
119
+
120
+
121
+ def build_cmd(grammar: str) -> str:
122
+ """Pre-process grammar to construct top-level command."""
123
+ if ';' not in grammar:
124
+ raise ValueError('a semi-colon exist at the end of the primary rule')
125
+
126
+ # Pre-space
127
+ m = re.match(r'^\s*', grammar)
128
+ space = m.group(0) if m else ''
129
+
130
+ # Split on ';' on a line by itself
131
+ begin, end = grammar.split(';', 1)
132
+
133
+ # Get statement keywords
134
+ keywords = get_keywords(begin)
135
+ cmd = '_'.join(x.lower() for x in keywords) + '_cmd'
136
+
137
+ # Collapse multi-line to one
138
+ begin = re.sub(r'\s+', r' ', begin)
139
+
140
+ return f'{space}{cmd} ={begin}\n{end}'
141
+
142
+
143
+ def build_help(grammar: str) -> str:
144
+ """Construct full help syntax."""
145
+ if ';' not in grammar:
146
+ raise ValueError('a semi-colon exist at the end of the primary rule')
147
+
148
+ # Split on ';' on a line by itself
149
+ cmd, end = grammar.split(';', 1)
150
+
151
+ rules = {}
152
+ for line in end.split('\n'):
153
+ line = line.strip()
154
+ if not line:
155
+ continue
156
+ name, value = line.split('=', 1)
157
+ name = name.strip()
158
+ value = value.strip()
159
+ rules[name] = value
160
+
161
+ while re.search(r' [a-z0-9_]+ ', cmd):
162
+ cmd = re.sub(r' ([a-z0-9_]+) ', functools.partial(expand_rules, rules), cmd)
163
+
164
+ return textwrap.dedent(cmd).rstrip() + ';'
165
+
166
+
167
+ def strip_comments(grammar: str) -> str:
168
+ """Strip comments from grammar."""
169
+ return re.sub(r'^\s*#.*$', r'', grammar, flags=re.M)
170
+
171
+
172
+ def get_rule_info(grammar: str) -> Dict[str, Any]:
173
+ """Compute metadata about rule used in coallescing parsed output."""
174
+ return dict(
175
+ n_keywords=len(get_keywords(grammar)),
176
+ repeats=',...' in grammar,
177
+ )
178
+
179
+
180
+ def process_grammar(grammar: str) -> Tuple[Grammar, Tuple[str, ...], Dict[str, Any], str]:
181
+ """
182
+ Convert SQL grammar to a Parsimonious grammar.
183
+
184
+ Parameters
185
+ ----------
186
+ grammar : str
187
+ The SQL grammar
188
+
189
+ Returns
190
+ -------
191
+ (Grammar, Tuple[str, ...], Dict[str, Any], str) - Grammar is the parsimonious
192
+ grammar object. The tuple is a series of the keywords that start the command.
193
+ The dictionary is a set of metadata about each rule. The final string is
194
+ a human-readable version of the grammar for documentation and errors.
195
+
196
+ """
197
+ out = []
198
+ rules = {}
199
+ rule_info = {}
200
+
201
+ grammar = strip_comments(grammar)
202
+ command_key = get_keywords(grammar)
203
+ help_txt = build_help(grammar)
204
+ grammar = build_cmd(grammar)
205
+
206
+ # Make sure grouping characters all have whitespace around them
207
+ grammar = re.sub(r' *(\[|\{|\||\}|\]) *', r' \1 ', grammar)
208
+
209
+ for line in grammar.split('\n'):
210
+ if not line.strip():
211
+ continue
212
+
213
+ op, sql = line.split('=', 1)
214
+ op = op.strip()
215
+ sql = sql.strip()
216
+ sql = split_unions(sql)
217
+
218
+ rules[op] = sql
219
+ rule_info[op] = get_rule_info(sql)
220
+
221
+ # Convert consecutive optionals to a union
222
+ sql = re.sub(r'\]\s+\[', r' | ', sql)
223
+
224
+ # Lower-case keywords and make them case-insensitive
225
+ sql = re.sub(r'\b([A-Z0-9]+)\b', lower_and_regex, sql)
226
+
227
+ # Convert literal strings to 'qs'
228
+ sql = re.sub(r"'[^']+'", r'qs', sql)
229
+
230
+ # Convert [...] groups to (...)*
231
+ sql = re.sub(r'\[([^\]]+)\]', process_optional, sql)
232
+
233
+ # Convert {...} groups to (...)
234
+ sql = re.sub(r'\{([^\}]+)\}', process_alternates, sql)
235
+
236
+ # Convert <...> to ... (<...> is the form for core types)
237
+ sql = re.sub(r'<([a-z0-9_]+)>', r'\1', sql)
238
+
239
+ # Insert ws between every token to allow for whitespace and comments
240
+ sql = ' ws '.join(re.split(r'\s+', sql)) + ' ws'
241
+
242
+ # Remove ws in optional groupings
243
+ sql = sql.replace('( ws', '(')
244
+ sql = sql.replace('| ws', '|')
245
+
246
+ # Convert | to /
247
+ sql = sql.replace('|', '/')
248
+
249
+ # Remove ws after operation names, all operations contain ws at the end
250
+ sql = re.sub(r'(\s+[a-z0-9_]+)\s+ws\b', r'\1', sql)
251
+
252
+ # Convert foo,... to foo ("," foo)*
253
+ sql = re.sub(r'(\S+),...', process_repeats, sql)
254
+
255
+ # Remove ws before / and )
256
+ sql = re.sub(r'(\s*\S+\s+)ws\s+/', r'\1/', sql)
257
+ sql = re.sub(r'(\s*\S+\s+)ws\s+\)', r'\1)', sql)
258
+
259
+ # Make sure every operation ends with ws
260
+ sql = re.sub(r'\s+ws\s+ws$', r' ws', sql + ' ws')
261
+
262
+ out.append(f'{op} = {sql}')
263
+
264
+ for k, v in list(rules.items()):
265
+ while re.search(r' ([a-z0-9_]+) ', v):
266
+ v = re.sub(r' ([a-z0-9_]+) ', functools.partial(expand_rules, rules), v)
267
+ rules[k] = v
268
+
269
+ for k, v in list(rules.items()):
270
+ while re.search(r' <([a-z0-9_]+)> ', v):
271
+ v = re.sub(r' <([a-z0-9_]+)> ', r' \1 ', v)
272
+ rules[k] = v
273
+
274
+ cmds = ' / '.join(x for x in rules if x.endswith('_cmd'))
275
+ cmds = f'init = ws ( {cmds} ) ws ";"? ws\n'
276
+
277
+ return Grammar(cmds + CORE_GRAMMAR + '\n'.join(out)), command_key, rule_info, help_txt
278
+
279
+
280
+ def flatten(items: Iterable[Any]) -> List[Any]:
281
+ """Flatten a list of iterables."""
282
+ out = []
283
+ for x in items:
284
+ if isinstance(x, (str, bytes, dict)):
285
+ out.append(x)
286
+ elif isinstance(x, Iterable):
287
+ for sub_x in flatten(x):
288
+ if sub_x is not None:
289
+ out.append(sub_x)
290
+ elif x is not None:
291
+ out.append(x)
292
+ return out
293
+
294
+
295
+ def merge_dicts(items: List[Dict[str, Any]]) -> Dict[str, Any]:
296
+ """Merge list of dictionaries together."""
297
+ out: Dict[str, Any] = {}
298
+ for x in items:
299
+ if isinstance(x, dict):
300
+ same = list(set(x.keys()).intersection(set(out.keys())))
301
+ if same:
302
+ raise ValueError(f"found duplicate rules for '{same[0]}'")
303
+ out.update(x)
304
+ return out
305
+
306
+
307
+ class SQLHandler(NodeVisitor):
308
+ """Base class for all SQL handler classes."""
309
+
310
+ #: Parsimonious grammar object
311
+ grammar: Grammar = Grammar(CORE_GRAMMAR)
312
+
313
+ #: SQL keywords that start the command
314
+ command_key: Tuple[str, ...] = ()
315
+
316
+ #: Metadata about the parse rules
317
+ rule_info: Dict[str, Any] = {}
318
+
319
+ #: Help string for use in error messages
320
+ help: str = ''
321
+
322
+ #: Rule validation functions
323
+ validators: Dict[str, Callable[..., Any]] = {}
324
+
325
+ _grammar: str = CORE_GRAMMAR
326
+ _is_compiled: bool = False
327
+
328
+ def __init__(self, connection: Connection):
329
+ self.connection = connection
330
+
331
+ @classmethod
332
+ def compile(cls, grammar: str = '') -> None:
333
+ """
334
+ Compile the grammar held in the docstring.
335
+
336
+ This method modifies attributes on the class: ``grammar``,
337
+ ``command_key``, ``rule_info``, and ``help``.
338
+
339
+ Parameters
340
+ ----------
341
+ grammar : str, optional
342
+ Grammar to use instead of docstring
343
+
344
+ """
345
+ if cls._is_compiled:
346
+ return
347
+
348
+ cls.grammar, cls.command_key, cls.rule_info, cls.help = \
349
+ process_grammar(grammar or cls.__doc__ or '')
350
+
351
+ cls._grammar = grammar or cls.__doc__ or ''
352
+ cls._is_compiled = True
353
+
354
+ def create_result(self) -> result.FusionSQLResult:
355
+ """Return a new result object."""
356
+ return result.FusionSQLResult(self.connection)
357
+
358
+ @classmethod
359
+ def register(cls, overwrite: bool = False) -> None:
360
+ """
361
+ Register the handler class.
362
+
363
+ Paraemeters
364
+ -----------
365
+ overwrite : bool, optional
366
+ Overwrite an existing command with the same name?
367
+
368
+ """
369
+ from . import registry
370
+ cls.compile()
371
+ registry.register_handler(cls, overwrite=overwrite)
372
+
373
+ def execute(self, sql: str) -> result.FusionSQLResult:
374
+ """
375
+ Parse the SQL and invoke the handler method.
376
+
377
+ Parameters
378
+ ----------
379
+ sql : str
380
+ SQL statement to execute
381
+
382
+ Returns
383
+ -------
384
+ DummySQLResult
385
+
386
+ """
387
+ type(self).compile()
388
+ try:
389
+ params = self.visit(type(self).grammar.parse(sql))
390
+ for k, v in params.items():
391
+ params[k] = self.validate_rule(k, v)
392
+ res = self.run(params)
393
+ if res is not None:
394
+ return res
395
+ res = result.FusionSQLResult(self.connection)
396
+ res.set_rows([])
397
+ return res
398
+ except ParseError as exc:
399
+ s = str(exc)
400
+ msg = ''
401
+ m = re.search(r'(The non-matching portion.*$)', s)
402
+ if m:
403
+ msg = ' ' + m.group(1)
404
+ m = re.search(r"(Rule) '.+?'( didn't match at.*$)", s)
405
+ if m:
406
+ msg = ' ' + m.group(1) + m.group(2)
407
+ raise ValueError(
408
+ f'Could not parse statement.{msg} '
409
+ 'Expecting:\n' + textwrap.indent(type(self).help, ' '),
410
+ )
411
+
412
+ @abc.abstractmethod
413
+ def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
414
+ """
415
+ Run the handler command.
416
+
417
+ Parameters
418
+ ----------
419
+ params : Dict[str, Any]
420
+ Values parsed from the SQL query. Each rule in the grammar
421
+ results in a key/value pair in the ``params` dictionary.
422
+
423
+ Returns
424
+ -------
425
+ SQLResult - tuple containing the column definitions and
426
+ rows of data in the result
427
+
428
+ """
429
+ raise NotImplementedError
430
+
431
+ def create_like_func(self, like: Optional[str]) -> Callable[[str], bool]:
432
+ """
433
+ Construct a function to apply the LIKE clause.
434
+
435
+ Calling the resulting function will return a boolean indicating
436
+ whether the given string matched the ``like`` pattern.
437
+
438
+ Parameters
439
+ ----------
440
+ like : str
441
+ A LIKE pattern (i.e., string with '%' as a wildcard)
442
+
443
+ Returns
444
+ -------
445
+ function
446
+
447
+ """
448
+ if like is None:
449
+ def is_like(x: Any) -> bool:
450
+ return True
451
+ else:
452
+ regex = re.compile(
453
+ '^{}$'.format(
454
+ re.sub(r'\\%', r'.*', re.sub(r'([^\w])', r'\\\1', like)),
455
+ ), flags=re.I,
456
+ )
457
+
458
+ def is_like(x: Any) -> bool:
459
+ return bool(regex.match(x))
460
+
461
+ return is_like
462
+
463
+ def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:
464
+ """Quoted strings."""
465
+ if node is None:
466
+ return None
467
+ return node.match.group(1) or node.match.group(2) or \
468
+ node.match.group(3) or node.match.group(4)
469
+
470
+ def visit_number(self, node: Node, visited_children: Iterable[Any]) -> Any:
471
+ """Numeric value."""
472
+ return float(node.match.group(0))
473
+
474
+ def visit_integer(self, node: Node, visited_children: Iterable[Any]) -> Any:
475
+ """Integer value."""
476
+ return int(node.match.group(0))
477
+
478
+ def visit_ws(self, node: Node, visited_children: Iterable[Any]) -> Any:
479
+ """Whitespace and comments."""
480
+ return
481
+
482
+ def visit_comma(self, node: Node, visited_children: Iterable[Any]) -> Any:
483
+ """Single comma."""
484
+ return
485
+
486
+ def visit_open_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:
487
+ """Open parenthesis."""
488
+ return
489
+
490
+ def visit_close_paren(self, node: Node, visited_children: Iterable[Any]) -> Any:
491
+ """Close parenthesis."""
492
+ return
493
+
494
+ def visit_init(self, node: Node, visited_children: Iterable[Any]) -> Any:
495
+ """Entry point of the grammar."""
496
+ _, out, *_ = visited_children
497
+ return out
498
+
499
+ def generic_visit(self, node: Node, visited_children: Iterable[Any]) -> Any:
500
+ """
501
+ Handle all undefined rules.
502
+
503
+ This method processes all user-defined rules. Each rule results in
504
+ a dictionary with a single key corresponding to the rule name, with
505
+ a value corresponding to the data value following the rule keywords.
506
+
507
+ If no value exists, the value True is used. If the rule is not a
508
+ rule with possible repeated values, a single value is used. If the
509
+ rule can have repeated values, a list of values is returned.
510
+
511
+ """
512
+ # Call a grammar rule
513
+ if node.expr_name in type(self).rule_info:
514
+ n_keywords = type(self).rule_info[node.expr_name]['n_keywords']
515
+ repeats = type(self).rule_info[node.expr_name]['repeats']
516
+
517
+ # If this is the top-level command, create the final result
518
+ if node.expr_name.endswith('_cmd'):
519
+ return merge_dicts(flatten(visited_children)[n_keywords:])
520
+
521
+ # Filter out stray empty strings
522
+ out = [x for x in flatten(visited_children)[n_keywords:] if x]
523
+
524
+ if repeats or len(out) > 1:
525
+ return {node.expr_name: out}
526
+
527
+ return {node.expr_name: out[0] if out else True}
528
+
529
+ if hasattr(node, 'match'):
530
+ if not visited_children and not node.match.groups():
531
+ return node.text
532
+ return visited_children or list(node.match.groups())
533
+
534
+ return visited_children or node.text
535
+
536
+ def validate_rule(self, rule: str, value: Any) -> Any:
537
+ """
538
+ Validate the value of the given rule.
539
+
540
+ Paraemeters
541
+ -----------
542
+ rule : str
543
+ Name of the grammar rule the value belongs to
544
+ value : Any
545
+ Value parsed from the query
546
+
547
+ Returns
548
+ -------
549
+ Any - result of the validator function
550
+
551
+ """
552
+ validator = type(self).validators.get(rule)
553
+ if validator is not None:
554
+ return validator(value)
555
+ return value