draft form interface
This commit is contained in:
parent
9dafe3da8f
commit
9dbe472bbf
119
src/yamlhttpforms/form.py
Normal file
119
src/yamlhttpforms/form.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from yaml import safe_load
|
||||||
|
|
||||||
|
|
||||||
|
class FormField:
|
||||||
|
def __init__(self, name: str, default: str = None, options: Dict[str, str] = None, hidden: bool = False):
|
||||||
|
self.name = name
|
||||||
|
self._default = default
|
||||||
|
self.options = options
|
||||||
|
self.hidden = hidden
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
s = '<InputField' if self.options is None else '<SelectField'
|
||||||
|
s += f': name="{self.name}"'
|
||||||
|
if self.hidden:
|
||||||
|
s += ', hidden=True'
|
||||||
|
if self._default is not None:
|
||||||
|
s += f', default="{self.default}"'
|
||||||
|
return s + '>'
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return repr(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default(self) -> str:
|
||||||
|
return self._default() if callable(self._default) else self._default
|
||||||
|
|
||||||
|
@property
|
||||||
|
def special(self) -> bool:
|
||||||
|
return self.hidden and self._default is None
|
||||||
|
|
||||||
|
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:
|
||||||
|
if self.options is None or value in self.options.keys():
|
||||||
|
return value
|
||||||
|
# Try to find an unambiguous match in the visible option texts:
|
||||||
|
key = None
|
||||||
|
for option_key, option_text in self.options.items():
|
||||||
|
if option_text == value:
|
||||||
|
if key is not None:
|
||||||
|
raise LookupError(f'More than one options key has the text "{value}" in {self}')
|
||||||
|
key = option_key
|
||||||
|
if key is not None:
|
||||||
|
return key
|
||||||
|
raise ValueError(f'"{value}" is not a valid option for {self}')
|
||||||
|
|
||||||
|
|
||||||
|
class Form(SimpleNamespace):
|
||||||
|
def __init__(self, definition: Dict[str, Dict], full_payload: bool = True):
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
Args:
|
||||||
|
definition:
|
||||||
|
The field definition dictionary
|
||||||
|
full_payload (optional):
|
||||||
|
Defines the behavior during payload validation/creation.
|
||||||
|
If True (default), the fields' predefined default values will be inserted into the payload,
|
||||||
|
wherever no value (or `None`) is explicitly passed for that field.
|
||||||
|
If False, fields for which no values were explicitly passed, will not be included in the payload.
|
||||||
|
The exception is special fields (i.e. hidden fields with no default value);
|
||||||
|
these will cause an error, if no value is provided.
|
||||||
|
"""
|
||||||
|
self._full_payload_always = full_payload
|
||||||
|
for alias, field_def in definition.items():
|
||||||
|
setattr(self, alias, FormField(**field_def))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fields(self) -> Dict[str, FormField]:
|
||||||
|
return {k: v for k, v in self.__dict__.items() if isinstance(v, FormField)}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def special_fields(self) -> Dict[str, FormField]:
|
||||||
|
return {k: v for k, v in self.__dict__.items() if isinstance(v, FormField) if v.special}
|
||||||
|
|
||||||
|
def get_payload(self, **kwargs) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Creates a request payload from the form's fields as a dictionary,
|
||||||
|
where the keys represent the name attributes of the form fields and the values the actual values to be passed.
|
||||||
|
|
||||||
|
This payload can then be used for example in a GET request in the form of url parameters (after encoding it
|
||||||
|
in the appropriate string form) or in a regular POST request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kwargs (optional):
|
||||||
|
Every key must correspond to a key in the internal fields dictionary.
|
||||||
|
Values will be passed into the payload (if they pass validation).
|
||||||
|
Select fields with predefined options will only allow one of the options to be passed.
|
||||||
|
If `None` is passed as a value, the corresponding field's default value will be used in the payload.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validated name-value-mapping to be used for HTTP requests from the form's fields.
|
||||||
|
"""
|
||||||
|
payload = {}
|
||||||
|
for key, field in self.fields.items():
|
||||||
|
if key in kwargs.keys():
|
||||||
|
value = kwargs[key]
|
||||||
|
if value is None:
|
||||||
|
payload[field.name] = field.default
|
||||||
|
else:
|
||||||
|
payload[field.name] = field.clean_value(value)
|
||||||
|
elif field.special:
|
||||||
|
raise ValueError(f"`{key}` is a special field (hidden, no-defeault), but no argument was passed.")
|
||||||
|
elif self._full_payload_always:
|
||||||
|
payload[field.name] = field.default
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def load_form(*def_paths: Path, full_payload: bool = True) -> Form:
|
||||||
|
definition = {}
|
||||||
|
for path in def_paths:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
definition.update(safe_load(f))
|
||||||
|
return Form(definition, full_payload=full_payload)
|
Loading…
Reference in New Issue
Block a user