iker-python-common 1.0.44__tar.gz → 1.0.45__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.
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/PKG-INFO +1 -1
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/argutils.py +67 -1
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/csv.py +52 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/dbutils.py +97 -13
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/dtutils.py +109 -49
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/funcutils.py +53 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/jsonutils.py +108 -0
- iker_python_common-1.0.45/src/iker/common/utils/logger.py +117 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/numutils.py +48 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/randutils.py +146 -1
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/retry.py +85 -21
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/s3utils.py +63 -63
- iker_python_common-1.0.45/src/iker/common/utils/sequtils.py +703 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/shutils.py +48 -51
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/span.py +15 -15
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/typeutils.py +60 -16
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker_python_common.egg-info/PKG-INFO +1 -1
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/sequtils_test.py +65 -28
- iker_python_common-1.0.44/src/iker/common/utils/logger.py +0 -64
- iker_python_common-1.0.44/src/iker/common/utils/sequtils.py +0 -546
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/.editorconfig +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/.github/workflows/pr.yml +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/.github/workflows/push.yml +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/.gitignore +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/MANIFEST.in +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/README.md +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/VERSION +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/pyproject.toml +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/config/config.cfg +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/csv/data.csv +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/csv/data.tsv +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.foo/file.bar +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.foo/file.baz +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/resources/unittest/shutils/dir.foo/file.foo +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/setup.cfg +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/setup.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/__init__.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/__init__.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/config.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/dockerutils.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/strutils.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker/common/utils/testutils.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker_python_common.egg-info/SOURCES.txt +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker_python_common.egg-info/not-zip-safe +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker_python_common.egg-info/requires.txt +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/src/iker_python_common.egg-info/top_level.txt +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/__init__.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/argutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/config_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/csv_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/dbutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/dockerutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/dtutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/funcutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/jsonutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/logger_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/numutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/randutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/retry_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/s3utils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/shutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/span_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/strutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/testutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/common/utils/typeutils_test.py +0 -0
- {iker_python_common-1.0.44 → iker_python_common-1.0.45}/test/iker_tests/docker_fixtures.py +0 -0
|
@@ -19,6 +19,14 @@ import sys
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class ParserTreeNode(object):
|
|
22
|
+
"""
|
|
23
|
+
Represents a node in the parser tree, holding a command, its parser, and any child nodes. Each node may have
|
|
24
|
+
subparsers and a list of child nodes representing subcommands.
|
|
25
|
+
|
|
26
|
+
:param command: The command string for this node.
|
|
27
|
+
:param parser: The ``ArgumentParser`` associated with this node.
|
|
28
|
+
"""
|
|
29
|
+
|
|
22
30
|
def __init__(self, command: str, parser: argparse.ArgumentParser):
|
|
23
31
|
self.command = command
|
|
24
32
|
self.parser = parser
|
|
@@ -32,6 +40,16 @@ def construct_parser_tree(
|
|
|
32
40
|
command_key_prefix: str,
|
|
33
41
|
**kwargs,
|
|
34
42
|
) -> list[ParserTreeNode]:
|
|
43
|
+
"""
|
|
44
|
+
Constructs a parser tree by traversing or creating nodes for each command in the command chain. Returns the path
|
|
45
|
+
from the ``root_node`` to the last node in the chain.
|
|
46
|
+
|
|
47
|
+
:param root_node: The root node of the parser tree.
|
|
48
|
+
:param command_chain: A list of command strings representing the path.
|
|
49
|
+
:param command_key_prefix: Prefix for command keys in the parser.
|
|
50
|
+
:param kwargs: Additional keyword arguments for parser creation.
|
|
51
|
+
:return: A list of ``ParserTreeNode`` objects representing the path from root to the last command.
|
|
52
|
+
"""
|
|
35
53
|
node_path = [root_node]
|
|
36
54
|
if len(command_chain) == 0:
|
|
37
55
|
return node_path
|
|
@@ -58,16 +76,37 @@ def construct_parser_tree(
|
|
|
58
76
|
|
|
59
77
|
|
|
60
78
|
class ParserTree(object):
|
|
79
|
+
"""
|
|
80
|
+
Represents a tree structure for managing ``argparse`` parsers and subcommands. Provides methods to add subcommand
|
|
81
|
+
parsers and parse arguments, returning the command chain and parsed namespace.
|
|
82
|
+
|
|
83
|
+
:param root_parser: The root ``ArgumentParser``.
|
|
84
|
+
:param command_key_prefix: Prefix for command keys in the parser tree.
|
|
85
|
+
"""
|
|
86
|
+
|
|
61
87
|
def __init__(self, root_parser: argparse.ArgumentParser, command_key_prefix: str = "command"):
|
|
62
88
|
self.root_node = ParserTreeNode("", root_parser)
|
|
63
89
|
self.command_key_prefix = command_key_prefix
|
|
64
90
|
|
|
65
91
|
def add_subcommand_parser(self, command_chain: list[str], **kwargs) -> argparse.ArgumentParser:
|
|
92
|
+
"""
|
|
93
|
+
Adds a subcommand parser for the specified command chain, creating intermediate nodes as needed.
|
|
94
|
+
|
|
95
|
+
:param command_chain: A list of command strings representing the subcommand path.
|
|
96
|
+
:param kwargs: Additional keyword arguments for parser creation.
|
|
97
|
+
:return: The ``ArgumentParser`` for the last command in the chain.
|
|
98
|
+
"""
|
|
66
99
|
*_, last_node = construct_parser_tree(self.root_node, command_chain, self.command_key_prefix, **kwargs)
|
|
67
100
|
return last_node.parser
|
|
68
101
|
|
|
69
102
|
def parse_args(self, args: list[str] | None = None) -> tuple[list[str], argparse.Namespace]:
|
|
70
|
-
|
|
103
|
+
"""
|
|
104
|
+
Parses the provided argument list, returning the command chain and the parsed namespace.
|
|
105
|
+
|
|
106
|
+
:param args: The list of arguments to parse. If ``None``, parses ``sys.argv``.
|
|
107
|
+
:return: A tuple containing the list of command strings and the parsed ``Namespace``.
|
|
108
|
+
"""
|
|
109
|
+
# Before Python 3.12 the ``exit_on_error`` attribute does not take effect properly
|
|
71
110
|
# if unknown arguments encountered. We have to employ this workaround
|
|
72
111
|
if sys.version_info < (3, 12):
|
|
73
112
|
if self.root_node.parser.exit_on_error:
|
|
@@ -92,6 +131,19 @@ class ParserTree(object):
|
|
|
92
131
|
|
|
93
132
|
@dataclasses.dataclass(frozen=True)
|
|
94
133
|
class ArgParseSpec(object):
|
|
134
|
+
"""
|
|
135
|
+
Specification for an argument to be added to an ``ArgumentParser``. Allows detailed configuration of argument
|
|
136
|
+
properties such as ``flag``, ``name``, ``type``, ``action``, ``default``, ``choices``, and help text.
|
|
137
|
+
|
|
138
|
+
:param flag: The optional flag for the argument (e.g., '-f').
|
|
139
|
+
:param name: The name of the argument (e.g., '--file').
|
|
140
|
+
:param action: The action to be taken by the argument parser.
|
|
141
|
+
:param default: The default value for the argument.
|
|
142
|
+
:param type: The type of the argument value.
|
|
143
|
+
:param choices: A list of valid choices for the argument.
|
|
144
|
+
:param required: Whether the argument is required.
|
|
145
|
+
:param help: The help text for the argument.
|
|
146
|
+
"""
|
|
95
147
|
flag: str | None = None
|
|
96
148
|
name: str | None = None
|
|
97
149
|
action: str | None = None
|
|
@@ -102,6 +154,12 @@ class ArgParseSpec(object):
|
|
|
102
154
|
help: str | None = None
|
|
103
155
|
|
|
104
156
|
def make_kwargs(self) -> dict[str, Any]:
|
|
157
|
+
"""
|
|
158
|
+
Constructs a dictionary of keyword arguments for ``ArgumentParser.add_argument``, omitting any that are
|
|
159
|
+
``None``.
|
|
160
|
+
|
|
161
|
+
:return: A dictionary of argument properties suitable for ``ArgumentParser.add_argument``.
|
|
162
|
+
"""
|
|
105
163
|
kwargs = dict(
|
|
106
164
|
action=self.action,
|
|
107
165
|
default=self.default,
|
|
@@ -118,6 +176,14 @@ argparse_spec = ArgParseSpec
|
|
|
118
176
|
|
|
119
177
|
|
|
120
178
|
def make_argparse(func, parser: argparse.ArgumentParser = None) -> argparse.ArgumentParser:
|
|
179
|
+
"""
|
|
180
|
+
Automatically generates an ``ArgumentParser`` for the given function by inspecting its signature and parameter
|
|
181
|
+
annotations. Supports ``ArgParseSpec`` for detailed argument configuration.
|
|
182
|
+
|
|
183
|
+
:param func: The function whose parameters will be used to generate arguments.
|
|
184
|
+
:param parser: An optional ``ArgumentParser`` to add arguments to. If ``None``, a new parser is created.
|
|
185
|
+
:return: The ``ArgumentParser`` with arguments added based on the function signature.
|
|
186
|
+
"""
|
|
121
187
|
if parser is None:
|
|
122
188
|
parser = argparse.ArgumentParser()
|
|
123
189
|
|
|
@@ -11,6 +11,16 @@ __all__ = [
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CSVColumn(object):
|
|
14
|
+
"""
|
|
15
|
+
Represents a column in a CSV schema, including its name, loader function, dumper function, and the string used for
|
|
16
|
+
null values.
|
|
17
|
+
|
|
18
|
+
:param name: The name of the column.
|
|
19
|
+
:param loader: A function to convert a string to the column's value type.
|
|
20
|
+
:param dumper: A function to convert the column's value to a string.
|
|
21
|
+
:param null_str: The string representation of a null value in this column.
|
|
22
|
+
"""
|
|
23
|
+
|
|
14
24
|
def __init__(
|
|
15
25
|
self,
|
|
16
26
|
name: str,
|
|
@@ -25,6 +35,15 @@ class CSVColumn(object):
|
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
class CSVView(object):
|
|
38
|
+
"""
|
|
39
|
+
Represents a view for reading and writing CSV data according to a schema. Supports loading and dumping lines or
|
|
40
|
+
files, with options for headers and dictionary or list output.
|
|
41
|
+
|
|
42
|
+
:param schema: The sequence of ``CSVColumn`` objects defining the schema.
|
|
43
|
+
:param row_delim: The delimiter for rows (default is ``'\n'``).
|
|
44
|
+
:param col_delim: The delimiter for columns (default is ``','``).
|
|
45
|
+
"""
|
|
46
|
+
|
|
28
47
|
def __init__(self, schema: Sequence[CSVColumn], *, row_delim: str = "\n", col_delim: str = ","):
|
|
29
48
|
self.schema = schema
|
|
30
49
|
self.row_delim = row_delim
|
|
@@ -36,6 +55,15 @@ class CSVView(object):
|
|
|
36
55
|
has_header: bool = True,
|
|
37
56
|
ret_dict: bool = False,
|
|
38
57
|
) -> Generator[list[Any] | dict[str, Any], None, None]:
|
|
58
|
+
"""
|
|
59
|
+
Loads CSV data from an iterable of lines, optionally using the first line as a header. Returns each row as a
|
|
60
|
+
list or dictionary, depending on ``ret_dict``.
|
|
61
|
+
|
|
62
|
+
:param lines: An iterable of CSV lines.
|
|
63
|
+
:param has_header: Whether the first line is a header.
|
|
64
|
+
:param ret_dict: Whether to return rows as dictionaries (``True``) or lists (``False``).
|
|
65
|
+
:return: A generator yielding each row as a list or dictionary.
|
|
66
|
+
"""
|
|
39
67
|
rows_iter = iter(lines)
|
|
40
68
|
if has_header:
|
|
41
69
|
header_row = next(rows_iter)
|
|
@@ -59,6 +87,13 @@ class CSVView(object):
|
|
|
59
87
|
data: Iterable[Sequence[Any] | Mapping[str, Any]],
|
|
60
88
|
has_header: bool = True,
|
|
61
89
|
) -> Generator[str, None, None]:
|
|
90
|
+
"""
|
|
91
|
+
Dumps data to CSV lines according to the schema, optionally including a header row.
|
|
92
|
+
|
|
93
|
+
:param data: An iterable of rows, each as a sequence or mapping.
|
|
94
|
+
:param has_header: Whether to include a header row.
|
|
95
|
+
:return: A generator yielding CSV lines as strings.
|
|
96
|
+
"""
|
|
62
97
|
if has_header:
|
|
63
98
|
yield self.col_delim.join(c.name for c in self.schema)
|
|
64
99
|
for cols in data:
|
|
@@ -78,6 +113,15 @@ class CSVView(object):
|
|
|
78
113
|
ret_dict: bool = False,
|
|
79
114
|
**kwargs,
|
|
80
115
|
) -> Generator[list[Any] | dict[str, Any], None, None]:
|
|
116
|
+
"""
|
|
117
|
+
Loads CSV data from a file, splitting by row delimiter and using the ``schema`` for parsing.
|
|
118
|
+
|
|
119
|
+
:param file_path: The path to the CSV file.
|
|
120
|
+
:param has_header: Whether the first line is a header.
|
|
121
|
+
:param ret_dict: Whether to return rows as dictionaries (``True``) or lists (``False``).
|
|
122
|
+
:param kwargs: Additional keyword arguments for file opening.
|
|
123
|
+
:return: A generator yielding each row as a list or dictionary.
|
|
124
|
+
"""
|
|
81
125
|
with open(file_path, mode="r", **kwargs) as fh:
|
|
82
126
|
lines = fh.read().split(self.row_delim)
|
|
83
127
|
yield from self.load_lines(lines, has_header, ret_dict)
|
|
@@ -89,6 +133,14 @@ class CSVView(object):
|
|
|
89
133
|
has_header: bool = True,
|
|
90
134
|
**kwargs,
|
|
91
135
|
) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Dumps data to a CSV file according to the ``schema``, optionally including a header row.
|
|
138
|
+
|
|
139
|
+
:param data: An iterable of rows, each as a sequence or mapping.
|
|
140
|
+
:param file_path: The path to the output CSV file.
|
|
141
|
+
:param has_header: Whether to include a header row.
|
|
142
|
+
:param kwargs: Additional keyword arguments for file opening.
|
|
143
|
+
"""
|
|
92
144
|
with open(file_path, mode="w", **kwargs) as fh:
|
|
93
145
|
for line in self.dump_lines(data, has_header):
|
|
94
146
|
fh.write(line)
|
|
@@ -25,7 +25,17 @@ __all__ = [
|
|
|
25
25
|
|
|
26
26
|
class ConnectionMaker(object):
|
|
27
27
|
"""
|
|
28
|
-
Provides utilities
|
|
28
|
+
Provides utilities to simplify establishing database connections and sessions, including connection string
|
|
29
|
+
construction, engine and session creation, and model management.
|
|
30
|
+
|
|
31
|
+
:param driver: The database driver string (e.g., ``mysql+pymysql``).
|
|
32
|
+
:param host: The database host.
|
|
33
|
+
:param port: The database port.
|
|
34
|
+
:param user: The database user.
|
|
35
|
+
:param password: The database password.
|
|
36
|
+
:param database: The database name.
|
|
37
|
+
:param engine_opts: Optional dictionary of SQLAlchemy engine options.
|
|
38
|
+
:param session_opts: Optional dictionary of SQLAlchemy session options.
|
|
29
39
|
"""
|
|
30
40
|
|
|
31
41
|
class Drivers:
|
|
@@ -59,6 +69,15 @@ class ConnectionMaker(object):
|
|
|
59
69
|
engine_opts: dict[str, Any] | None = None,
|
|
60
70
|
session_opts: dict[str, Any] | None = None,
|
|
61
71
|
) -> "ConnectionMaker":
|
|
72
|
+
"""
|
|
73
|
+
Creates a ``ConnectionMaker`` instance from a URL string or ``ParseResult``, extracting connection parameters.
|
|
74
|
+
|
|
75
|
+
:param driver: The database driver string.
|
|
76
|
+
:param url: The database URL as a string or ``ParseResult``.
|
|
77
|
+
:param engine_opts: Optional dictionary of SQLAlchemy engine options.
|
|
78
|
+
:param session_opts: Optional dictionary of SQLAlchemy session options.
|
|
79
|
+
:return: A ``ConnectionMaker`` instance.
|
|
80
|
+
"""
|
|
62
81
|
if isinstance(url, str):
|
|
63
82
|
return ConnectionMaker.from_url(driver, urllib.parse.urlparse(url), engine_opts, session_opts)
|
|
64
83
|
if isinstance(url, urllib.parse.ParseResult):
|
|
@@ -74,6 +93,11 @@ class ConnectionMaker(object):
|
|
|
74
93
|
|
|
75
94
|
@property
|
|
76
95
|
def connection_string(self) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Constructs the SQLAlchemy connection string for the database using the provided parameters.
|
|
98
|
+
|
|
99
|
+
:return: The connection string as a string.
|
|
100
|
+
"""
|
|
77
101
|
port_part = "" if self.port is None else (":%d" % self.port)
|
|
78
102
|
user_part = urllib.parse.quote(self.user, safe="")
|
|
79
103
|
password_part = "" if is_blank(self.password) else (":%s" % urllib.parse.quote(self.password, safe=""))
|
|
@@ -83,15 +107,37 @@ class ConnectionMaker(object):
|
|
|
83
107
|
|
|
84
108
|
@property
|
|
85
109
|
def engine(self) -> sqlalchemy.Engine:
|
|
110
|
+
"""
|
|
111
|
+
Returns a SQLAlchemy ``Engine`` instance for the configured connection string and engine options.
|
|
112
|
+
|
|
113
|
+
:return: The SQLAlchemy ``Engine``.
|
|
114
|
+
"""
|
|
86
115
|
return sqlalchemy.create_engine(self.connection_string, **self.engine_opts)
|
|
87
116
|
|
|
88
117
|
def make_connection(self):
|
|
118
|
+
"""
|
|
119
|
+
Establishes and returns a new database connection using the SQLAlchemy engine.
|
|
120
|
+
|
|
121
|
+
:return: A database connection object.
|
|
122
|
+
"""
|
|
89
123
|
return self.engine.connect()
|
|
90
124
|
|
|
91
125
|
def make_session(self, **kwargs) -> contextlib.AbstractContextManager[sqlalchemy.orm.Session]:
|
|
126
|
+
"""
|
|
127
|
+
Creates a context-managed SQLAlchemy session with the configured engine and session options.
|
|
128
|
+
|
|
129
|
+
:param kwargs: Additional keyword arguments for session creation.
|
|
130
|
+
:return: A context manager yielding a SQLAlchemy ``Session``.
|
|
131
|
+
"""
|
|
92
132
|
return contextlib.closing(sqlalchemy.orm.sessionmaker(self.engine, **{**self.session_opts, **kwargs})())
|
|
93
133
|
|
|
94
134
|
def create_model(self, orm_base):
|
|
135
|
+
"""
|
|
136
|
+
Creates all tables defined in the given ORM base using the current engine.
|
|
137
|
+
|
|
138
|
+
:param orm_base: The SQLAlchemy ORM base class.
|
|
139
|
+
:return: None.
|
|
140
|
+
"""
|
|
95
141
|
if packaging.version.parse(sqlalchemy.__version__) >= packaging.version.parse("2"):
|
|
96
142
|
if not isinstance(orm_base, type) or not issubclass(orm_base, sqlalchemy.orm.DeclarativeBase):
|
|
97
143
|
raise TypeError("not a subclass of 'sqlalchemy.orm.DeclarativeBase'")
|
|
@@ -99,6 +145,12 @@ class ConnectionMaker(object):
|
|
|
99
145
|
orm_base.metadata.create_all(self.engine)
|
|
100
146
|
|
|
101
147
|
def drop_model(self, orm_base):
|
|
148
|
+
"""
|
|
149
|
+
Drops all tables defined in the given ORM base using the current engine.
|
|
150
|
+
|
|
151
|
+
:param orm_base: The SQLAlchemy ORM base class.
|
|
152
|
+
:return: None.
|
|
153
|
+
"""
|
|
102
154
|
if packaging.version.parse(sqlalchemy.__version__) >= packaging.version.parse("2"):
|
|
103
155
|
if not isinstance(orm_base, type) or not issubclass(orm_base, sqlalchemy.orm.DeclarativeBase):
|
|
104
156
|
raise TypeError("not a subclass of 'sqlalchemy.orm.DeclarativeBase'")
|
|
@@ -107,10 +159,11 @@ class ConnectionMaker(object):
|
|
|
107
159
|
|
|
108
160
|
def execute(self, sql: str, **params):
|
|
109
161
|
"""
|
|
110
|
-
Executes the given SQL statement with the
|
|
162
|
+
Executes the given SQL statement with the specified parameters.
|
|
111
163
|
|
|
112
|
-
:param sql: SQL statement to execute
|
|
113
|
-
:param params: parameters
|
|
164
|
+
:param sql: The SQL statement to execute.
|
|
165
|
+
:param params: The parameters dictionary for the SQL statement.
|
|
166
|
+
:return: None.
|
|
114
167
|
"""
|
|
115
168
|
with self.make_connection() as connection:
|
|
116
169
|
connection.execute(sqlalchemy.text(sql), params)
|
|
@@ -118,11 +171,11 @@ class ConnectionMaker(object):
|
|
|
118
171
|
|
|
119
172
|
def query_all(self, sql: str, **params) -> list[tuple]:
|
|
120
173
|
"""
|
|
121
|
-
Executes the given SQL query with the
|
|
174
|
+
Executes the given SQL query with the specified parameters and returns all result tuples.
|
|
122
175
|
|
|
123
|
-
:param sql: SQL query to execute
|
|
124
|
-
:param params: parameters
|
|
125
|
-
:return: result tuples
|
|
176
|
+
:param sql: The SQL query to execute.
|
|
177
|
+
:param params: The parameters dictionary for the SQL query.
|
|
178
|
+
:return: A list of result tuples.
|
|
126
179
|
"""
|
|
127
180
|
with self.make_connection() as connection:
|
|
128
181
|
with contextlib.closing(connection.execute(sqlalchemy.text(sql), params)) as proxy:
|
|
@@ -130,11 +183,12 @@ class ConnectionMaker(object):
|
|
|
130
183
|
|
|
131
184
|
def query_first(self, sql: str, **params) -> tuple | None:
|
|
132
185
|
"""
|
|
133
|
-
Executes the given SQL query with the
|
|
186
|
+
Executes the given SQL query with the specified parameters and returns the first result tuple, or ``None`` if no
|
|
187
|
+
results are found.
|
|
134
188
|
|
|
135
|
-
:param sql: SQL query to execute
|
|
136
|
-
:param params: parameters
|
|
137
|
-
:return:
|
|
189
|
+
:param sql: The SQL query to execute.
|
|
190
|
+
:param params: The parameters dictionary for the SQL query.
|
|
191
|
+
:return: The first result tuple, or ``None`` if no results are found.
|
|
138
192
|
"""
|
|
139
193
|
return head_or_none(self.query_all(sql, **params))
|
|
140
194
|
|
|
@@ -143,6 +197,13 @@ DBAdapter = ConnectionMaker
|
|
|
143
197
|
|
|
144
198
|
|
|
145
199
|
def orm_to_dict(orm, exclude: set[str] = None) -> dict[str, Any]:
|
|
200
|
+
"""
|
|
201
|
+
Converts an ORM object to a dictionary, optionally excluding specified fields.
|
|
202
|
+
|
|
203
|
+
:param orm: The ORM object to convert.
|
|
204
|
+
:param exclude: An optional set of field names to exclude from the result.
|
|
205
|
+
:return: A dictionary mapping field names to their values.
|
|
206
|
+
"""
|
|
146
207
|
if packaging.version.parse(sqlalchemy.__version__) >= packaging.version.parse("2"):
|
|
147
208
|
if not isinstance(orm, sqlalchemy.orm.DeclarativeBase):
|
|
148
209
|
raise TypeError("not an instance of 'sqlalchemy.orm.DeclarativeBase'")
|
|
@@ -152,6 +213,14 @@ def orm_to_dict(orm, exclude: set[str] = None) -> dict[str, Any]:
|
|
|
152
213
|
|
|
153
214
|
|
|
154
215
|
def orm_clone(orm, exclude: set[str] = None, no_autoincrement: bool = False):
|
|
216
|
+
"""
|
|
217
|
+
Creates a clone of the given ORM object, optionally excluding specified fields or auto-increment fields.
|
|
218
|
+
|
|
219
|
+
:param orm: The ORM object to clone.
|
|
220
|
+
:param exclude: An optional set of field names to exclude from the clone.
|
|
221
|
+
:param no_autoincrement: If ``True``, excludes auto-increment fields from the clone.
|
|
222
|
+
:return: A new ORM object with the specified fields cloned.
|
|
223
|
+
"""
|
|
155
224
|
if packaging.version.parse(sqlalchemy.__version__) >= packaging.version.parse("2"):
|
|
156
225
|
if not isinstance(orm, sqlalchemy.orm.DeclarativeBase):
|
|
157
226
|
raise TypeError("not an instance of 'sqlalchemy.orm.DeclarativeBase'")
|
|
@@ -173,6 +242,13 @@ def orm_clone(orm, exclude: set[str] = None, no_autoincrement: bool = False):
|
|
|
173
242
|
|
|
174
243
|
|
|
175
244
|
def mysql_insert_ignore(enabled: bool = True):
|
|
245
|
+
"""
|
|
246
|
+
Registers a SQLAlchemy compiler extension to add ``IGNORE`` to MySQL ``INSERT`` statements if ``enabled``.
|
|
247
|
+
|
|
248
|
+
:param enabled: Whether to enable the ``IGNORE`` prefix for MySQL ``INSERT`` statements.
|
|
249
|
+
:return: None.
|
|
250
|
+
"""
|
|
251
|
+
|
|
176
252
|
@sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, "mysql")
|
|
177
253
|
def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
|
|
178
254
|
if not enabled:
|
|
@@ -182,13 +258,21 @@ def mysql_insert_ignore(enabled: bool = True):
|
|
|
182
258
|
|
|
183
259
|
|
|
184
260
|
def postgresql_insert_on_conflict_do_nothing(enabled: bool = True):
|
|
261
|
+
"""
|
|
262
|
+
Registers a SQLAlchemy compiler extension to add ``ON CONFLICT DO NOTHING`` to PostgreSQL ``INSERT`` statements if
|
|
263
|
+
``enabled``.
|
|
264
|
+
|
|
265
|
+
:param enabled: Whether to enable the ``ON CONFLICT DO NOTHING`` clause for PostgreSQL ``INSERT`` statements.
|
|
266
|
+
:return: None.
|
|
267
|
+
"""
|
|
268
|
+
|
|
185
269
|
@sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, "postgresql")
|
|
186
270
|
def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
|
|
187
271
|
if not enabled:
|
|
188
272
|
return compiler.visit_insert(insert, **kwargs)
|
|
189
273
|
|
|
190
274
|
statement = compiler.visit_insert(insert, **kwargs)
|
|
191
|
-
# If we have a
|
|
275
|
+
# If we have a ``RETURNING`` clause, we must insert before it
|
|
192
276
|
returning_position = statement.find("RETURNING")
|
|
193
277
|
if returning_position >= 0:
|
|
194
278
|
return statement[:returning_position] + " ON CONFLICT DO NOTHING " + statement[returning_position:]
|