plexus-python-common 1.0.74__tar.gz → 1.1.82__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 (135) hide show
  1. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/.github/workflows/pr.yml +1 -1
  2. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/.github/workflows/push.yml +1 -1
  3. plexus_python_common-1.1.82/PKG-INFO +23 -0
  4. plexus_python_common-1.1.82/README.md +29 -0
  5. plexus_python_common-1.1.82/VERSION +1 -0
  6. plexus_python_common-1.1.82/pyproject.toml +75 -0
  7. plexus_python_common-1.1.82/resources/unittest/config/config.cfg +10 -0
  8. plexus_python_common-1.1.82/resources/unittest/csvutils/data.csv +7 -0
  9. plexus_python_common-1.1.82/resources/unittest/csvutils/data.tsv +7 -0
  10. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/setup.py +1 -1
  11. plexus_python_common-1.1.82/src/plexus/common/utils/argutils.py +221 -0
  12. plexus_python_common-1.1.82/src/plexus/common/utils/config.py +124 -0
  13. plexus_python_common-1.1.82/src/plexus/common/utils/csvutils.py +241 -0
  14. plexus_python_common-1.1.82/src/plexus/common/utils/dbutils.py +431 -0
  15. plexus_python_common-1.1.82/src/plexus/common/utils/dtutils.py +395 -0
  16. plexus_python_common-1.1.82/src/plexus/common/utils/funcutils.py +306 -0
  17. plexus_python_common-1.1.82/src/plexus/common/utils/iterutils.py +912 -0
  18. plexus_python_common-1.1.82/src/plexus/common/utils/jsonutils.py +482 -0
  19. plexus_python_common-1.1.82/src/plexus/common/utils/logger.py +117 -0
  20. plexus_python_common-1.1.82/src/plexus/common/utils/numutils.py +142 -0
  21. plexus_python_common-1.1.82/src/plexus/common/utils/pathutils.py +268 -0
  22. plexus_python_common-1.1.82/src/plexus/common/utils/randutils.py +350 -0
  23. plexus_python_common-1.1.82/src/plexus/common/utils/retry.py +252 -0
  24. plexus_python_common-1.1.82/src/plexus/common/utils/span.py +256 -0
  25. plexus_python_common-1.1.82/src/plexus/common/utils/strutils.py +198 -0
  26. plexus_python_common-1.1.82/src/plexus/common/utils/testutils.py +164 -0
  27. plexus_python_common-1.1.82/src/plexus/common/utils/typeutils.py +212 -0
  28. plexus_python_common-1.1.82/src/plexus_python_common.egg-info/PKG-INFO +23 -0
  29. plexus_python_common-1.1.82/src/plexus_python_common.egg-info/SOURCES.txt +68 -0
  30. plexus_python_common-1.1.82/src/plexus_python_common.egg-info/requires.txt +17 -0
  31. plexus_python_common-1.1.82/test/plexus_tests/common/utils/argutils_test.py +565 -0
  32. plexus_python_common-1.1.82/test/plexus_tests/common/utils/config_test.py +439 -0
  33. plexus_python_common-1.1.82/test/plexus_tests/common/utils/csvutils_test.py +315 -0
  34. plexus_python_common-1.1.82/test/plexus_tests/common/utils/dbutils_test.py +1560 -0
  35. plexus_python_common-1.1.82/test/plexus_tests/common/utils/dtutils_test.py +384 -0
  36. plexus_python_common-1.1.82/test/plexus_tests/common/utils/funcutils_test.py +748 -0
  37. plexus_python_common-1.1.82/test/plexus_tests/common/utils/iterutils_test.py +2798 -0
  38. plexus_python_common-1.1.82/test/plexus_tests/common/utils/jsonutils_test.py +1474 -0
  39. plexus_python_common-1.1.82/test/plexus_tests/common/utils/logger_test.py +21 -0
  40. plexus_python_common-1.1.82/test/plexus_tests/common/utils/numutils_test.py +427 -0
  41. plexus_python_common-1.1.82/test/plexus_tests/common/utils/pathutils_test.py +607 -0
  42. plexus_python_common-1.1.82/test/plexus_tests/common/utils/randutils_test.py +192 -0
  43. plexus_python_common-1.1.82/test/plexus_tests/common/utils/retry_test.py +220 -0
  44. plexus_python_common-1.1.82/test/plexus_tests/common/utils/span_test.py +318 -0
  45. plexus_python_common-1.1.82/test/plexus_tests/common/utils/strutils_test.py +489 -0
  46. plexus_python_common-1.1.82/test/plexus_tests/common/utils/testutils_test.py +406 -0
  47. plexus_python_common-1.1.82/test/plexus_tests/common/utils/typeutils_test.py +436 -0
  48. plexus_python_common-1.0.74/PKG-INFO +0 -42
  49. plexus_python_common-1.0.74/README.md +0 -5
  50. plexus_python_common-1.0.74/VERSION +0 -1
  51. plexus_python_common-1.0.74/pyproject.toml +0 -115
  52. plexus_python_common-1.0.74/resources/unittest/jsonutils/dummy.0.jsonl +0 -10
  53. plexus_python_common-1.0.74/resources/unittest/jsonutils/dummy.1.jsonl +0 -10
  54. plexus_python_common-1.0.74/resources/unittest/jsonutils/dummy.2.jsonl +0 -10
  55. plexus_python_common-1.0.74/resources/unittest/pathutils/0-dummy +0 -0
  56. plexus_python_common-1.0.74/resources/unittest/pathutils/1-dummy +0 -0
  57. plexus_python_common-1.0.74/resources/unittest/pathutils/2-dummy +0 -0
  58. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
  59. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
  60. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.0.jsonl +0 -10
  61. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
  62. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
  63. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.1.jsonl +0 -10
  64. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
  65. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
  66. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.2.jsonl +0 -10
  67. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.csv.part0 +0 -0
  68. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.csv.part1 +0 -0
  69. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.csv.part2 +0 -0
  70. plexus_python_common-1.0.74/resources/unittest/pathutils/dummy.txt +0 -0
  71. plexus_python_common-1.0.74/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
  72. plexus_python_common-1.0.74/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
  73. plexus_python_common-1.0.74/src/plexus/common/carto/OSMFile.py +0 -259
  74. plexus_python_common-1.0.74/src/plexus/common/carto/OSMNode.py +0 -25
  75. plexus_python_common-1.0.74/src/plexus/common/carto/OSMTags.py +0 -101
  76. plexus_python_common-1.0.74/src/plexus/common/carto/OSMWay.py +0 -24
  77. plexus_python_common-1.0.74/src/plexus/common/carto/__init__.py +0 -11
  78. plexus_python_common-1.0.74/src/plexus/common/resources/tags/__init__.py +0 -35
  79. plexus_python_common-1.0.74/src/plexus/common/resources/tags/unittest-1.0.0.tagset.yaml +0 -98
  80. plexus_python_common-1.0.74/src/plexus/common/resources/tags/universal-1.0.0.tagset.yaml +0 -1390
  81. plexus_python_common-1.0.74/src/plexus/common/utils/apiutils.py +0 -31
  82. plexus_python_common-1.0.74/src/plexus/common/utils/bagutils.py +0 -331
  83. plexus_python_common-1.0.74/src/plexus/common/utils/config.py +0 -63
  84. plexus_python_common-1.0.74/src/plexus/common/utils/datautils.py +0 -233
  85. plexus_python_common-1.0.74/src/plexus/common/utils/dockerutils.py +0 -181
  86. plexus_python_common-1.0.74/src/plexus/common/utils/gisutils.py +0 -406
  87. plexus_python_common-1.0.74/src/plexus/common/utils/jsonutils.py +0 -96
  88. plexus_python_common-1.0.74/src/plexus/common/utils/ormutils.py +0 -1638
  89. plexus_python_common-1.0.74/src/plexus/common/utils/pathutils.py +0 -234
  90. plexus_python_common-1.0.74/src/plexus/common/utils/s3utils.py +0 -939
  91. plexus_python_common-1.0.74/src/plexus/common/utils/sqlutils.py +0 -9
  92. plexus_python_common-1.0.74/src/plexus/common/utils/strutils.py +0 -401
  93. plexus_python_common-1.0.74/src/plexus/common/utils/tagutils.py +0 -1476
  94. plexus_python_common-1.0.74/src/plexus/common/utils/testutils.py +0 -172
  95. plexus_python_common-1.0.74/src/plexus_python_common.egg-info/PKG-INFO +0 -42
  96. plexus_python_common-1.0.74/src/plexus_python_common.egg-info/SOURCES.txt +0 -89
  97. plexus_python_common-1.0.74/src/plexus_python_common.egg-info/requires.txt +0 -37
  98. plexus_python_common-1.0.74/test/plexus_tests/common/carto/__init__.py +0 -0
  99. plexus_python_common-1.0.74/test/plexus_tests/common/carto/osm_file_test.py +0 -96
  100. plexus_python_common-1.0.74/test/plexus_tests/common/carto/osm_tags_test.py +0 -196
  101. plexus_python_common-1.0.74/test/plexus_tests/common/utils/__init__.py +0 -0
  102. plexus_python_common-1.0.74/test/plexus_tests/common/utils/bagutils_test.py +0 -82
  103. plexus_python_common-1.0.74/test/plexus_tests/common/utils/datautils_test.py +0 -38
  104. plexus_python_common-1.0.74/test/plexus_tests/common/utils/dockerutils_test.py +0 -611
  105. plexus_python_common-1.0.74/test/plexus_tests/common/utils/gisutils_test.py +0 -246
  106. plexus_python_common-1.0.74/test/plexus_tests/common/utils/jsonutils_test.py +0 -37
  107. plexus_python_common-1.0.74/test/plexus_tests/common/utils/ormutils_test.py +0 -1219
  108. plexus_python_common-1.0.74/test/plexus_tests/common/utils/pathutils_test.py +0 -262
  109. plexus_python_common-1.0.74/test/plexus_tests/common/utils/s3utils_test.py +0 -605
  110. plexus_python_common-1.0.74/test/plexus_tests/common/utils/strutils_test.py +0 -978
  111. plexus_python_common-1.0.74/test/plexus_tests/common/utils/tagutils_test.py +0 -706
  112. plexus_python_common-1.0.74/test/plexus_tests/common/utils/testutils_test.py +0 -50
  113. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/.editorconfig +0 -0
  114. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/.gitignore +0 -0
  115. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/MANIFEST.in +0 -0
  116. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.baz/file.bar.baz +0 -0
  117. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.baz/file.foo.bar +0 -0
  118. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.baz/file.foo.baz +0 -0
  119. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  120. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  121. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  122. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  123. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.foo/file.bar +0 -0
  124. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.foo/file.baz +0 -0
  125. {plexus_python_common-1.0.74/resources/unittest/s3utils → plexus_python_common-1.1.82/resources/unittest/pathutils}/dir.foo/file.foo +0 -0
  126. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/setup.cfg +0 -0
  127. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/src/plexus/common/__init__.py +0 -0
  128. {plexus_python_common-1.0.74/src/plexus/common/resources → plexus_python_common-1.1.82/src/plexus/common/utils}/__init__.py +0 -0
  129. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/src/plexus_python_common.egg-info/dependency_links.txt +0 -0
  130. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/src/plexus_python_common.egg-info/not-zip-safe +0 -0
  131. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/src/plexus_python_common.egg-info/top_level.txt +0 -0
  132. {plexus_python_common-1.0.74/src/plexus/common/utils → plexus_python_common-1.1.82/test/plexus_tests}/__init__.py +0 -0
  133. {plexus_python_common-1.0.74/test/plexus_tests → plexus_python_common-1.1.82/test/plexus_tests/common}/__init__.py +0 -0
  134. {plexus_python_common-1.0.74/test/plexus_tests/common → plexus_python_common-1.1.82/test/plexus_tests/common/utils}/__init__.py +0 -0
  135. {plexus_python_common-1.0.74 → plexus_python_common-1.1.82}/test/testenv.py +0 -0
