Skip to content

Commit a656fa3

Browse files
authored
Use referencing library (#241)
* use referencing * update tests * types * mypy * mypy * mypy * pre-commit * add mypy,ini * revert * update workflow * update changelog
1 parent 7983a81 commit a656fa3

11 files changed

+291
-209
lines changed

.github/workflows/test-runner.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
with:
2626
python-version: ${{ matrix.python-version }}
2727

28-
- name: Run unit tests
28+
- name: Run mypy
2929
run: |
3030
pip install .
3131
pip install -r requirements-dev.txt

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ repos:
1212
rev: 24.1.1
1313
hooks:
1414
- id: black
15-
language_version: python3.10
15+
# language_version: python3.11
1616
- repo: https://github.com/pre-commit/mirrors-mypy
1717
rev: v1.8.0
1818
hooks:

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
1212
- Configure whether to open URLs when validating assets [#238](https://github.com/stac-utils/stac-validator/pull/238)
1313
- Allow to provide HTTP headers [#239](https://github.com/stac-utils/stac-validator/pull/239)
1414

15+
### Changed
16+
17+
- Switched to the referencing library for dynamic JSON schema validation and reference resolution [#241](https://github.com/stac-utils/stac-validator/pull/241)
18+
1519
## [v3.4.0] - 2024-10-08
1620

1721
### Added

setup.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@
2626
long_description_content_type="text/markdown",
2727
url="https://github.com/stac-utils/stac-validator",
2828
install_requires=[
29-
"requests>=2.19.1",
30-
"jsonschema>=3.2.0",
31-
"click>=8.0.0",
29+
"requests>=2.32.3",
30+
"jsonschema>=4.23.0",
31+
"click>=8.1.8",
32+
"referencing>=0.35.1",
3233
],
3334
extras_require={
3435
"dev": [

stac_validator/utilities.py

+95-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import functools
22
import json
33
import ssl
4-
from typing import Dict
4+
from typing import Dict, Optional
55
from urllib.parse import urlparse
66
from urllib.request import Request, urlopen
77

8+
import jsonschema
89
import requests # type: ignore
10+
from jsonschema import Draft202012Validator
11+
from referencing import Registry, Resource
12+
from referencing.jsonschema import DRAFT202012
13+
from referencing.retrieval import to_cached_resource
14+
from referencing.typing import URI
915

1016
NEW_VERSIONS = [
1117
"1.0.0-beta.2",
@@ -77,7 +83,7 @@ def get_stac_type(stac_content: Dict) -> str:
7783
return str(e)
7884

7985

80-
def fetch_and_parse_file(input_path: str, headers: Dict = {}) -> Dict:
86+
def fetch_and_parse_file(input_path: str, headers: Optional[Dict] = None) -> Dict:
8187
"""Fetches and parses a JSON file from a URL or local file.
8288
8389
Given a URL or local file path to a JSON file, this function fetches the file,
@@ -184,3 +190,90 @@ def link_request(
184190
else:
185191
initial_message["request_invalid"].append(link["href"])
186192
initial_message["format_invalid"].append(link["href"])
193+
194+
195+
def fetch_remote_schema(uri: str) -> dict:
196+
"""
197+
Fetch a remote schema from a URI.
198+
199+
Args:
200+
uri (str): The URI of the schema to fetch.
201+
202+
Returns:
203+
dict: The fetched schema content as a dictionary.
204+
205+
Raises:
206+
requests.RequestException: If the request to fetch the schema fails.
207+
"""
208+
response = requests.get(uri)
209+
response.raise_for_status()
210+
return response.json()
211+
212+
213+
@to_cached_resource() # type: ignore
214+
def cached_retrieve(uri: URI) -> str:
215+
"""
216+
Retrieve and cache a remote schema.
217+
218+
Args:
219+
uri (str): The URI of the schema.
220+
221+
Returns:
222+
str: The raw JSON string of the schema.
223+
224+
Raises:
225+
requests.RequestException: If the request to fetch the schema fails.
226+
Exception: For any other unexpected errors.
227+
"""
228+
try:
229+
response = requests.get(uri, timeout=10) # Set a timeout for robustness
230+
response.raise_for_status() # Raise an error for HTTP response codes >= 400
231+
return response.text
232+
except requests.exceptions.RequestException as e:
233+
raise requests.RequestException(
234+
f"Failed to fetch schema from {uri}: {str(e)}"
235+
) from e
236+
except Exception as e:
237+
raise Exception(
238+
f"Unexpected error while retrieving schema from {uri}: {str(e)}"
239+
) from e
240+
241+
242+
def validate_with_ref_resolver(schema_path: str, content: dict) -> None:
243+
"""
244+
Validate a JSON document against a JSON Schema with dynamic reference resolution.
245+
246+
Args:
247+
schema_path (str): Path or URI of the JSON Schema.
248+
content (dict): JSON content to validate.
249+
250+
Raises:
251+
jsonschema.exceptions.ValidationError: If validation fails.
252+
requests.RequestException: If fetching a remote schema fails.
253+
FileNotFoundError: If a local schema file is not found.
254+
Exception: If any other error occurs during validation.
255+
"""
256+
# Load the schema
257+
if schema_path.startswith("http"):
258+
schema = fetch_remote_schema(schema_path)
259+
else:
260+
try:
261+
with open(schema_path, "r") as f:
262+
schema = json.load(f)
263+
except FileNotFoundError as e:
264+
raise FileNotFoundError(f"Schema file not found: {schema_path}") from e
265+
266+
# Set up the resource and registry for schema resolution
267+
resource: Resource = Resource(contents=schema, specification=DRAFT202012) # type: ignore
268+
registry: Registry = Registry(retrieve=cached_retrieve).with_resource( # type: ignore
269+
uri=schema_path, resource=resource
270+
) # type: ignore
271+
272+
# Validate the content against the schema
273+
try:
274+
validator = Draft202012Validator(schema, registry=registry)
275+
validator.validate(content)
276+
except jsonschema.exceptions.ValidationError as e:
277+
raise jsonschema.exceptions.ValidationError(f"{e.message}") from e
278+
except Exception as e:
279+
raise Exception(f"Unexpected error during validation: {str(e)}") from e

0 commit comments

Comments
 (0)