argumentor-gardehal 0.0.0__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 (26) hide show
  1. argumentor_gardehal-0.0.0/.gitattributes +2 -0
  2. argumentor_gardehal-0.0.0/.github/workflows/publish.yml +60 -0
  3. argumentor_gardehal-0.0.0/.gitignore +183 -0
  4. argumentor_gardehal-0.0.0/.vscode/settings.json +13 -0
  5. argumentor_gardehal-0.0.0/Argumentor/Argument.py +72 -0
  6. argumentor_gardehal-0.0.0/Argumentor/ArgumentValidation.py +225 -0
  7. argumentor_gardehal-0.0.0/Argumentor/Argumentor.py +153 -0
  8. argumentor_gardehal-0.0.0/Argumentor/BoolFlag.py +30 -0
  9. argumentor_gardehal-0.0.0/Argumentor/Command.py +66 -0
  10. argumentor_gardehal-0.0.0/Argumentor/Flag.py +49 -0
  11. argumentor_gardehal-0.0.0/Argumentor/Result.py +60 -0
  12. argumentor_gardehal-0.0.0/Argumentor/__init__.py +7 -0
  13. argumentor_gardehal-0.0.0/LICENSE +21 -0
  14. argumentor_gardehal-0.0.0/PKG-INFO +103 -0
  15. argumentor_gardehal-0.0.0/README.md +90 -0
  16. argumentor_gardehal-0.0.0/argumentor_gardehal.egg-info/PKG-INFO +103 -0
  17. argumentor_gardehal-0.0.0/argumentor_gardehal.egg-info/SOURCES.txt +24 -0
  18. argumentor_gardehal-0.0.0/argumentor_gardehal.egg-info/dependency_links.txt +1 -0
  19. argumentor_gardehal-0.0.0/argumentor_gardehal.egg-info/top_level.txt +1 -0
  20. argumentor_gardehal-0.0.0/pyproject.toml +19 -0
  21. argumentor_gardehal-0.0.0/setup.cfg +4 -0
  22. argumentor_gardehal-0.0.0/tests/ArgumentorTests.py +505 -0
  23. argumentor_gardehal-0.0.0/tests/ExampleAdvanced.py +106 -0
  24. argumentor_gardehal-0.0.0/tests/ExampleBasic.py +39 -0
  25. argumentor_gardehal-0.0.0/tests/enums/CommandHitValues.py +6 -0
  26. argumentor_gardehal-0.0.0/tests/enums/Measurement.py +5 -0
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,60 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ build:
14
+ name: Build Argumentor ${{github.ref_name}}
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ version: ${{steps.get_version.outputs.version}}
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ with:
21
+ fetch-depth: 0
22
+ persist-credentials: false
23
+
24
+ - name: Set up Python
25
+ uses: actions/setup-python@v6
26
+ with:
27
+ python-version: "3.x"
28
+
29
+ - name: Install build dependencies
30
+ run: python -m pip install --upgrade build setuptools_scm twine
31
+
32
+ - name: Build distributions
33
+ run: python -m build
34
+
35
+ - name: Upload artifact
36
+ uses: actions/upload-artifact@v4
37
+ with:
38
+ name: argumentor
39
+ path: dist/
40
+
41
+ - name: List built files
42
+ run: ls -la dist/
43
+
44
+ publish:
45
+ name: Publish to PyPI
46
+ needs: build
47
+ runs-on: ubuntu-latest
48
+ environment:
49
+ name: pypi
50
+ url: https://pypi.org/project/argumentor-gardehal/
51
+ permissions:
52
+ id-token: write
53
+ steps:
54
+ - uses: actions/download-artifact@v4
55
+ with:
56
+ name: argumentor
57
+ path: dist/
58
+
59
+ - name: Publish to PyPI
60
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,183 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+ # Cursor
177
+ # Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to
178
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
179
+ # refer to https://docs.cursor.com/context/ignore-files
180
+ .cursorignore
181
+ .cursorindexingignore
182
+
183
+ Main.py
@@ -0,0 +1,13 @@
1
+ {
2
+ "cSpell.words": [
3
+ "Argumentor",
4
+ "HITVALUE",
5
+ "ints",
6
+ "nosuchflag",
7
+ "someargument",
8
+ "somecommand",
9
+ "someflag",
10
+ "someprefix",
11
+ "updateexternal"
12
+ ]
13
+ }
@@ -0,0 +1,72 @@
1
+ import re
2
+
3
+ from typing import TypeVar, Type, Callable
4
+
5
+ T = TypeVar("T")
6
+
7
+ class Argument():
8
+ name: str
9
+ alias: list[str]
10
+ typeT: Type[T]
11
+ optional: bool
12
+ castFunc: Callable[[str], T]
13
+ validateFunc: Callable[[T], bool]
14
+ useDefaultValue: bool
15
+ defaultValue: T
16
+ description: str
17
+
18
+ def __init__(self, name: str,
19
+ alias: list[str],
20
+ typeT: Type[T],
21
+ optional: bool = False,
22
+ castFunc: Callable[[str], T] = None,
23
+ validateFunc: Callable[[T], bool] = None,
24
+ useDefaultValue: bool = False,
25
+ defaultValue: T = None,
26
+ description: str = None):
27
+ """
28
+ Designates values input as arguments after commands
29
+ eg. height in
30
+ $ -dimensions height:100
31
+
32
+ Args:
33
+ name (str): Name of argument, key for dictionary in Return
34
+ alias (list[str]): Alias of argument.
35
+ typeT (Type[T]): Type of argument, str, int, bool, enum, etc.
36
+ optional (bool, optional): Argument is optional/nullable (from input). Defaults to False. Note that this implies the argument can be None in result, unless useDefaultValue and defaultValue are both set.
37
+ castFunc (Callable[[str], T], optional): Optional function for custom casting of input to typeT. Must take in 1 argument: str and return typeT. Defaults to None.
38
+ validateFunc (Callable[[T], bool], optional): Optional function for custom validation. Must take in 1 argument: typeT and return bool. Defaults to None.
39
+ useDefaultValue (bool, optional): Use a default value if casting and validation fails. Defaults to False.
40
+ defaultValue (T, optional): The default value to use if casting and validation fails, and useDefaultValue is True. Must be typeT. Defaults to None.
41
+ description (str, optional): Explaining what the argument is for. Defaults to None.
42
+ """
43
+
44
+ invalidCharactersRegex = r"\W"
45
+ invalidNames = [e for e in alias + [name] if (re.search(invalidCharactersRegex, e))]
46
+ if(invalidNames):
47
+ raise AttributeError(f"Argument \"{name}\" name or alias ({invalidNames}) contain invalid characters, must be alphanumeric.")
48
+
49
+ self.name = name
50
+ self.alias = alias
51
+ self.typeT = typeT
52
+ self.optional = optional
53
+ self.castFunc = castFunc
54
+ self.validateFunc = validateFunc
55
+ self.useDefaultValue = useDefaultValue
56
+ self.defaultValue = defaultValue
57
+ self.description = description
58
+
59
+ def getFormattedDescription(self) -> str:
60
+ """
61
+ Get the description of arguments with formatting.
62
+
63
+ Returns:
64
+ str: String description.
65
+ """
66
+
67
+ optionalDisplayString = "optional" if self.optional else "required"
68
+ typeDisplayString = f", type: {self.typeT.__name__}"
69
+ aliasDisplayString = f", alias: {", ".join(self.alias)}" if self.alias else ""
70
+ defaultDisplayString = f", default: {str(self.defaultValue)}" if self.useDefaultValue else ""
71
+ return f"* Argument {self.name} ({optionalDisplayString}{typeDisplayString}{defaultDisplayString}{aliasDisplayString}): \
72
+ \n\t{self.description}"
@@ -0,0 +1,225 @@
1
+ from .Argument import Argument
2
+ from .Command import Command
3
+ from .Flag import Flag
4
+
5
+ import re
6
+
7
+ class ArgumentValidation():
8
+ isValid: bool
9
+ namedArguments: dict[str, str]
10
+ validatedArguments: dict[str, str]
11
+ finalizedArguments: dict[str, object]
12
+ messages: list[str]
13
+
14
+ namedInputRegex: str
15
+ flagInputRegex: str
16
+
17
+ def __init__(self, inputList: list[str], command: Command, namedArgDelim: str, flagPrefix: str):
18
+ """
19
+ Internal validation in Argumentor.
20
+
21
+ Args:
22
+ inputList (list[str]): List of inputs from user
23
+ command (Command): Command to validate input against
24
+ namedArgDelim (str): Delimiter used for named input, e.g. ":" in key:value
25
+ flagPrefix (str): Prefix for flags, e.g. "--updateexternal".
26
+ """
27
+
28
+ self.isValid = False
29
+ self.namedArguments = {}
30
+ self.validatedArguments = {}
31
+ self.finalizedArguments = {}
32
+ self.messages = []
33
+
34
+ self.namedInputRegex = fr"^\w+{namedArgDelim}\S+"
35
+ self.flagInputRegex = fr"^{flagPrefix}\w+"
36
+
37
+ if(command.arguments):
38
+ self.__populateNamedArguments(inputList, namedArgDelim)
39
+ self.__validateNamedArguments(command.arguments)
40
+ self.__addPositionalArguments(inputList, command)
41
+ self.__castAndValidateArguments(command)
42
+ else:
43
+ self.isValid = True
44
+
45
+ if(command.flags):
46
+ self.__addFlags(inputList, flagPrefix, command.flags)
47
+
48
+ def toString(self) -> str:
49
+ """
50
+ Returns string with class properties.
51
+
52
+ Returns:
53
+ str: String of class properties.
54
+ """
55
+
56
+ return f""" \
57
+ isValid: {self.isValid},
58
+ namedArguments: {self.namedArguments},
59
+ validatedArguments: {self.validatedArguments},
60
+ finalizedArguments: {self.finalizedArguments},
61
+ messages: {self.messages},
62
+ """
63
+
64
+ def __populateNamedArguments(self, inputList: list[str], namedArgDelim: str):
65
+ namedInputs = [e for e in inputList if(namedArgDelim in e)]
66
+ namedArguments = {}
67
+ for input in namedInputs:
68
+ namedSplit = input.split(namedArgDelim)
69
+ key = namedSplit[0]
70
+ value = namedArgDelim.join(namedSplit[1:])
71
+ namedArguments[key] = value
72
+
73
+ self.namedArguments = namedArguments
74
+
75
+ def __validateNamedArguments(self, arguments: list[Argument]):
76
+ argumentAliasMap = {}
77
+ for argument in arguments:
78
+ argumentAliasMap[argument.name] = argument.name
79
+ for alias in argument.alias:
80
+ argumentAliasMap[alias] = argument.name
81
+
82
+ for key in self.namedArguments.keys():
83
+ if(key not in argumentAliasMap.keys()):
84
+ self.messages.append(self.__formatArgumentError(key, "Not a valid argument alias"))
85
+ continue
86
+
87
+ if(key in self.validatedArguments.keys()):
88
+ self.messages.append(self.__formatArgumentError(key, "Alias was already added"))
89
+ continue
90
+
91
+ self.validatedArguments[argumentAliasMap[key]] = self.namedArguments[key]
92
+
93
+ def __addPositionalArguments(self, inputList: list[str], command: Command):
94
+ unnamedInput = [e for e in inputList if(not re.search(self.namedInputRegex, e) and not re.search(self.flagInputRegex, e))]
95
+ remainingArgument = [e for e in command.arguments if(e.name not in self.validatedArguments.keys())]
96
+
97
+ for i in range(len(unnamedInput)):
98
+ if(i >= len(remainingArgument)):
99
+ self.messages.append(f"Received more positional arguments ({len(unnamedInput)}) than expected ({len(remainingArgument)})")
100
+ for extraArg in unnamedInput[i:]:
101
+ self.messages.append(f"{extraArg} not added, exceeds expected Arguments length")
102
+
103
+ break # unnamedInput loop
104
+
105
+ unnamedArg = unnamedInput[i]
106
+ positionalArg = remainingArgument[i]
107
+ if(positionalArg.name in self.validatedArguments.keys()):
108
+ self.messages.append(self.__formatArgumentError(positionalArg.name, f"Already added as named argument {unnamedArg}"))
109
+ continue
110
+
111
+ self.validatedArguments[positionalArg.name] = unnamedArg
112
+
113
+ def __castAndValidateArguments(self, command: Command):
114
+ inputIsValid = True
115
+
116
+ # Adding optional arguments not in input
117
+ # TODO new method?
118
+ for argument in command.arguments:
119
+ if(argument.optional and argument.name not in self.validatedArguments):
120
+ self.validatedArguments[argument.name] = None
121
+
122
+ for key in self.validatedArguments.keys():
123
+ argument = [e for e in command.arguments if e.name is key][0]
124
+ if(argument is None):
125
+ self.messages.append(self.__formatArgumentError(key, "Critical error! No Argument object found"))
126
+ inputIsValid = False
127
+ continue
128
+
129
+ value = self.validatedArguments[key]
130
+ if(value is None):
131
+ if(argument.useDefaultValue):
132
+ self.messages.append(self.__formatArgumentError(key, f"Value was None and not optional, default value {argument.defaultValue} was applied"))
133
+ castValue = argument.defaultValue
134
+ continue
135
+ elif(argument.optional):
136
+ self.finalizedArguments[key] = None
137
+ continue
138
+ else:
139
+ self.messages.append(self.__formatArgumentError(key, f"Critical error! Value was None, and Argument is not optional"))
140
+ inputIsValid = False
141
+ continue
142
+
143
+ castSuccess = False
144
+ castValue = None
145
+ try:
146
+ if(argument.castFunc):
147
+ castValue = argument.castFunc(value)
148
+ else:
149
+ castValue = (argument.typeT)(value)
150
+
151
+ if(castValue is None and not argument.optional):
152
+ if(argument.useDefaultValue):
153
+ self.messages.append(self.__formatArgumentError(key, f"Value was None but argument was not optional, default value {argument.defaultValue} was applied"))
154
+ castValue = argument.defaultValue
155
+ continue
156
+ else:
157
+ self.messages.append(self.__formatArgumentError(key, f"Critical error! Value was None, not optional, and no default was given")) # Remember useDefaultValue
158
+ inputIsValid = False
159
+ continue
160
+
161
+ castSuccess = True
162
+ except Exception as ex:
163
+ if(argument.useDefaultValue):
164
+ self.messages.append(self.__formatArgumentError(key, f"{value} could not be cast, default value {argument.defaultValue} was applied"))
165
+ castValue = argument.defaultValue
166
+ continue
167
+ else:
168
+ self.messages.append(self.__formatArgumentError(key, f"Critical error! {value} could not be cast to {argument.typeT.__name__}"))
169
+ inputIsValid = False
170
+ continue
171
+
172
+ if(castSuccess and argument.validateFunc):
173
+ try:
174
+ resultValid = argument.validateFunc(castValue)
175
+ if(not resultValid):
176
+ if(argument.useDefaultValue):
177
+ self.messages.append(self.__formatArgumentError(key, f"{value} did not pass validation, default value {argument.defaultValue} was applied"))
178
+ castValue = argument.defaultValue
179
+ continue
180
+ else:
181
+ self.messages.append(self.__formatArgumentError(key, f"Critical error! {value} did not pass validation"))
182
+ inputIsValid = False
183
+ continue
184
+ except Exception as ex:
185
+ if(argument.useDefaultValue):
186
+ self.messages.append(self.__formatArgumentError(key, f"{value} validation raised an exception, default value {argument.defaultValue} was applied"))
187
+ castValue = argument.defaultValue
188
+ continue
189
+ else:
190
+ self.messages.append(self.__formatArgumentError(key, f"Critical error! {value} validation raised an exception and no defaults were given"))
191
+ inputIsValid = False
192
+ continue
193
+
194
+ self.finalizedArguments[key] = castValue
195
+
196
+ requiredArgumentNames = [e.name for e in command.arguments if not e.optional]
197
+ if(len(self.finalizedArguments.keys())) < len(requiredArgumentNames):
198
+ self.messages.append(f"Critical error! Required arguments are missing (got {len(self.finalizedArguments.keys())}/{len(requiredArgumentNames)})")
199
+ inputIsValid = False
200
+
201
+ if(inputIsValid):
202
+ for argument in command.arguments:
203
+ if(argument.name not in self.finalizedArguments.keys() and argument.useDefaultValue):
204
+ self.finalizedArguments[argument.name] = argument.defaultValue
205
+
206
+ self.isValid = inputIsValid
207
+
208
+ def __addFlags(self, inputList: list[str], flagPrefix: str, flags: list[Flag]):
209
+ flagInputs = [e.removeprefix(flagPrefix) for e in inputList if(re.search(self.flagInputRegex, e))]
210
+ for flag in flags:
211
+ intersections = list(set(flagInputs) & set(flag.alias + [flag.name]))
212
+ if(intersections):
213
+ self.finalizedArguments[flag.name] = flag.value
214
+ for intersection in intersections:
215
+ flagInputs.remove(intersection)
216
+ else:
217
+ self.finalizedArguments[flag.name] = flag.defaultValue
218
+
219
+ if(flagInputs):
220
+ self.messages.append(self.__formatArgumentError(", ".join(flagInputs), f"No such flag(s)"))
221
+
222
+ def __formatArgumentError(self, arg: str, error: str) -> str:
223
+ return f"{arg} error: {error}"
224
+
225
+