@@ -11,7 +11,7 @@ jobs:
11
11
  build-python:
12
12
  runs-on: ubuntu-latest
13
13
  container:
14
- image: ruyangshou/plexus-basedev-dev-python:latest
14
+ image: ruyangshou/plexus-basedev-ci:latest
15
15
  options: --user github
16
16
  strategy:
17
17
  matrix:
@@ -11,7 +11,7 @@ jobs:
11
11
  push:
12
12
  runs-on: ubuntu-latest
13
13
  container:
14
- image: ruyangshou/plexus-basedev-dev-python:latest
14
+ image: ruyangshou/plexus-basedev-ci:latest
15
15
  options: --user github
16
16
  steps:
17
17
  - name: Checkout
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: plexus-python-common
3
+ Version: 1.1.82
4
+ Classifier: Programming Language :: Python :: 3
5
+ Classifier: Programming Language :: Python :: 3.12
6
+ Classifier: Programming Language :: Python :: 3.13
7
+ Classifier: Programming Language :: Python :: 3.14
8
+ Requires-Python: <3.15,>=3.12
9
+ Requires-Dist: asyncpg>=0.30
10
+ Requires-Dist: numpy>=2.3
11
+ Requires-Dist: psycopg>=3.2
12
+ Requires-Dist: pymysql>=1.1
13
+ Requires-Dist: sqlalchemy>=2.0
14
+ Provides-Extra: all
15
+ Requires-Dist: plexus-python-common; extra == "all"
16
+ Provides-Extra: test
17
+ Requires-Dist: ddt>=1.7; extra == "test"
18
+ Requires-Dist: pytest-asyncio>=1.2; extra == "test"
19
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
20
+ Requires-Dist: pytest-mysql>=3.0; extra == "test"
21
+ Requires-Dist: pytest-order>=1.3; extra == "test"
22
+ Requires-Dist: pytest-postgresql>=6.1; extra == "test"
23
+ Requires-Dist: pytest>=8.3; extra == "test"
@@ -0,0 +1,29 @@
1
+ # Plexus Python Common Module
2
+
3
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/plexus-python-common?style=for-the-badge)
4
+ ![PyPI - Version](https://img.shields.io/pypi/v/plexus-python-common?style=for-the-badge)
5
+ ![Codecov](https://img.shields.io/codecov/c/github/ruyangshou/plexus-python-common?token=0FGT4M40CD&style=for-the-badge)
6
+
7
+ ## Build and Deploy
8
+
9
+ ### Using Conda
10
+
11
+ We recommend using Conda. You need to install Anaconda packages from
12
+ the [official site](https://www.anaconda.com/products/distribution)
13
+
14
+ Create a Conda environment and install the modules and their dependencies in it
15
+
16
+ ```shell
17
+ conda create -n plexus python=3.14
18
+ conda activate plexus
19
+
20
+ pip install .
21
+
22
+ conda deactivate
23
+ ```
24
+
25
+ To remove the existing Conda environment (and create a brand new one)
26
+
27
+ ```shell
28
+ conda env remove -n plexus
29
+ ```
@@ -0,0 +1 @@
1
+ 1.1
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=80.0",
4
+ "setuptools-scm>=9.0",
5
+ "plexus-python-setup>=1.1",
6
+ ]
7
+ build-backend = "setuptools.build_meta"
8
+
9
+ [dependency-groups]
10
+ dev = [
11
+ "asyncpg>=0.30",
12
+ "numpy>=2.3",
13
+ "psycopg>=3.2",
14
+ "pymysql>=1.1",
15
+ "sqlalchemy>=2.0",
16
+ ]
17
+ test = [
18
+ "ddt>=1.7",
19
+ "pytest-asyncio>=1.2",
20
+ "pytest-cov>=5.0",
21
+ "pytest-mysql>=3.0",
22
+ "pytest-order>=1.3",
23
+ "pytest-postgresql>=6.1",
24
+ "pytest>=8.3",
25
+ ]
26
+
27
+ [project]
28
+ name = "plexus-python-common"
29
+ dynamic = ["version"]
30
+ requires-python = ">=3.12,<3.15"
31
+ classifiers = [
32
+ "Programming Language :: Python :: 3",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Programming Language :: Python :: 3.14",
36
+ ]
37
+ dependencies = [
38
+ "asyncpg>=0.30",
39
+ "numpy>=2.3",
40
+ "psycopg>=3.2",
41
+ "pymysql>=1.1",
42
+ "sqlalchemy>=2.0",
43
+ ]
44
+
45
+ [project.optional-dependencies]
46
+ all = [
47
+ "plexus-python-common",
48
+ ]
49
+ test = [
50
+ "ddt>=1.7",
51
+ "pytest-asyncio>=1.2",
52
+ "pytest-cov>=5.0",
53
+ "pytest-mysql>=3.0",
54
+ "pytest-order>=1.3",
55
+ "pytest-postgresql>=6.1",
56
+ "pytest>=8.3",
57
+ ]
58
+
59
+ [tool.setuptools]
60
+ package-dir = { "" = "src" }
61
+ zip-safe = false
62
+ include-package-data = true
63
+
64
+ [tool.setuptools.packages.find]
65
+ where = ["src"]
66
+ namespaces = true
67
+
68
+ [tool.pytest.ini_options]
69
+ pythonpath = ["src"]
70
+
71
+ [tool.coverage.run]
72
+ branch = true
73
+ include = [
74
+ "src/plexus/*",
75
+ ]
@@ -0,0 +1,10 @@
1
+ [dummy_section_1]
2
+ dummy_option_1 = True
3
+ dummy_option_2 = False
4
+ dummy_option_3 = 1
5
+ dummy_option_4 = -1
6
+ dummy_option_5 = 1.0
7
+ dummy_option_6 = -1.0
8
+ dummy_option_7 = 1.e+0
9
+ dummy_option_8 = -1.e-0
10
+ dummy_option_9 = dummy_value_alpha
@@ -0,0 +1,7 @@
1
+ dummy_str,dummy_bool,dummy_int,dummy_float,dummy_datetime,dummy_params
2
+ foo,True,1,1.0,2020-01-01T00:00:00,
3
+ bar,False,-1,-1.0,2020-01-01T00:00:00,"key_1=value_1,key_2,!key_3"
4
+ baz,\N,100,inf,2020-01-01T00:00:00,\N
5
+ ,\N,-100,-inf,2020-01-01T00:00:00,
6
+ \N,\N,0,nan,2020-01-01T00:00:00,\N
7
+ \N,\N,\N,\N,\N,\N
@@ -0,0 +1,7 @@
1
+ dummy_str dummy_bool dummy_int dummy_float dummy_datetime dummy_params
2
+ foo True 1 1.0 2020-01-01T00:00:00
3
+ bar False -1 -1.0 2020-01-01T00:00:00 key_1:value_1;key_2;-key_3
4
+ baz <null> 100 inf 2020-01-01T00:00:00 <null>
5
+ <null> -100 -inf 2020-01-01T00:00:00
6
+ <null> <null> 0 nan 2020-01-01T00:00:00 <null>
7
+ <null> <null> <null> <null> <null> <null>
@@ -1,6 +1,6 @@
1
1
  import argparse
2
2
 
3
- from iker.setup import setup, version_string
3
+ from plexus.setup import setup, version_string
4
4
 
5
5
  if __name__ == "__main__":
6
6
  parser = argparse.ArgumentParser(description="setup script integrating dynamic version printer")
@@ -0,0 +1,221 @@
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 plexus.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
+
19
+ class ParserTreeNode(object):
20
+ """
21
+ Represents a node in the parser tree, holding a command, its parser, and any child nodes. Each node may have
22
+ subparsers and a list of child nodes representing subcommands.
23
+
24
+ :param command: The command string for this node.
25
+ :param parser: The ``ArgumentParser`` associated with this node.
26
+ """
27
+
28
+ def __init__(self, command: str, parser: argparse.ArgumentParser):
29
+ self.command = command
30
+ self.parser = parser
31
+ self.subparsers = None
32
+ self.child_nodes: list[ParserTreeNode] = []
33
+
34
+
35
+ def construct_parser_tree(
36
+ root_node: ParserTreeNode,
37
+ command_chain: list[str],
38
+ command_key_prefix: str,
39
+ **kwargs,
40
+ ) -> list[ParserTreeNode]:
41
+ """
42
+ Constructs a parser tree by traversing or creating nodes for each command in the command chain. Returns the path
43
+ from the ``root_node`` to the last node in the chain.
44
+
45
+ :param root_node: The root node of the parser tree.
46
+ :param command_chain: A list of command strings representing the path.
47
+ :param command_key_prefix: Prefix for command keys in the parser.
48
+ :param kwargs: Additional keyword arguments for parser creation.
49
+ :return: A list of ``ParserTreeNode`` objects representing the path from root to the last command.
50
+ """
51
+ node_path = [root_node]
52
+ if len(command_chain) == 0:
53
+ return node_path
54
+
55
+ node = root_node
56
+ for depth, command in enumerate(command_chain):
57
+ if node.subparsers is None:
58
+ node.subparsers = node.parser.add_subparsers(dest=f"{command_key_prefix}:{depth}")
59
+ for child_node in node.child_nodes:
60
+ if child_node.command == command:
61
+ node = child_node
62
+ break
63
+ else:
64
+ if depth == len(command_chain) - 1:
65
+ child_parser = node.subparsers.add_parser(command, **kwargs)
66
+ else:
67
+ child_parser = node.subparsers.add_parser(command)
68
+ child_node = ParserTreeNode(command, child_parser)
69
+ node.child_nodes.append(child_node)
70
+ node = child_node
71
+ node_path.append(node)
72
+
73
+ return node_path
74
+
75
+
76
+ class ParserTree(object):
77
+ """
78
+ Represents a tree structure for managing ``argparse`` parsers and subcommands. Provides methods to add subcommand
79
+ parsers and parse arguments, returning the command chain and parsed namespace.
80
+
81
+ :param root_parser: The root ``ArgumentParser``.
82
+ :param command_key_prefix: Prefix for command keys in the parser tree.
83
+ """
84
+
85
+ def __init__(self, root_parser: argparse.ArgumentParser, command_key_prefix: str = "command"):
86
+ self.root_node = ParserTreeNode("", root_parser)
87
+ self.command_key_prefix = command_key_prefix
88
+
89
+ def add_subcommand_parser(self, command_chain: list[str], **kwargs) -> argparse.ArgumentParser:
90
+ """
91
+ Adds a subcommand parser for the specified command chain, creating intermediate nodes as needed.
92
+
93
+ :param command_chain: A list of command strings representing the subcommand path.
94
+ :param kwargs: Additional keyword arguments for parser creation.
95
+ :return: The ``ArgumentParser`` for the last command in the chain.
96
+ """
97
+ *_, last_node = construct_parser_tree(self.root_node, command_chain, self.command_key_prefix, **kwargs)
98
+ return last_node.parser
99
+
100
+ def parse_args(self, args: list[str] | None = None) -> tuple[list[str], argparse.Namespace]:
101
+ """
102
+ Parses the provided argument list, returning the command chain and the parsed namespace.
103
+
104
+ :param args: The list of arguments to parse. If ``None``, parses ``sys.argv``.
105
+ :return: A tuple containing the list of command strings and the parsed ``Namespace``.
106
+ """
107
+ known_args_namespace = self.root_node.parser.parse_args(args)
108
+
109
+ command_pairs = []
110
+ namespace = argparse.Namespace()
111
+ for key, value in dict(vars(known_args_namespace)).items():
112
+ if key.startswith(self.command_key_prefix) and value is not None:
113
+ command_pairs.append((key, value))
114
+ else:
115
+ setattr(namespace, key, value)
116
+
117
+ return list(command for _, command in sorted(command_pairs)), namespace
118
+
119
+
120
+ @dataclasses.dataclass(frozen=True)
121
+ class ArgParseSpec(object):
122
+ """
123
+ Specification for an argument to be added to an ``ArgumentParser``. Allows detailed configuration of argument
124
+ properties such as ``flag``, ``name``, ``type``, ``action``, ``default``, ``choices``, and help text.
125
+
126
+ :param flag: The optional flag for the argument (e.g., '-f').
127
+ :param name: The name of the argument (e.g., '--file').
128
+ :param action: The action to be taken by the argument parser.
129
+ :param default: The default value for the argument.
130
+ :param type: The type of the argument value.
131
+ :param choices: A list of valid choices for the argument.
132
+ :param required: Whether the argument is required.
133
+ :param help: The help text for the argument.
134
+ """
135
+ flag: str | None = None
136
+ name: str | None = None
137
+ action: str | None = None
138
+ default: Any = None
139
+ type: typing.Type | None = None
140
+ choices: list[Any] | None = None
141
+ required: bool | None = None
142
+ help: str | None = None
143
+
144
+ def make_kwargs(self) -> dict[str, Any]:
145
+ """
146
+ Constructs a dictionary of keyword arguments for ``ArgumentParser.add_argument``, omitting any that are
147
+ ``None``.
148
+
149
+ :return: A dictionary of argument properties suitable for ``ArgumentParser.add_argument``.
150
+ """
151
+ kwargs = dict(
152
+ action=self.action,
153
+ default=self.default,
154
+ type=self.type,
155
+ choices=self.choices,
156
+ required=self.required,
157
+ help=self.help,
158
+ )
159
+
160
+ return {key: value for key, value in kwargs.items() if value is not None}
161
+
162
+
163
+ argparse_spec = ArgParseSpec
164
+
165
+
166
+ def make_argparse(func, parser: argparse.ArgumentParser = None) -> argparse.ArgumentParser:
167
+ """
168
+ Automatically generates an ``ArgumentParser`` for the given function by inspecting its signature and parameter
169
+ annotations. Supports ``ArgParseSpec`` for detailed argument configuration.
170
+
171
+ :param func: The function whose parameters will be used to generate arguments.
172
+ :param parser: An optional ``ArgumentParser`` to add arguments to. If ``None``, a new parser is created.
173
+ :return: The ``ArgumentParser`` with arguments added based on the function signature.
174
+ """
175
+ if parser is None:
176
+ parser = argparse.ArgumentParser()
177
+
178
+ def is_type_of(a: Any, *bs) -> bool:
179
+ return any(is_identical_type(a, b, strict_optional=False, covariant=True) for b in bs)
180
+
181
+ sig = inspect.signature(func)
182
+ for name, param in sig.parameters.items():
183
+
184
+ arg_name = f"--{name.replace('_', '-')}"
185
+
186
+ if param.annotation is None:
187
+ arg_type = str
188
+ elif is_type_of(param.annotation, str, Sequence[str]):
189
+ arg_type = str
190
+ elif is_type_of(param.annotation, int, Sequence[int]):
191
+ arg_type = int
192
+ elif is_type_of(param.annotation, float, Sequence[float]):
193
+ arg_type = float
194
+ elif is_type_of(param.annotation, bool, Sequence[bool]):
195
+ arg_type = bool
196
+ else:
197
+ arg_type = str
198
+
199
+ arg_action = "append" if typing.get_origin(param.annotation) in {list, Sequence} else None
200
+ arg_default = None if param.default is inspect.Parameter.empty else param.default
201
+
202
+ if isinstance(arg_default, ArgParseSpec):
203
+ spec = arg_default
204
+ spec = dataclasses.replace(spec,
205
+ name=spec.name or arg_name,
206
+ type=spec.type if spec.type is not None else arg_type,
207
+ action=spec.action if spec.action is not None else arg_action)
208
+
209
+ if spec.flag is None:
210
+ parser.add_argument(spec.name, **spec.make_kwargs())
211
+ else:
212
+ parser.add_argument(spec.flag, spec.name, **spec.make_kwargs())
213
+
214
+ else:
215
+ parser.add_argument(arg_name,
216
+ type=arg_type,
217
+ action=arg_action,
218
+ required=arg_default is None,
219
+ default=arg_default)
220
+
221
+ return parser
@@ -0,0 +1,124 @@
1
+ import configparser
2
+ import os
3
+ import pathlib
4
+ from typing import Self
5
+
6
+ from plexus.common.utils import logger
7
+ from plexus.common.utils.strutils import is_blank, trim_to_empty
8
+
9
+
10
+ class Config(object):
11
+ def __init__(self, config_path: str | os.PathLike[str] | None = None):
12
+ if not is_blank(config_path := trim_to_empty(None if config_path is None else str(config_path))):
13
+ self.config_path = pathlib.Path(config_path)
14
+ else:
15
+ self.config_path = None
16
+ self.config_parser: configparser.RawConfigParser = configparser.RawConfigParser(strict=False)
17
+
18
+ def __len__(self):
19
+ return sum(len(self.config_parser.options(section)) for section in self.config_parser.sections())
20
+
21
+ def update(self, tuples: list[tuple[str, str, str]], *, overwrite: bool = False):
22
+ for section, option, value in tuples:
23
+ if not self.config_parser.has_section(section):
24
+ self.config_parser.add_section(section)
25
+ if overwrite or not self.config_parser.has_option(section, option):
26
+ self.config_parser.set(section, option, value)
27
+
28
+ def restore(self) -> bool:
29
+ self.config_parser = configparser.RawConfigParser(strict=False)
30
+ if self.config_path is None:
31
+ return False
32
+ try:
33
+ if not self.config_path.exists():
34
+ raise IOError("file not found")
35
+ self.config_parser.read(self.config_path, encoding="utf-8")
36
+ return True
37
+ except IOError as e:
38
+ logger.exception("Failed to restore config from file <'%s'>", self.config_path)
39
+ return False
40
+
41
+ def persist(self) -> bool:
42
+ if self.config_path is None:
43
+ return False
44
+ try:
45
+ with open(self.config_path, "w") as fh:
46
+ self.config_parser.write(fh)
47
+ return True
48
+ except IOError as e:
49
+ logger.exception("Failed to persist config to file <'%s'>", self.config_path)
50
+ return False
51
+
52
+ def has_section(self, section: str) -> bool:
53
+ return self.config_parser.has_section(section)
54
+
55
+ def has(self, section: str, option: str) -> bool:
56
+ return self.config_parser.has_option(section, option)
57
+
58
+ def get(self, section: str, option: str, default_value: str = None) -> str:
59
+ if self.config_parser.has_option(section, option):
60
+ return self.config_parser.get(section, option)
61
+ return default_value
62
+
63
+ def getint(self, section: str, option: str, default_value: int = None) -> int:
64
+ if self.config_parser.has_option(section, option):
65
+ return self.config_parser.getint(section, option)
66
+ return default_value
67
+
68
+ def getfloat(self, section: str, option: str, default_value: float = None) -> float:
69
+ if self.config_parser.has_option(section, option):
70
+ return self.config_parser.getfloat(section, option)
71
+ return default_value
72
+
73
+ def getboolean(self, section: str, option: str, default_value: bool = None) -> bool:
74
+ if self.config_parser.has_option(section, option):
75
+ return self.config_parser.getboolean(section, option)
76
+ return default_value
77
+
78
+ def set(self, section: str, option: str, value: str):
79
+ if not self.config_parser.has_section(section):
80
+ self.config_parser.add_section(section)
81
+ self.config_parser.set(section, option, value)
82
+
83
+ def sections(self) -> list[str]:
84
+ return self.config_parser.sections()
85
+
86
+ def options(self, section: str) -> list[str]:
87
+ if not self.config_parser.has_section(section):
88
+ return []
89
+ return self.config_parser.options(section)
90
+
91
+ def tuples(self) -> list[tuple[str, str, str]]:
92
+ result = []
93
+ for section in self.config_parser.sections():
94
+ for option in self.config_parser.options(section):
95
+ value = self.config_parser.get(section, option)
96
+ result.append((section, option, value))
97
+ return result
98
+
99
+
100
+ class ConfigVisitor(object):
101
+ def __init__(self, config: Config, section: str, prefix: str = "", separator: str = "."):
102
+ self.config = config
103
+ self.section = section
104
+ self.prefix = prefix
105
+ self.separator = separator
106
+
107
+ def __str__(self):
108
+ return self.config.get(self.section, self.prefix)
109
+
110
+ def __int__(self):
111
+ return self.config.getint(self.section, self.prefix)
112
+
113
+ def __float__(self):
114
+ return self.config.getfloat(self.section, self.prefix)
115
+
116
+ def __bool__(self):
117
+ return self.config.getboolean(self.section, self.prefix)
118
+
119
+ def __getattr__(self, suffix: str) -> Self:
120
+ return self[suffix]
121
+
122
+ def __getitem__(self, suffix: str) -> Self:
123
+ new_prefix = suffix if is_blank(self.prefix) else self.separator.join([self.prefix, suffix])
124
+ return ConfigVisitor(self.config, self.section, new_prefix, self.separator)