Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature json schema #56

Merged
merged 6 commits into from
May 1, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add schema; update tests
brynpickering committed Apr 16, 2024

Verified

This commit was signed with the committer’s verified signature.
brynpickering Bryn Pickering
commit ce3f9140982fb0e24ed17eb4cbac453509b8366f
3 changes: 1 addition & 2 deletions src/osmox/build.py
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@

OSMTag = namedtuple("OSMtag", "key value")
OSMObject = namedtuple("OSMobject", "idx, activity_tags, geom")
AVAILABLE_FEATURES = ["area", "levels", "floor_area", "units", "transit_distance"]


class Object:
@@ -62,7 +61,7 @@ def __init__(self, idx, osm_tags, activity_tags, geom) -> None:
self.activity_tags = activity_tags
self.geom = geom
self.activities = None
self.features = {}
self.features: dict = {}

def add_features(self, features):
available = {
65 changes: 25 additions & 40 deletions src/osmox/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import importlib
import json
import logging

from osmox import build
import jsonschema

logger = logging.getLogger(__name__)
SCHEMA_FILE = importlib.resources.files("osmox") / "schema.json"
with importlib.resources.as_file(SCHEMA_FILE) as f:
SCHEMA = json.load(f.open())


def load(config_path):
@@ -12,6 +16,13 @@ def load(config_path):
return json.load(read_file)


def validate(config):
validator = jsonschema.Draft202012Validator
brynpickering marked this conversation as resolved.
Show resolved Hide resolved
validator.META_SCHEMA["unevaluatedProperties"] = False
validator.check_schema(SCHEMA)
jsonschema.validate(config, SCHEMA)


def get_acts(config):
activity_config = config.get("activity_mapping")
if activity_config:
@@ -40,48 +51,22 @@ def get_tags(config):


def validate_activity_config(config):
validate(config)
keys, tags = get_tags(config)
logger.warning(f"Configured OSM tag keys: {sorted(keys)}")
brynpickering marked this conversation as resolved.
Show resolved Hide resolved

filter_config = config.get("filter")
if not filter_config:
logger.error("No 'filter' found in config.")

else:
keys, tags = get_tags(config)
logger.warning(f"Configured OSM tag keys: {sorted(keys)}")

activity_mapping = config.get("activity_mapping")
if activity_mapping:
acts = get_acts(config)
logger.warning(f"Configured activities: {sorted(acts)}")

else:
logger.error("No 'activity_config' found in config.")

if config.get("object_features"):
available = set(build.AVAILABLE_FEATURES)
unsupported = set(config.get("object_features")) - available
if unsupported:
logger.error(
f"Unsupported features in config: {unsupported}, please choose from: {available}."
)
acts = get_acts(config)
logger.warning(f"Configured activities: {sorted(acts)}")
brynpickering marked this conversation as resolved.
Show resolved Hide resolved

if "distance_to_nearest" in config:
acts = get_acts(config=config)
for act in config["distance_to_nearest"]:
if act not in acts:
logger.error(f"'Distance to nearest' has a non-configured activity '{act}'")
act_diff = set(config["distance_to_nearest"]).difference(acts)
if act_diff:
raise ValueError(f"'Distance to nearest' has non-configured activities: {act_diff}")

if "fill_missing_activities" in config:
required_keys = {"area_tags", "required_acts", "new_tags", "size", "spacing"}
acts = get_acts(config=config)

for group in config["fill_missing_activities"]:
keys = list(group)
for k in required_keys:
if k not in keys:
logger.error(f"'Fill missing activities' group is missing required key: {k}")
for act in group.get("required_acts", []):
if act not in acts:
logger.error(
f"'Fill missing activities' group has a non-configured activity '{act}'"
)
act_diff = set(group.get("required_acts", [])).difference(acts)
if act_diff:
raise ValueError(
f"'Fill missing activities' group has non-configured activities: {act_diff}"
)
113 changes: 113 additions & 0 deletions src/osmox/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "OSMOX config schema",
"description": "Schema for the OSMOX config JSON file",
"type": "object",
"additionalProperties": false,
"$defs": {
"stringList": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(\\*|\\w+)$"
}
}
},
"required": ["filter", "activity_mapping", "object_features"],
"properties": {
"filter": {
"type": "object",
"additionalProperties": false,
"patternProperties": { "^(\\*|\\w+)$": { "$ref": "#/$defs/stringList" } }
},
"object_features": {
"type": "array",
"items": {
"type": "string",
"enum": ["area", "levels", "floor_area", "units", "transit_distance"]
}
},
"distance_to_nearest": { "$ref": "#/$defs/stringList" },
"default_tags": { "type": "array", "items": { "$ref": "#/$defs/stringList" } },
"activity_mapping": {
"type": "object",
"require": [],
"additionalProperties": false,
"patternProperties": {
"^(\\*|\\w+)$": {
"type": "object",
"additionalProperties": false,
"patternProperties": { "^(\\*|\\w+)$": { "$ref": "#/$defs/stringList" } }
}
}
},
"fill_missing_activities": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["area_tags", "required_acts", "new_tags"],
"allOf": [
{
"if": { "properties": {"fill_method": { "const": "spacing" } } },
"then": { "required": ["spacing"] }
},
{
"if": { "properties": {"fill_method": { "const": "point_source" } }, "required": ["fill_method"] },
"then": { "required": ["point_source"] },
"else": { "required": ["spacing"] }
}
],
"properties": {
"area_tags": {
"type": "array",
"items": {
"$ref": "#/$defs/stringList"
}
},
"required_acts": {
"oneOf": [
{"type": "string", "pattern": "^(\\*|\\w+)$"},
{"$ref": "#/$defs/stringList"}
]
},
"new_tags": {
"type": "array",
"items": {
"$ref": "#/$defs/stringList"
}
},
"size": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "number",
"exclusiveMinimum": 0
}
},
"spacing": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "number",
"exclusiveMinimum": 0
}
},
"fill_method": {
"type": "string",
"enum": ["spacing", "point_source"]
},
"point_source": {
"type": "string"
},
"max_existing_acts_fraction": {
"type": "number",
"minimum": 0
}
}
}
}
}
}
9 changes: 8 additions & 1 deletion tests/fixtures/test_config_infill.json
Original file line number Diff line number Diff line change
@@ -159,7 +159,13 @@
},
"office": {
"*": ["work"]
}
},
"public_transport": {
brynpickering marked this conversation as resolved.
Show resolved Hide resolved
"*": ["transit"]
},
"highway": {
"bus_stop": ["transit"]
}
},

