added functions to check interfaces against their HTML counterparts; optional dependency on bs4
This commit is contained in:
parent
1452e1e6ba
commit
6f6d2192da
@ -1,3 +1,2 @@
|
|||||||
-r aio.txt
|
-r full.txt
|
||||||
-r req.txt
|
|
||||||
coverage
|
coverage
|
3
requirements/full.txt
Normal file
3
requirements/full.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-r aio.txt
|
||||||
|
-r req.txt
|
||||||
|
-r html.txt
|
2
requirements/html.txt
Normal file
2
requirements/html.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-r common.txt
|
||||||
|
beautifulsoup4
|
@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = yamlhttpforms
|
name = yamlhttpforms
|
||||||
version = 0.0.2
|
version = 0.0.3
|
||||||
author = Daniil F.
|
author = Daniil F.
|
||||||
author_email = mail@placeholder123.to
|
author_email = mail@placeholder123.to
|
||||||
description = HTTP forms defined in YAML
|
description = HTTP forms defined in YAML
|
||||||
@ -26,6 +26,8 @@ req =
|
|||||||
requests
|
requests
|
||||||
aio =
|
aio =
|
||||||
aiohttp
|
aiohttp
|
||||||
|
html =
|
||||||
|
beautifulsoup4
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = src
|
where = src
|
||||||
|
@ -4,6 +4,7 @@ from typing import Dict, Callable, Union, Optional, Any, TYPE_CHECKING
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from aiohttp import ClientSession as AioSession, ClientResponse as AioResponse
|
from aiohttp import ClientSession as AioSession, ClientResponse as AioResponse
|
||||||
from requests import Session as ReqSession, Response as ReqResponse
|
from requests import Session as ReqSession, Response as ReqResponse
|
||||||
|
from bs4.element import Tag as BS4Tag
|
||||||
|
|
||||||
from .utils import PathT, yaml_overload
|
from .utils import PathT, yaml_overload
|
||||||
|
|
||||||
@ -67,6 +68,17 @@ class FormField:
|
|||||||
return key
|
return key
|
||||||
raise ValueError(f'"{value}" is not a valid option for {self}')
|
raise ValueError(f'"{value}" is not a valid option for {self}')
|
||||||
|
|
||||||
|
def check_with_html(self, field_tag: 'BS4Tag', check_defaults: bool = True) -> None:
|
||||||
|
from .html import check_field_interface
|
||||||
|
check_field_interface(field_tag, self, check_defaults=check_defaults)
|
||||||
|
|
||||||
|
def get_value_from_html_form(self, form_tag: 'BS4Tag') -> Optional[str]:
|
||||||
|
from .html import get_field_value
|
||||||
|
try:
|
||||||
|
return get_field_value(form_tag, self.name)
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Form:
|
class Form:
|
||||||
|
|
||||||
@ -199,6 +211,14 @@ class Form:
|
|||||||
from requests import post
|
from requests import post
|
||||||
return post(self.url, data=self.get_payload(**kwargs))
|
return post(self.url, data=self.get_payload(**kwargs))
|
||||||
|
|
||||||
|
def check_with_html(self, form_tag: 'BS4Tag', check_defaults: bool = True) -> None:
|
||||||
|
from .html import check_form_interface
|
||||||
|
check_form_interface(form_tag, self, check_defaults=check_defaults)
|
||||||
|
|
||||||
|
def get_values_from_html_form(self, form_tag: 'BS4Tag', required_fields_only: bool = True) -> Dict[str, str]:
|
||||||
|
from .html import get_field_values
|
||||||
|
return get_field_values(form_tag, self, required_fields_only=required_fields_only)
|
||||||
|
|
||||||
|
|
||||||
def load_form(*def_paths: PathT, dir_sort_key: Callable[[PathT], Any] = None, dir_sort_reverse: bool = False,
|
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:
|
full_payload: bool = True, url: str = None) -> Form:
|
||||||
|
156
src/yamlhttpforms/html.py
Normal file
156
src/yamlhttpforms/html.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
from typing import Dict, TYPE_CHECKING
|
||||||
|
from bs4.element import Tag
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .form import FormField, Form
|
||||||
|
|
||||||
|
|
||||||
|
INPUT, SELECT, OPTION = 'input', 'select', 'option'
|
||||||
|
NAME, VALUE, SELECTED = 'name', 'value', 'selected'
|
||||||
|
|
||||||
|
|
||||||
|
class WrongInterface(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FieldInterfaceWrong(WrongInterface):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SelectOptionsWrong(FieldInterfaceWrong):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IncompleteForm(WrongInterface):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownField(WrongInterface):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def check_select_field_options(field_tag: Tag, field_interface: 'FormField', check_defaults: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Compares the `options` and `default` attributes of a `'FormField'` object with the options of its HTML counterpart.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_tag: The `bs4.Tag` object representing the <select> HTML tag
|
||||||
|
field_interface: The `'FormField'` to check against the HTML tag
|
||||||
|
check_defaults (optional): If set to `False`, pre-selected options are not compared to the defined `default`
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`SelectOptionsWrong`
|
||||||
|
if the `options` dictionary is not entirely equal to a dictionary with the <option> tag `values` as keys
|
||||||
|
and their visible text as values
|
||||||
|
`FieldInterfaceWrong`
|
||||||
|
if the `default` is not equal to the value of the <option> tag which has the `selected` attribute
|
||||||
|
"""
|
||||||
|
options = {tag[VALUE]: tag.get_text(strip=True) for tag in field_tag.find_all(OPTION)}
|
||||||
|
if options != field_interface.options:
|
||||||
|
raise SelectOptionsWrong(f"Wrong options in {field_interface}")
|
||||||
|
if check_defaults:
|
||||||
|
default = field_tag.find(lambda tag: tag.name == OPTION and tag.has_attr(SELECTED))[VALUE]
|
||||||
|
if default != field_interface.default:
|
||||||
|
raise FieldInterfaceWrong(f"Default option '{default}' missing for {field_interface}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_field_interface(field_tag: Tag, field_interface: 'FormField', check_defaults: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Compares all attributes of a `'FormField'` object with its HTML counterpart.
|
||||||
|
Calls `check_select_field_options` on select fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_tag: The `bs4.Tag` object representing the <input> or <select> HTML tag
|
||||||
|
field_interface: The `'FormField'` to check against the HTML tag
|
||||||
|
check_defaults (optional): If set to `False`, pre-set field values or pre-selected options are not checked
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`FieldInterfaceWrong`
|
||||||
|
when dealing with an <input> tag, but `options` were defined on the interface, or
|
||||||
|
when the `default` does not match the tag's `value` attribute
|
||||||
|
"""
|
||||||
|
assert field_tag[NAME] == field_interface.name
|
||||||
|
if field_tag.name == INPUT:
|
||||||
|
if field_interface.options is not None:
|
||||||
|
raise FieldInterfaceWrong(f"Options provided for input field '{field_interface.name}'")
|
||||||
|
if check_defaults:
|
||||||
|
default = field_tag.attrs.get(VALUE)
|
||||||
|
if field_interface.default != default:
|
||||||
|
raise FieldInterfaceWrong(f"Input field default not correct on '{field_interface}'")
|
||||||
|
if field_tag.name == SELECT:
|
||||||
|
check_select_field_options(field_tag, field_interface, check_defaults=check_defaults)
|
||||||
|
|
||||||
|
|
||||||
|
def check_form_interface(form_tag: Tag, form_interface: 'Form', check_defaults: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Compares all fields of a `Form` object with the fields found in its HTML counterpart.
|
||||||
|
Calls `check_field_interface` on each field.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
form_tag: The `bs4.Tag` object representing the <form> HTML tag
|
||||||
|
form_interface: The `Form` to check against the HTML tag
|
||||||
|
check_defaults (optional): If set to `False`, pre-set field values or pre-selected options are not checked
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`UnknownField`
|
||||||
|
if any of the fields' name does not correspond to the `name` attribute of a field in the HTML form
|
||||||
|
`IncompleteForm`
|
||||||
|
if a field in the HTML form has not been defined in the form interface
|
||||||
|
"""
|
||||||
|
field_tags: Dict[str, Tag] = {tag[NAME]: tag for tag in form_tag.find_all(INPUT) + form_tag.find_all(SELECT)}
|
||||||
|
for field in form_interface.fields.values():
|
||||||
|
tag = field_tags.pop(field.name)
|
||||||
|
if tag is None:
|
||||||
|
raise UnknownField(f"The defined field does not exist in the form: {field}")
|
||||||
|
check_field_interface(tag, field, check_defaults=check_defaults)
|
||||||
|
if len(field_tags) > 0:
|
||||||
|
raise IncompleteForm(f"Form interface missing fields: {list(field_tags.keys())}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_field_value(form_tag: Tag, field_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Returns the string value of the `value` attribute of a form field with a specified name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
form_tag: The `bs4.Tag` object representing the <form> HTML tag
|
||||||
|
field_name: The value of the `name` attribute of the form field of interest
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`ValueError`
|
||||||
|
if no field with the specified name exists in the HTML form
|
||||||
|
`AttributeError`
|
||||||
|
if the field with the specified name does not have a `value` attribute
|
||||||
|
"""
|
||||||
|
def form_field_has_the_name(tag: Tag):
|
||||||
|
return tag.name in {INPUT, SELECT} and tag.has_attr(NAME) and tag[NAME] == field_name
|
||||||
|
field = form_tag.find(form_field_has_the_name)
|
||||||
|
if field is None:
|
||||||
|
raise ValueError(f"No form field with the name '{field_name}' found")
|
||||||
|
if not field.has_attr(VALUE):
|
||||||
|
raise AttributeError(f"Field with the name '{field_name}' has no `value` attribute")
|
||||||
|
return field[VALUE]
|
||||||
|
|
||||||
|
|
||||||
|
def get_field_values(form_tag: Tag, form_interface: 'Form', required_fields_only: bool = True) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Goes through the fields of a `Form` object and tries to retrieve a value for each from an HTML <form>.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
form_tag: The `bs4.Tag` object representing the <form> HTML tag
|
||||||
|
form_interface: The `Form` for which to get the field values
|
||||||
|
required_fields_only (optional): If `True` (default), values will be retrieved only for the required fields
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with each key-value-pair representing a (required) field for which a `value` attribute existed in the
|
||||||
|
HTML form, with the key being identical to that of the field in the `Form.required_fields` dictionary and the
|
||||||
|
value being the actual `value` attribute's string value
|
||||||
|
"""
|
||||||
|
output_dict = {}
|
||||||
|
for key, field in form_interface.fields.items():
|
||||||
|
if required_fields_only and not field.required:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
output_dict[key] = get_field_value(form_tag, field.name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return output_dict
|
Loading…
Reference in New Issue
Block a user