iker-python-common 1.0.35__tar.gz → 1.0.57__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.
Files changed (97) hide show
  1. iker_python_common-1.0.57/.github/workflows/pr.yml +32 -0
  2. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/.github/workflows/push.yml +8 -7
  3. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/PKG-INFO +6 -2
  4. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/pyproject.toml +29 -3
  5. iker_python_common-1.0.57/src/iker/common/utils/argutils.py +233 -0
  6. iker_python_common-1.0.57/src/iker/common/utils/csv.py +224 -0
  7. iker_python_common-1.0.57/src/iker/common/utils/dbutils.py +307 -0
  8. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/dockerutils.py +2 -2
  9. iker_python_common-1.0.57/src/iker/common/utils/dtutils.py +403 -0
  10. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/funcutils.py +88 -1
  11. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/jsonutils.py +125 -9
  12. iker_python_common-1.0.57/src/iker/common/utils/logger.py +117 -0
  13. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/numutils.py +48 -0
  14. iker_python_common-1.0.57/src/iker/common/utils/randutils.py +348 -0
  15. iker_python_common-1.0.57/src/iker/common/utils/retry.py +245 -0
  16. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/s3utils.py +65 -65
  17. iker_python_common-1.0.57/src/iker/common/utils/sequtils.py +754 -0
  18. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/shutils.py +49 -52
  19. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/span.py +15 -15
  20. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/testutils.py +3 -3
  21. iker_python_common-1.0.57/src/iker/common/utils/typeutils.py +212 -0
  22. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker_python_common.egg-info/PKG-INFO +6 -2
  23. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker_python_common.egg-info/requires.txt +6 -0
  24. iker_python_common-1.0.57/test/iker_tests/common/utils/argutils_test.py +565 -0
  25. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/csv_test.py +0 -1
  26. iker_python_common-1.0.57/test/iker_tests/common/utils/dbutils_test.py +1021 -0
  27. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/dockerutils_test.py +32 -17
  28. iker_python_common-1.0.57/test/iker_tests/common/utils/dtutils_test.py +384 -0
  29. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/funcutils_test.py +263 -2
  30. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/jsonutils_test.py +32 -20
  31. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/numutils_test.py +68 -35
  32. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/randutils_test.py +28 -4
  33. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/retry_test.py +36 -19
  34. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/s3utils_test.py +12 -6
  35. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/sequtils_test.py +807 -137
  36. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/shutils_test.py +36 -19
  37. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/span_test.py +17 -9
  38. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/strutils_test.py +48 -25
  39. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/testutils_test.py +6 -6
  40. iker_python_common-1.0.57/test/iker_tests/common/utils/typeutils_test.py +432 -0
  41. iker_python_common-1.0.35/.github/workflows/pr.yml +0 -66
  42. iker_python_common-1.0.35/src/iker/common/utils/argutils.py +0 -167
  43. iker_python_common-1.0.35/src/iker/common/utils/csv.py +0 -99
  44. iker_python_common-1.0.35/src/iker/common/utils/dbutils.py +0 -196
  45. iker_python_common-1.0.35/src/iker/common/utils/dtutils.py +0 -187
  46. iker_python_common-1.0.35/src/iker/common/utils/logger.py +0 -64
  47. iker_python_common-1.0.35/src/iker/common/utils/randutils.py +0 -170
  48. iker_python_common-1.0.35/src/iker/common/utils/retry.py +0 -182
  49. iker_python_common-1.0.35/src/iker/common/utils/sequtils.py +0 -386
  50. iker_python_common-1.0.35/src/iker/common/utils/typeutils.py +0 -71
  51. iker_python_common-1.0.35/test/iker_tests/common/utils/argutils_test.py +0 -257
  52. iker_python_common-1.0.35/test/iker_tests/common/utils/dbutils_test.py +0 -339
  53. iker_python_common-1.0.35/test/iker_tests/common/utils/dtutils_test.py +0 -163
  54. iker_python_common-1.0.35/test/iker_tests/common/utils/typeutils_test.py +0 -206
  55. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/.editorconfig +0 -0
  56. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/.gitignore +0 -0
  57. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/MANIFEST.in +0 -0
  58. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/README.md +0 -0
  59. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/VERSION +0 -0
  60. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/config/config.cfg +0 -0
  61. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/csv/data.csv +0 -0
  62. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/csv/data.tsv +0 -0
  63. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  64. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  65. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  66. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  67. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  68. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  69. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  70. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  71. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  72. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  73. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  74. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  75. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  76. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  77. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  78. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  79. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  80. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  81. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  82. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  83. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/setup.cfg +0 -0
  84. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/setup.py +0 -0
  85. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/__init__.py +0 -0
  86. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/__init__.py +0 -0
  87. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/config.py +0 -0
  88. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker/common/utils/strutils.py +0 -0
  89. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker_python_common.egg-info/SOURCES.txt +0 -0
  90. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  91. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  92. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/src/iker_python_common.egg-info/top_level.txt +0 -0
  93. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_test.py +0 -0
  94. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/__init__.py +0 -0
  95. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/config_test.py +0 -0
  96. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/common/utils/logger_test.py +0 -0
  97. {iker_python_common-1.0.35 → iker_python_common-1.0.57}/test/iker_tests/docker_fixtures.py +0 -0