"fill_missing_activities":
@@ -168,6 +174,7 @@
"area_tags": [["landuse", "residential"]],
"required_acts": ["home"],
"new_tags": [["building", "house"]],
"fill_method": "spacing",
"size": [10, 10],
"spacing": [50, 50]
}
70 changes: 49 additions & 21 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os

import jsonschema
import pytest
from osmox import config

@@ -41,6 +42,21 @@ def valid_config():
}


@pytest.fixture
def strict_schema_validator():
strict = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/strict",
"$ref": "https://json-schema.org/draft/2020-12/schema",
"unevaluatedProperties": False,
}
return jsonschema.Draft202012Validator(strict)


def test_schema(strict_schema_validator):
strict_schema_validator.check_schema(config.SCHEMA)


def test_load_config():
cnfg = config.load(test_config_path)
assert cnfg
@@ -76,40 +92,52 @@ def test_valid_config_logging(caplog, valid_config):
assert "['delivery', 'food_shop', 'home', 'shop', 'social', 'transit', 'work']" in caplog.text


def test_config_with_missing_filter_logging(caplog, valid_config):
def test_config_with_missing_filter_logging(valid_config):
valid_config.pop("filter")
config.validate_activity_config(valid_config)
assert "No 'filter' found in config." in caplog.text
with pytest.raises(
jsonschema.exceptions.ValidationError, match="'filter' is a required property"
):
config.validate_activity_config(valid_config)


def test_config_with_missing_activity_mapping_logging(caplog, valid_config):
def test_config_with_missing_activity_mapping_logging(valid_config):
valid_config.pop("activity_mapping")
config.validate_activity_config(valid_config)
assert "No 'activity_config' found in config." in caplog.text

with pytest.raises(
jsonschema.exceptions.ValidationError, match="'activity_mapping' is a required property"
):
config.validate_activity_config(valid_config)


def test_config_with_unsupported_object_features_logging(caplog, valid_config):
def test_config_with_unsupported_object_features_logging(valid_config):
valid_config["object_features"].append("invalid_feature")
config.validate_activity_config(valid_config)
assert "Unsupported features in config: {'invalid_feature'}," in caplog.text
with pytest.raises(
jsonschema.exceptions.ValidationError, match="'invalid_feature' is not one of"
):
config.validate_activity_config(valid_config)


def test_config_with_unsupported_distance_to_nearest_activity_logging(caplog, valid_config):
def test_config_with_unsupported_distance_to_nearest_activity_logging(valid_config):
valid_config["distance_to_nearest"].append("invalid_activity")
config.validate_activity_config(valid_config)
assert "'Distance to nearest' has a non-configured activity 'invalid_activity'" in caplog.text
with pytest.raises(
ValueError,
match="'Distance to nearest' has non-configured activities: {'invalid_activity'}",
):
config.validate_activity_config(valid_config)


def test_config_with_missing_fill_missing_activities_key_logging(caplog, valid_config):
def test_config_with_missing_fill_missing_activities_key_logging(valid_config):
valid_config["fill_missing_activities"][0].pop("required_acts")
config.validate_activity_config(valid_config)
assert "'Fill missing activities' group is missing required key: required_acts" in caplog.text
with pytest.raises(
jsonschema.exceptions.ValidationError, match="'required_acts' is a required property"
):
config.validate_activity_config(valid_config)


def test_config_with_invalid_activity_for_fill_missing_activities_logging(caplog, valid_config):
def test_config_with_invalid_activity_for_fill_missing_activities_logging(valid_config):
valid_config["fill_missing_activities"][0]["required_acts"].append("invalid_activity")
config.validate_activity_config(valid_config)
assert (
"'Fill missing activities' group has a non-configured activity 'invalid_activity'"
in caplog.text
)
with pytest.raises(
ValueError,
match="'Fill missing activities' group has non-configured activities: {'invalid_activity'}",
):
config.validate_activity_config(valid_config)