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}.")
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
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()
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), )
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())
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