Example #1
0
def validate_type(value: Any, correct_type: str) -> bool:
    # `number`s can be `float`s or `int`s but are stored as `float`s.
    if correct_type == "number" and type(value) is int:
        value = float(value)

    # For types bound to native python types, its just a matter of checking `value`'s `type()`
    if correct_type in NATIVE_TYPES_MAPPING.values():
        return type(value) is swap_keys_and_values(
            NATIVE_TYPES_MAPPING)[correct_type]

    # Datetime checking: ISO-8601
    if correct_type in ("date", "datetime", "time"):
        PATTERNS = {
            "date": "YYYY-MM-DDZZ",
            "time": "HH:mm:ssZZ",
            "datetime": "YYYY-MM-DD[T]HH:mm:ssZZ",
        }
        try:
            arrow.get(value, PATTERNS[correct_type])
            return True
        except arrow.parser.ParserError:
            return False

    if correct_type == "slug":
        log.debug('    ' * 2 + 'Slugified value: {}', f'{slugify(value)!r}')
        return type(value) is str and slugify(value) == value

    raise ValueError(f"Can't check for unknown type {correct_type!r}.")
Example #2
0
def add_computed_values_to_request_data(
    resource: ResourceConfig,
    new_data: Dict[str, Any],
    old_data: Dict[str, Any],
    exec_context_data: Dict[str, Any],
) -> Dict[str, Any]:
    new_data = flatten_dict(new_data)
    computed_fields = [f for f in resource.fields if f.computed]
    for field in computed_fields:
        if value_needs_recomputation(field, new_data, old_data):
            # TODO: check if "set" key is in "computation"
            code = field.computation["set"]
            computed = compute_computed_fields(code, context=exec_context_data)
            log.debug("Computed value of field {}: {}", field.name,
                      f"{computed!r}")
            new_data[field.name] = computed
    return new_data
Example #3
0
def run(args: dict):
    bound_address = "%s:%s" % (args["--address"], args["--port"])
    scheme = "http" if args["--address"] in ("localhost",
                                             "127.0.0.1") else "https"
    workers_count = get_workers_count(args["--workers"])
    couchdb = Service("couchdb") if not args['--no-couchdb-start'] else None

    try:
        if couchdb and couchdb.is_running() or args["--force-couchdb-start"]:
            log.info("Starting CouchDB")
            couchdb.start()

        log.info("Spinning up a webserver listening on {}",
                 f"{scheme}://{bound_address}")

        log.debug("Giving {} workers to gunicorn", workers_count)

        project_yaml_files = [
            get_path("endpoints", f)
            for f in listdir(get_path("endpoints")) if f.endswith(".yaml")
        ] + [get_path("types.yaml"),
             get_path("config.yaml")]

        config = {
            "bind": bound_address,
            "workers": workers_count,
            "log-level": "debug" if args["--debug-gunicorn"] else "error",
            "reload": args["--watch"],
            # "reload-extra-file": project_yaml_files if args["--watch"] else None,
        }

        if config["reload"]:
            log.info("Watching for file changes...")

        subprocess.call([
            "poetry", "run", "gunicorn", "restapiboys.server:requests_handler"
        ] + config_dict_to_cli_args(config))
    except KeyboardInterrupt:
        if couchdb and couchdb.is_running():
            log.info("Shutting down CouchDB...")
            couchdb.stop()
Example #4
0
def make_request_with_credentials(
    method: str,
    url: str,
    data: Union[dict, list, None] = None,
    headers: Optional[Dict[str, Any]] = None,
    params: Optional[Dict[str, Any]] = None,
) -> requests.Response:
    headers = headers or {}
    params = params or {"include_docs": True}

    base_url = f"http://127.0.0.1:{COUCHDB_PORT}"
    qs = serialize_query_string(params)
    full_url = base_url + "/" + url
    creds = get_db_credentials()

    log.debug("Requesting CouchDB:")
    log.debug("\t{0} {1}", method, full_url)
    if headers:
        log.verbatim.debug(
            "\t" + "\n\t".join([f"{k}: {v}" for k, v in headers.items()]))
    if data:
        log.verbatim.debug("\t" + json.dumps(data))
    if params:
        log.debug("\t With params {}", qs)

    return requests.request(
        url=full_url,
        method=method,
        json=data,
        headers=headers,
        params=params,
        auth=(creds.username, creds.password),
    )
Example #5
0
def value_needs_recomputation(field: ResourceFieldConfig, new_data: Dict[str,
                                                                         Any],
                              old_data: Dict[str, Any]) -> bool:
    react_on = field.computation["react"]
    # Handle react's special value '*'
    if react_on == "*":
        return True
    # Handle one-item shortcut
    if type(react_on) is not list:
        react_on = [react_on]
    # If any of the fields to react on has its value different in the `new_data`
    # compared to the `old_data`, we need to recompute the field
    # Get the map of field_name: value changed?
    changes = {key: new_data.get(key) != old_data.get(key) for key in react_on}
    log.debug("\tValues that changed:")
    for key, changed in changes.items():
        if changed:
            log.debug(
                "\t- {}: {} ~> {}",
                key,
                f"{new_data.get(key)!r}",
                f"{old_data.get(key)!r}",
            )
    return any(changes.values())
