def get_ref(*, ref: str, schemas: types.Schemas) -> NameSchema: """ Get the schema referenced by ref. Raises SchemaNotFoundError if a $ref resolution fails. Args: ref: The reference to the schema. schemas: The schemas to use to resolve the ref. Returns: The schema referenced by ref. """ # Check for remote $ref if not ref.startswith("#"): return get_remote_ref(ref=ref) # Checking value of $ref match = _REF_PATTER.match(ref) if not match: raise exceptions.SchemaNotFoundError( f"{ref} format incorrect, expected #/components/schemas/<SchemaName>" ) # Retrieving new schema ref_name = match.group(1) ref_schema = schemas.get(ref_name) if ref_schema is None: raise exceptions.SchemaNotFoundError( f"{ref_name} was not found in schemas.") return ref_name, ref_schema
def _spec_to_schema_name( *, spec: types.AnyUnique, schema_names: typing.Optional[typing.List[str]] = None) -> str: """ Convert a specification to the name of the matched schema. Use the schema names defined in common-schemas.json to find the first matching schema. Args: spec: The specification to convert. schema_names: The names of the schemas to check. Returns: The name of the specification. """ if schema_names is None: schema_names = _COMMON_SCHEMAS.keys() for name in schema_names: try: jsonschema.validate(instance=spec, schema=_COMMON_SCHEMAS[name], resolver=_resolver) return name except jsonschema.ValidationError: continue raise exceptions.SchemaNotFoundError( "Specification did not match any schemas.")
def _add_backref_to_schemas(*, name: str, schemas: types.Schemas, backref: types.Schema, property_name: str) -> None: """ Add backref schema for a model that does not exist to its schema. Raise SchemaNotFoundError if the schema does not exist. Look for model in schemas. If it already has allOf, append backref, otherwise wrap the schema in an allOf with x-backrefs key. Args: name: The name of the model of the backref. schemas: All the model schemas. backref: The schema for the backref. property_name: The name under which to add the schema. """ # Retrieve schema for model schema = schemas.get(name) if schema is None: raise exceptions.SchemaNotFoundError( f"The schema {name} was not found in schemas.") # Calculate the schema addition backref_schema = {"type": "object", "x-backrefs": {property_name: backref}} # Add backref to allOf all_of = schema.get("allOf") if all_of is None: schemas[name] = {"allOf": [schemas[name], backref_schema]} return all_of.append(backref_schema)
def _retrieve_schema(*, schemas: types.Schemas, path: str) -> NameSchema: """ Retrieve schema at a path from schemas. Raise SchemaNotFoundError if the schema is not found at the path. Args: schemas: All the schemas. path: The location to retrieve the schema from. Returns: The schema at the path from the schemas. """ # Strip leading / if path.startswith("/"): path = path[1:] # Get the first directory/file as the head and the remaining path as the tail path_components = path.split("/", 1) try: # Base case, no tail if len(path_components) == 1: return path_components[0], schemas[path_components[0]] # Recursive case, call again with path tail return _retrieve_schema(schemas=schemas[path_components[0]], path=path_components[1]) except KeyError as exc: raise exceptions.SchemaNotFoundError( f"The schema was not found in the remote schemas. Path subsection: {path}" ) from exc
def get_schemas(self, *, context: str) -> types.Schema: """ Retrieve the schemas for a context. Raise MissingArgumentError if the context for the original OpenAPI specification has not been set. Raise SchemaNotFoundError if the context doesn't exist or is not a json nor yaml file. Args: context: The path, relative to the original OpenAPI specification, for the file containing the schemas. Returns: The schemas. """ # Check whether the context is already loaded if context in self._schemas: return self._schemas[context] if self.spec_context is None: raise exceptions.MissingArgumentError( "Cannot find the file containing the remote reference, either " "initialize OpenAlchemy with init_json or init_yaml or pass the path " "to the OpenAPI specification to OpenAlchemy.") # Check for json, yaml or yml file extension _, extension = os.path.splitext(context) extension = extension.lower() if extension not in {".json", ".yaml", ".yml"}: raise exceptions.SchemaNotFoundError( "The remote context is not a JSON nor YAML file. The path is: " f"{context}") # Get context manager with file try: if _URL_REF_PATTERN.search(context) is not None: file_cm = request.urlopen(context) else: spec_dir = os.path.dirname(self.spec_context) remote_spec_filename = os.path.join(spec_dir, context) file_cm = open(remote_spec_filename) except (FileNotFoundError, error.HTTPError) as exc: raise exceptions.SchemaNotFoundError( "The file with the remote reference was not found. The path is: " f"{context}") from exc # Calculate location of schemas with file_cm as in_file: if extension == ".json": try: schemas = json.load(in_file) except json.JSONDecodeError as exc: raise exceptions.SchemaNotFoundError( "The remote reference file is not valid JSON. The path " f"is: {context}") from exc else: # Import as needed to make yaml optional import yaml # pylint: disable=import-outside-toplevel try: schemas = yaml.safe_load(in_file) except yaml.scanner.ScannerError as exc: raise exceptions.SchemaNotFoundError( "The remote reference file is not valid YAML. The path " f"is: {context}") from exc # Store for faster future retrieval self._schemas[context] = schemas return schemas
def _add_remote_context(*, context: str, ref: str) -> str: """ Add remote context to any $ref within a schema retrieved from a remote reference. There are 5 cases: 1. The $ref value starts with # in which case the context is prepended. 2. The $ref starts with a filename in which case only the directory portion of the context is prepended. 3. The $ref starts with a relative path and ends with a file in which case the directory portion of the context is prepended and merged so that the shortest possible relative path is used. 4. The $ref starts with a HTTP protocol, in which case no changes are made. 5. The $ref starts with // in which case the HTTP protocol of the context is prepended. Raise SchemaNotFoundError if the $ref starts with // when the context does not start with a HTTP protocol. After the paths are merged the following operations are done: 1. a normalized relative path is calculated (eg. turning ./dir1/../dir2 to ./dir2) and 2. the case is normalized. Args: context: The context of the document from which the schema was retrieved which is the relative path to the file on the system from the base OpenAPI specification. ref: The value of a $ref within the schema. Returns: The $ref value with the context of the document included. """ # Check for URL reference url_match = _URL_REF_PATTERN.search(ref) if url_match is not None: return ref if ref.startswith("//"): context_protocol = _URL_REF_PATTERN.search(context) if context_protocol is None: raise exceptions.SchemaNotFoundError( "A reference starting with // is only valid from within a document " f"loaded from a URL. The reference is {ref}, the location of the " f"document with the reference is {context}.") return f"{context_protocol.group(1)}{ref}" # Handle reference within document ref_context, ref_schema = _separate_context_path(ref=ref) if not ref_context: return f"{context}{ref}" # Break context into components # Default where context is not a URL context_hostname = "" context_path = context # Gather components if the context is a URL hostname_match = _HOSTNAME_REF_PATTERM.search(context) if hostname_match is not None: context_hostname = hostname_match.group(1) context_path = hostname_match.group(2) context_path_head, _ = os.path.split(context_path) # Handle reference outside document new_ref_context_path = os.path.join(context_path_head, ref_context) norm_new_ref_context_path = _norm_context(context=new_ref_context_path) return f"{context_hostname}{norm_new_ref_context_path}#{ref_schema}"