recursive yaml loading; readme text; minor refactoring
This commit is contained in:
parent
4b1536c19a
commit
987e83c110
44
README.md
44
README.md
@ -1,3 +1,47 @@
|
|||||||
# HTTP forms defined in YAML
|
# HTTP forms defined in YAML
|
||||||
|
|
||||||
|
This module allows creating simple interfaces to forms/payloads for use in HTTP POST requests by defining them in highly readable and easily maintainable YAML files.
|
||||||
|
|
||||||
|
## Form definition
|
||||||
|
|
||||||
|
A form is defined by its fields.
|
||||||
|
|
||||||
|
A field is defined by an **alias** (for internal use) and the field's **name**, which is the parameter name in the payload sent during a `POST` request, and which typically corresponds to the `name` attribute of a `<select>` or `<input>` HTML tag.
|
||||||
|
|
||||||
|
Optionally, a field can have a **default** value, value **options** (as `<select>` tags do), and may be declared **required** (i.e. non-optional).
|
||||||
|
|
||||||
|
A form definition in YAML format will consist of the field aliases as top-level keys, and the corresponding fields' definitions as key-value-pairs below them.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
```yaml
|
||||||
|
# definition.yaml
|
||||||
|
|
||||||
|
field1:
|
||||||
|
name: payload_param_1
|
||||||
|
field2:
|
||||||
|
name: payload_param_2
|
||||||
|
default: foo
|
||||||
|
choice_field:
|
||||||
|
name: payload_param_3
|
||||||
|
options:
|
||||||
|
value1: text for option 1
|
||||||
|
value2: text for option 2
|
||||||
|
default: value1
|
||||||
|
mandatory_field:
|
||||||
|
name: payload_param_0
|
||||||
|
required: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```
|
||||||
|
>>> from yamlhttpforms import load_form
|
||||||
|
>>> form_interface = load_form('definition.yaml')
|
||||||
|
>>> form_interface.get_payload(field1='abc', field2='bar', mandatory_field='420')
|
||||||
|
{'payload_param_1': 'abc', 'payload_param_2': 'bar', 'payload_param_3': 'value1', 'payload_param_0': '420'}
|
||||||
|
>>> form_interface.get_payload(field1='abc', choice_field='baz', mandatory_field='420')
|
||||||
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
|
ValueError: "baz" is not a valid option for <SelectField: name="payload_param_3", default="value1", options={'value1': 'text for option 1', 'value2': 'text for option 2'}>
|
||||||
|
```
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
from yamlhttpforms.form import Form, yaml_overload, load_form
|
@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Callable, Union, Optional, TYPE_CHECKING
|
from typing import Dict, Callable, Union, Optional, Any, TYPE_CHECKING
|
||||||
|
|
||||||
from yaml import safe_load
|
from yaml import safe_load
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -41,7 +41,7 @@ class FormField:
|
|||||||
def valid_option(self, option: str) -> bool:
|
def valid_option(self, option: str) -> bool:
|
||||||
return self.options is None or option in self.options.keys() or option in self.options.values()
|
return self.options is None or option in self.options.keys() or option in self.options.values()
|
||||||
|
|
||||||
def clean_value(self, value: str) -> str:
|
def clean(self, value: str) -> str:
|
||||||
if self.options is None or value in self.options.keys():
|
if self.options is None or value in self.options.keys():
|
||||||
return value
|
return value
|
||||||
# Try to find an unambiguous match in the visible option texts:
|
# Try to find an unambiguous match in the visible option texts:
|
||||||
@ -73,7 +73,7 @@ class Form:
|
|||||||
url (optional):
|
url (optional):
|
||||||
Can be set in advance to the url that requests using this form's payload should be made to.
|
Can be set in advance to the url that requests using this form's payload should be made to.
|
||||||
"""
|
"""
|
||||||
self._fields: Dict[str, FormField] = {alias: FormField(**field_def) for alias, field_def in definition.items()}
|
self.fields: Dict[str, FormField] = {alias: FormField(**field_def) for alias, field_def in definition.items()}
|
||||||
self.full_payload_always: bool = full_payload
|
self.full_payload_always: bool = full_payload
|
||||||
self.url: Optional[str] = url
|
self.url: Optional[str] = url
|
||||||
|
|
||||||
@ -81,13 +81,9 @@ class Form:
|
|||||||
fields = ', '.join(f"'{alias}': {field}" for alias, field in self.fields.items())
|
fields = ', '.join(f"'{alias}': {field}" for alias, field in self.fields.items())
|
||||||
return f'Form({fields})'
|
return f'Form({fields})'
|
||||||
|
|
||||||
@property
|
|
||||||
def fields(self) -> Dict[str, FormField]:
|
|
||||||
return self._fields.copy()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def required_fields(self) -> Dict[str, FormField]:
|
def required_fields(self) -> Dict[str, FormField]:
|
||||||
return {alias: field for alias, field in self._fields.items() if field.required}
|
return {alias: field for alias, field in self.fields.items() if field.required}
|
||||||
|
|
||||||
def get_payload(self, **kwargs: str) -> Dict[str, str]:
|
def get_payload(self, **kwargs: str) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@ -115,7 +111,7 @@ class Form:
|
|||||||
if value is None:
|
if value is None:
|
||||||
payload[field.name] = field.default
|
payload[field.name] = field.default
|
||||||
else:
|
else:
|
||||||
payload[field.name] = field.clean_value(value)
|
payload[field.name] = field.clean(value)
|
||||||
elif field.required:
|
elif field.required:
|
||||||
raise ValueError(f"`{key}` is a required field, but no argument was passed.")
|
raise ValueError(f"`{key}` is a required field, but no argument was passed.")
|
||||||
elif self.full_payload_always:
|
elif self.full_payload_always:
|
||||||
@ -167,18 +163,55 @@ class Form:
|
|||||||
return post(self.url, data=self.get_payload(**kwargs))
|
return post(self.url, data=self.get_payload(**kwargs))
|
||||||
|
|
||||||
|
|
||||||
def load_form(*def_paths: PathT, full_payload: bool = True) -> Form:
|
def yaml_overload(*paths: PathT, dir_sort_key: Callable[[PathT], Any] = None, dir_sort_reverse: bool = False) -> dict:
|
||||||
|
"""
|
||||||
|
Loads YAML files from any number of paths, recursively going through any directories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paths:
|
||||||
|
Each argument should be a path to either a YAML file or a directory containing YAML files;
|
||||||
|
only files with the extension `.yaml` are loaded.
|
||||||
|
dir_sort_key (optional):
|
||||||
|
If one of the paths is a directory, its contents are sorted, before recursively passing them into this
|
||||||
|
function; to apply a specific comparison key for each sub-path, a callable can be used here, which is
|
||||||
|
passed into the builtin `sorted` function.
|
||||||
|
dir_sort_reverse (optional):
|
||||||
|
Same as with the parameter above, this argument is also passed into the `sorted` function.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary comprised of the contents of iteratively loaded YAML files.
|
||||||
|
NOTE: Since it is updated each time a file is loaded, their load order matters!
|
||||||
|
"""
|
||||||
|
output_dict = {}
|
||||||
|
for path in paths:
|
||||||
|
path = Path(path)
|
||||||
|
if path.is_dir():
|
||||||
|
output_dict.update(yaml_overload(*sorted(path.iterdir(), key=dir_sort_key, reverse=dir_sort_reverse)))
|
||||||
|
elif path.suffix == '.yaml':
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
output_dict.update(safe_load(f))
|
||||||
|
return output_dict
|
||||||
|
|
||||||
|
|
||||||
|
def load_form(*def_paths: PathT, dir_sort_key: Callable[[PathT], Any] = None, dir_sort_reverse: bool = False,
|
||||||
|
full_payload: bool = True, url: str = None) -> Form:
|
||||||
"""
|
"""
|
||||||
Creates a form instance from an arbitrary number of definition files in YAML format.
|
Creates a form instance from an arbitrary number of definition files in YAML format.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
def_paths:
|
def_paths:
|
||||||
Each element must be a path to a readable YAML file containing a valid form definition.
|
Paths to the YAML files containing the form definitions; see the `paths` parameter in `yaml_overload`
|
||||||
|
dir_sort_key (optional):
|
||||||
|
See `yaml_overload`
|
||||||
|
dir_sort_reverse (optional):
|
||||||
|
See `yaml_overload`
|
||||||
full_payload (optional):
|
full_payload (optional):
|
||||||
Passed directly into the Form constructor.
|
See the `Form` constructor
|
||||||
|
url (optional):
|
||||||
|
See the `Form` constructor
|
||||||
"""
|
"""
|
||||||
definition = {}
|
return Form(
|
||||||
for path in def_paths:
|
definition=yaml_overload(*def_paths, dir_sort_key=dir_sort_key, dir_sort_reverse=dir_sort_reverse),
|
||||||
with open(path, 'r') as f:
|
full_payload=full_payload,
|
||||||
definition.update(safe_load(f))
|
url=url
|
||||||
return Form(definition, full_payload=full_payload)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user