Example #6
0
def validate_request_data(
        req: Request) -> Optional[Tuple[str, Dict[str, Any]]]:
    log.debug("Starting validation")
    resource_id, uuid = extract_uuid_from_path(req.route) or (req.route, None)
    resource = get_resource_config_of_route(resource_id)
    fields_by_name = {field.name: field for field in resource.fields}
    # If the request has no associated resource config, this is a custom route.
    # Skip traditional validation, go straigth to custom validators
    if not resource:
        return None
    if req.method in BODYLESS_REQUEST_METHODS:
        return None
    # 1. Check if its well-formed JSON
    try:
        req_data: dict = json.loads(req.body)
    except JSONDecodeError:
        return "The JSON request body is malformed", {}
    log.debug("Request is well-formed JSON")

    # 3. For inserting NEW objects, check if the required fields are there
    if req.method in ("POST", "PUT"):
        missing_fields = []
        required_fields = [f for f in resource.fields if f.required]
        log.debug("Got required fields to test for: {}",
                  [f.name for f in required_fields])
        for field in required_fields:
            # TODO: handle fieldname.subfieldname (nested objects)
            if field.name not in req_data.keys():
                missing_fields.append(field.name)

        if missing_fields:
            return "Some fields are missing", {
                "missing_fields": missing_fields
            }

    # 4. To _modify_ objects, check that we aren't trying to modify read-only field
    if req.method in ('PATCH', 'PUT', 'POST'):
        readonly_fields_names = {
            f.name
            for f in resource.fields if f.read_only
        }
        request_fields_names = {name for name, value in req_data.items()}
        readonly_fields_in_request = list(readonly_fields_names
                                          & request_fields_names)

        if readonly_fields_in_request:
            return "Some fields are read-only", {
                'readonly_fields': list(readonly_fields_names),
                'fields_to_remove': readonly_fields_in_request
            }

    # 4. Check the type of each field
    log.debug("Checking types")
    fields_with_wrong_types = []
    unknown_fields = []
    for name, value in req_data.items():
        # Get the field configuration
        field = fields_by_name.get(name)
        if field is None:
            log.debug("    Request field {} is unknown", name)
            unknown_fields.append(name)
            continue
        # Check the type
        log.debug(
            "    Checking if type of {0} (with value {1}) is {2}",
            field.name,
            repr(value),
            field.type,
        )
        # Check for `multiple` types
        if field.multiple:
            if type(value) is not list:
                validated = False
            else:
                validated = all(
                    (validate_type(item, field.type) for item in value))
        else:
            validated = validate_type(value, field.type)

        if not validated:
            fields_with_wrong_types.append(field)
    if fields_with_wrong_types:
        return (
            "Some fields' values are of the wrong type",
            {
                "correct_types_for_fields": {
                    field.name: field.type + ("[]" if field.multiple else "")
                    for field in fields_with_wrong_types
                }
            },
        )
    if unknown_fields:
        return ("Unknown fields in request", {
            "unknown_fields": unknown_fields,
        })

    # 5. Check max/min (length)
    for name, value in req_data.items():
        # Get the field configuration
        field = fields_by_name.get(name)
        # Check for min/max values
        # TODO: Move `return message, data` to `validate_*` functions
        if field.minimum is not None or field.maximum is not None:
            log.debug(
                "    Checking for bounds of {0}: {1} ∈ [{2}, {3}]",
                repr(value),
                name,
                (field.minimum if field.minimum is not None else "-∞"),
                (field.maximum if field.maximum is not None else "+∞"),
            )
        if not validate_max_min(value, field.minimum, field.maximum):
            return (
                f"`{name}` is out of bounds",
                {
                    "actual_value": value,
                    "minimum_value": field.minimum,
                    "maximum_value": field.maximum,
                },
            )
        # If we don't allow empty values, the min length is one.
        if not field.allow_empty and (field.type in ("string")
                                      or field.multiple):
            field = field._replace(min_length=1)
        if field.min_length is not None or field.max_length is not None:
            log.debug(
                "    Checking for length of {0}: len({1}) ∈ [{2}, {3}]",
                repr(value),
                name,
                (field.min_length if field.min_length is not None else "-∞"),
                (field.max_length if field.max_length is not None else "+∞"),
            )
        if not validate_max_min_length(value, field.min_length,
                                       field.max_length):
            return (
                f"`{name}` is either too long or too short",
                {
                    "actual_length": len(value),
                    "minimum_length": (field.min_length or 0),
                    "maximum_length": field.max_length,
                },
            )

    # 6. Check

    return None