@@ -0,0 +1,32 @@
1
+ name: Pull Request
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ "master" ]
6
+
7
+ env:
8
+ BUILD_NUMBER: ${{ github.run_number }}
9
+
10
+ jobs:
11
+ build-python:
12
+ runs-on: ubuntu-latest
13
+ container:
14
+ image: ruyangshou/iker-basedev-dev:latest
15
+ options: --user github
16
+ strategy:
17
+ matrix:
18
+ python-version: [ "3.11", "3.12", "3.13", "3.14" ]
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Setup Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "${{ matrix.python-version }}"
27
+
28
+ - name: Build and Test
29
+ run: |
30
+ python -m pip install --upgrade pip
31
+ python -m pip install .[test,all]
32
+ python -m pytest . --cov --cov-report xml --cov-config pyproject.toml
@@ -10,21 +10,22 @@ env:
10
10
  jobs:
11
11
  push:
12
12
  runs-on: ubuntu-latest
13
+ container:
14
+ image: ruyangshou/iker-basedev-dev:latest
15
+ options: --user github
13
16
  steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v4
19
+
14
20
  - name: Setup Python
15
21
  uses: actions/setup-python@v5
16
22
  with:
17
- python-version: "3.13"
18
-
19
- - name: Checkout
20
- uses: actions/checkout@v4
23
+ python-version: "3.14"
21
24
 
22
25
  - name: Build and Upload
23
26
  run: |
24
- sudo apt-get update
25
- sudo apt-get install libxml2-dev libxslt1-dev llvm-14-dev
26
27
  python -m pip install --upgrade pip build twine
27
- python -m pip install .[test]
28
+ python -m pip install .[test,all]
28
29
  python -m pytest . --cov --cov-report xml --cov-config pyproject.toml
29
30
  python -m build -sw .
