singlestoredb 0.8.9__cp36-abi3-win_amd64.whl → 0.9.0__cp36-abi3-win_amd64.whl
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.
- _singlestoredb_accel.pyd +0 -0
- singlestoredb/__init__.py +1 -1
- singlestoredb/exceptions.py +24 -0
- singlestoredb/functions/__init__.py +1 -0
- singlestoredb/functions/decorator.py +165 -0
- singlestoredb/functions/dtypes.py +1396 -0
- singlestoredb/functions/ext/__init__.py +2 -0
- singlestoredb/functions/ext/asgi.py +357 -0
- singlestoredb/functions/ext/json.py +49 -0
- singlestoredb/functions/ext/rowdat_1.py +111 -0
- singlestoredb/functions/signature.py +607 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/manager.py +42 -1
- singlestoredb/management/organization.py +85 -0
- singlestoredb/management/utils.py +118 -1
- singlestoredb/management/workspace.py +881 -5
- singlestoredb/mysql/__init__.py +12 -10
- singlestoredb/mysql/charset.py +12 -11
- singlestoredb/mysql/constants/CLIENT.py +0 -1
- singlestoredb/mysql/constants/COMMAND.py +0 -1
- singlestoredb/mysql/constants/CR.py +0 -2
- singlestoredb/mysql/constants/ER.py +0 -1
- singlestoredb/mysql/constants/FIELD_TYPE.py +0 -1
- singlestoredb/mysql/constants/FLAG.py +0 -1
- singlestoredb/mysql/constants/SERVER_STATUS.py +0 -1
- singlestoredb/mysql/converters.py +49 -28
- singlestoredb/mysql/err.py +3 -3
- singlestoredb/mysql/optionfile.py +4 -4
- singlestoredb/mysql/times.py +3 -4
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_management.py +393 -3
- singlestoredb/tests/test_udf.py +698 -0
- {singlestoredb-0.8.9.dist-info → singlestoredb-0.9.0.dist-info}/METADATA +1 -1
- {singlestoredb-0.8.9.dist-info → singlestoredb-0.9.0.dist-info}/RECORD +37 -25
- {singlestoredb-0.8.9.dist-info → singlestoredb-0.9.0.dist-info}/LICENSE +0 -0
- {singlestoredb-0.8.9.dist-info → singlestoredb-0.9.0.dist-info}/WHEEL +0 -0
- {singlestoredb-0.8.9.dist-info → singlestoredb-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""SingleStoreDB Cloud Billing Usage."""
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import Dict
|
|
6
|
+
from typing import List
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .manager import Manager
|
|
10
|
+
from .utils import camel_to_snake
|
|
11
|
+
from .utils import vars_to_str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UsageItem(object):
|
|
15
|
+
"""Usage statistics."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
start_time: datetime.datetime,
|
|
20
|
+
end_time: datetime.datetime,
|
|
21
|
+
owner_id: str,
|
|
22
|
+
resource_id: str,
|
|
23
|
+
resource_name: str,
|
|
24
|
+
resource_type: str,
|
|
25
|
+
value: str,
|
|
26
|
+
):
|
|
27
|
+
#: Starting time for the usage duration
|
|
28
|
+
self.start_time = start_time
|
|
29
|
+
|
|
30
|
+
#: Ending time for the usage duration
|
|
31
|
+
self.end_time = end_time
|
|
32
|
+
|
|
33
|
+
#: Owner ID
|
|
34
|
+
self.owner_id = owner_id
|
|
35
|
+
|
|
36
|
+
#: Resource ID
|
|
37
|
+
self.resource_id = resource_id
|
|
38
|
+
|
|
39
|
+
#: Resource name
|
|
40
|
+
self.resource_name = resource_name
|
|
41
|
+
|
|
42
|
+
#: Resource type
|
|
43
|
+
self.resource_type = resource_type
|
|
44
|
+
|
|
45
|
+
#: Usage statistic value
|
|
46
|
+
self.value = value
|
|
47
|
+
|
|
48
|
+
self._manager: Optional[Manager] = None
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
"""Return string representation."""
|
|
52
|
+
return vars_to_str(self)
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
"""Return string representation."""
|
|
56
|
+
return str(self)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(
|
|
60
|
+
cls,
|
|
61
|
+
obj: Dict[str, Any],
|
|
62
|
+
manager: Manager,
|
|
63
|
+
) -> 'UsageItem':
|
|
64
|
+
"""
|
|
65
|
+
Convert dictionary to a ``UsageItem`` object.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
obj : dict
|
|
70
|
+
Key-value pairs to retrieve billling usage information from
|
|
71
|
+
manager : WorkspaceManager, optional
|
|
72
|
+
The WorkspaceManager the UsageItem belongs to
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
:class:`UsageItem`
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
out = cls(
|
|
80
|
+
end_time=datetime.datetime.fromisoformat(obj['endTime']),
|
|
81
|
+
start_time=datetime.datetime.fromisoformat(obj['startTime']),
|
|
82
|
+
owner_id=obj['ownerId'],
|
|
83
|
+
resource_id=obj['resourceId'],
|
|
84
|
+
resource_name=obj['resourceName'],
|
|
85
|
+
resource_type=obj['resource_type'],
|
|
86
|
+
value=obj['value'],
|
|
87
|
+
)
|
|
88
|
+
out._manager = manager
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BillingUsageItem(object):
|
|
93
|
+
"""Billing usage item."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
description: str,
|
|
98
|
+
metric: str,
|
|
99
|
+
usage: List[UsageItem],
|
|
100
|
+
):
|
|
101
|
+
"""Use :attr:`WorkspaceManager.billing.usage` instead."""
|
|
102
|
+
#: Description of the usage metric
|
|
103
|
+
self.description = description
|
|
104
|
+
|
|
105
|
+
#: Name of the usage metric
|
|
106
|
+
self.metric = metric
|
|
107
|
+
|
|
108
|
+
#: Usage statistics
|
|
109
|
+
self.usage = list(usage)
|
|
110
|
+
|
|
111
|
+
self._manager: Optional[Manager] = None
|
|
112
|
+
|
|
113
|
+
def __str__(self) -> str:
|
|
114
|
+
"""Return string representation."""
|
|
115
|
+
return vars_to_str(self)
|
|
116
|
+
|
|
117
|
+
def __repr__(self) -> str:
|
|
118
|
+
"""Return string representation."""
|
|
119
|
+
return str(self)
|
|
120
|
+
|
|
121
|
+
@ classmethod
|
|
122
|
+
def from_dict(
|
|
123
|
+
cls,
|
|
124
|
+
obj: Dict[str, Any],
|
|
125
|
+
manager: Manager,
|
|
126
|
+
) -> 'BillingUsageItem':
|
|
127
|
+
"""
|
|
128
|
+
Convert dictionary to a ``BillingUsageItem`` object.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
obj : dict
|
|
133
|
+
Key-value pairs to retrieve billling usage information from
|
|
134
|
+
manager : WorkspaceManager, optional
|
|
135
|
+
The WorkspaceManager the BillingUsageItem belongs to
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
:class:`BillingUsageItem`
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
out = cls(
|
|
143
|
+
description=obj['description'],
|
|
144
|
+
metric=str(camel_to_snake(obj['metric'])),
|
|
145
|
+
usage=[UsageItem.from_dict(x, manager) for x in obj['Usage']],
|
|
146
|
+
)
|
|
147
|
+
out._manager = manager
|
|
148
|
+
return out
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""SingleStoreDB Base Manager."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
3
5
|
import time
|
|
4
6
|
from typing import Any
|
|
5
7
|
from typing import Dict
|
|
@@ -48,6 +50,7 @@ class Manager(object):
|
|
|
48
50
|
base_url or type(self).default_base_url,
|
|
49
51
|
version or type(self).default_version,
|
|
50
52
|
) + '/'
|
|
53
|
+
self._params: Dict[str, str] = {}
|
|
51
54
|
|
|
52
55
|
def _check(
|
|
53
56
|
self, res: requests.Response, url: str, params: Dict[str, Any],
|
|
@@ -65,6 +68,8 @@ class Manager(object):
|
|
|
65
68
|
requests.Response
|
|
66
69
|
|
|
67
70
|
"""
|
|
71
|
+
if config.get_option('debug.queries'):
|
|
72
|
+
print(os.path.join(self._base_url, url), params, file=sys.stderr)
|
|
68
73
|
if res.status_code >= 400:
|
|
69
74
|
txt = res.text.strip()
|
|
70
75
|
msg = f'{txt}: /{url}'
|
|
@@ -75,7 +80,7 @@ class Manager(object):
|
|
|
75
80
|
if 'password' in k.lower() and v:
|
|
76
81
|
new_params['json'][k] = '*' * len(v)
|
|
77
82
|
msg += ': {}'.format(str(new_params))
|
|
78
|
-
raise ManagementError(errno=res.status_code, msg=msg)
|
|
83
|
+
raise ManagementError(errno=res.status_code, msg=msg, response=txt)
|
|
79
84
|
return res
|
|
80
85
|
|
|
81
86
|
def _get(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
|
|
@@ -96,6 +101,8 @@ class Manager(object):
|
|
|
96
101
|
requests.Response
|
|
97
102
|
|
|
98
103
|
"""
|
|
104
|
+
if self._params:
|
|
105
|
+
kwargs['params'] = self._params
|
|
99
106
|
return self._check(
|
|
100
107
|
self._sess.get(
|
|
101
108
|
urljoin(self._base_url, path),
|
|
@@ -122,6 +129,8 @@ class Manager(object):
|
|
|
122
129
|
requests.Response
|
|
123
130
|
|
|
124
131
|
"""
|
|
132
|
+
if self._params:
|
|
133
|
+
kwargs['params'] = self._params
|
|
125
134
|
return self._check(
|
|
126
135
|
self._sess.post(
|
|
127
136
|
urljoin(self._base_url, path),
|
|
@@ -130,6 +139,34 @@ class Manager(object):
|
|
|
130
139
|
path, kwargs,
|
|
131
140
|
)
|
|
132
141
|
|
|
142
|
+
def _put(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
|
|
143
|
+
"""
|
|
144
|
+
Invoke a PUT request.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
path : str
|
|
149
|
+
Path of the resource
|
|
150
|
+
*args : positional arguments, optional
|
|
151
|
+
Arguments to add to the POST request
|
|
152
|
+
**kwargs : keyword arguments, optional
|
|
153
|
+
Keyword arguments to add to the POST request
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
requests.Response
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
if self._params:
|
|
161
|
+
kwargs['params'] = self._params
|
|
162
|
+
return self._check(
|
|
163
|
+
self._sess.put(
|
|
164
|
+
urljoin(self._base_url, path),
|
|
165
|
+
*args, **kwargs,
|
|
166
|
+
),
|
|
167
|
+
path, kwargs,
|
|
168
|
+
)
|
|
169
|
+
|
|
133
170
|
def _delete(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
|
|
134
171
|
"""
|
|
135
172
|
Invoke a DELETE request.
|
|
@@ -148,6 +185,8 @@ class Manager(object):
|
|
|
148
185
|
requests.Response
|
|
149
186
|
|
|
150
187
|
"""
|
|
188
|
+
if self._params:
|
|
189
|
+
kwargs['params'] = self._params
|
|
151
190
|
return self._check(
|
|
152
191
|
self._sess.delete(
|
|
153
192
|
urljoin(self._base_url, path),
|
|
@@ -174,6 +213,8 @@ class Manager(object):
|
|
|
174
213
|
requests.Response
|
|
175
214
|
|
|
176
215
|
"""
|
|
216
|
+
if self._params:
|
|
217
|
+
kwargs['params'] = self._params
|
|
177
218
|
return self._check(
|
|
178
219
|
self._sess.patch(
|
|
179
220
|
urljoin(self._base_url, path),
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""SingleStoreDB Cloud Organization."""
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from .manager import Manager
|
|
9
|
+
from .utils import vars_to_str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def listify(x: Union[str, List[str]]) -> List[str]:
|
|
13
|
+
if isinstance(x, list):
|
|
14
|
+
return x
|
|
15
|
+
return [x]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def stringify(x: Union[str, List[str]]) -> str:
|
|
19
|
+
if isinstance(x, list):
|
|
20
|
+
return x[0]
|
|
21
|
+
return x
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Organization(object):
|
|
25
|
+
"""
|
|
26
|
+
Organization in SingleStoreDB Cloud portal.
|
|
27
|
+
|
|
28
|
+
This object is not directly instantiated. It is used in results
|
|
29
|
+
of ``WorkspaceManager`` API calls.
|
|
30
|
+
|
|
31
|
+
See Also
|
|
32
|
+
--------
|
|
33
|
+
:attr:`WorkspaceManager.organization`
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, id: str, name: str, firewall_ranges: List[str]):
|
|
38
|
+
"""Use :attr:`WorkspaceManager.organization` instead."""
|
|
39
|
+
#: Unique ID of the organization
|
|
40
|
+
self.id = id
|
|
41
|
+
|
|
42
|
+
#: Name of the organization
|
|
43
|
+
self.name = name
|
|
44
|
+
|
|
45
|
+
#: Firewall ranges of the organization
|
|
46
|
+
self.firewall_ranges = list(firewall_ranges)
|
|
47
|
+
|
|
48
|
+
self._manager: Optional[Manager] = None
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
"""Return string representation."""
|
|
52
|
+
return vars_to_str(self)
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
"""Return string representation."""
|
|
56
|
+
return str(self)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(
|
|
60
|
+
cls,
|
|
61
|
+
obj: Dict[str, Union[str, List[str]]],
|
|
62
|
+
manager: Manager,
|
|
63
|
+
) -> 'Organization':
|
|
64
|
+
"""
|
|
65
|
+
Convert dictionary to an ``Organization`` object.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
obj : dict
|
|
70
|
+
Key-value pairs to retrieve organization information from
|
|
71
|
+
manager : WorkspaceManager, optional
|
|
72
|
+
The WorkspaceManager the Organization belongs to
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
:class:`Organization`
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
out = cls(
|
|
80
|
+
id=stringify(obj['orgID']),
|
|
81
|
+
name=stringify(obj.get('name', '<unknown>')),
|
|
82
|
+
firewall_ranges=listify(obj.get('firewallRanges', [])),
|
|
83
|
+
)
|
|
84
|
+
out._manager = manager
|
|
85
|
+
return out
|
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""SingleStoreDB Cluster Management."""
|
|
3
3
|
import datetime
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
4
7
|
from typing import Any
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from typing import List
|
|
5
10
|
from typing import Optional
|
|
6
11
|
from typing import Union
|
|
7
12
|
|
|
8
13
|
from .. import converters
|
|
9
14
|
|
|
15
|
+
JSON = Union[str, List[str], Dict[str, 'JSON']]
|
|
16
|
+
JSONObj = Dict[str, JSON]
|
|
17
|
+
JSONList = List[JSON]
|
|
18
|
+
|
|
19
|
+
if sys.version_info < (3, 10):
|
|
20
|
+
PathLike = Union[str, os.PathLike]
|
|
21
|
+
PathLikeABC = os.PathLike
|
|
22
|
+
else:
|
|
23
|
+
PathLike = Union[str, os.PathLike[str]]
|
|
24
|
+
PathLikeABC = os.PathLike[str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def enable_http_tracing() -> None:
|
|
28
|
+
"""Enable tracing of HTTP requests."""
|
|
29
|
+
import logging
|
|
30
|
+
import http.client as http_client
|
|
31
|
+
http_client.HTTPConnection.debuglevel = 1
|
|
32
|
+
logging.basicConfig()
|
|
33
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
34
|
+
requests_log = logging.getLogger('requests.packages.urllib3')
|
|
35
|
+
requests_log.setLevel(logging.DEBUG)
|
|
36
|
+
requests_log.propagate = True
|
|
37
|
+
|
|
10
38
|
|
|
11
39
|
def to_datetime(
|
|
12
40
|
obj: Optional[Union[str, datetime.datetime]],
|
|
@@ -16,6 +44,8 @@ def to_datetime(
|
|
|
16
44
|
return None
|
|
17
45
|
if isinstance(obj, datetime.datetime):
|
|
18
46
|
return obj
|
|
47
|
+
if obj == '0001-01-01T00:00:00Z':
|
|
48
|
+
return None
|
|
19
49
|
obj = obj.replace('Z', '')
|
|
20
50
|
# Fix datetimes with truncated zeros
|
|
21
51
|
if '.' in obj:
|
|
@@ -30,11 +60,98 @@ def to_datetime(
|
|
|
30
60
|
return out
|
|
31
61
|
|
|
32
62
|
|
|
63
|
+
def from_datetime(
|
|
64
|
+
obj: Union[str, datetime.datetime],
|
|
65
|
+
) -> Optional[str]:
|
|
66
|
+
"""Convert datetime to string."""
|
|
67
|
+
if not obj:
|
|
68
|
+
return None
|
|
69
|
+
if isinstance(obj, str):
|
|
70
|
+
return obj
|
|
71
|
+
out = obj.isoformat()
|
|
72
|
+
if not re.search(r'[A-Za-z]$', out):
|
|
73
|
+
out = f'{out}Z'
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
33
77
|
def vars_to_str(obj: Any) -> str:
|
|
34
78
|
"""Render a string representation of vars(obj)."""
|
|
35
79
|
attrs = []
|
|
36
|
-
|
|
80
|
+
obj_vars = vars(obj)
|
|
81
|
+
if 'name' in obj_vars:
|
|
82
|
+
attrs.append('name={}'.format(repr(obj_vars['name'])))
|
|
83
|
+
if 'id' in obj_vars:
|
|
84
|
+
attrs.append('id={}'.format(repr(obj_vars['id'])))
|
|
85
|
+
for name, value in sorted(obj_vars.items()):
|
|
86
|
+
if name in ('name', 'id'):
|
|
87
|
+
continue
|
|
37
88
|
if not value or name.startswith('_'):
|
|
38
89
|
continue
|
|
39
90
|
attrs.append('{}={}'.format(name, repr(value)))
|
|
40
91
|
return '{}({})'.format(type(obj).__name__, ', '.join(attrs))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def single_item(s: Any) -> Any:
|
|
95
|
+
"""Return only item if ``s`` is a list, otherwise return ``s``."""
|
|
96
|
+
if isinstance(s, list):
|
|
97
|
+
if len(s) != 1:
|
|
98
|
+
raise ValueError('list must only contain a singleitem')
|
|
99
|
+
return s[0]
|
|
100
|
+
return s
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def stringify(s: JSON) -> str:
|
|
104
|
+
"""Convert list of strings to single string."""
|
|
105
|
+
if isinstance(s, (tuple, list)):
|
|
106
|
+
if len(s) > 1:
|
|
107
|
+
raise ValueError('list contains more than one item')
|
|
108
|
+
return s[0]
|
|
109
|
+
if isinstance(s, dict):
|
|
110
|
+
raise TypeError('only strings and lists are valid arguments')
|
|
111
|
+
return s
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def listify(s: JSON) -> List[str]:
|
|
115
|
+
"""Convert string to list of strings."""
|
|
116
|
+
if isinstance(s, (tuple, list)):
|
|
117
|
+
return list(s)
|
|
118
|
+
if isinstance(s, dict):
|
|
119
|
+
raise TypeError('only strings and lists are valid arguments')
|
|
120
|
+
return [s]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def listify_obj(s: JSON) -> List[JSONObj]:
|
|
124
|
+
"""Convert object to list of objects."""
|
|
125
|
+
if isinstance(s, (tuple, list)):
|
|
126
|
+
for item in s:
|
|
127
|
+
if not isinstance(item, dict):
|
|
128
|
+
raise TypeError('only dicts and lists of dicts are valid parameters')
|
|
129
|
+
return list(s) # type: ignore
|
|
130
|
+
if not isinstance(s, dict):
|
|
131
|
+
raise TypeError('only dicts and lists of dicts are valid parameters')
|
|
132
|
+
return [s]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _upper_match(m: Any) -> str:
|
|
136
|
+
"""Upper-case the first match group."""
|
|
137
|
+
return m.group(1).upper()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def snake_to_camel(s: Optional[str], cap_first: bool = False) -> Optional[str]:
|
|
141
|
+
"""Convert snake-case to camel-case."""
|
|
142
|
+
if s is None:
|
|
143
|
+
return None
|
|
144
|
+
out = re.sub(r'_[A-Za-z]', _upper_match, s.lower())
|
|
145
|
+
if cap_first and out:
|
|
146
|
+
return out[0].upper() + out[1:]
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def camel_to_snake(s: Optional[str]) -> Optional[str]:
|
|
151
|
+
"""Convert camel-case to snake-case."""
|
|
152
|
+
if s is None:
|
|
153
|
+
return None
|
|
154
|
+
out = re.sub(r'([A-Z]+)', r'_\1', s).lower()
|
|
155
|
+
if out and out[0] == '_':
|
|
156
|
+
return out[1:]
|
|
157
|
+
return out
|