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
|
||||
|
||||
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 typing import Dict, Callable, Union, Optional, TYPE_CHECKING
|
||||
from typing import Dict, Callable, Union, Optional, Any, TYPE_CHECKING
|
||||
|
||||
from yaml import safe_load
|
||||
if TYPE_CHECKING:
|
||||
@ -41,7 +41,7 @@ class FormField:
|
||||
def valid_option(self, option: str) -> bool:
|
||||
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():
|
||||
return value
|
||||
# Try to find an unambiguous match in the visible option texts:
|
||||
@ -73,7 +73,7 @@ class Form:
|
||||
url (optional):
|
||||
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.url: Optional[str] = url
|
||||
|
||||
@ -81,13 +81,9 @@ class Form:
|
||||
fields = ', '.join(f"'{alias}': {field}" for alias, field in self.fields.items())
|
||||
return f'Form({fields})'
|
||||
|
||||
@property
|
||||
def fields(self) -> Dict[str, FormField]:
|
||||
return self._fields.copy()
|
||||
|
||||
@property
|
||||
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]:
|
||||
"""
|
||||
@ -115,7 +111,7 @@ class Form:
|
||||
if value is None:
|
||||
payload[field.name] = field.default
|
||||
else:
|
||||
payload[field.name] = field.clean_value(value)
|
||||
payload[field.name] = field.clean(value)
|
||||
elif field.required:
|
||||
raise ValueError(f"`{key}` is a required field, but no argument was passed.")
|
||||
elif self.full_payload_always:
|
||||
@ -167,18 +163,55 @@ class Form:
|
||||
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.
|
||||
|
||||
Args:
|
||||
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):
|
||||
Passed directly into the Form constructor.
|
||||
See the `Form` constructor
|
||||
url (optional):
|
||||
See the `Form` constructor
|
||||
"""
|
||||
definition = {}
|
||||
for path in def_paths:
|
||||
with open(path, 'r') as f:
|
||||
definition.update(safe_load(f))
|
||||
return Form(definition, full_payload=full_payload)
|
||||
return Form(
|
||||
definition=yaml_overload(*def_paths, dir_sort_key=dir_sort_key, dir_sort_reverse=dir_sort_reverse),
|
||||
full_payload=full_payload,
|
||||
url=url
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user