30
31
  python -m twine upload --username __token__ --password ${{ secrets.PYPI_TOKEN }} dist/*
@@ -1,17 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.35
3
+ Version: 1.0.57
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
7
7
  Classifier: Programming Language :: Python :: 3.13
8
- Requires-Python: <3.14,>=3.11
8
+ Classifier: Programming Language :: Python :: 3.14
9
+ Requires-Python: <3.15,>=3.11
9
10
  Requires-Dist: boto3>=1.35
10
11
  Requires-Dist: docker>=7.1
11
12
  Requires-Dist: numpy>=1.26
12
13
  Requires-Dist: psycopg>=3.2
13
14
  Requires-Dist: pymysql>=1.1
14
15
  Requires-Dist: sqlalchemy>=1.4
16
+ Requires-Dist: typing-extensions; python_version < "3.12"
17
+ Provides-Extra: all
18
+ Requires-Dist: iker-python-common; extra == "all"
15
19
  Provides-Extra: test
16
20
  Requires-Dist: ddt>=1.7; extra == "test"
17
21
  Requires-Dist: moto[all,ec2,s3]>=5.0; extra == "test"
@@ -1,20 +1,42 @@
1
1
  [build-system]
2
2
  requires = [
3
- "setuptools>=75.0",
4
- "setuptools-scm>=8.0",
3
+ "setuptools>=80.0",
4
+ "setuptools-scm>=9.0",
5
5
  "iker-python-setup>=1.0",
6
6
  ]
7
7
  build-backend = "setuptools.build_meta"
8
8
 
9
+ [dependency-groups]
10
+ dev = [
11
+ "boto3>=1.35",
12
+ "docker>=7.1",
13
+ "numpy>=1.26",
14
+ "psycopg>=3.2",
15
+ "pymysql>=1.1",
16
+ "sqlalchemy>=1.4",
17
+ "typing-extensions; python_version<'3.12'"
18
+ ]
19
+ test = [
20
+ "ddt>=1.7",
21
+ "moto[ec2,s3,all]>=5.0",
22
+ "pytest-cov>=5.0",
23
+ "pytest-mysql>=3.0",
24
+ "pytest-order>=1.3",
25
+ "pytest-postgresql>=6.1",
26
+ "pytest>=8.3",
27
+ "sqlalchemy>=2.0",
28
+ ]
29
+
9
30
  [project]
10
31
  name = "iker-python-common"
11
32
  dynamic = ["version"]
12
- requires-python = ">=3.11,<3.14"
33
+ requires-python = ">=3.11,<3.15"
13
34
  classifiers = [
14
35
  "Programming Language :: Python :: 3",
15
36
  "Programming Language :: Python :: 3.11",
16
37
  "Programming Language :: Python :: 3.12",
17
38
  "Programming Language :: Python :: 3.13",
39
+ "Programming Language :: Python :: 3.14",
18
40
  ]
19
41
  dependencies = [
20
42
  "boto3>=1.35",
@@ -23,9 +45,13 @@ dependencies = [
23
45
  "psycopg>=3.2",
24
46
  "pymysql>=1.1",
25
47
  "sqlalchemy>=1.4",
48
+ "typing-extensions; python_version<'3.12'"
26
49
  ]
27
50
 
28
51
  [project.optional-dependencies]
52
+ all = [
53
+ "iker-python-common",
54
+ ]
29
55
  test = [
30
56
  "ddt>=1.7",
31
57
  "moto[ec2,s3,all]>=5.0",
@@ -0,0 +1,233 @@
1
+ import argparse
2
+ import dataclasses
3
+ import inspect
4
+ import typing
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ from iker.common.utils.typeutils import is_identical_type
9
+
10
+ __all__ = [
11
+ "ParserTreeNode",
12
+ "ParserTree",
13
+ "ArgParseSpec",
14
+ "argparse_spec",
15
+ "make_argparse"
16
+ ]
17
+
18
+ import sys
19
+
20
+
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
+
30
+ def __init__(self, command: str, parser: argparse.ArgumentParser):
31
+ self.command = command
32
+ self.parser = parser
33
+ self.subparsers = None
34
+ self.child_nodes: list[ParserTreeNode] = []
35
+
36
+
37
+ def construct_parser_tree(
38
+ root_node: ParserTreeNode,
39
+ command_chain: list[str],
40
+ command_key_prefix: str,
41
+ **kwargs,
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
+ """
53
+ node_path = [root_node]
54
+ if len(command_chain) == 0:
55
+ return node_path
56
+
57
+ node = root_node
58
+ for depth, command in enumerate(command_chain):
59
+ if node.subparsers is None:
60
+ node.subparsers = node.parser.add_subparsers(dest=f"{command_key_prefix}:{depth}")
61
+ for child_node in node.child_nodes:
62
+ if child_node.command == command:
63
+ node = child_node
64
+ break
65
+ else:
66
+ if depth == len(command_chain) - 1:
67
+ child_parser = node.subparsers.add_parser(command, **kwargs)
68
+ else:
69
+ child_parser = node.subparsers.add_parser(command)
70
+ child_node = ParserTreeNode(command, child_parser)
71
+ node.child_nodes.append(child_node)
72
+ node = child_node
73
+ node_path.append(node)
74
+
75
+ return node_path
76
+
77
+
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
+
87
+ def __init__(self, root_parser: argparse.ArgumentParser, command_key_prefix: str = "command"):
88
+ self.root_node = ParserTreeNode("", root_parser)
89
+ self.command_key_prefix = command_key_prefix
90
+
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
+ """
99
+ *_, last_node = construct_parser_tree(self.root_node, command_chain, self.command_key_prefix, **kwargs)
100
+ return last_node.parser
101
+
102
+ def parse_args(self, args: list[str] | None = None) -> tuple[list[str], argparse.Namespace]:
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
110
+ # if unknown arguments encountered. We have to employ this workaround
111
+ if sys.version_info < (3, 12):
112
+ if self.root_node.parser.exit_on_error:
113
+ known_args_namespace = self.root_node.parser.parse_args(args)
114
+ else:
115
+ known_args_namespace, unknown_args = self.root_node.parser.parse_known_args(args)
116
+ if len(unknown_args or []) > 0:
117
+ raise argparse.ArgumentError(None, f"unrecognized arguments '{unknown_args}'")
118
+ else:
119
+ known_args_namespace = self.root_node.parser.parse_args(args)
120
+
121
+ command_pairs = []
122
+ namespace = argparse.Namespace()
123
+ for key, value in dict(vars(known_args_namespace)).items():
124
+ if key.startswith(self.command_key_prefix) and value is not None:
125
+ command_pairs.append((key, value))
126
+ else:
127
+ setattr(namespace, key, value)
128
+
129
+ return list(command for _, command in sorted(command_pairs)), namespace
130
+
131
+
132
+ @dataclasses.dataclass(frozen=True)
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
+ """
147
+ flag: str | None = None
148
+ name: str | None = None
149
+ action: str | None = None
150
+ default: Any = None
151
+ type: typing.Type | None = None
152
+ choices: list[Any] | None = None
153
+ required: bool | None = None
154
+ help: str | None = None
155
+
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
+ """
163
+ kwargs = dict(
164
+ action=self.action,
165
+ default=self.default,
166
+ type=self.type,
167
+ choices=self.choices,
168
+ required=self.required,
169
+ help=self.help,
170
+ )
171
+
172
+ return {key: value for key, value in kwargs.items() if value is not None}
173
+
174
+
175
+ argparse_spec = ArgParseSpec
176
+
177
+
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
+ """
187
+ if parser is None:
188
+ parser = argparse.ArgumentParser()
189
+
190
+ def is_type_of(a: Any, *bs) -> bool:
191
+ return any(is_identical_type(a, b, strict_optional=False, covariant=True) for b in bs)
192
+
193
+ sig = inspect.signature(func)
194
+ for name, param in sig.parameters.items():
195
+
196
+ arg_name = f"--{name.replace('_', '-')}"
197
+
198
+ if param.annotation is None:
199
+ arg_type = str
200
+ elif is_type_of(param.annotation, str, Sequence[str]):
201
+ arg_type = str
202
+ elif is_type_of(param.annotation, int, Sequence[int]):
203
+ arg_type = int
204
+ elif is_type_of(param.annotation, float, Sequence[float]):
205
+ arg_type = float
206
+ elif is_type_of(param.annotation, bool, Sequence[bool]):
207
+ arg_type = bool
208
+ else:
209
+ arg_type = str
210
+
211
+ arg_action = "append" if typing.get_origin(param.annotation) in {list, Sequence} else None
212
+ arg_default = None if param.default is inspect.Parameter.empty else param.default
213
+
214
+ if isinstance(arg_default, ArgParseSpec):
215
+ spec = arg_default
216
+ spec = dataclasses.replace(spec,
217
+ name=spec.name or arg_name,
218
+ type=spec.type if spec.type is not None else arg_type,
219
+ action=spec.action if spec.action is not None else arg_action)
220
+
221
+ if spec.flag is None:
222
+ parser.add_argument(spec.name, **spec.make_kwargs())
223
+ else:
224
+ parser.add_argument(spec.flag, spec.name, **spec.make_kwargs())
225
+
226
+ else:
227
+ parser.add_argument(arg_name,
228
+ type=arg_type,
229
+ action=arg_action,
230
+ required=arg_default is None,
231
+ default=arg_default)
232
+
233
+ return parser
@@ -0,0 +1,224 @@
1
+ import os
2
+ from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
3
+ from typing import Any
4
+ from typing import overload
5
+
6
+ __all__ = [
7
+ "CSVColumn",
8
+ "CSVView",
9
+ "column",
10
+ "view",
11
+ ]
12
+
13
+
14
+ class CSVColumn(object):
15
+ """
16
+ Represents a column in a CSV schema, including its name, loader function, dumper function, and the string used for
17
+ null values.
18
+
19
+ :param name: The name of the column.
20
+ :param loader: A function to convert a string to the column's value type.
21
+ :param dumper: A function to convert the column's value to a string.
22
+ :param null_str: The string representation of a null value in this column.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ name: str,
28
+ loader: Callable[[str], Any] = str,
29
+ dumper: Callable[[Any], str] = str,
30
+ null_str: str = "",
31
+ ):
32
+ self.name = name
33
+ self.loader = loader
34
+ self.dumper = dumper
35
+ self.null_str = null_str
36
+
37
+
38
+ class CSVView(object):
39
+ """
40
+ Represents a view for reading and writing CSV data according to a schema. Supports loading and dumping lines or
41
+ files, with options for headers and dictionary or list output.
42
+
43
+ :param schema: The sequence of ``CSVColumn`` objects defining the schema.
44
+ :param row_delim: The delimiter for rows (default is ``'\n'``).
45
+ :param col_delim: The delimiter for columns (default is ``','``).
46
+ """
47
+
48
+ def __init__(self, schema: Sequence[CSVColumn], *, row_delim: str = "\n", col_delim: str = ","):
49
+ self.schema = schema
50
+ self.row_delim = row_delim
51
+ self.col_delim = col_delim
52
+
53
+ @overload
54
+ def load_lines(
55
+ self,
56
+ lines: Iterable[str],
57
+ has_header: bool,
58
+ ret_dict: False = False,
59
+ ) -> Generator[list[Any], None, None]:
60
+ ...
61
+
62
+ @overload
63
+ def load_lines(
64
+ self,
65
+ lines: Iterable[str],
66
+ ret_dict: False = False,
67
+ ) -> Generator[list[Any], None, None]:
68
+ ...
69
+
70
+ @overload
71
+ def load_lines(
72
+ self,
73
+ lines: Iterable[str],
74
+ has_header: bool,
75
+ ret_dict: True = True,
76
+ ) -> Generator[dict[str, Any], None, None]:
77
+ ...
78
+
79
+ @overload
80
+ def load_lines(
81
+ self,
82
+ lines: Iterable[str],
83
+ ret_dict: True = True,
84
+ ) -> Generator[dict[str, Any], None, None]:
85
+ ...
86
+
87
+ def load_lines(
88
+ self,
89
+ lines: Iterable[str],
90
+ has_header: bool = True,
91
+ ret_dict: bool = False,
92
+ ) -> Generator[list[Any] | dict[str, Any], None, None]:
93
+ """
94
+ Loads CSV data from an iterable of lines, optionally using the first line as a header. Returns each row as a
95
+ list or dictionary, depending on ``ret_dict``.
96
+
97
+ :param lines: An iterable of CSV lines.
98
+ :param has_header: Whether the first line is a header.
99
+ :param ret_dict: Whether to return rows as dictionaries (``True``) or lists (``False``).
100
+ :return: A generator yielding each row as a list or dictionary.
101
+ """
102
+ rows_iter = iter(lines)
103
+ if has_header:
104
+ header_row = next(rows_iter)
105
+ header_cols = header_row.split(self.col_delim)
106
+ if len(self.schema) != len(header_cols):
107
+ raise ValueError("size of the schema is not identical to size of the columns")
108
+ for c, header_col in zip(self.schema, header_cols):
109
+ if c.name != header_col:
110
+ raise ValueError("name of the schema is not equal to the name of the columns")
111
+ for row in rows_iter:
112
+ cols = row.split(self.col_delim)
113
+ if len(self.schema) != len(cols):
114
+ continue
115
+ if ret_dict:
116
+ yield {c.name: None if col == c.null_str else c.loader(col) for c, col in zip(self.schema, cols)}
117
+ else:
118
+ yield [None if col == c.null_str else c.loader(col) for c, col in zip(self.schema, cols)]
119
+
120
+ def dump_lines(
121
+ self,
122
+ data: Iterable[Sequence[Any] | Mapping[str, Any]],
123
+ has_header: bool = True,
124
+ ) -> Generator[str, None, None]:
125
+ """
126
+ Dumps data to CSV lines according to the schema, optionally including a header row.
127
+
128
+ :param data: An iterable of rows, each as a sequence or mapping.
129
+ :param has_header: Whether to include a header row.
130
+ :return: A generator yielding CSV lines as strings.
131
+ """
132
+ if has_header:
133
+ yield self.col_delim.join(c.name for c in self.schema)
134
+ for cols in data:
135
+ if isinstance(cols, Sequence):
136
+ if len(self.schema) != len(cols):
137
+ raise ValueError("size of the schema is not identical to size of the columns")
138
+ yield self.col_delim.join(c.null_str if col is None else c.dumper(col)
139
+ for c, col in zip(self.schema, cols))
140
+ if isinstance(cols, Mapping):
141
+ yield self.col_delim.join(c.null_str if cols.get(c.name) is None else c.dumper(cols.get(c.name))
142
+ for c in self.schema)
143
+
144
+ @overload
145
+ def load_file(
146
+ self,
147
+ file_path: os.PathLike | str,
148
+ has_header: bool,
149
+ ret_dict: False = False,
150
+ **kwargs,
151
+ ) -> Generator[list[Any], None, None]:
152
+ ...
153
+
154
+ @overload
155
+ def load_file(
156
+ self,
157
+ file_path: os.PathLike | str,
158
+ ret_dict: False = False,
159
+ **kwargs,
160
+ ) -> Generator[list[Any], None, None]:
161
+ ...
162
+
163
+ @overload
164
+ def load_file(
165
+ self,
166
+ file_path: os.PathLike | str,
167
+ has_header: bool,
168
+ ret_dict: True = True,
169
+ **kwargs,
170
+ ) -> Generator[dict[str, Any], None, None]:
171
+ ...
172
+
173
+ @overload
174
+ def load_file(
175
+ self,
176
+ file_path: os.PathLike | str,
177
+ ret_dict: True = True,
178
+ **kwargs,
179
+ ) -> Generator[dict[str, Any], None, None]:
180
+ ...
181
+
182
+ def load_file(
183
+ self,
184
+ file_path: os.PathLike | str,
185
+ has_header: bool = True,
186
+ ret_dict: bool = False,
187
+ **kwargs,
188
+ ) -> Generator[list[Any] | dict[str, Any], None, None]:
189
+ """
190
+ Loads CSV data from a file, splitting by row delimiter and using the ``schema`` for parsing.
191
+
192
+ :param file_path: The path to the CSV file.
193
+ :param has_header: Whether the first line is a header.
194
+ :param ret_dict: Whether to return rows as dictionaries (``True``) or lists (``False``).
195
+ :param kwargs: Additional keyword arguments for file opening.
196
+ :return: A generator yielding each row as a list or dictionary.
197
+ """
198
+ with open(file_path, mode="r", **kwargs) as fh:
199
+ lines = fh.read().split(self.row_delim)
200
+ yield from self.load_lines(lines, has_header, ret_dict)
201
+
202
+ def dump_file(
203
+ self,
204
+ data: Iterable[Sequence[Any] | Mapping[str, Any]],
205
+ file_path: os.PathLike | str,
206
+ has_header: bool = True,
207
+ **kwargs,
208
+ ) -> None:
209
+ """
210
+ Dumps data to a CSV file according to the ``schema``, optionally including a header row.
211
+
212
+ :param data: An iterable of rows, each as a sequence or mapping.
213
+ :param file_path: The path to the output CSV file.
214
+ :param has_header: Whether to include a header row.
215
+ :param kwargs: Additional keyword arguments for file opening.
216
+ """
217
+ with open(file_path, mode="w", **kwargs) as fh:
218
+ for line in self.dump_lines(data, has_header):
219
+ fh.write(line)
220
+ fh.write(self.row_delim)
221
+
222
+
223
+ column = CSVColumn
224
+ view = CSVView