├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── rocketchat-notifications-feedback-requests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── COPYRIGHTS.md
├── Dockerfile
├── Jenkinsfile
├── LICENSE.txt
├── README.md
├── config
└── sample.config.yaml
├── docker-compose.yml
├── docs
├── api.md
├── configuration.md
├── development_setup.md
├── faq.md
├── img
│ ├── UML-Class-Diagram.drawio
│ ├── UML-Class-Diagram.png
│ ├── UML-Component-Diagram.drawio
│ ├── UML-Component-Diagram.png
│ ├── UML-Sequence-Diagram.drawio
│ ├── UML-Sequence-Diagram.png
│ ├── oqt_website_step1.png
│ └── oqt_website_step2.png
├── indicator.md
├── indicators.md
├── topic.md
└── vector_datasets.md
├── ohsome_quality_api
├── __init__.py
├── api
│ ├── __init__.py
│ ├── api.py
│ ├── request_context.py
│ ├── request_models.py
│ ├── response_models.py
│ └── static
│ │ ├── favicon-32x32.png
│ │ ├── redoc.standalone.js
│ │ ├── swagger-ui-bundle.js
│ │ └── swagger-ui.css
├── attributes
│ ├── __init__.py
│ ├── attributes.yaml
│ ├── definitions.py
│ └── models.py
├── config.py
├── definitions.py
├── geodatabase
│ ├── __init__.py
│ ├── client.py
│ ├── regions_as_geojson.sql
│ ├── select_coverage.sql
│ ├── select_intersection.sql
│ └── select_shdi.sql
├── indicators
│ ├── __init__.py
│ ├── attribute_completeness
│ │ ├── __init__.py
│ │ ├── indicator.py
│ │ └── templates.yaml
│ ├── base.py
│ ├── building_comparison
│ │ ├── __init__.py
│ │ ├── datasets.yaml
│ │ ├── indicator.py
│ │ ├── query.sql
│ │ └── templates.yaml
│ ├── currentness
│ │ ├── __init__.py
│ │ ├── indicator.py
│ │ ├── templates.yaml
│ │ └── thresholds.yaml
│ ├── definitions.py
│ ├── indicators.yaml
│ ├── land_cover_thematic_accuracy
│ │ ├── __init__.py
│ │ ├── indicator.py
│ │ ├── query-multi-classes.sql
│ │ ├── query-single-class.sql
│ │ └── templates.yaml
│ ├── mapping_saturation
│ │ ├── __init__.py
│ │ ├── indicator.py
│ │ ├── models.py
│ │ ├── ssdoubles.R
│ │ └── templates.yaml
│ ├── minimal
│ │ ├── __init__.py
│ │ ├── indicator.py
│ │ └── templates.yaml
│ ├── models.py
│ └── road_comparison
│ │ ├── __init__.py
│ │ ├── datasets.yaml
│ │ ├── indicator.py
│ │ ├── query.sql
│ │ └── templates.yaml
├── ohsome
│ ├── __init__.py
│ └── client.py
├── oqt.py
├── projects
│ ├── __init__.py
│ ├── definitions.py
│ ├── models.py
│ └── projects.yaml
├── quality_dimensions
│ ├── __init__.py
│ ├── definitions.py
│ ├── models.py
│ └── quality_dimensions.yaml
├── topics
│ ├── __init__.py
│ ├── definitions.py
│ ├── models.py
│ └── presets.yaml
└── utils
│ ├── __init__.py
│ ├── exceptions.py
│ ├── helper.py
│ ├── helper_asyncio.py
│ ├── helper_geo.py
│ └── validators.py
├── pipeline_config.groovy
├── pyproject.toml
├── regression-tests
├── README.md
├── __run_hurl_tests_for_stage.sh
├── building-comparison.hurl
├── building-comparison.json
├── buildingcount_bbox.json
├── buildingcount_bbox_attributecompleteness.hurl
├── buildingcount_bbox_currentness.hurl
├── buildingcount_bbox_housenumber.json
├── hospitals_adminarea.json
├── hospitals_adminarea_mappingsaturation__no_features.hurl
├── long-running
│ ├── roads_bbox.json
│ └── roads_bbox_currentness.hurl
├── metadata.hurl
├── road-comparison.hurl
├── road-comparison.json
├── roads_polygon.json
├── roads_polygon_attributecompleteness.hurl
├── roads_polygon_mappingsaturation.hurl
├── roads_polygon_maxspeed.json
├── run_hurl_tests_DEV.sh
├── run_hurl_tests_PROD.sh
└── run_hurl_tests_TEST.sh
├── scripts
├── create_new_github_release.sh
├── functions.sh
├── release.sh
├── run_mapping_saturation_models.py
└── update_swagger_scripts.sh
├── sonar-project.properties
├── tests
├── __init__.py
├── approvals
│ └── integrationtests
│ │ ├── api
│ │ ├── test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt
│ │ ├── test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt
│ │ ├── test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt
│ │ ├── test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt
│ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt
│ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt
│ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt
│ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].received.txt
│ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt
│ │ ├── test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].received.txt
│ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt
│ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt
│ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt
│ │ ├── test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt
│ │ ├── test_indicators_land_cover_thematic_accuracy.py-test_invalid_class.approved.json
│ │ └── test_indicators_land_cover_thematic_accuracy.py-test_invalid_topic.approved.json
│ │ └── indicators
│ │ ├── test_attribute_completeness.py-TestCalculation-test_calculate[indicator0].approved.txt
│ │ ├── test_attribute_completeness.py-TestCalculation-test_calculate[indicator1].approved.txt
│ │ ├── test_attribute_completeness.py-TestFigure-test_create_figure[indicator0].approved.json
│ │ ├── test_attribute_completeness.py-TestFigure-test_create_figure[indicator1].approved.json
│ │ ├── test_attribute_completeness.py-test_create_description_attribute_filter.approved.txt
│ │ ├── test_attribute_completeness.py-test_create_description_attribute_keys_multiple.approved.txt
│ │ ├── test_attribute_completeness.py-test_create_description_attribute_keys_single.approved.txt
│ │ ├── test_building_comparison.py-TestCalculate-test_calculate.approved.txt
│ │ ├── test_building_comparison.py-TestCalculate-test_calculate_above_one_th.approved.txt
│ │ ├── test_building_comparison.py-TestCalculate-test_calculate_above_one_th_and_expected.approved.txt
│ │ ├── test_building_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt
│ │ ├── test_building_comparison.py-TestCalculate-test_calculate_some_intersection.approved.txt
│ │ ├── test_building_comparison.py-TestFigure-test_create_figure.approved.json
│ │ ├── test_building_comparison.py-TestFigure-test_create_figure_above_one_th.approved.json
│ │ ├── test_building_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json
│ │ ├── test_currentness.py-TestCalculation-test_calculate.approved.txt
│ │ ├── test_currentness.py-TestCalculation-test_low_contributions.approved.txt
│ │ ├── test_currentness.py-TestCalculation-test_months_without_edit.approved.txt
│ │ ├── test_currentness.py-TestCalculation-test_no_amenities.approved.txt
│ │ ├── test_currentness.py-TestCalculation-test_no_subway_stations.approved.txt
│ │ ├── test_currentness.py-TestFigure-test_create_figure.approved.json
│ │ ├── test_currentness.py-TestFigure-test_outdated_features_plotting.approved.txt
│ │ ├── test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers0-schema0.approved.txt
│ │ ├── test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers1-schema1.approved.txt
│ │ ├── test_land-cover-thematic-accuracy.py-test_calculate.approved.txt
│ │ ├── test_land-cover-thematic-accuracy.py-test_calculate.received.txt
│ │ ├── test_land_cover_thematic_accuracy.py-test_calculate_multi_class
│ │ ├── description.approved.txt
│ │ └── report.approved.txt
│ │ ├── test_land_cover_thematic_accuracy.py-test_calculate_single_class
│ │ ├── description.approved.txt
│ │ └── report.approved.txt
│ │ ├── test_land_cover_thematic_accuracy.py-test_figure_multi_class.approved.json
│ │ ├── test_land_cover_thematic_accuracy.py-test_figure_single_class.approved.json
│ │ ├── test_mapping_saturation.py-TestCalculation-test_calculate.approved.txt
│ │ ├── test_mapping_saturation.py-TestFigure-test_create_figure.approved.json
│ │ ├── test_road_comparison.py-TestCalculate-test_calculate.approved.txt
│ │ ├── test_road_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt
│ │ ├── test_road_comparison.py-TestCalculate-test_calculate_reference_lenght_0.approved.txt
│ │ ├── test_road_comparison.py-TestFigure-test_create_figure.approved.json
│ │ └── test_road_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json
├── approvaltests_namers.py
├── approvaltests_reporters.py
├── approvaltests_scrubbers.py
├── conftest.py
├── fixtures
│ ├── empty.png
│ ├── feature-collection-germany-heidelberg.geojson
│ ├── feature-collection-heidelberg-bahnstadt-bergheim-weststadt.geojson
│ ├── feature-germany-berlin-friedrichshain-kreuzberg.geojson
│ ├── feature-germany-heidelberg.geojson
│ ├── feature-malta.geojson
│ ├── land-cover-thematic-accuracy-db-fetch-results.json
│ └── land-cover-thematic-accuracy-single-class-db-fetch-results.json
├── integrationtests
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── response_schema.py
│ │ ├── test_indicators.py
│ │ ├── test_indicators_attribute_completeness.py
│ │ ├── test_indicators_land_cover_thematic_accuracy.py
│ │ ├── test_indicators_mapping_saturation_data.py
│ │ ├── test_metadata.py
│ │ ├── test_metadata_attributes.py
│ │ ├── test_metadata_indicators.py
│ │ ├── test_metadata_projects.py
│ │ ├── test_metadata_quality_dimensions.py
│ │ ├── test_metadata_topics.py
│ │ ├── test_ohsome_timeout.py
│ │ └── test_response_models.py
│ ├── conftest.py
│ ├── fixtures
│ │ ├── algeria-touggourt-feature.geojson
│ │ ├── algeria-touggourt-hexcells.geojson
│ │ ├── europe.geojson
│ │ ├── heidelberg-altstadt-feature.geojson
│ │ ├── niger-kanan-bakache.geojson
│ │ ├── rasters
│ │ │ ├── GHS_BUILT_LDS2014_GLOBE_R2018A_54009_1K_V2_0.tif
│ │ │ ├── GHS_POP_E2015_GLOBE_R2019A_54009_1K_V1_0.tif
│ │ │ ├── GHS_SMOD_POP2015_GLOBE_R2019A_54009_1K_V2_0.tif
│ │ │ └── VNL_v2_npp_2020_global_vcmslcfg_c202102150000.average_masked.tif
│ │ └── vcr_cassettes
│ │ │ ├── api
│ │ │ ├── test_indicators.yaml
│ │ │ ├── test_indicators_attribute_completeness.yaml
│ │ │ └── test_response_models.yaml
│ │ │ ├── indicators
│ │ │ ├── test_attribute_completeness.yaml
│ │ │ ├── test_building_comparison.yaml
│ │ │ ├── test_currentness.yaml
│ │ │ ├── test_mapping_saturation.yaml
│ │ │ └── test_minimal.yaml
│ │ │ ├── test_ohsome_client.yaml
│ │ │ └── test_oqt.yaml
│ ├── indicators
│ │ ├── __init__.py
│ │ ├── test_attribute_completeness.py
│ │ ├── test_building_comparison.py
│ │ ├── test_currentness.py
│ │ ├── test_land_cover_thematic_accuracy.py
│ │ ├── test_mapping_saturation.py
│ │ ├── test_minimal.py
│ │ └── test_road_comparison.py
│ ├── test_base_indicator.py
│ ├── test_geodatabase.py
│ ├── test_ohsome_client.py
│ ├── test_oqt.py
│ ├── test_postgres.py
│ └── utils.py
└── unittests
│ ├── __init__.py
│ ├── api
│ ├── __init__.py
│ ├── test_request_models.py
│ ├── test_request_models_data.py
│ └── test_response_models.py
│ ├── fixtures
│ ├── GHS_BUILT_R2018A-Heidelberg.tif
│ ├── config.yaml
│ ├── heidelberg-altstadt-feature.geojson
│ ├── heidelberg-altstadt-featurecollection.geojson
│ ├── heidelberg-altstadt-wsf2019.tif
│ ├── nodata.tif
│ ├── ohsome-response-200-invalid.geojson
│ ├── ohsome-response-200-valid.geojson
│ └── ohsome-response-400-time.json
│ ├── mapping_saturation
│ ├── __init__.py
│ ├── fixtures.py
│ └── test_models.py
│ ├── test_api.py
│ ├── test_attribute_definitions.py
│ ├── test_attributes.py
│ ├── test_config.py
│ ├── test_definitions.py
│ ├── test_helper.py
│ ├── test_helper_asyncio.py
│ ├── test_helper_geo.py
│ ├── test_indicators_definitions.py
│ ├── test_load_metadata.py
│ ├── test_logging.py
│ ├── test_ohsome_client.py
│ ├── test_project.py
│ ├── test_project_definitions.py
│ ├── test_quality_dimension.py
│ ├── test_quality_dimension_definitions.py
│ ├── test_topic.py
│ ├── test_topics_definitions.py
│ ├── test_validators.py
│ ├── test_version.py
│ └── utils.py
└── uv.lock
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Please add the **type of change** as label. If your PR is not ready for review and merge, please add `🚧 ` to the PR title.
2 |
3 | ### Description
4 | Please add a clear and concise description of what your PR solves.
5 |
6 | ### Corresponding issue
7 | Closes #
8 |
9 | ### New or changed dependencies
10 | -
11 |
12 | ### Checklist
13 | - [ ] I have ensured my branch is mergeable with `main` (e.g. through `git rebase main` if necessary)
14 | - [ ] My code follows the [style guide](https://github.com/GIScience/ohsome-quality-api/blob/main/CONTRIBUTING.md#style-guide) and was checked with [pre-commit](https://github.com/GIScience/ohsome-quality-api/blob/main/CONTRIBUTING.md#pre-commit) before committing
15 | - [ ] I have commented my code
16 | - [ ] I have added sufficient unit and integration [tests](https://github.com/GIScience/ohsome-quality-api/blob/main/docs/development_setup.md#tests)
17 | - [ ] I have added new Hurl regression tests and checked all existing [tests](https://github.com/GIScience/ohsome-quality-api/blob/main/regression-tests/README.md)
18 | - [ ] I have updated the [CHANGELOG.md](https://github.com/GIScience/ohsome-quality-api/blob/main/CHANGELOG.md)
19 |
20 | Please check all finished tasks. If some tasks do not apply to your PR, please cross their text out (by using `~...~`) and remove their checkboxes.
21 |
--------------------------------------------------------------------------------
/.github/workflows/rocketchat-notifications-feedback-requests.yml:
--------------------------------------------------------------------------------
1 | name: Rocket.Chat Pull Request push notifications
2 |
3 | on:
4 | pull_request:
5 | branches: [ master, main ]
6 | types: [ labeled ]
7 | issues:
8 | types: [ labeled ]
9 |
10 | jobs:
11 | rocketchat_pullrequest_notification:
12 | name: Someone labeled their Pull Request
13 | runs-on: ubuntu-latest
14 | if: ${{ github.event.pull_request && contains('comments welcome|help wanted|waiting for review', github.event.label.name) }}
15 | steps:
16 | - name: Push notification for a Pull Request labeled as "comments welcome"
17 | if: ${{ github.event.label.name == 'comments welcome' }}
18 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} would like to get feedback on pull request ${{ github.event.pull_request.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}"
19 | - name: Push notification for a Pull Request labeled as "help wanted"
20 | if: ${{ github.event.label.name == 'help wanted' }}
21 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} needs help with pull request ${{ github.event.pull_request.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}"
22 | - name: Push notification for a Pull Request labeled as "waiting for review"
23 | if: ${{ github.event.label.name == 'waiting for review' }}
24 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} is waiting for a code review of pull request ${{ github.event.pull_request.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}"
25 | rocketchat_issue_notification:
26 | name: Someone labeled their Issue
27 | runs-on: ubuntu-latest
28 | if: ${{ github.event.issue && contains('comments welcome|help wanted', github.event.label.name) }}
29 | steps:
30 | - name: Push notification for an Issue labeled as "comments welcome"
31 | if: ${{ github.event.label.name == 'comments welcome' }}
32 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} would like to get feedback on issue ${{ github.event.issue.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}"
33 | - name: Push notification for an Issue labeled as "help wanted"
34 | if: ${{ github.event.label.name == 'help wanted' }}
35 | run: curl "${{ secrets.ROCKETCHAT_SERVER }}/hooks/${{ secrets.ROCKETCHAT_HOOK_PR_NOTIFICATIONS }}" -d "text=${{ github.event.sender.login }} needs help with issue ${{ github.event.issue.html_url }}" -d "username=${{ github.event.sender.login }}" -d "avatar=${{ github.event.sender.avatar_url }}"
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv*/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # pyenv config
117 | .python-version
118 |
119 | # Spyder project settings
120 | .spyderproject
121 | .spyproject
122 |
123 | # Rope project settings
124 | .ropeproject
125 |
126 | # mkdocs documentation
127 | /site
128 |
129 | # mypy
130 | .mypy_cache/
131 | .dmypy.json
132 | dmypy.json
133 |
134 | # Pyre type checker
135 | .pyre/
136 |
137 | # pytype static type analyzer
138 | .pytype/
139 |
140 | # Cython debug symbols
141 | cython_debug/
142 |
143 | # Pycharm
144 | .idea/
145 |
146 | # Ruff
147 | .ruff_cache/
148 |
149 | # oqapi
150 | /data/*
151 | /config/*
152 | !/config/sample.config.yaml
153 | /tests/*/fixtures/approved/*.received.txt
154 | /regression-tests/report/
155 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/astral-sh/ruff-pre-commit
3 | # Ruff version.
4 | rev: v0.11.9
5 | hooks:
6 | # Run the linter.
7 | - id: ruff
8 | # Run the formatter.
9 | - id: ruff-format
10 | args: ["--check", "--diff"]
11 | - repo: https://github.com/pre-commit/pre-commit-hooks
12 | rev: v4.4.0
13 | hooks:
14 | - id: check-added-large-files
15 | args: ["--maxkb=1024"]
16 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## How to Contribute
4 |
5 | Please contribute to this repository through creating [issues](https://github.com/GIScience/ohsome-quality-api/issues/new) and [pull requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests).
6 |
7 |
8 | ### Issues
9 |
10 | Bugs reports and enhancement suggestions are tracked via issues. Each issue can contain following information:
11 |
12 | - A clear and descriptive title
13 | - Description
14 | - Current behavior and expected behavior
15 | - Error message and stack trace
16 |
17 | Issues should serve as the basis for creating a merge request. They should have at least one tag associated with them.
18 |
19 |
20 | ### Merge Requests
21 |
22 | Merge requests are created to address one single issue or multiple. Either the assignee or the creator of the merge request is responsible for merging.
23 | Each merge request has to be approved by at least one reviewer before merging it. A person can be assigned as reviewer by either mark them as such or asking for a review by tagging the person in the description/comment of the merge request.
24 |
25 | A merge request can be made even if the branch is not ready to yet to be merged. When doing so please mark them as Work-in-Progress by writing `WIP` at the beginning of the title. This way people/reviewer know that currently someone is working to address an issue. This gives them the opportunity share their thoughts knowing that the merge request is still subject to change and does not need a full review yet.
26 |
27 | The [CHANGELOG.md](CHANGELOG.md) describes changes made in a merge request. It should contain a short description of the performed changes, as well as (a) link(s) to issue(s) or merge request.
28 |
29 |
30 | #### Review Process
31 |
32 | 1. Dev makes a PR/MR.
33 | 2. Rev reviews and raises some comments.
34 | 3. Dev addresses the comments and leaves responses explaining what has to be done. In cases where Dev just implemented Rev's suggestion, a simple "Done" is sufficient.
35 | 4. Rev reviews the changes and
36 | - If Rev is happy with a change, then Rev resolves the comment.
37 | - If Rev is still unsatisfied with a change, then Rev adds another comment explaining what is still missing.
38 | 5. Restart from 3 until all comments are resolved.
39 |
40 |
41 | ### Git Workflow
42 |
43 | All development work is based on the main branch (`main`). Merge requests are expected to target the main branch.
44 |
45 |
46 | ## Style Guide
47 |
48 | ### Tools
49 |
50 | This project uses [`ruff`](https://github.com/astral-sh/ruff) to ensure consistent code style. See the `pyproject.toml` file for configuration.
51 |
52 |
53 | #### A Note on the Configuration
54 |
55 | ```bash
56 | uv run ruff format .
57 | uv run ruff check --fix .
58 | ```
59 |
60 |
61 | ### Pre-Commit
62 |
63 | In addition, [pre-commit](https://pre-commit.com/) is set up to run those tools prior to any git commit. In contrast to above described commands running these hooks will not apply any changes to the code base. Instead, 'pre-commit' checks if there would be any changes to be made. In that case simply run above commands manually.
64 |
65 |
66 | ## Tests
67 |
68 | Please provide [tests](/docs/development_setup.md#tests).
69 |
70 |
71 | ## Miscellaneous
72 |
73 | - How are indicators structured? -> [docs/indicator.md](/docs/indicator.md).
74 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # build python app
2 | FROM python:3.13-slim AS builder
3 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
4 |
5 | WORKDIR /app
6 |
7 | # install R (with build dependencies)
8 | RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
9 | echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
10 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
11 | --mount=type=cache,target=/var/lib/apt,sharing=locked \
12 | apt update \
13 | && apt install -y --no-upgrade --no-install-recommends \
14 | r-base-dev \
15 | build-essential
16 |
17 | ENV UV_LINK_MODE=copy \
18 | UV_HTTP_TIMEOUT=300
19 |
20 | # install only the dependencies
21 | RUN --mount=type=cache,target=/root/.cache/uv \
22 | --mount=type=bind,source=uv.lock,target=uv.lock \
23 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
24 | uv sync --locked --no-install-project --no-editable --no-dev
25 |
26 | # add project files and install the project
27 | COPY ohsome_quality_api ohsome_quality_api
28 |
29 | RUN --mount=type=cache,target=/root/.cache/uv \
30 | --mount=type=bind,source=uv.lock,target=uv.lock \
31 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
32 | uv sync --locked --no-editable --no-dev
33 |
34 | FROM python:3.13-slim
35 |
36 | WORKDIR /app
37 |
38 | ENV VIRTUAL_ENV=/app/.venv \
39 | PATH="/app/.venv/bin:$PATH"
40 |
41 | # install R (without build dependencies)
42 | RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
43 | echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
44 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
45 | --mount=type=cache,target=/var/lib/apt,sharing=locked \
46 | apt update \
47 | && apt install -y --no-upgrade --no-install-recommends \
48 | r-base
49 |
50 | # copy environment but not the source code
51 | COPY --from=builder --chown=app:app $VIRTUAL_ENV $VIRTUAL_ENV
52 |
53 | EXPOSE 8000/tcp
54 |
55 | # run the application
56 | CMD ["/app/.venv/bin/uvicorn","ohsome_quality_api.api.api:app","--host","0.0.0.0"]
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ohsome quality API
2 |
3 | [](https://jenkins.heigit.org/job/OQAPI/job/main/)
4 | [](https://sonarcloud.io/dashboard?id=ohsome-quality-api)
5 | [](https://hub.docker.com/r/heigit/ohsome-quality-api)
6 | [](LICENSE.txt)
7 | [](https://dashboard.ohsome.org/#backend=oqtApi)
8 | [](https://github.com/GIScience/badges#active)
9 |
10 | The ohsome quality API computes and provides data quality estimations (indicators) for OpenStreetMap.
11 |
12 | Thanks to the [ohsome dashboard](https://dashboard.ohsome.org) generating quality estimations of OpenStreetMap data for an arbitrary region is as easy as pie.
13 |
14 | The software is developed by [Heidelberg Institute for Geoinformation Technology (HeiGIT)](https://heigit.org/) and based on the [ohsome platform](https://heigit.org/big-spatial-data-analytics-en/ohsome/).
15 |
16 | ## Usage
17 |
18 | ### Dashboard
19 |
20 | For quick insights, you can start exploring quality estimations through the [ohsome dashboard](https://dashboard.ohsome.org).
21 |
22 | ### API
23 |
24 | Check out the interactive [Swagger API documentation](https://api.quality.ohsome.org/v1/docs) and the [Jupyter Notebook examples](https://github.com/GIScience/ohsome-quality-api-examples).
25 |
26 | ## About
27 |
28 | On our [website](https://api.quality.ohsome.org) you can find more details about the ohsome quality API. If you want to read up on the history of ohsome quality API, which was named ohsome quality analyst (OQT) in the past, or want to stay up-to-date, take a look at our [blog posts](https://heigit.org/tag/ohsome-quality-api-en).
29 |
30 | ## Contributing
31 |
32 | Contributions of any form are more than welcome! Please take a look at our [contributing
33 | guidelines](CONTRIBUTING.md) for details on our git workflow and style guide.
34 |
35 | ## Development
36 |
37 | Please refer to the [development setup documentation](/docs/development_setup.md) for guidance.
38 |
--------------------------------------------------------------------------------
/config/sample.config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Database connection parameters;
3 | postgres_host: localhost
4 | postgres_port: 5445
5 | postgres_db: oqapi
6 | postgres_user: oqapi
7 | postgres_password: oqapi
8 | # Restrict size of input geometry
9 | geom_size_limit: 1000
10 | # Python logging level
11 | log_level: INFO
12 | # ohsome API URL
13 | ohsome_api: https://api.ohsome.org/v1
14 | # Limit number of concurrent Indicator computations
15 | concurrent_computations: 4
16 | # User-Agent header for request to the ohsome API
17 | # Default: 'ohsome-quality-api/{version}'
18 | user_agent: ohsome-quality-api
19 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | api:
4 | build: .
5 | environment:
6 | OQAPI_CONFIG: /config/config.yaml
7 | command: uv run python scripts/start_api.py --host 0.0.0.0
8 | volumes:
9 | - ./config:/config
10 | ports:
11 | - "127.0.0.1:8080:8080"
12 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | Please take a look at the [interactive Swagger UI](https://api.quality.ohsome.org/v1/docs) to explore the API.
4 |
5 | We also provide a [Jupyter Notebook](https://github.com/GIScience/ohsome-quality-api-examples) with examples on how to use the ohsome quality API with Python.
6 |
7 | Have a look at this [blog post](https://heigit.org/de/visualizing-oqt-api-results-in-qgis-2/) to learn how to visualize the ohsome quality API Response in QGIS.
8 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | ## Indicators
4 |
5 | ### Mapping Saturation
6 |
7 | #### How does the Mapping Saturation relate to mapping activity?
8 |
9 | A saturation of 100 % means that the gradient or slope of the last bit (i.e. last 3 years) of the fitted curve is 1.0 (horizontal line). This again means that no mapping has been done in this period. Hopefully because there is nothing to map. That is the premise of the Mapping Saturation indicator: Each aggregation of features (e.g. length of roads or count of building) has a maximum. After increased mapping activity saturation is reached near this maximum.
10 |
11 | #### Why does the curve of Mapping Saturation decline sometimes?
12 |
13 | In case that features or tags are deleted the fitted curve can decline.
14 |
--------------------------------------------------------------------------------
/docs/img/UML-Class-Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/UML-Class-Diagram.png
--------------------------------------------------------------------------------
/docs/img/UML-Component-Diagram.drawio:
--------------------------------------------------------------------------------
1 | 7Vxbd6I6FP41rnXOgy6uoo9t7W2mnV70TKd9OStK1EyReCC2Or/+JBAEkhS1ItrO0D7ATkLCvnz7IwnWzJPJ/DwA0/E1dqFXMzR3XjM7NcMwbMOosX/NXcQS3dHNWDIKkMtlqaCLfkEu1Lh0hlwY5ioSjD2CpnnhAPs+HJCcDAQBfs1XG2Iv3+sUjKAk6A6AJ0sfkEvGXKprWlpwAdFozLtu2bygDwbPowDPfN5fzTCH0REXT0ByL14/HAMXv2ZE5mnNPAkwJvHZZH4CPabcRG1xu7M3SpfjDqBP1mlgheb1FQTnc6ePrwkYn7rjbt2O7/ICvBlMHiMaLFkkCooeEbKbaDXz+HWMCOxOwYCVvlKfoLIxmXj0Sqen8qD4OF9gQOA8I+KDPId4AkmwoFV4ab3NFcY9qm7wQb6m5nEcXmectYzFhYC7xGh571Qt9IRrRq2l57vwafQI4eTsy9MPs33x9dK16npToaamR/s97tOTETu5uevRGg84eIZBuCwNkuJEQvvvpzJB1UxJiLrmkYdGPhURzNQL+JUHh6xVSJWP/FGPlXVaqeAqKu4YqeSe60aPZGMwZd0MZn3ILuNA1JlNXRTQyEKYdRLiGVPy8RD7pMtHZuVtPESed4I9HESDNoc2+2O3JAF+hpmSZnTwm2Xk8VGOt7TyzmIkYZtxFstUOItp78pZWpJZoUsRh1/igIzxCPvAO02lx/kYS+tc4cjKTOs/ISELDp9gRvCacUftGQxgwXB5cBEQjCApqMdDgD1LoV0C6AGCXvLoWrqSZdw6ur2UFJ/4/AS7s0jP6lBKIkfL1BhQRULqr8fqmBRjYDg0BgNVDLjNftNuluPrui44e1N29mXeyjr7znzdlNNFBc4O54j8YM0bNr96zJR05vzO0cUiufDp82YascvHbFnaLLpK2r0/sJprBpauH1RkmeZHMWkFpjHLNg1veosRHfMysE0hsKXkFI+UtxIMvBzG+20u05uIzXxuNDWsQ0PTxAsqDr0EGfUcLqYwWToyOh+ScjhSkJxcfXrKYTqHFiRGe69BojU2DJPo6hYGiD4+s+6WsZOwhT3kru2wLXG9NHzu4ZRaIvzsIaQ3heTu7D2EZCjbc0w5G3HylCw6VbDFJJJWhpxVFV1s2jmPslrr0cWjIACLTLUpqxAW9CPMxFlNYYZxs/r0JB5BqdzV3Mt8y5avoJv5exWua1XjuhQNG047c+Q92Xashmllinfj12L8WMV+Td/Hiurvxq8T02VA+tJ3aU4jOPj0OdPU8xo/gJypSzqvFGbaGZhJQecNoKEXJTLO1rqMs10RhghpxhRsHj+RhBLSfXRh7cBqN7LI1Go1d4I+YrdmBWCyH4JXgc85ZbvcuiChXE21PhwVyb/M7o6HtHYCDZuGniHQDd1Zkfg1rah+Plbl1i2B3OTxpW29D7bEQS2nZCqaLE5sWfT2mNARMCCMHqzaNTD00PRiFWhsMLPryDsGLAVZEBVX3mRHW6EiYb/AiYdgZNa/HmA/pOr5+88Wgd1sEdCFgGkruKRRKZe0JPe46V50ZGMvDbLwEE0Sgbk6lPpxOrnqLwXLbUo3M0JvA7k8sa7SLC2jb8ZmyRvStWHLtUri+EYeHpsKjt9SmGVpztLtYq2BbBUkdMkaWnREJSAgR2zTG3MKD4QhGiTiM+SlE2tuUsnHzOBMwsu3y+MJiu5hKmy77SJyxO3Hshn7cdNkjafljZdaOLVfsYNUYFmj9Jmi7WJWW51q8TikvdFK0R6hP0l2F0nW1ESyu+8saxmSZ9Bh/25ZVhcWcA8gy8oTmzc8QH+vSU1r75Oalr2PLFjl9r2SV9+TZZLV/Mc4rCwp8x86BhcQ0Afhpw88cQW+0sB7Quajc/d99hN+aw17858utJ8Ue4n2EYguCMdR+9JeGm7Cc/94CF4feuGvL91u55rmxPq6O1b2FzNFw1YQSxe9JNTxO4xnuI5qjNvExSgp/PZPr1szaO/a+VHnOj67uevV7+GIkrwwQ0WR4jORTC+Kjgu/MClueg/CKESVYz6/6P57e3NbytiklsrBbn7/axQO3hj+comUKjmceaRQyat19wYu+phAQ0UDV9DGhA2238RHEWglyqixP5nKL+VlwKXAGO01mbxexvdXY1fTRr2HYV3Xe5e9U2PcAV/rMpOXTLM9K8ngLfT6+DULte9nDFUtioqrirqYuzZYFW0YWubIk9YdrjYoTS+/LCgA4QosKKAZmguHyEckBlf2xSgByKdn+QkAqur/ZjAksUVBxAzDxg6Q97dED1vYVm/ZRkNejlECSLJeVjp+rPGVq0x/cuxI8fqSnK+zI2OLNw7FDq6iQFmJMxkj2AWUd0s4skQvaL8TjixxT6GYZHaMP29/+ZsJ9ezGLI0aOQKfyXRGqP+wT9IZNoVTOEBDFEloFaCCEQ34rNhjcNZYH2cOD6JaHwqiTKvZUHxkrpoWazuNXYHUOu+EBwpSqm2mRdF0KCAlrEDammDbdUHKtrXiG5UHUsrphMNY3Sp/OkH5sIbsawf2XW3RsFXfv7Bk0WdrBuxXQ3w2JLRMKAVp4JAA3zlowK8LiyCm6is2FSN1djUB+GE+Wig/qIt+hyYb1EX1dh7T9DL9ZZ4YptPfPzJP/wc=
--------------------------------------------------------------------------------
/docs/img/UML-Component-Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/UML-Component-Diagram.png
--------------------------------------------------------------------------------
/docs/img/UML-Sequence-Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/UML-Sequence-Diagram.png
--------------------------------------------------------------------------------
/docs/img/oqt_website_step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/oqt_website_step1.png
--------------------------------------------------------------------------------
/docs/img/oqt_website_step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/docs/img/oqt_website_step2.png
--------------------------------------------------------------------------------
/docs/indicator.md:
--------------------------------------------------------------------------------
1 | # Indicator
2 |
3 | An indicator estimates data quality for OpenStreetMap (OSM). It takes an area of interest in form of a GeoJSON Feature and a Topic object describing the set of OSM features as input.
4 |
5 | ## Base Class
6 |
7 | Each particular indicator class inherits from the `BaseIndicator` class of the `ohsome_quality_api/indicators/base.py` module. Following class diagram gives an overview of abstract methods and properties of this class:
8 |
9 | 
10 |
11 | ### Metadata
12 |
13 | Metadata is automatically loaded from a `metadata.yaml` file found in the module of a particular indicator.
14 | Metadata describes basic information about the indicator: The indicator name, for which projects the indicator can be used, the quality dimension, a short description of what it does and how it works, and a standardized interpretation of its possible results.
15 |
16 | ### Topic
17 |
18 | Please take a look at the [topic documentation](topic.md).
19 |
20 | ### Result
21 |
22 | The result object consists of following attributes:
23 |
24 | - `description (str)`: The result description
25 | - `timestamp (datetime)`: Timestamp of the creation of the indicator
26 | - `timestamp_osm (datetime)`: Timestamp of the used OSM data (e.g. the latest timestamp of the ohsome API results)
27 | - `value (float)`: The result value
28 | - `class (int)`: The result class. An integer between 1 and 5. It maps to following result labels:
29 | - `1`: `red`
30 | - `2`/`3`: `yellow`
31 | - `4`/`5`: `green`
32 | - `figure (dict)`: A plotly figure
33 | - `label (str)`: Traffic lights like quality label (`green`, `yellow` or `red`)
34 |
35 |
36 | ## Indicator Class
37 |
38 | A particular indicator class (child) need to implement three functions:
39 | 1. `preprocess`
40 | 2. `calculate`
41 | 3. `create_figure`
42 |
43 |
44 | Following sequence diagram shows when those functions are called during the lifecycle of the indicator:
45 |
46 | 
47 |
48 |
49 | ### `preprocess`
50 |
51 | This function is used to fetch and preprocess the data needed for an indicator. Usually this involves querying the ohsome API by using the module `ohsome/client.py` which does so using Pythons async/await pattern
52 |
53 | All data created during the preprocessing will be stored as attributes of the indicator object.
54 |
55 | ### `calculate`
56 |
57 | This functions does the computation of the indicator. At the end the result object should be fully initialized and all attributes set (except the figure).
58 |
59 | ### `create_figure`
60 |
61 | This function creates plotly figure base on the indicator result.
62 |
--------------------------------------------------------------------------------
/docs/indicators.md:
--------------------------------------------------------------------------------
1 | # Indicators
2 |
3 | ## Mapping Saturation
4 |
5 | Calculate the saturation within the last 3 years.
6 | Time period is one month since 2008.
7 |
8 |
9 | Different statistical models are used to find out if saturation is reached.
10 |
11 |
12 | ### Methods and Data
13 |
14 | - Intrinsic approach.
15 |
16 | Premise:
17 |
18 | The added number of OSM objects of a specific feature class per time period decreases as the
19 | number of mapped objects converges against the (unknown) true number of objects.
20 |
21 | Each aggregation of features (e.g. length of roads or count of building)
22 | has a maximum. After increased mapping activity saturation is reached near this
23 | maximum.
24 |
25 |
26 |
27 | ### References
28 |
29 | - Gröchenig S et al. (2014): Digging into the history of VGI data-sets: results from
30 | a worldwide study on OpenStreetMap mapping activity
31 | (https://doi.org/10.1080/17489725.2014.978403)
32 | - Barrington-Leigh C and Millard-Ball A (2017): The world’s user-generated road map
33 | is more than 80% complete
34 | (https://doi.org/10.1371/journal.pone.0180698 pmid:28797037)
35 | - Josephine Brückner, Moritz Schott, Alexander Zipf, and Sven Lautenbach (2021):
36 | Assessing shop completeness in OpenStreetMap for two federal states in Germany
37 | (https://doi.org/10.5194/agile-giss-2-20-2021)
38 |
--------------------------------------------------------------------------------
/docs/topic.md:
--------------------------------------------------------------------------------
1 | # Topic
2 |
3 | A topic describes the request which should be made to the [ohsome API](https://api.ohsome.org). Each topic is representative of a specific set of features, aggregated information or user statistics derived from the OpenStreetMap database. Each topic is defined by the ohsome API `endpoint`, an `aggregation_type` and the `filter` parameter. In addition, each topic preset has a key, name, description, a list of valid indicators and a list of projects the topic belongs to. Topic presets are written down as YAML file at `ohsome_quality_api/topics/presets.yaml`
4 |
5 | ## Example
6 |
7 | ```yaml
8 | building-count:
9 | name: Building Count
10 | description: >-
11 | All buildings as defined by all objects tagged with 'building=*'.
12 | endpoint: elements
13 | aggregation_type: count
14 | filter: building=* and geometry:polygon
15 | indicators:
16 | - mapping-saturation
17 | - currentness
18 | - attribute-completeness
19 | projects:
20 | - core
21 | ```
22 |
23 | ## How to Add a New Topic?
24 |
25 | First create an ohsome API query to retrieve desired information from the ohsome API. Helpful resources for this task are:
26 | - The Swagger UI of the ohsome API:
27 | https://api.ohsome.org/v1/swagger-ui.html
28 | - ohsome API documentation on the `aggregation_type` and `endpoint` parameters:
29 | https://docs.ohsome.org/ohsome-api/stable/endpoints.html
30 | - ohsome API documentation on the `filter` parameter:
31 | https://docs.ohsome.org/ohsome-api/stable/filter.html
32 |
33 | Second translate the query parameters into a topic preset and extent this file:
34 | `ohsome_quality_api/topics/presets.yaml`.
35 |
--------------------------------------------------------------------------------
/docs/vector_datasets.md:
--------------------------------------------------------------------------------
1 | # Vector datasets (PostGIS Database)
2 |
3 | The ohsome quality API operates on vector datasets stored in a PostGIS database.
4 |
5 |
6 | ### The Human Development Index (SHDI)
7 |
8 | Information:
9 | - https://globaldatalab.org/shdi/
10 | - product: GDL Shapefiles V4, epoch: 1990–2017, resolution: Subnational, coordinate system: EPSG:4326
11 |
--------------------------------------------------------------------------------
/ohsome_quality_api/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.9.0"
2 | __title__ = "ohsome quality API"
3 | __description__ = "Data quality estimations for OpenStreetMap"
4 | __author__ = "ohsome team"
5 | __email__ = "ohsome@heigit.org"
6 | __homepage__ = "https://api.quality.ohsome.org/"
7 |
--------------------------------------------------------------------------------
/ohsome_quality_api/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/api/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/api/request_context.py:
--------------------------------------------------------------------------------
1 | from contextvars import ContextVar
2 | from dataclasses import dataclass
3 |
4 | from fastapi import Request
5 |
6 |
7 | @dataclass
8 | class RequestContext:
9 | path_parameters: dict
10 |
11 |
12 | request_context: ContextVar[RequestContext] = ContextVar("request_context")
13 |
14 |
15 | async def set_request_context(request: Request):
16 | """Set request context for the duration of a request.
17 |
18 | After leaving the context manager (after the request is processed)
19 | the request context is reset again.
20 | """
21 | token = request_context.set(RequestContext(path_parameters=request.path_params))
22 | try:
23 | yield
24 | finally:
25 | request_context.reset(token)
26 |
--------------------------------------------------------------------------------
/ohsome_quality_api/api/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/api/static/favicon-32x32.png
--------------------------------------------------------------------------------
/ohsome_quality_api/attributes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/attributes/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/attributes/definitions.py:
--------------------------------------------------------------------------------
1 | import os
2 | from enum import Enum
3 | from typing import List
4 |
5 | import yaml
6 |
7 | from ohsome_quality_api.attributes.models import Attribute
8 | from ohsome_quality_api.topics.definitions import get_topic_preset, load_topic_presets
9 | from ohsome_quality_api.utils.helper import get_module_dir
10 |
11 |
12 | def load_attributes() -> dict[str, dict[str, Attribute]]:
13 | """Read attributes from YAML file."""
14 | directory = get_module_dir("ohsome_quality_api.attributes")
15 | file = os.path.join(directory, "attributes.yaml")
16 | with open(file, "r") as f:
17 | raw = yaml.safe_load(f)
18 | attributes = {}
19 | for topic_key, value in raw.items():
20 | attributes[topic_key] = {}
21 | for attribute_key, value_ in value.items():
22 | attributes[topic_key][attribute_key] = Attribute(**value_)
23 | return attributes
24 |
25 |
26 | def get_attributes() -> dict[str, dict[str, Attribute]]:
27 | return load_attributes()
28 |
29 |
30 | def get_attribute(topic_key, a_key: str | None) -> Attribute:
31 | attributes = get_attributes()
32 | try:
33 | # TODO: Workaround to be able to display indicator in dashboard.
34 | # Remove if dashboard handles attribution key selection.
35 | if a_key is None:
36 | return next(iter(attributes[topic_key].values()))
37 | return attributes[topic_key][a_key]
38 | except KeyError as error:
39 | raise KeyError("Invalid topic or attribute key(s).") from error
40 |
41 |
42 | # TODO: Remove since it is the same as above?
43 | def get_attribute_preset(topic_key: str) -> List[Attribute]:
44 | """Get ohsome API parameters of a list of Attributes based on topic key."""
45 | attributes = load_attributes()
46 | try:
47 | return attributes[topic_key]
48 | except KeyError as error:
49 | topics = load_topic_presets()
50 | raise KeyError(
51 | "Invalid topic key. Valid topic keys are: " + str(topics.keys())
52 | ) from error
53 |
54 |
55 | def build_attribute_filter(attribute_key: List[str] | str, topic_key: str) -> str:
56 | """Build attribute filter for ohsome API query."""
57 | attributes = get_attributes()
58 | try:
59 | if isinstance(attribute_key, str):
60 | return get_topic_preset(topic_key).filter + " and (" + attribute_key + ")"
61 | else:
62 | attribute_filter = get_topic_preset(topic_key).filter
63 | for key in attribute_key:
64 | attribute_filter += " and (" + attributes[topic_key][key].filter + ")"
65 | return attribute_filter
66 | except KeyError as error:
67 | raise KeyError("Invalid topic or attribute key(s).") from error
68 |
69 |
70 | attribute_keys = {
71 | inner_key
72 | for outer_dict in load_attributes().values()
73 | for inner_key in outer_dict.keys()
74 | }
75 |
76 | AttributeEnum = Enum("AttributeEnum", {name: name for name in attribute_keys})
77 |
--------------------------------------------------------------------------------
/ohsome_quality_api/attributes/models.py:
--------------------------------------------------------------------------------
1 | """Pydantic Models for Attributes."""
2 |
3 | from pydantic import BaseModel, ConfigDict
4 |
5 |
6 | class Attribute(BaseModel):
7 | filter: str
8 | name: str
9 | description: str
10 | model_config = ConfigDict(
11 | extra="forbid",
12 | frozen=True,
13 | title="Attribute",
14 | )
15 |
--------------------------------------------------------------------------------
/ohsome_quality_api/definitions.py:
--------------------------------------------------------------------------------
1 | """Global Variables and Functions."""
2 |
3 | import glob
4 | from enum import Enum
5 | from types import MappingProxyType
6 | from typing import Literal
7 |
8 | import yaml
9 |
10 | from ohsome_quality_api.indicators.models import IndicatorMetadata
11 | from ohsome_quality_api.utils.helper import (
12 | get_module_dir,
13 | )
14 |
15 | ATTRIBUTION_TEXTS = MappingProxyType(
16 | {
17 | "OSM": "© OpenStreetMap contributors",
18 | "GHSL": "© European Union, 1995-2022, Global Human Settlement Topic Data",
19 | "VNL": "Earth Observation Group Nighttime Light Data",
20 | "EUBUCCO": "European building stock characteristics in a common and open "
21 | + "database",
22 | "Microsoft Buildings": "Microsoft Building Footprints (ODbL)",
23 | }
24 | )
25 |
26 | ATTRIBUTION_URL = (
27 | "https://github.com/GIScience/ohsome-quality-api/blob/main/COPYRIGHTS.md"
28 | )
29 |
30 |
31 | # default colors of the Sematic UI CSS Framework
32 | # used by the ohsome dashboard
33 | class Color(Enum):
34 | RED = "#DB2828"
35 | ORANGE = "#F2711C"
36 | YELLOW = "#FBBD08"
37 | OLIVE = "#B5CC18"
38 | GREEN = "#21BA45"
39 | TEAL = "#00B5AD"
40 | BLUE = "#2185D0"
41 | VIOLET = "#6435C9"
42 | PURPLE = "#A333C8"
43 | PINK = "#E03997"
44 | BROWN = "#A5673F"
45 | GREY = "#767676"
46 | BLACK = "#1B1C1D"
47 |
48 |
49 | def load_metadata(
50 | module_name: Literal["indicators"],
51 | ) -> dict[str, IndicatorMetadata]:
52 | """Read metadata of all indicators from YAML files.
53 |
54 | Those text files are located in the directory of each indicator.
55 |
56 | Args:
57 | module_name: indicators.
58 | Returns:
59 | A Dict with the class names of the indicators
60 | as keys and metadata as values.
61 | """
62 | assert module_name == "indicators"
63 | directory = get_module_dir("ohsome_quality_api.{}".format(module_name))
64 | files = glob.glob(directory + "/**/metadata.yaml", recursive=True)
65 | raw = {}
66 | for file in files:
67 | with open(file, "r") as f:
68 | raw.update(yaml.safe_load(f)) # Merge dicts
69 | metadata = {}
70 | match module_name:
71 | case "indicators":
72 | for k, v in raw.items():
73 | metadata[k] = IndicatorMetadata(**v)
74 | return metadata
75 |
76 |
77 | def get_attribution(data_keys: list) -> str:
78 | """Return attribution text. Individual attributions are separated by semicolons."""
79 | assert set(data_keys) <= {"OSM", "GHSL", "VNL", "EUBUCCO", "Microsoft Buildings"}
80 | filtered = dict(filter(lambda d: d[0] in data_keys, ATTRIBUTION_TEXTS.items()))
81 | return "; ".join([str(v) for v in filtered.values()])
82 |
--------------------------------------------------------------------------------
/ohsome_quality_api/geodatabase/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/geodatabase/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/geodatabase/regions_as_geojson.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | row_to_json(feature_collection)
3 | FROM (
4 | SELECT
5 | 'FeatureCollection' AS "type",
6 | array_to_json(array_agg(feature)) AS "features"
7 | FROM (
8 | SELECT
9 | 'Feature' AS "type",
10 | /* Make sure to conform with RFC7946 by using WGS84 lon, lat*/
11 | ST_AsGeoJSON(ST_Transform(geom, 4326), 6)::json AS "geometry",
12 | (
13 | SELECT
14 | json_strip_nulls(row_to_json(t))
15 | FROM (
16 | SELECT
17 | ogc_fid as id,
18 | name
19 | ) AS t
20 | ) AS "properties"
21 | FROM
22 | regions
23 | ) AS feature
24 | ) AS feature_collection
25 |
--------------------------------------------------------------------------------
/ohsome_quality_api/geodatabase/select_coverage.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | ST_AsGeoJSON (ST_Transform (geom, 4326)) as geom
3 | FROM
4 | {table_name};
5 |
--------------------------------------------------------------------------------
/ohsome_quality_api/geodatabase/select_intersection.sql:
--------------------------------------------------------------------------------
1 | WITH bpoly AS (
2 | SELECT
3 | ST_Setsrid (ST_GeomFromGeoJSON ($1), 4326) AS geom
4 | )
5 | SELECT
6 | -- ratio of area within coverage (empty if outside, between 0-1 if intersection)
7 | ST_Area (ST_Intersection (bpoly.geom, coverage.geom)) / ST_Area (bpoly.geom) as area_ratio,
8 | ST_AsGeoJSON (ST_Intersection (bpoly.geom, coverage.geom)) AS geom
9 | FROM
10 | bpoly,
11 | {table_name} coverage
12 | WHERE
13 | ST_Intersects (bpoly.geom, coverage.geom)
14 |
--------------------------------------------------------------------------------
/ohsome_quality_api/geodatabase/select_shdi.sql:
--------------------------------------------------------------------------------
1 | WITH bpoly AS (
2 | SELECT
3 | ST_Setsrid (public.ST_GeomFromGeoJSON (geojson), 4326) AS geom,
4 | -- Row number is used to make sure order of result is the same as order of input
5 | -- (Probably unnecessary)
6 | row_number() OVER () AS rownumber
7 | FROM
8 | unnest(cast($1 AS text[])) AS geojson
9 | )
10 | SELECT
11 | SUM(ST_Area (ST_Intersection (shdi.geom, bpoly.geom)::geography) / ST_Area
12 | (bpoly.geom::geography) * shdi.shdi) AS shdi,
13 | rownumber as rownumber
14 | FROM
15 | shdi,
16 | bpoly
17 | WHERE
18 | ST_Intersects (shdi.geom, bpoly.geom)
19 | GROUP BY
20 | bpoly.geom,
21 | bpoly.rownumber
22 | ORDER BY
23 | bpoly.rownumber;
24 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/attribute_completeness/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/attribute_completeness/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/attribute_completeness/templates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | label_description:
3 | red: >-
4 | The attribute completeness is low (<25%).
5 | yellow: >-
6 | The attribute completeness is medium (25%-75%).
7 | green: >-
8 | The attribute completeness is high (>75%).
9 | undefined: >-
10 | The quality level could not be calculated for this indicator.
11 | result_description: >-
12 | $result% of all "$topic" features (all: $all) in your area of interest have the selected additional $tags (matched: $matched).
13 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/building_comparison/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/building_comparison/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/building_comparison/datasets.yaml:
--------------------------------------------------------------------------------
1 | EUBUCCO:
2 | name: EUBUCCO
3 | link: https://docs.eubucco.com/
4 | date: Nov 3, 2022
5 | description: >-
6 | EUBUCCO is a dataset of building footprints for Europe.
7 | It is derived from administrative datasets.
8 | color: PURPLE
9 | coverage:
10 | simple: eubucco_coverage_simple
11 | inversed: eubucco_coverage_inversed
12 | table_name: eubucco
13 |
14 | Microsoft Buildings:
15 | name: Microsoft Building Footprints
16 | link: https://planetarycomputer.microsoft.com/dataset/ms-buildings
17 | date: July 5, 2022
18 | description: >-
19 | Microsoft Building Footprints is a dataset of building footprints for the world.
20 | It is derived from satellite imagery.
21 | color: ORANGE
22 | coverage:
23 | simple: microsoft_buildings_coverage_simple
24 | inversed: microsoft_buildings_coverage_inversed
25 | table_name: microsoft_buildings
26 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/building_comparison/query.sql:
--------------------------------------------------------------------------------
1 | WITH bpoly AS (
2 | SELECT
3 | -- split mutlipolygon into list of polygons for more efficient processing
4 | (ST_DUMP (ST_Setsrid (ST_GeomFromGeoJSON ($1), 4326))).geom AS geom
5 | )
6 | SELECT
7 | SUM({table_name}.area) as area
8 | FROM {table_name},
9 | bpoly
10 | WHERE
11 | ST_Intersects ({table_name}.centroid, bpoly.geom);
12 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/building_comparison/templates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | label_description:
3 | red: >-
4 | The completeness of OSM buildings in your area-of-interest is low.
5 | yellow: >-
6 | The completeness of OSM buildings in your area-of-interest is medium.
7 | green: >-
8 | The completeness of OSM buildings in your area-of-interest is high.
9 | undefined: >-
10 | Comparison could not be made.
11 | result_description: >-
12 | The completeness in comparison to $dataset is $ratio%.
13 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/currentness/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/currentness/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/currentness/templates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | label_description:
3 | red: >-
4 | Many features are out-of-date.
5 | yellow: >-
6 | Some features are up-to-date and some features are out-of-date.
7 | green: >-
8 | Most features are up-to-date.
9 | undefined: >-
10 | The quality level could not be calculated for this indicator.
11 | result_description: >-
12 | In the area of interest $up_to_date_contrib_rel% of the $num_of_elements features were edited (created or modified) for the last time in the period between $from_timestamp and $to_timestamp.
13 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/currentness/thresholds.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # thresholds are age in number of months since latest timestamp (today)
3 |
4 | roads:
5 | thresholds:
6 | up_to_date: 48
7 | out_of_date: 96
8 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
9 |
10 | railway-length:
11 | thresholds:
12 | up_to_date: 96
13 | out_of_date: 132
14 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
15 |
16 | bus-stops:
17 | thresholds:
18 | up_to_date: 48
19 | out_of_date: 96
20 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
21 |
22 | tram-stops:
23 | thresholds:
24 | up_to_date: 48
25 | out_of_date: 96
26 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
27 |
28 | subway-station:
29 | thresholds:
30 | up_to_date: 48
31 | out_of_date: 96
32 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
33 |
34 | mapaction-rail-length:
35 | thresholds:
36 | up_to_date: 96
37 | out_of_date: 132
38 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
39 |
40 | mapaction-major-roads-length:
41 | thresholds:
42 | up_to_date: 48
43 | out_of_date: 96
44 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
45 |
46 | local-food-shops:
47 | thresholds:
48 | up_to_date: 24
49 | out_of_date: 48
50 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
51 |
52 | fast-food-restaurants:
53 | thresholds:
54 | up_to_date: 24
55 | out_of_date: 48
56 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
57 |
58 | restaurants:
59 | thresholds:
60 | up_to_date: 24
61 | out_of_date: 48
62 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
63 |
64 | convenience-stores:
65 | thresholds:
66 | up_to_date: 24
67 | out_of_date: 48
68 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
69 |
70 | pubs-and-biergartens:
71 | thresholds:
72 | up_to_date: 24
73 | out_of_date: 48
74 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
75 |
76 | alcohol-and-beverages:
77 | thresholds:
78 | up_to_date: 24
79 | out_of_date: 48
80 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
81 |
82 | sweets-and-pasteries:
83 | thresholds:
84 | up_to_date: 24
85 | out_of_date: 48
86 | source: https://wiki.openstreetmap.org/wiki/StreetComplete/Quests
87 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/definitions.py:
--------------------------------------------------------------------------------
1 | import os
2 | from enum import Enum
3 |
4 | import yaml
5 | from geojson import FeatureCollection
6 |
7 | from ohsome_quality_api.indicators.models import IndicatorMetadata
8 | from ohsome_quality_api.projects.definitions import ProjectEnum
9 | from ohsome_quality_api.topics.definitions import load_topic_presets
10 | from ohsome_quality_api.utils.helper import (
11 | get_class_from_key,
12 | get_module_dir,
13 | )
14 |
15 |
16 | def get_indicator_keys() -> list[str]:
17 | return [str(t) for t in load_indicators().keys()]
18 |
19 |
20 | def load_indicators() -> dict[str, IndicatorMetadata]:
21 | directory = get_module_dir("ohsome_quality_api.indicators")
22 | file = os.path.join(directory, "indicators.yaml")
23 | with open(file, "r") as f:
24 | raw = yaml.safe_load(f)
25 | indicators = {}
26 | for k, v in raw.items():
27 | indicators[k] = IndicatorMetadata(**v)
28 | return indicators
29 |
30 |
31 | def get_indicator_metadata(project: ProjectEnum = None) -> dict[str, IndicatorMetadata]:
32 | indicators = load_indicators()
33 | if project is not None:
34 | return {k: v for k, v in indicators.items() if project in v.projects}
35 | else:
36 | return indicators
37 |
38 |
39 | def get_indicator(indicator_key: str) -> IndicatorMetadata:
40 | indicators = get_indicator_metadata()
41 | try:
42 | return indicators[indicator_key]
43 | except KeyError as error:
44 | raise KeyError(
45 | "Invalid project key. Valid project keys are: " + str(indicators.keys())
46 | ) from error
47 |
48 |
49 | def get_valid_indicators(topic_key: str) -> tuple:
50 | """Get valid Indicator/Topic combination of a Topic."""
51 | td = load_topic_presets()
52 | return tuple(td[topic_key].indicators)
53 |
54 |
55 | async def get_coverage(indicator_key: str, inverse: bool = False) -> FeatureCollection:
56 | indicator_class = get_class_from_key(class_type="indicator", key=indicator_key)
57 | features = await indicator_class.coverage(inverse)
58 | return FeatureCollection(features=features)
59 |
60 |
61 | IndicatorEnum = Enum("IndicatorEnum", {name: name for name in get_indicator_keys()})
62 | IndicatorEnumRequest = Enum(
63 | "IndicatorEnum",
64 | {name: name for name in get_indicator_keys() if name != "attribute-completeness"},
65 | )
66 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/indicators.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | mapping-saturation:
3 | name: Mapping Saturation
4 | projects:
5 | - core
6 | - corine-land-cover
7 | - expanse
8 | - experimental
9 | - idealvgi
10 | - mapaction
11 | - sketchmap
12 | - bkg
13 | - unicef
14 | quality_dimension: completeness
15 | description: >-
16 | Calculate if mapping has saturated.
17 | High saturation has been reached if the growth of the fitted curve is
18 | minimal.
19 | minimal:
20 | name: Minimal
21 | projects:
22 | - misc
23 | quality_dimension: minimal
24 | description: >-
25 | An minimal Indicator for testing purposes.
26 | currentness:
27 | name: Currentness
28 | projects:
29 | - core
30 | - bkg
31 | - unicef
32 | quality_dimension: currentness
33 | description: >-
34 | Estimate currentness of features by classifying contributions based on
35 | topic specific temporal thresholds into three groups: up-to-date,
36 | in-between and out-of-date.
37 | road-comparison:
38 | name: Road Comparison
39 | projects:
40 | - bkg
41 | - core
42 | - unicef
43 | quality_dimension: completeness
44 | description: >-
45 | Compare the road length of OSM roads with the road length of
46 | reference data.
47 | building-comparison:
48 | name: Building Comparison
49 | projects:
50 | - bkg
51 | - core
52 | quality_dimension: completeness
53 | description: >-
54 | Comparison of OSM buildings with the buildings of reference datasets.
55 | attribute-completeness:
56 | name: Attribute Completeness
57 | projects:
58 | - bkg
59 | - core
60 | quality_dimension: completeness
61 | description: >-
62 | Derive the ratio of OSM features compared to features which
63 | match additional expected tags (e.g. amenity=hospital vs
64 | amenity=hospital and wheelchair=yes).
65 | land-cover-thematic-accuracy:
66 | name: Land Cover Thematic Accuracy
67 | projects:
68 | - bkg
69 | quality_dimension: thematic-accuracy
70 | description: >-
71 | Thematic accuracy Open Street Map land cover data in comparison to the CORINE Land
73 | Cover (CLC) dataset.
74 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/land_cover_thematic_accuracy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/land_cover_thematic_accuracy/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/land_cover_thematic_accuracy/query-multi-classes.sql:
--------------------------------------------------------------------------------
1 | WITH bpoly AS (
2 | SELECT
3 | -- split mutlipolygon into list of polygons for more efficient processing
4 | (ST_Dump (ST_SetSRID (ST_GeomFromGeoJSON ($1), 4326))).geom AS geometry
5 | )
6 | SELECT
7 | CLC_class as clc_class_corine,
8 | osm_CLC_class as clc_class_osm,
9 | SUM (
10 | CASE WHEN ST_Within (o.geometry, b.geometry) THEN
11 | area
12 | ELSE
13 | ST_Area (ST_Intersection (o.geometry, b.geometry)::geography)
14 | END) AS area
15 | FROM
16 | osm_corine_2021_deu_intersection o,
17 | bpoly b
18 | WHERE
19 | ST_Intersects (o.geometry, b.geometry)
20 | --AND osm_CLC_class != 0
21 | --AND osm_CLC_class != 50
22 | GROUP BY 1,2
23 | ;
24 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/land_cover_thematic_accuracy/query-single-class.sql:
--------------------------------------------------------------------------------
1 | -- TODO: will index on classes make this query faster?
2 | WITH bpoly AS (
3 | SELECT
4 | -- split mutlipolygon into list of polygons for more efficient processing
5 | (ST_Dump(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326))).geom AS geometry
6 | )
7 | SELECT
8 | CLC_class as clc_class_corine,
9 | osm_CLC_class as clc_class_osm,
10 | CASE
11 | WHEN ST_Within(o.geometry, b.geometry) THEN area
12 | ELSE ST_Area(ST_Intersection(o.geometry, b.geometry)::geography)
13 | END AS area
14 | FROM
15 | osm_corine_2021_deu_intersection o,
16 | bpoly b
17 | WHERE
18 | ST_Intersects(o.geometry, b.geometry)
19 | AND (
20 | o.CLC_class = $2 OR o.osm_CLC_class = $2
21 | );
22 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/land_cover_thematic_accuracy/templates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | label_description:
3 | red: >-
4 | In your area-of-interes the thematic accuracy of $clc_class is $f1_score %
5 | (low agreement between OSM and CORINE). This suggests that OSM has a poor
6 | classification quality or incomplete land cover data.
7 | yellow: >-
8 | In your area-of-interes the thematic accuracy of $clc_class is $f1_score %
9 | (medium agreement between OSM and CORINE). This suggests that OSM does
10 | capture some land cover categories but may lack detail or accuracy.
11 | green: >-
12 | In your area-of-interes the thematic accuracy of $clc_class is $f1_score %
13 | (high agreement between OSM and CORINE). This suggests that OSM provides a
14 | reliable representation of land cover.
15 | undefined: >-
16 | The F1-score could not be calculated, possibly due to missing or incompatible data in the selected region.
17 | result_description: >-
18 | The F1-Score is used as
19 | statistical metric which considers both the correctness and completeness of
20 | the land cover categories.
21 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/mapping_saturation/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/mapping_saturation/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/mapping_saturation/ssdoubles.R:
--------------------------------------------------------------------------------
1 | # title: "Self-starting two-steps-sigmoidal function"
2 | # author: "Josephine Brückner"
3 |
4 | doubleSFun <- function(input, e, f, k, b, Z, c, ...) {
5 | e+(f-e)*1/2*(tanh(k*(input-b))+1)+(Z-f)*1/2*(tanh(k*(input-c))+1)
6 | }
7 |
8 | doubleS.init <- function(mCall, LHS, data, ...)
9 | { xy <- sortedXyData(mCall[["input"]], LHS, data)
10 | x <- xy[, "x"]; y <- xy[, "y"]
11 |
12 | e <- min(y)
13 | Z <- max(y)
14 | f <- max(y)/2
15 | b <- x[which.min(y)]
16 | c <- max(x)*0.5
17 | k <- 10
18 | model <- "NA"
19 | count <- 0
20 |
21 | while(model[1]=="NA")
22 | { k <- k/10
23 | count <- count + 1
24 |
25 | model <- tryCatch(
26 | nls(y ~ doubleSFun(x, e, f, k, b, Z, c), start=c(e=min(y), f=max(y)/2, b=x[which.min(y)], Z=max(y), c=c, k=k), data),
27 | error = function(e) paste0("NA"))
28 |
29 | if (count > 100)
30 | {break}
31 | }
32 |
33 | start <- c(e, f, k, b, Z, c)
34 | names(start) <- mCall[c("e", "f", "k", "b", "Z", "c")]
35 | start
36 | }
37 |
38 |
39 | SSdoubleS <- selfStart(doubleSFun, doubleS.init, parameters=c("e", "f", "k", "b", "Z", "c"))
40 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/mapping_saturation/templates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | label_description:
3 | red: >-
4 | No saturation identified (Saturation ≤ 30%).
5 | yellow: >-
6 | Saturation is in progress (30% < Saturation ≤ 97%).
7 | green: >-
8 | High saturation has been reached (97% < Saturation ≤ 100%).
9 | undefined: >-
10 | Saturation could not be calculated.
11 | result_description: >-
12 | The saturation of the last 3 years is $saturation%.
13 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/minimal/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/minimal/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/minimal/indicator.py:
--------------------------------------------------------------------------------
1 | """An Indicator for testing purposes."""
2 |
3 | from string import Template
4 |
5 | import dateutil.parser
6 | from geojson import Feature
7 |
8 | from ohsome_quality_api.indicators.base import BaseIndicator
9 | from ohsome_quality_api.ohsome import client as ohsome_client
10 | from ohsome_quality_api.topics.models import BaseTopic as Topic
11 |
12 |
13 | class Minimal(BaseIndicator):
14 | def __init__(self, topic: Topic, feature: Feature) -> None:
15 | super().__init__(topic=topic, feature=feature)
16 | self.count = 0
17 |
18 | async def preprocess(self) -> None:
19 | query_results = await ohsome_client.query(self.topic, self.feature)
20 | self.count = query_results["result"][0]["value"]
21 | self.result.timestamp_osm = dateutil.parser.isoparse(
22 | query_results["result"][0]["timestamp"]
23 | )
24 |
25 | def calculate(self) -> None:
26 | description = Template(self.templates.result_description).substitute()
27 | self.result.value = 1.0
28 | self.result.description = (
29 | description + self.templates.label_description["green"]
30 | )
31 |
32 | def create_figure(self) -> None:
33 | # Do nothing ...
34 | return None
35 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/minimal/templates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | label_description:
3 | red: >-
4 | Bad data quality.
5 | yellow: >-
6 | Medium data quality.
7 | green: >-
8 | Good data quality.
9 | undefined: >-
10 | The quality level could not be calculated for this indicator.
11 | result_description: >-
12 | Some description of the result.
13 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import Literal
3 |
4 | from pydantic import BaseModel, ConfigDict, Field, computed_field
5 |
6 | from ohsome_quality_api.projects.definitions import ProjectEnum
7 | from ohsome_quality_api.quality_dimensions.definitions import QualityDimensionEnum
8 | from ohsome_quality_api.utils.helper import snake_to_lower_camel
9 |
10 |
11 | class IndicatorMetadata(BaseModel):
12 | """Metadata of an indicator as defined in the metadata.yaml file."""
13 |
14 | name: str
15 | description: str
16 | projects: list[ProjectEnum]
17 | quality_dimension: QualityDimensionEnum
18 | model_config = ConfigDict(
19 | alias_generator=snake_to_lower_camel,
20 | title="Metadata",
21 | frozen=True,
22 | extra="forbid",
23 | populate_by_name=True,
24 | )
25 |
26 |
27 | class IndicatorTemplates(BaseModel):
28 | """Result text templates of an indicator as defined in the templates.yaml file."""
29 |
30 | label_description: dict[str, str]
31 | result_description: str
32 |
33 |
34 | class Result(BaseModel):
35 | """The result of the Indicator.
36 |
37 | Attributes:
38 | timestamp (datetime): Timestamp of the creation of the indicator
39 | timestamp_osm (datetime): Timestamp of the used OSM data
40 | (e.g. Latest timestamp of the ohsome API results)
41 | label (str): Traffic lights like quality label: `green`, `yellow` or `red`. The
42 | value is determined by the result classes
43 | value (float): The result value
44 | class_ (int): The result class. An integer between 1 and 5. It maps to the
45 | result labels. This value is used by the reports to determine an overall
46 | result.
47 | description (str): The result description.
48 | """
49 |
50 | description: str
51 | timestamp: datetime = Field(default=datetime.now(timezone.utc))
52 | timestamp_osm: datetime | None = Field(default=None, alias="timestampOSM")
53 | value: float | None = None
54 | class_: Literal[1, 2, 3, 4, 5] | None = None
55 | figure: dict | None = None
56 | model_config = ConfigDict(
57 | alias_generator=snake_to_lower_camel,
58 | extra="forbid",
59 | populate_by_name=True,
60 | )
61 |
62 | @computed_field
63 | @property
64 | def label(self) -> Literal["green", "yellow", "red", "undefined"]:
65 | labels = {1: "red", 2: "yellow", 3: "yellow", 4: "green", 5: "green"}
66 | return labels.get(self.class_, "undefined")
67 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/road_comparison/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/indicators/road_comparison/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/road_comparison/datasets.yaml:
--------------------------------------------------------------------------------
1 | Microsoft Roads:
2 | name: Microsoft Roads
3 | link: https://github.com/microsoft/RoadDetections
4 | date: February 27, 2023
5 | description: >-
6 | Microsoft Road Detections is a dataset of road world-wide.
7 | It is derived from satellite imagery.
8 | color: ORANGE
9 | coverage:
10 | simple: microsoft_roads_coverage_simple
11 | inversed: microsoft_roads_coverage_inversed
12 | table_name: microsoft_roads_midpoint
13 | processing_date: 2024-04-05
14 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/road_comparison/query.sql:
--------------------------------------------------------------------------------
1 | WITH bpoly AS (
2 | SELECT
3 | -- split mutlipolygon into list of polygons for more efficient processing
4 | (ST_DUMP (ST_Setsrid (ST_GeomFromGeoJSON ($1), 4326))).geom AS geom
5 | )
6 | SELECT
7 | SUM(cr.covered),
8 | SUM(cr.length)
9 | FROM
10 | bpoly
11 | LEFT JOIN {table_name} cr ON ST_Intersects (cr.midpoint, bpoly.geom);
12 |
--------------------------------------------------------------------------------
/ohsome_quality_api/indicators/road_comparison/templates.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | label_description:
3 | red: >-
4 | The completeness of OSM roads in your area-of-interest is low.
5 | yellow: >-
6 | The completeness of OSM roads in your area-of-interest is medium.
7 | green: >-
8 | The completeness of OSM roads in your area-of-interest is high.
9 |
10 | undefined: >-
11 | Comparison could not be made.
12 | result_description: >-
13 | The completeness in comparison to $dataset is $ratio%.
14 | $dataset have a total length of $length_total.
15 | $length_matched of $dataset are covered by OSM roads.
16 |
--------------------------------------------------------------------------------
/ohsome_quality_api/ohsome/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/ohsome/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/oqt.py:
--------------------------------------------------------------------------------
1 | """Controller for computing Indicators."""
2 |
3 | import logging
4 | from typing import Coroutine
5 |
6 | from geojson import Feature, FeatureCollection
7 |
8 | from ohsome_quality_api.indicators.base import BaseIndicator as Indicator
9 | from ohsome_quality_api.topics.models import BaseTopic as Topic
10 | from ohsome_quality_api.topics.models import TopicData, TopicDefinition
11 | from ohsome_quality_api.utils.helper import get_class_from_key
12 | from ohsome_quality_api.utils.helper_asyncio import gather_with_semaphore
13 | from ohsome_quality_api.utils.validators import validate_area
14 |
15 |
16 | async def create_indicator(
17 | key: str,
18 | bpolys: FeatureCollection,
19 | topic: TopicData | TopicDefinition,
20 | include_figure: bool = True,
21 | **kwargs,
22 | ) -> list[Indicator]:
23 | """Create indicator(s) for features of a GeoJSON FeatureCollection.
24 |
25 | Indicators are computed asynchronously utilizing semaphores.
26 | Properties of the input GeoJSON are preserved.
27 | """
28 | tasks: list[Coroutine] = []
29 | for i, feature in enumerate(bpolys.features):
30 | if "id" not in feature.keys():
31 | feature["id"] = i
32 | # Disable size limit for the Mapping Saturation indicator
33 | # TODO: Remove size restriction
34 | if isinstance(topic, TopicDefinition) and key not in [
35 | "mapping-saturation",
36 | "currentness",
37 | "building-comparison",
38 | "road-comparison",
39 | "attribute-completeness",
40 | "land-cover-thematic-accuracy",
41 | ]:
42 | validate_area(feature)
43 | tasks.append(
44 | _create_indicator(
45 | key,
46 | feature,
47 | topic,
48 | include_figure,
49 | **kwargs,
50 | )
51 | )
52 | return await gather_with_semaphore(tasks)
53 |
54 |
55 | async def _create_indicator(
56 | key: str,
57 | feature: Feature,
58 | topic: Topic,
59 | include_figure: bool = True,
60 | **kwargs,
61 | ) -> Indicator:
62 | """Create an indicator from scratch."""
63 |
64 | logging.info("Indicator key: {0:4}".format(key))
65 | logging.info("Topic key: {0:4}".format(topic.key))
66 | logging.info("Feature id: {0:4}".format(feature.get("id", "None")))
67 |
68 | indicator_class = get_class_from_key(class_type="indicator", key=key)
69 | indicator = indicator_class(
70 | topic,
71 | feature,
72 | **kwargs,
73 | )
74 |
75 | logging.info("Run preprocessing")
76 | await indicator.preprocess()
77 |
78 | logging.info("Run calculation")
79 | indicator.calculate()
80 |
81 | if include_figure:
82 | logging.info("Run figure creation")
83 | indicator.create_figure()
84 | else:
85 | indicator.result.figure = None
86 |
87 | return indicator
88 |
--------------------------------------------------------------------------------
/ohsome_quality_api/projects/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/projects/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/projects/definitions.py:
--------------------------------------------------------------------------------
1 | """Global Variables and Functions."""
2 |
3 | import os
4 | from enum import Enum
5 |
6 | import yaml
7 |
8 | from ohsome_quality_api.projects.models import Project
9 | from ohsome_quality_api.utils.helper import get_module_dir
10 |
11 |
12 | def get_project_keys() -> list[str]:
13 | return [str(t) for t in load_projects().keys()]
14 |
15 |
16 | def load_projects() -> dict[str, Project]:
17 | """Read definitions of projects.
18 |
19 | Returns:
20 | A dict with all projects included.
21 | """
22 | directory = get_module_dir("ohsome_quality_api.projects")
23 | file = os.path.join(directory, "projects.yaml")
24 | with open(file, "r") as f:
25 | raw = yaml.safe_load(f)
26 | projects = {}
27 | for k, v in raw.items():
28 | projects[k] = Project(**v)
29 | return projects
30 |
31 |
32 | def get_project_metadata() -> dict[str, Project]:
33 | projects = load_projects()
34 | return projects
35 |
36 |
37 | def get_project(project_key: str) -> Project:
38 | projects = get_project_metadata()
39 | try:
40 | return projects[project_key]
41 | except KeyError as error:
42 | raise KeyError(
43 | "Invalid project key. Valid project keys are: " + str(projects.keys())
44 | ) from error
45 |
46 |
47 | ProjectEnum = Enum("ProjectEnum", {key: key for key in get_project_keys() + ["all"]})
48 |
--------------------------------------------------------------------------------
/ohsome_quality_api/projects/models.py:
--------------------------------------------------------------------------------
1 | """Pydantic Models for Projects."""
2 |
3 | from pydantic import BaseModel, ConfigDict
4 |
5 |
6 | class Project(BaseModel):
7 | name: str
8 | description: str
9 | model_config = ConfigDict(
10 | title="Project",
11 | frozen=True,
12 | extra="forbid",
13 | populate_by_name=True,
14 | )
15 |
--------------------------------------------------------------------------------
/ohsome_quality_api/projects/projects.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # TODO description
3 |
4 | core:
5 | name: TODO
6 | description: >-
7 | something that is still a TODO
8 |
9 | mapaction:
10 | name: TODO
11 | description: >-
12 | something that is still a TODO
13 |
14 | sketchmap:
15 | name: TODO
16 | description: >-
17 | something that is still a TODO
18 |
19 | unicef:
20 | name: TODO
21 | description: >-
22 | something that is still a TODO
23 |
24 | misc:
25 | name: TODO
26 | description: >-
27 | something that is still a TODO
28 |
29 | experimental:
30 | name: TODO
31 | description: >-
32 | something that is still a TODO
33 |
34 | corine-land-cover:
35 | name: TODO
36 | description: >-
37 | something that is still a TODO
38 |
39 | idealvgi:
40 | name: TODO
41 | description: >-
42 | something that is still a TODO
43 |
44 | expanse:
45 | name: TODO
46 | description: >-
47 | something that is still a TODO
48 |
49 | bkg:
50 | name: TODO
51 | description: >-
52 | something that is still a TODO
53 |
--------------------------------------------------------------------------------
/ohsome_quality_api/quality_dimensions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/quality_dimensions/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/quality_dimensions/definitions.py:
--------------------------------------------------------------------------------
1 | """Global Variables and Functions."""
2 |
3 | import os
4 | from enum import Enum
5 |
6 | import yaml
7 |
8 | from ohsome_quality_api.quality_dimensions.models import QualityDimension
9 | from ohsome_quality_api.utils.helper import get_module_dir
10 |
11 |
12 | def load_quality_dimensions() -> dict[str, QualityDimension]:
13 | """Read definitions of quality dimensions.
14 |
15 | Returns:
16 | A dict with all quality dimensions included.
17 | """
18 | directory = get_module_dir("ohsome_quality_api.quality_dimensions")
19 | file = os.path.join(directory, "quality_dimensions.yaml")
20 | with open(file, "r") as f:
21 | raw = yaml.safe_load(f)
22 | quality_dimensions = {}
23 | for k, v in raw.items():
24 | quality_dimensions[k] = QualityDimension(**v)
25 | return quality_dimensions
26 |
27 |
28 | def get_quality_dimensions() -> dict[str, QualityDimension]:
29 | return load_quality_dimensions()
30 |
31 |
32 | def get_quality_dimension(qd_key: str) -> QualityDimension:
33 | quality_dimensions = get_quality_dimensions()
34 | try:
35 | return quality_dimensions[qd_key]
36 | except KeyError as error:
37 | raise KeyError(
38 | "Invalid quality dimension key. Valid quality dimension keys are: "
39 | + str(quality_dimensions.keys())
40 | ) from error
41 |
42 |
43 | def get_quality_dimension_keys() -> list[str]:
44 | return [str(t) for t in load_quality_dimensions().keys()]
45 |
46 |
47 | QualityDimensionEnum = Enum(
48 | "QualityDimensionEnum", {key: key for key in get_quality_dimension_keys()}
49 | )
50 |
--------------------------------------------------------------------------------
/ohsome_quality_api/quality_dimensions/models.py:
--------------------------------------------------------------------------------
1 | """Pydantic Models for Quality Dimensions."""
2 |
3 | from pydantic import BaseModel, ConfigDict
4 |
5 |
6 | class QualityDimension(BaseModel):
7 | name: str
8 | description: str
9 | source: str | None = None
10 | model_config = ConfigDict(
11 | title="Topic",
12 | frozen=True,
13 | extra="forbid",
14 | populate_by_name=True,
15 | )
16 |
--------------------------------------------------------------------------------
/ohsome_quality_api/quality_dimensions/quality_dimensions.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | completeness:
4 | name: Completeness
5 | description: >-
6 | The degree to which subject data associated with an entity has values for all expected attributes
7 | and related entity instances in a specific context of use.
8 | source:
9 | https://www.iso.org/standard/78900.html
10 |
11 | currentness:
12 | name: Currentness
13 | description: >-
14 | The degree to which data has attributes that are of the right age in a specific context of use.
15 | source:
16 | https://www.iso.org/standard/35736.html
17 |
18 | thematic-accuracy:
19 | name: Thematic Accuracy
20 | description: >-
21 | The degree to which attributes of data are correct (agree with "truth" of the reference dataset).
22 | source:
23 | https://onlinelibrary.wiley.com/doi/full/10.1155/2014/372349
24 |
25 | minimal:
26 | name: Minimal
27 | description: >-
28 | A minimal quality dimension definition for testing purposes.
29 |
30 | none:
31 | name: None
32 | description: >-
33 | No specific quality dimension
34 |
--------------------------------------------------------------------------------
/ohsome_quality_api/topics/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/topics/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/topics/definitions.py:
--------------------------------------------------------------------------------
1 | import os
2 | from enum import Enum
3 |
4 | import yaml
5 |
6 | from ohsome_quality_api.projects.definitions import ProjectEnum
7 | from ohsome_quality_api.topics.models import TopicDefinition
8 | from ohsome_quality_api.utils.helper import get_module_dir
9 |
10 |
11 | def load_topic_presets() -> dict[str, TopicDefinition]:
12 | """Read ohsome API parameters of all topic from YAML file."""
13 | directory = get_module_dir("ohsome_quality_api.topics")
14 | file = os.path.join(directory, "presets.yaml")
15 | with open(file, "r") as f:
16 | raw = yaml.safe_load(f)
17 | topics = {}
18 | for k, v in raw.items():
19 | v["filter"] = v.pop("filter")
20 | v["key"] = k
21 | topics[k] = TopicDefinition(**v)
22 | return topics
23 |
24 |
25 | def get_topic_keys() -> list[str]:
26 | return [str(t) for t in load_topic_presets().keys()]
27 |
28 |
29 | def get_topic_presets(project: ProjectEnum = None) -> dict[str, TopicDefinition]:
30 | topics = load_topic_presets()
31 | if project is not None:
32 | return {k: v for k, v in topics.items() if project in v.projects}
33 | else:
34 | return topics
35 |
36 |
37 | def get_topic_preset(topic_key: str) -> TopicDefinition:
38 | """Get ohsome API parameters of a single topic based on topic key."""
39 | topics = load_topic_presets()
40 | try:
41 | return topics[topic_key]
42 | except KeyError as error:
43 | raise KeyError(
44 | "Invalid topic key. Valid topic keys are: " + str(topics.keys())
45 | ) from error
46 |
47 |
48 | def get_valid_topics(indicator_name: str) -> tuple:
49 | """Get valid Indicator/Topic combination of an Indicator."""
50 | td = load_topic_presets()
51 | return tuple(topic for topic in td if indicator_name in td[topic].indicators)
52 |
53 |
54 | TopicEnum = Enum("TopicEnum", {name: name for name in get_topic_keys()})
55 |
--------------------------------------------------------------------------------
/ohsome_quality_api/topics/models.py:
--------------------------------------------------------------------------------
1 | """Pydantic Models for Topics
2 |
3 | Note:
4 | The topic key, name, description and the ohsome API endpoint and filter are defined
5 | in the `presets.yaml` file in the `topic` module.
6 | """
7 |
8 | from typing import Literal
9 |
10 | from pydantic import BaseModel, ConfigDict
11 |
12 | from ohsome_quality_api.projects.definitions import ProjectEnum
13 | from ohsome_quality_api.utils.helper import snake_to_lower_camel
14 |
15 |
16 | class BaseTopic(BaseModel):
17 | key: str
18 | name: str
19 | description: str
20 | model_config = ConfigDict(
21 | alias_generator=snake_to_lower_camel,
22 | extra="forbid",
23 | frozen=True,
24 | populate_by_name=True,
25 | title="Topic",
26 | )
27 |
28 |
29 | class TopicDefinition(BaseTopic):
30 | """Includes the ohsome API endpoint and parameters needed to retrieve the data."""
31 |
32 | endpoint: Literal["elements"]
33 | aggregation_type: Literal["area", "count", "length", "perimeter"]
34 | filter: str
35 | indicators: list[str]
36 | projects: list[ProjectEnum]
37 | source: str | None = None
38 | ratio_filter: str | None = None
39 |
40 |
41 | class TopicData(BaseTopic):
42 | """Includes the data associated with the topic."""
43 |
44 | data: dict
45 |
--------------------------------------------------------------------------------
/ohsome_quality_api/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/ohsome_quality_api/utils/__init__.py
--------------------------------------------------------------------------------
/ohsome_quality_api/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | """Custom exception classes."""
2 |
3 | from schema import SchemaError
4 |
5 |
6 | class OhsomeApiError(Exception):
7 | """Request to ohsome API failed."""
8 |
9 | def __init__(self, message):
10 | self.name = "OhsomeApiError"
11 | self.message = message
12 |
13 |
14 | class SizeRestrictionError(ValueError):
15 | """Exception raised if size of input GeoJSON Geometry is too big."""
16 |
17 | def __init__(self, geom_size_limit, geom_size):
18 | self.name = "SizeRestrictionError"
19 | self.message = (
20 | f"Input GeoJSON Geometry is too big ({geom_size} km²). "
21 | f"The area should be less than {geom_size_limit} km²."
22 | )
23 |
24 |
25 | class DatabaseError(Exception):
26 | pass
27 |
28 |
29 | class EmptyRecordError(DatabaseError):
30 | def __init__(self):
31 | self.name = "EmptyRecordError"
32 | self.message = "Query returned no record."
33 |
34 |
35 | class TopicDataSchemaError(Exception):
36 | def __init__(self, message, schema_error: SchemaError):
37 | self.name = "TopicDataSchemaError"
38 | self.message = "{0}\n{1}".format(message, schema_error)
39 |
--------------------------------------------------------------------------------
/ohsome_quality_api/utils/helper_asyncio.py:
--------------------------------------------------------------------------------
1 | """Helper functions for `asyncio`."""
2 |
3 | import asyncio
4 | from typing import Coroutine
5 |
6 |
7 | async def gather_with_semaphore(tasks: list, *args, **kwargs) -> Coroutine:
8 | """A wrapper around `gather` to limit the number of tasks executed at a time."""
9 | # Semaphore needs to initiated inside of the event loop
10 | semaphore = asyncio.Semaphore(4)
11 |
12 | async def sem_task(task):
13 | async with semaphore:
14 | return await task
15 |
16 | return await asyncio.gather(*(sem_task(task) for task in tasks), *args, **kwargs)
17 |
18 |
19 | def filter_exceptions(results: list) -> list[Exception]:
20 | """Return all exceptions"""
21 | return [i for i in results if isinstance(i, Exception)]
22 |
--------------------------------------------------------------------------------
/ohsome_quality_api/utils/helper_geo.py:
--------------------------------------------------------------------------------
1 | from geojson import Feature
2 | from geojson.utils import coords
3 | from pyproj import Geod
4 |
5 |
6 | def calculate_area(feature: Feature) -> float:
7 | """Calculate area in meter."""
8 | geod = Geod(ellps="WGS84")
9 | coordinates = tuple(coords(feature))
10 | lons = tuple(c[0] for c in coordinates)
11 | lats = tuple(c[1] for c in coordinates)
12 | area, _ = geod.polygon_area_perimeter(lons=lons, lats=lats)
13 | return area
14 |
--------------------------------------------------------------------------------
/ohsome_quality_api/utils/validators.py:
--------------------------------------------------------------------------------
1 | from geojson import Feature
2 |
3 | from ohsome_quality_api.config import get_config_value
4 | from ohsome_quality_api.utils.exceptions import (
5 | SizeRestrictionError,
6 | )
7 | from ohsome_quality_api.utils.helper_geo import calculate_area
8 |
9 |
10 | def validate_area(feature: Feature):
11 | """Check area size of feature against size limit configuration value."""
12 | size_limit = float(get_config_value("geom_size_limit"))
13 | area = calculate_area(feature) / (1000 * 1000) # sqkm
14 | if area > size_limit:
15 | raise SizeRestrictionError(size_limit, area)
16 |
--------------------------------------------------------------------------------
/pipeline_config.groovy:
--------------------------------------------------------------------------------
1 | libraries{
2 | misc
3 | rocketchat
4 | }
5 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "ohsome-quality-api"
3 | version = "1.9.0"
4 | description = "Data quality estimations for OpenStreetMap."
5 | authors = [{email="ohsome@heigit.org"}]
6 | keywords = [
7 | "osm",
8 | "gis",
9 | "geography",
10 | "metric",
11 | "ohsome",
12 | "quality",
13 | ]
14 | requires-python = ">=3.12"
15 | dependencies = [
16 | "async-lru>=2.0.5",
17 | "asyncpg>=0.30.0",
18 | "fastapi>=0.115.12",
19 | "geojson>=3.2.0",
20 | "geojson-pydantic>=2.0.0",
21 | "httpx>=0.28.1",
22 | "kaleido==0.2.1",
23 | "plotly>=6.0.1",
24 | "pydantic>=2.11.4",
25 | "pyproj>=3.7.1",
26 | "python-dateutil>=2.9.0.post0",
27 | "pyyaml>=6.0.2",
28 | "rpy2>=3.5.17",
29 | "schema>=0.7.7",
30 | "scikit-learn>=1.6.1",
31 | "uvicorn>=0.34.2",
32 | ]
33 | [project.urls]
34 | Homepage = "https://api.quality.ohsome.org"
35 | Repository = "https://github.com/GIScience/ohsome-quality-api"
36 |
37 | [dependency-groups]
38 | dev = [
39 | "approvaltests>=14.5.0",
40 | "click>=8.2.0",
41 | "fastapi[standard]>=0.115.12",
42 | "pre-commit>=4.2.0",
43 | "pygments>=2.19.1", # add syntax highlighting to pytest
44 | "pytest>=8.3.5",
45 | "pytest-asyncio>=0.26.0",
46 | "pytest-cov>=6.1.1",
47 | "pytest-mock>=3.14.0",
48 | "pytest-sugar>=1.0.0",
49 | "pytest-xdist>=3.6.1",
50 | "ruff==0.11.9", # fixed because of pre-commit
51 | "vcrpy>=7.0.0",
52 | ]
53 |
54 | [build-system]
55 | requires = ["uv_build>=0.7.5"]
56 | build-backend = "uv_build"
57 |
58 | [tool.uv.build-backend]
59 | module-root = ""
60 |
61 | [tool.ruff.lint]
62 | select = [
63 | "E", # pycodestyle Error
64 | "F", # Pyflakes
65 | "I", # isort
66 | "N", # pep8-nameing
67 | "Q", # flake8-quotes
68 | "W", # pycodestyle Warning
69 | "C90", # mccabe
70 | ]
71 |
72 | [tool.ruff.lint.per-file-ignores]
73 | "ohsome_quality_api/indicators/mapping_saturation/models.py" = [
74 | "N803",
75 | "N806",
76 | ]
77 |
78 | [tool.pytest.ini_options]
79 | testpaths = ["tests"]
80 | asyncio_default_fixture_loop_scope = "function"
81 |
82 |
--------------------------------------------------------------------------------
/regression-tests/README.md:
--------------------------------------------------------------------------------
1 | # Regression tests with hurl
2 |
3 | Hurl is an HTTP command line client and HTTP test tool:
4 |
5 | https://hurl.dev
6 |
7 | In order to run the HTTP tests in this directory, `hurl` must be installed.
8 | The tests can be run against the different stages using the following scripts:
9 |
10 | * [run_hurl_tests_DEV.sh](./run_hurl_tests_DEV.sh)
11 | * [run_hurl_tests_TEST.sh](./run_hurl_tests_TEST.sh)
12 | * [run_hurl_tests_PROD.sh](./run_hurl_tests_PROD.sh)
13 |
14 | Please note that you may get different results depending on the supported endpoints and connected spatial database of the system running on a given stage.
15 |
16 | An HTML-report with the results is generated here:
17 |
18 | [report/index.html](./report/index.html)
19 |
20 |
21 |
--------------------------------------------------------------------------------
/regression-tests/__run_hurl_tests_for_stage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # requires the env variable $HURL_BASE_URL
4 |
5 | rm -rf report
6 | mkdir report
7 |
8 | hurl *.hurl --report-html report
9 | printf "\n\nhurl report: file://$PWD/report/index.html\n"
10 |
11 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then
12 | xdg-open report/index.html
13 | elif [[ "$OSTYPE" == "darwin"* ]]; then
14 | open report/index.html
15 | else
16 | printf "\nOS could not be detected. Please open report manually!\n"
17 | fi
--------------------------------------------------------------------------------
/regression-tests/building-comparison.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/building-comparison
2 | accept: application/json
3 | file, building-comparison.json;
4 | HTTP 200
5 | [Captures]
6 | duration_in_ms: duration
7 |
--------------------------------------------------------------------------------
/regression-tests/buildingcount_bbox.json:
--------------------------------------------------------------------------------
1 | {
2 | "topic": "building-count",
3 | "bpolys": {
4 | "type": "FeatureCollection",
5 | "features": [
6 | {
7 | "type": "Feature",
8 | "id": "box 1",
9 | "bbox": [
10 | 8.748488,
11 | 49.47397,
12 | 8.7532061,
13 | 49.4767595
14 | ],
15 | "properties": {
16 | "id": "box 1"
17 | },
18 | "geometry": {
19 | "type": "Polygon",
20 | "coordinates": [
21 | [
22 | [
23 | 8.748488,
24 | 49.47397
25 | ],
26 | [
27 | 8.7532061,
28 | 49.47397
29 | ],
30 | [
31 | 8.7532061,
32 | 49.4767595
33 | ],
34 | [
35 | 8.748488,
36 | 49.4767595
37 | ],
38 | [
39 | 8.748488,
40 | 49.47397
41 | ]
42 | ]
43 | ]
44 | }
45 | }
46 | ]
47 | }
48 | }
--------------------------------------------------------------------------------
/regression-tests/buildingcount_bbox_attributecompleteness.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/attribute-completeness
2 | accept: application/json
3 | [Options]
4 | verbose: true
5 | file,buildingcount_bbox_housenumber.json;
6 |
7 | HTTP *
8 |
9 | [Asserts]
10 | duration < 20000
11 | status == 200
12 | bytes count > 1700
13 |
14 | jsonpath "$.result[0].metadata.name" == "Attribute Completeness"
15 | jsonpath "$.result[0].topic.name" == "Building Count"
16 | jsonpath "$.result[0].result.description" matches /^85\.\d{1}% of all "building count" features \(all: 6\d{1} elements\) in your area of interest have the selected additional attribute house number address \(matched: 5\d{1} elements\)\. The attribute completeness is high \(>75%\)\.$/
17 | jsonpath "$.result[0].result.figure.data[0].gauge.steps[0].color" == "tomato"
18 | jsonpath "$.result[0].result.label" == "green"
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/regression-tests/buildingcount_bbox_currentness.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/currentness
2 | accept: application/json
3 | [Options]
4 | verbose: true
5 | file,buildingcount_bbox.json;
6 |
7 | HTTP *
8 |
9 | [Asserts]
10 | duration < 20000
11 | status == 200
12 | bytes count > 20000
13 |
14 | jsonpath "$.result[0].topic.name" == "Building Count"
15 | jsonpath "$.result[0].result.description" matches /^In the area of interest 1\d{1}% of the 6\d{1} features were edited \(created or modified\) for the last time in the period between .. ... 20.. and .. ... 20...
16 | |Many features are out-of-date.$/
17 | jsonpath "$.result[0].result.figure.data[1].marker.color" == "#21BA45"
18 | jsonpath "$.result[0].result.label" == "red"
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/regression-tests/buildingcount_bbox_housenumber.json:
--------------------------------------------------------------------------------
1 | {
2 | "topic": "building-count",
3 | "attributes": ["address-housenumber"],
4 | "bpolys": {
5 | "type": "FeatureCollection",
6 | "features": [
7 | {
8 | "type": "Feature",
9 | "id": "box 1",
10 | "bbox": [
11 | 8.748488,
12 | 49.47397,
13 | 8.7532061,
14 | 49.4767595
15 | ],
16 | "properties": {
17 | "id": "box 1"
18 | },
19 | "geometry": {
20 | "type": "Polygon",
21 | "coordinates": [
22 | [
23 | [
24 | 8.748488,
25 | 49.47397
26 | ],
27 | [
28 | 8.7532061,
29 | 49.47397
30 | ],
31 | [
32 | 8.7532061,
33 | 49.4767595
34 | ],
35 | [
36 | 8.748488,
37 | 49.4767595
38 | ],
39 | [
40 | 8.748488,
41 | 49.47397
42 | ]
43 | ]
44 | ]
45 | }
46 | }
47 | ]
48 | }
49 | }
--------------------------------------------------------------------------------
/regression-tests/hospitals_adminarea_mappingsaturation__no_features.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/mapping-saturation
2 | accept: application/json
3 | [Options]
4 | verbose: true
5 | file,hospitals_adminarea.json;
6 |
7 | HTTP *
8 |
9 | [Asserts]
10 | duration < 20000
11 | status == 200
12 | bytes count > 1600
13 |
14 | jsonpath "$.result[0].topic.name" == "Hospitals"
15 | jsonpath "$.result[0].result.description" == "No features were mapped in this region."
16 | jsonpath "$.result[0].result.figure.data[0].labels[0]" == "The creation of the Indicator was unsuccessful."
17 |
18 |
19 |
--------------------------------------------------------------------------------
/regression-tests/long-running/roads_bbox.json:
--------------------------------------------------------------------------------
1 | {
2 | "topic": "roads",
3 | "bpolys": {
4 | "type": "FeatureCollection",
5 | "features": [
6 | {
7 | "type": "Feature",
8 | "id": "box 1",
9 | "bbox": [
10 | 8.748488,
11 | 49.47397,
12 | 8.7532061,
13 | 49.4767595
14 | ],
15 | "properties": {
16 | "id": "box 1"
17 | },
18 | "geometry": {
19 | "type": "Polygon",
20 | "coordinates": [
21 | [
22 | [
23 | 8.748488,
24 | 49.47397
25 | ],
26 | [
27 | 8.7532061,
28 | 49.47397
29 | ],
30 | [
31 | 8.7532061,
32 | 49.4767595
33 | ],
34 | [
35 | 8.748488,
36 | 49.4767595
37 | ],
38 | [
39 | 8.748488,
40 | 49.47397
41 | ]
42 | ]
43 | ]
44 | }
45 | }
46 | ]
47 | }
48 | }
--------------------------------------------------------------------------------
/regression-tests/long-running/roads_bbox_currentness.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/currentness
2 | accept: application/json
3 | [Options]
4 | verbose: true
5 | file,roads_bbox.json;
6 |
7 | HTTP *
8 |
9 | [Asserts]
10 | duration < 300000
11 | status == 200
12 | bytes count > 20000
13 |
14 | jsonpath "$.result[0].topic.name" == "Roads"
15 | jsonpath "$.result[0].result.description" contains "Please note that in the area of interest less than 25 features of the selected topic are present today."
16 | jsonpath "$.result[0].result.figure.data[1].marker.color" == "#21BA45"
17 | jsonpath "$.result[0].result.label" == "green"
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/regression-tests/metadata.hurl:
--------------------------------------------------------------------------------
1 | GET {{BASE_URL}}/metadata?project=all
2 | accept: application/json
3 | [Options]
4 | verbose: true
5 |
6 | HTTP *
7 |
8 | [Asserts]
9 | duration < 2000
10 | status == 200
11 | bytes count > 25000
12 |
13 | jsonpath "$.result.indicators.mapping-saturation.description" contains "Calculate if mapping has saturated."
14 | jsonpath "$.result.indicators.currentness.description" contains "Estimate currentness of features"
15 | jsonpath "$.result.topics.building-count.aggregationType" == "count"
16 | jsonpath "$.result.topics.roads.aggregationType" == "length"
17 | jsonpath "$.result.qualityDimensions.minimal.name" == "Minimal"
18 | jsonpath "$.result.projects.core.description" == "something that is still a TODO"
19 |
--------------------------------------------------------------------------------
/regression-tests/road-comparison.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/road-comparison
2 | accept: application/json
3 | file, road-comparison.json;
4 | HTTP 200
5 | [Captures]
6 | duration_in_ms: duration
7 |
--------------------------------------------------------------------------------
/regression-tests/roads_polygon.json:
--------------------------------------------------------------------------------
1 | {
2 | "topic": "roads",
3 | "bpolys": {
4 | "type": "FeatureCollection",
5 | "features": [
6 | {
7 | "type": "Feature",
8 | "id": "area 1",
9 | "properties": {
10 | "id": "area 1"
11 | },
12 | "geometry": {
13 | "type": "Polygon",
14 | "coordinates": [
15 | [
16 | [
17 | 100.4674621,
18 | 5.1745498
19 | ],
20 | [
21 | 100.4832462,
22 | 5.1827608
23 | ],
24 | [
25 | 100.4952558,
26 | 5.1701022
27 | ],
28 | [
29 | 100.4870207,
30 | 5.1547062
31 | ],
32 | [
33 | 100.4678053,
34 | 5.1588118
35 | ],
36 | [
37 | 100.4674621,
38 | 5.1745498
39 | ]
40 | ]
41 | ]
42 | }
43 | }
44 | ]
45 | }
46 | }
--------------------------------------------------------------------------------
/regression-tests/roads_polygon_attributecompleteness.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/attribute-completeness
2 | accept: application/json
3 | [Options]
4 | verbose: true
5 | file,roads_polygon_maxspeed.json;
6 |
7 | HTTP *
8 |
9 | [Asserts]
10 | duration < 5000
11 | status == 200
12 | bytes count > 1900
13 |
14 | jsonpath "$.result[0].metadata.name" == "Attribute Completeness"
15 | jsonpath "$.result[0].topic.name" == "Roads"
16 | jsonpath "$.result[0].result.description" matches /^26\.\d{1}% of all "roads" features \(all: 103\d{1}\.\d{2} km\) in your area of interest have the selected additional attribute maxspeed \(matched: 27\d{1}\.\d{2} km\)\. The attribute completeness is medium \(25%-75%\)\.$/
17 | jsonpath "$.result[0].result.figure.data[0].gauge.steps[0].color" == "tomato"
18 | jsonpath "$.result[0].result.label" == "yellow"
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/regression-tests/roads_polygon_mappingsaturation.hurl:
--------------------------------------------------------------------------------
1 | POST {{BASE_URL}}/indicators/mapping-saturation
2 | accept: application/json
3 | [Options]
4 | verbose: true
5 | file,roads_polygon.json;
6 |
7 | HTTP *
8 |
9 | [Asserts]
10 | duration < 20000
11 | status == 200
12 | bytes count > 24000
13 |
14 | jsonpath "$.result[0].topic.name" == "Roads"
15 | jsonpath "$.result[0].result.description" matches /^The saturation of the last 3 years is 98.\d{2}%.High saturation has been reached \(97% < Saturation ≤ 100%\).$/
16 | jsonpath "$.result[0].result.figure.data[0].line.color" == "#2185D0"
17 | jsonpath "$.result[0].result.label" == "green"
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/regression-tests/roads_polygon_maxspeed.json:
--------------------------------------------------------------------------------
1 | {
2 | "topic": "roads",
3 | "attributes": ["maxspeed"],
4 | "bpolys": {
5 | "type": "FeatureCollection",
6 | "features": [
7 | {
8 | "type": "Feature",
9 | "id": "area 1",
10 | "properties": {
11 | "id": "area 1"
12 | },
13 | "geometry": {
14 | "type": "Polygon",
15 | "coordinates": [
16 | [
17 | [
18 | 8.643064,
19 | 49.4412261
20 | ],
21 | [
22 | 8.6595344,
23 | 49.3665657
24 | ],
25 | [
26 | 8.790611,
27 | 49.4280458
28 | ],
29 | [
30 | 8.643064,
31 | 49.4412261
32 | ]
33 | ]
34 | ]
35 | }
36 | }
37 | ]
38 | }
39 | }
--------------------------------------------------------------------------------
/regression-tests/run_hurl_tests_DEV.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export HURL_BASE_URL=http://127.0.0.1:8080
4 |
5 | cd "$(dirname "$0")"
6 | ./__run_hurl_tests_for_stage.sh
--------------------------------------------------------------------------------
/regression-tests/run_hurl_tests_PROD.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export HURL_BASE_URL=https://api.quality.ohsome.org/v1
4 |
5 | cd "$(dirname "$0")"
6 | ./__run_hurl_tests_for_stage.sh
--------------------------------------------------------------------------------
/regression-tests/run_hurl_tests_TEST.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export HURL_BASE_URL=https://api.quality.ohsome.org/v1-test
4 |
5 | cd "$(dirname "$0")"
6 | ./__run_hurl_tests_for_stage.sh
--------------------------------------------------------------------------------
/scripts/create_new_github_release.sh:
--------------------------------------------------------------------------------
1 |
2 | gh release create 1.4.0 -t "1.4.0" \
3 | -n "See the [changelog](https://github.com/GIScience/ohsome-quality-api/blob/main/CHANGELOG.md#release-140) for release details."
4 |
--------------------------------------------------------------------------------
/scripts/functions.sh:
--------------------------------------------------------------------------------
1 | # donation by mcauer as used in the 'ohsome-dashboard' project
2 |
3 | prompt_user() {
4 | local question="$1" # Capture the first argument as the question
5 | while true; do
6 | read -p "$question (y/n): " choice
7 | case "$choice" in
8 | y|Y )
9 | echo "You chose Yes."
10 | return 0 # Exit the function and proceed
11 | ;;
12 | n|N )
13 | echo "You chose No. Exiting."
14 | exit 0 # Exit the script
15 | ;;
16 | * )
17 | echo "Invalid input. Please enter 'y' or 'n'."
18 | ;;
19 | esac
20 | done
21 | }
22 |
23 |
24 | run_sed() {
25 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then
26 | sed --in-place=.bak "$1" "$2"
27 | elif [[ "$OSTYPE" == "darwin"* ]]; then
28 | sed -i .bak "$1" "$2"
29 | else
30 | printf "\nOS could not be detected. Please open report manually!\n"
31 | fi
32 | }
33 |
34 | run_open() {
35 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then
36 | xdg-open "$1"
37 | elif [[ "$OSTYPE" == "darwin"* ]]; then
38 | open "$1"
39 | else
40 | printf "\nOS could not be detected. Please open URL manually!\n"
41 | fi
42 | }
43 |
44 | # Example usage
45 | #prompt_user "Do you want to continue?"
46 |
47 | # import in other script
48 |
49 | ## Get the directory of the current script
50 | #SCRIPT_DIR="$(dirname "$0")"
51 | #
52 | ## import user prompt
53 | #source "$SCRIPT_DIR/functions.sh"
54 |
--------------------------------------------------------------------------------
/scripts/update_swagger_scripts.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cd "$(dirname "$0")"/../ohsome_quality_api/api/static && \
4 | wget https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js -O swagger-ui-bundle.js && \
5 | wget https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css -O swagger-ui.css && \
6 | wget https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js -O redoc.standalone.js
7 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.organization=giscience
2 | sonar.projectKey=ohsome-quality-api
3 | sonar.projectName=ohsome quality API
4 |
5 | # settings for pull requests
6 | sonar.pullrequest.provider=github
7 | sonar.pullrequest.github.repository=GIScience/ohsome-quality-api
8 | sonar.pullrequest.github.endpoint=https://api.github.com/
9 |
10 | # supported Python versions
11 | sonar.python.version=3.10, 3.11
12 |
13 | # disable PL/SQL
14 | sonar.plsql.file.suffixes=""
15 |
16 | # exclude static files
17 | sonar.exclusions=ohsome_quality_api/api/static/*
18 |
19 | # exclude non-API worker files from coverage report
20 | sonar.coverage.exclusions=database/**,scripts/**,tests/**
21 |
22 | # the fixtures file only contains values, no code that can be duplicated
23 | sonar.cpd.exclusions=tests/unittests/mapping_saturation/fixtures.py
24 |
25 | sonar.issue.ignore.multicriteria=e1
26 | # S117: local variable and function parameter names should comply with a naming convention
27 | # Ignore for math formula parameter
28 | sonar.issue.ignore.multicriteria.e1.ruleKey=python:S117
29 | # S1192: String literals should not be duplicated
30 | sonar.issue.ignore.multicriteria.e1.ruleKey=python:S1192
31 | sonar.issue.ignore.multicriteria.e1.resourceKey=ohsome_quality_api/indicators/mapping_saturation/models.py
32 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/__init__.py
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators.py::test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt:
--------------------------------------------------------------------------------
1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt:
--------------------------------------------------------------------------------
1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].received.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_atribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].received.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers0-schema0].approved.txt:
--------------------------------------------------------------------------------
1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_filter_invalid[headers1-schema1].approved.txt:
--------------------------------------------------------------------------------
1 | Querying the ohsome API failed! Invalid filter syntax. Please look at the additional info and examples about the filter parameter at https://docs.ohsome.org/ohsome-api. Detailed error message: line 1, column 63: whitespaces, EQUALS (=), NOT_EQUALS (!=) or in expected, f encountered.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers0-schema0].approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_attribute_completeness.py-test_indicators_attribute_completeness_with_invalid_attribute_for_topic[headers1-schema1].approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour, source
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/api/test_indicators_land_cover_thematic_accuracy.py-test_invalid_class.approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "1.9.0",
3 | "detail": [
4 | {
5 | "ctx": {
6 | "expected": "'11', '12', '13', '14', '21', '22', '23', '24', '31', '32', '33', '41', '42', '51' or '52'"
7 | },
8 | "input": "1",
9 | "loc": [
10 | "body",
11 | "corineLandCoverClass"
12 | ],
13 | "msg": "Input should be '11', '12', '13', '14', '21', '22', '23', '24', '31', '32', '33', '41', '42', '51' or '52'",
14 | "type": "enum"
15 | }
16 | ],
17 | "type": "RequestValidationError"
18 | }
19 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestCalculation-test_calculate[indicator0].approved.txt:
--------------------------------------------------------------------------------
1 | 39.8% of all "buildings (count)" features (all: 30337 elements) in your area of interest have the selected additional attribute height of buildings (matched: 12083 elements). The attribute completeness is medium (25%-75%).
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestCalculation-test_calculate[indicator1].approved.txt:
--------------------------------------------------------------------------------
1 | 39.8% of all "buildings (count)" features (all: 30337 elements) in your area of interest have the selected additional attribute Height (matched: 12083 elements). The attribute completeness is medium (25%-75%).
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestFigure-test_create_figure[indicator0].approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "domain": {
5 | "x": [
6 | 0,
7 | 1
8 | ],
9 | "y": [
10 | 0,
11 | 1
12 | ]
13 | },
14 | "gauge": {
15 | "axis": {
16 | "range": [
17 | 0,
18 | 100
19 | ],
20 | "tickcolor": "darkblue",
21 | "tickfont": {
22 | "color": "black",
23 | "size": 20
24 | },
25 | "ticksuffix": "%",
26 | "tickwidth": 1
27 | },
28 | "bar": {
29 | "color": "black"
30 | },
31 | "steps": [
32 | {
33 | "color": "tomato",
34 | "range": [
35 | 0,
36 | 25.0
37 | ]
38 | },
39 | {
40 | "color": "gold",
41 | "range": [
42 | 25.0,
43 | 75.0
44 | ]
45 | },
46 | {
47 | "color": "darkseagreen",
48 | "range": [
49 | 75.0,
50 | 100
51 | ]
52 | }
53 | ]
54 | },
55 | "mode": "gauge+number",
56 | "number": {
57 | "suffix": "%"
58 | },
59 | "type": "indicator",
60 | "value": 39.8293
61 | }
62 | ],
63 | "layout": {
64 | "autosize": true,
65 | "font": {
66 | "color": "black",
67 | "family": "Arial"
68 | },
69 | "plot_bgcolor": "rgba(0,0,0,0)",
70 | "xaxis": {
71 | "fixedrange": true,
72 | "range": [
73 | -1,
74 | 1
75 | ],
76 | "showgrid": false,
77 | "visible": false
78 | },
79 | "yaxis": {
80 | "fixedrange": true,
81 | "range": [
82 | 0,
83 | 1
84 | ],
85 | "showgrid": false,
86 | "visible": false
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_attribute_completeness.py-TestFigure-test_create_figure[indicator1].approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "domain": {
5 | "x": [
6 | 0,
7 | 1
8 | ],
9 | "y": [
10 | 0,
11 | 1
12 | ]
13 | },
14 | "gauge": {
15 | "axis": {
16 | "range": [
17 | 0,
18 | 100
19 | ],
20 | "tickcolor": "darkblue",
21 | "tickfont": {
22 | "color": "black",
23 | "size": 20
24 | },
25 | "ticksuffix": "%",
26 | "tickwidth": 1
27 | },
28 | "bar": {
29 | "color": "black"
30 | },
31 | "steps": [
32 | {
33 | "color": "tomato",
34 | "range": [
35 | 0,
36 | 25.0
37 | ]
38 | },
39 | {
40 | "color": "gold",
41 | "range": [
42 | 25.0,
43 | 75.0
44 | ]
45 | },
46 | {
47 | "color": "darkseagreen",
48 | "range": [
49 | 75.0,
50 | 100
51 | ]
52 | }
53 | ]
54 | },
55 | "mode": "gauge+number",
56 | "number": {
57 | "suffix": "%"
58 | },
59 | "type": "indicator",
60 | "value": 39.8293
61 | }
62 | ],
63 | "layout": {
64 | "autosize": true,
65 | "font": {
66 | "color": "black",
67 | "family": "Arial"
68 | },
69 | "plot_bgcolor": "rgba(0,0,0,0)",
70 | "xaxis": {
71 | "fixedrange": true,
72 | "range": [
73 | -1,
74 | 1
75 | ],
76 | "showgrid": false,
77 | "visible": false
78 | },
79 | "yaxis": {
80 | "fixedrange": true,
81 | "range": [
82 | 0,
83 | 1
84 | ],
85 | "showgrid": false,
86 | "visible": false
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_attribute_completeness.py-test_create_description_attribute_filter.approved.txt:
--------------------------------------------------------------------------------
1 | 20.0% of all "buildings (count)" features (all: 10 elements) in your area of interest have the selected additional attribute Height (matched: 2 elements).
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_attribute_completeness.py-test_create_description_attribute_keys_multiple.approved.txt:
--------------------------------------------------------------------------------
1 | 20.0% of all "buildings (count)" features (all: 10 elements) in your area of interest have the selected additional attributes height of buildings, house number, street address (matched: 2 elements).
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_attribute_completeness.py-test_create_description_attribute_keys_single.approved.txt:
--------------------------------------------------------------------------------
1 | 20.0% of all "buildings (count)" features (all: 10 elements) in your area of interest have the selected additional attribute height of buildings (matched: 2 elements).
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate.approved.txt:
--------------------------------------------------------------------------------
1 | The completeness of OSM buildings in your area-of-interest is high. The completeness in comparison to EUBUCCO is 101.04%. The completeness in comparison to Microsoft Building Footprints is 101.04%.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_above_one_th.approved.txt:
--------------------------------------------------------------------------------
1 | Comparison could not be made. OSM has substantivly more buildings than the reference datasets. The reference dataset is likely to miss many buildings. The completeness in comparison to EUBUCCO is 505.21%. The completeness in comparison to Microsoft Building Footprints is 505.21%.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_above_one_th_and_expected.approved.txt:
--------------------------------------------------------------------------------
1 | The completeness of OSM buildings in your area-of-interest is medium. The completeness in comparison to EUBUCCO is 84.2%. The completeness in comparison to Microsoft Building Footprints is 84.2%.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt:
--------------------------------------------------------------------------------
1 | Comparison could not be made. EUBUCCO does not cover your area-of-interest. Microsoft Buildings does not cover your area-of-interest.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestCalculate-test_calculate_some_intersection.approved.txt:
--------------------------------------------------------------------------------
1 | The completeness of OSM buildings in your area-of-interest is high. EUBUCCO does not cover your area-of-interest. The completeness in comparison to Microsoft Building Footprints is 101.04%.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestFigure-test_create_figure.approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "hoverinfo": "text",
5 | "hovertext": [
6 | "OSM (Feb 02, 2025)",
7 | "OSM (Feb 02, 2025)"
8 | ],
9 | "marker": {
10 | "color": "#767676"
11 | },
12 | "name": "OSM building area (5.05 km\u00b2, 5.05 km\u00b2)",
13 | "type": "bar",
14 | "x": [
15 | "EUBUCCO",
16 | "Microsoft Building Footprints"
17 | ],
18 | "y": [
19 | 5.05,
20 | 5.05
21 | ]
22 | },
23 | {
24 | "hoverinfo": "text",
25 | "hovertext": [
26 | "EUBUCCO (Nov 3, 2022)",
27 | "Microsoft Building Footprints (July 5, 2022)"
28 | ],
29 | "legendgroup": "Reference",
30 | "marker": {
31 | "color": [
32 | "#A333C8",
33 | "#F2711C"
34 | ]
35 | },
36 | "name": "EUBUCCO (5.0 km\u00b2)",
37 | "type": "bar",
38 | "x": [
39 | "EUBUCCO",
40 | "Microsoft Building Footprints"
41 | ],
42 | "y": [
43 | 5.0,
44 | 5.0
45 | ]
46 | }
47 | ],
48 | "layout": {
49 | "barmode": "group",
50 | "legend": {
51 | "entrywidth": 270,
52 | "orientation": "h",
53 | "x": 0.5,
54 | "xanchor": "center",
55 | "y": -0.1,
56 | "yanchor": "top"
57 | },
58 | "shapes": [
59 | {
60 | "fillcolor": "#F2711C",
61 | "layer": "below",
62 | "legendgroup": "Reference",
63 | "line": {
64 | "width": 0
65 | },
66 | "name": "Microsoft Building Footprints (5.0 km\u00b2)",
67 | "showlegend": true,
68 | "type": "rect",
69 | "x0": 0,
70 | "x1": 0,
71 | "y0": 0,
72 | "y1": 0
73 | }
74 | ],
75 | "showlegend": true,
76 | "title": {
77 | "text": "Building Comparison"
78 | },
79 | "yaxis": {
80 | "title": {
81 | "text": "Building Area [km\u00b2]"
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestFigure-test_create_figure_above_one_th.approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "hoverinfo": "text",
5 | "hovertext": [
6 | "OSM (Feb 02, 2025)",
7 | "OSM (Feb 02, 2025)"
8 | ],
9 | "marker": {
10 | "color": "#767676"
11 | },
12 | "name": "OSM building area (5.05 km\u00b2, 5.05 km\u00b2)",
13 | "type": "bar",
14 | "x": [
15 | "EUBUCCO",
16 | "Microsoft Building Footprints"
17 | ],
18 | "y": [
19 | 5.05,
20 | 5.05
21 | ]
22 | },
23 | {
24 | "hoverinfo": "text",
25 | "hovertext": [
26 | "EUBUCCO (Nov 3, 2022)",
27 | "Microsoft Building Footprints (July 5, 2022)"
28 | ],
29 | "legendgroup": "Reference",
30 | "marker": {
31 | "color": [
32 | "#A333C8",
33 | "#F2711C"
34 | ]
35 | },
36 | "name": "EUBUCCO (5.0 km\u00b2)",
37 | "type": "bar",
38 | "x": [
39 | "EUBUCCO",
40 | "Microsoft Building Footprints"
41 | ],
42 | "y": [
43 | 5.0,
44 | 5.0
45 | ]
46 | }
47 | ],
48 | "layout": {
49 | "barmode": "group",
50 | "legend": {
51 | "entrywidth": 270,
52 | "orientation": "h",
53 | "x": 0.5,
54 | "xanchor": "center",
55 | "y": -0.1,
56 | "yanchor": "top"
57 | },
58 | "shapes": [
59 | {
60 | "fillcolor": "#F2711C",
61 | "layer": "below",
62 | "legendgroup": "Reference",
63 | "line": {
64 | "width": 0
65 | },
66 | "name": "Microsoft Building Footprints (5.0 km\u00b2)",
67 | "showlegend": true,
68 | "type": "rect",
69 | "x0": 0,
70 | "x1": 0,
71 | "y0": 0,
72 | "y1": 0
73 | }
74 | ],
75 | "showlegend": true,
76 | "title": {
77 | "text": "Building Comparison"
78 | },
79 | "yaxis": {
80 | "title": {
81 | "text": "Building Area [km\u00b2]"
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_building_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "hoverinfo": "none",
5 | "labels": [
6 | "The creation of the Indicator was unsuccessful."
7 | ],
8 | "marker": {
9 | "colors": [
10 | "rgba(0, 0, 0, 0)"
11 | ]
12 | },
13 | "textposition": "inside",
14 | "texttemplate": "%{label}",
15 | "type": "pie",
16 | "values": [
17 | 1
18 | ]
19 | }
20 | ],
21 | "layout": {
22 | "paper_bgcolor": "white",
23 | "plot_bgcolor": "white",
24 | "showlegend": false,
25 | "title": {
26 | "text": "Building Comparison"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_calculate.approved.txt:
--------------------------------------------------------------------------------
1 | In the area of interest 55% of the 29931 features were edited (created or modified) for the last time in the period between 02 Feb 2022 and 02 Feb 2025.
2 | Most features are up-to-date.
3 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_low_contributions.approved.txt:
--------------------------------------------------------------------------------
1 | Please note that in the area of interest less than 25 features of the selected topic are present today. In the area of interest 55% of the 20 features were edited (created or modified) for the last time in the period between 02 Feb 2022 and 02 Feb 2025.
2 | Most features are up-to-date.
3 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_months_without_edit.approved.txt:
--------------------------------------------------------------------------------
1 | Please note that there was no mapping activity for 13 months in this region. In the area of interest 55% of the 30 features were edited (created or modified) for the last time in the period between 02 Feb 2022 and 02 Feb 2025.
2 | Most features are up-to-date.
3 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_no_amenities.approved.txt:
--------------------------------------------------------------------------------
1 | In the area of interest no features of the selected topic are present today.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_currentness.py-TestCalculation-test_no_subway_stations.approved.txt:
--------------------------------------------------------------------------------
1 | In the area of interest no features of the selected topic are present today.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers0-schema0.approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_indicators-test_indicators_attribute_completeness_with_invalid_attribute_for_topic-headers1-schema1.approved.txt:
--------------------------------------------------------------------------------
1 | Value error, Invalid combination of attribute maxspeed and topic building-count. Topic building-count supports these attributes: height, house-number, address-street, address-city, address-postcode, address-country, address-state, address-suburb, address-district, address-housenumber, building-levels, roof-shape, roof-levels, building-material, roof-material, roof-colour, building-colour
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_land-cover-thematic-accuracy.py-test_calculate.approved.txt:
--------------------------------------------------------------------------------
1 | precision recall f1-score support
2 |
3 | 0 0.00 0.00 0.00 0.0
4 | 11 0.94 0.86 0.90 13.681696054340627
5 | 12 0.84 0.87 0.85 4.868706606602494
6 | 13 0.00 0.00 0.00 0.0
7 | 14 0.13 0.42 0.20 0.3617314972878062
8 | 21 0.86 0.91 0.89 15.526750634345152
9 | 22 0.64 0.76 0.70 2.2997486561073313
10 | 23 0.75 0.44 0.55 6.712711587504546
11 | 24 0.00 0.00 0.00 0.084999664294624
12 | 31 0.82 0.92 0.87 10.490463269571688
13 | 32 0.00 0.00 0.00 0.2576135038588164
14 | 50 0.00 0.00 0.00 0.0
15 | 51 0.00 0.00 0.00 0.033118424422509736
16 |
17 | accuracy 0.82 54.31753989833559
18 | macro avg 0.38 0.40 0.38 54.31753989833559
19 | weighted avg 0.84 0.82 0.82 54.31753989833559
20 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_land-cover-thematic-accuracy.py-test_calculate.received.txt:
--------------------------------------------------------------------------------
1 | precision recall f1-score support
2 |
3 | 0 0.00 0.00 0.00 0.0
4 | 11 0.94 0.86 0.90 13.681696054340627
5 | 12 0.84 0.87 0.85 4.868706606602494
6 | 13 0.00 0.00 0.00 0.0
7 | 14 0.13 0.42 0.20 0.3617314972878062
8 | 21 0.86 0.91 0.89 15.526750634345152
9 | 22 0.64 0.76 0.70 2.2997486561073313
10 | 23 0.75 0.44 0.55 6.712711587504546
11 | 24 0.00 0.00 0.00 0.084999664294624
12 | 31 0.82 0.92 0.87 10.490463269571688
13 | 32 0.00 0.00 0.00 0.2576135038588164
14 | 50 0.00 0.00 0.00 0.0
15 | 51 0.00 0.00 0.00 0.033118424422509736
16 |
17 | accuracy 0.82 54.31753989833559
18 | macro avg 0.38 0.40 0.38 54.31753989833559
19 | weighted avg 0.84 0.82 0.82 54.31753989833559
20 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_multi_class/description.approved.txt:
--------------------------------------------------------------------------------
1 | In your area-of-interes the thematic accuracy of all CLC classes is 82.32 % (medium agreement between OSM and CORINE). This suggests that OSM does capture some land cover categories but may lack detail or accuracy. The F1-Score is used as statistical metric which considers both the correctness and completeness of the land cover categories.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_multi_class/report.approved.txt:
--------------------------------------------------------------------------------
1 | precision recall f1-score support
2 |
3 | 0 0.00 nan 0.00 0.0
4 | 11 0.94 0.86 0.90 13.681696054340627
5 | 12 0.84 0.87 0.85 4.868706606602494
6 | 13 0.00 nan 0.00 0.0
7 | 14 0.13 0.42 0.20 0.3617314972878062
8 | 21 0.86 0.91 0.89 15.526750634345152
9 | 22 0.64 0.76 0.70 2.2997486561073313
10 | 23 0.75 0.44 0.55 6.712711587504546
11 | 24 nan 0.00 0.00 0.084999664294624
12 | 31 0.82 0.92 0.87 10.490463269571688
13 | 32 0.00 0.00 0.00 0.2576135038588164
14 | 50 0.00 nan 0.00 0.0
15 | 51 nan 0.00 0.00 0.033118424422509736
16 |
17 | accuracy 0.82 54.31753989833559
18 | macro avg 0.45 0.52 0.38 54.31753989833559
19 | weighted avg 0.84 0.82 0.82 54.31753989833559
20 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_single_class/description.approved.txt:
--------------------------------------------------------------------------------
1 | In your area-of-interes the thematic accuracy of CLC class Pastures is 47.95 % (low agreement between OSM and CORINE). This suggests that OSM has a poor classification quality or incomplete land cover data. The F1-Score is used as statistical metric which considers both the correctness and completeness of the land cover categories.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_calculate_single_class/report.approved.txt:
--------------------------------------------------------------------------------
1 | precision recall f1-score support
2 |
3 | 0 0.00 0.00 0.00 1.000418439925055
4 | 1 0.75 0.44 0.55 6.712711587504546
5 |
6 | accuracy 0.38 7.713130027429601
7 | macro avg 0.37 0.22 0.28 7.713130027429601
8 | weighted avg 0.65 0.38 0.48 7.713130027429601
9 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_land_cover_thematic_accuracy.py-test_figure_single_class.approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "colorbar": {
5 | "title": {
6 | "text": "Proportion"
7 | }
8 | },
9 | "colorscale": [
10 | [
11 | 0.0,
12 | "#440154"
13 | ],
14 | [
15 | 0.1111111111111111,
16 | "#482878"
17 | ],
18 | [
19 | 0.2222222222222222,
20 | "#3e4989"
21 | ],
22 | [
23 | 0.3333333333333333,
24 | "#31688e"
25 | ],
26 | [
27 | 0.4444444444444444,
28 | "#26828e"
29 | ],
30 | [
31 | 0.5555555555555556,
32 | "#1f9e89"
33 | ],
34 | [
35 | 0.6666666666666666,
36 | "#35b779"
37 | ],
38 | [
39 | 0.7777777777777778,
40 | "#6ece58"
41 | ],
42 | [
43 | 0.8888888888888888,
44 | "#b5de2b"
45 | ],
46 | [
47 | 1.0,
48 | "#fde725"
49 | ]
50 | ],
51 | "hovertemplate": "Predicted: %{x}
Actual: %{y}
Value: %{z:.2%}",
52 | "text": {
53 | "bdata": "AAAAAAAAAADkP/opHprAP/6TkLrPXd8/D0xyMCFV2D8=",
54 | "dtype": "f8",
55 | "shape": "2, 2"
56 | },
57 | "texttemplate": "%{text:.2%}",
58 | "type": "heatmap",
59 | "x": [
60 | "Other classes",
61 | "Agricultural areas
Pastures"
62 | ],
63 | "y": [
64 | "Other classes",
65 | "Agricultural areas
Pastures"
66 | ],
67 | "z": {
68 | "bdata": "AAAAAAAAAADkP/opHprAP/6TkLrPXd8/D0xyMCFV2D8=",
69 | "dtype": "f8",
70 | "shape": "2, 2"
71 | }
72 | }
73 | ],
74 | "layout": {
75 | "xaxis": {
76 | "ticktext": [
77 | "Other classes",
78 | "Agricultural areas
Pastures"
79 | ],
80 | "title": {
81 | "text": "Corine Land Cover Class in OSM"
82 | }
83 | },
84 | "yaxis": {
85 | "ticktext": [
86 | "Other classes",
87 | "Agricultural areas
Pastures"
88 | ],
89 | "title": {
90 | "text": "Corine Land Cover Class (actual)"
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_mapping_saturation.py-TestCalculation-test_calculate.approved.txt:
--------------------------------------------------------------------------------
1 | The saturation of the last 3 years is 99.5%. High saturation has been reached (97% < Saturation ≤ 100%).
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_road_comparison.py-TestCalculate-test_calculate.approved.txt:
--------------------------------------------------------------------------------
1 | Microsoft Roads has a road length of 0.03 km, of which 0.02 km are covered by roads in OSM. The completeness of OSM roads in your area-of-interest is medium.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_road_comparison.py-TestCalculate-test_calculate_no_intersection.approved.txt:
--------------------------------------------------------------------------------
1 | Reference dataset Microsoft Roads does not cover area-of-interest. Comparison could not be made.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_road_comparison.py-TestCalculate-test_calculate_reference_lenght_0.approved.txt:
--------------------------------------------------------------------------------
1 | Microsoft Roads does not contain roads for your area-of-interest. Comparison could not be made.
2 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_road_comparison.py-TestFigure-test_create_figure.approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "hoverinfo": "text",
5 | "hovertext": "OSM Covered: 0.02 km (Apr 05, 2024)",
6 | "marker": {
7 | "color": "#767676",
8 | "line": {
9 | "color": "#767676",
10 | "width": 1
11 | }
12 | },
13 | "name": "80.0% of Microsoft Roads are matched by OSM",
14 | "type": "bar",
15 | "width": 0.4,
16 | "x": [
17 | "Microsoft Roads"
18 | ],
19 | "y": [
20 | 80.0
21 | ]
22 | },
23 | {
24 | "hoverinfo": "text",
25 | "hovertext": "Not OSM Covered: 0.01 km (Apr 05, 2024)",
26 | "marker": {
27 | "color": "rgba(0,0,0,0)",
28 | "line": {
29 | "color": "#767676",
30 | "width": 1
31 | }
32 | },
33 | "name": "20.0% of Microsoft Roads are not matched by OSM",
34 | "textposition": "outside",
35 | "type": "bar",
36 | "width": 0.4,
37 | "x": [
38 | "Microsoft Roads"
39 | ],
40 | "y": [
41 | 20.0
42 | ]
43 | }
44 | ],
45 | "layout": {
46 | "barmode": "stack",
47 | "legend": {
48 | "entrywidth": 270,
49 | "orientation": "h",
50 | "x": 0.5,
51 | "xanchor": "center",
52 | "y": -0.1,
53 | "yanchor": "top"
54 | },
55 | "title": {
56 | "text": "Road Comparison"
57 | },
58 | "yaxis": {
59 | "title": {
60 | "text": "Matched road length [%]"
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tests/approvals/integrationtests/indicators/test_road_comparison.py-TestFigure-test_create_figure_building_area_zero.approved.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "hoverinfo": "none",
5 | "labels": [
6 | "The creation of the Indicator was unsuccessful."
7 | ],
8 | "marker": {
9 | "colors": [
10 | "rgba(0, 0, 0, 0)"
11 | ]
12 | },
13 | "textposition": "inside",
14 | "texttemplate": "%{label}",
15 | "type": "pie",
16 | "values": [
17 | 1
18 | ]
19 | }
20 | ],
21 | "layout": {
22 | "paper_bgcolor": "white",
23 | "plot_bgcolor": "white",
24 | "showlegend": false,
25 | "title": {
26 | "text": "Road Comparison"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/approvaltests_namers.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 | from typing import Dict
4 |
5 | from approvaltests.namer.namer_base import NamerBase
6 |
7 |
8 | class PytestNamer(NamerBase):
9 | def __init__(self, extension: str = ".txt", postfix: str = ""):
10 | """An approval tests Namer for naming approved and received files.
11 |
12 | This Namer uses the `PYTEST_CURRENT_TEST` environment variable, which
13 | consist of the node ID and the current stage to derive names:
14 | `relative/path/to/test_file.py::TestClass::test_func[a] (call)`
15 |
16 | Above example will be translated to:
17 | `relative/path/to/test_file.py--TestClass--test_func[a]`
18 |
19 | Following changes are applied to the `PYTEST_CURRENT_TEST` environmen
20 | variables:
21 | - To avoid the forbidden character `:` in system paths, it is
22 | replaced by `-`.
23 | - During a pytest test session, stages can be setup, teardown or
24 | call. Approval tests should only be used during the call stage
25 | and therefore the ` (call)` postfix is removed.
26 |
27 | If verify is called multiple times use `postfix` parameter to
28 | differentiate names.
29 | """
30 | self.nodeid: Path = Path(os.environ["PYTEST_CURRENT_TEST"])
31 | self.postfix = postfix
32 | NamerBase.__init__(self, extension)
33 |
34 | def get_file_name(self) -> Path:
35 | """File name is pytest nodeid w/out directory name and pytest."""
36 | file_name = str(self.nodeid.name).replace(" (call)", "").replace("::", "-")
37 | return Path(file_name) / self.postfix
38 |
39 | def get_directory(self) -> Path:
40 | """Directory is `tests/approval/{module}` derived from pytest nodeid."""
41 | base_dir = Path(__file__).parent / "approvals"
42 | parts = self.nodeid.parent.parts
43 | directory = Path(*[p for p in parts if p not in ["tests"]])
44 | return base_dir / directory
45 |
46 | def get_config(self) -> Dict[str, str]:
47 | return {}
48 |
--------------------------------------------------------------------------------
/tests/approvaltests_scrubbers.py:
--------------------------------------------------------------------------------
1 | import json
2 | import math
3 | import re
4 |
5 |
6 | def round_fitted_y_values(figure: dict) -> dict:
7 | for data in figure["data"]:
8 | for i, number in enumerate(data["y"]):
9 | data["y"][i] = round(number)
10 | return figure
11 |
12 |
13 | def round_axis_range(figure: dict) -> dict:
14 | figure["layout"]["yaxis"]["range"][1] = math.ceil(
15 | figure["layout"]["yaxis"]["range"][1]
16 | )
17 | return figure
18 |
19 |
20 | def replace_float_in_hovertext(figure: dict) -> dict:
21 | for data in figure["data"]:
22 | if "hovertext" in data.keys():
23 | data["hovertext"] = re.sub(
24 | "[+-]?([0-9]*[.])?[0-9]+",
25 | "scrubbed",
26 | data["hovertext"],
27 | )
28 | return figure
29 |
30 |
31 | def scrub_mapping_saturation_figure(figure: str):
32 | fig = json.loads(figure)
33 | fig = round_fitted_y_values(fig)
34 | fig = replace_float_in_hovertext(fig)
35 | fig = round_axis_range(fig)
36 | return json.dumps(fig)
37 |
--------------------------------------------------------------------------------
/tests/fixtures/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/fixtures/empty.png
--------------------------------------------------------------------------------
/tests/fixtures/feature-germany-berlin-friedrichshain-kreuzberg.geojson:
--------------------------------------------------------------------------------
1 | {"type": "Feature", "geometry": {"type":"MultiPolygon","coordinates":[[[[13.3682291,52.4933357],[13.3765623,52.4916704],[13.373518,52.4879703],[13.3740185,52.4851671],[13.3716065,52.484979],[13.3942612,52.4857753],[13.3946131,52.4840216],[13.4063457,52.4827923],[13.4078874,52.4888602],[13.4237047,52.4863837],[13.42535,52.488182],[13.4203999,52.4958638],[13.4392625,52.4896095],[13.4455486,52.494856],[13.4529296,52.4977068],[13.4633549,52.4954855],[13.4642795,52.4937189],[13.476147,52.4899443],[13.4786285,52.4870327],[13.481678,52.487638],[13.4829538,52.4860501],[13.4914434,52.4882828],[13.4730756,52.4990026],[13.4685734,52.4996572],[13.4711643,52.5051377],[13.4762658,52.5104393],[13.4758875,52.5148631],[13.4777288,52.5147204],[13.4723904,52.5205711],[13.4626951,52.5199276],[13.4552901,52.5212723],[13.4561572,52.5224576],[13.4521828,52.5277958],[13.4471683,52.5264084],[13.4422774,52.5310256],[13.4387482,52.5287778],[13.4236421,52.527915],[13.4197532,52.5255456],[13.4291879,52.5212028],[13.4259266,52.5183933],[13.4227803,52.5122343],[13.4294017,52.5085626],[13.4271997,52.5056674],[13.4140723,52.5040371],[13.4099694,52.5069278],[13.4080301,52.5061832],[13.4002283,52.5093816],[13.3992301,52.5080775],[13.3776503,52.5079658],[13.3749797,52.5033759],[13.3736093,52.504164],[13.3695143,52.498879],[13.3682291,52.4933357]]]]}, "properties": {"osm_id": -55764, "boundary": "administrative", "admin_level": 9, "parents": "-62422,-51477", "name": "Friedrichshain-Kreuzberg", "local_name": "Friedrichshain-Kreuzberg", "name_en": null}}
2 |
--------------------------------------------------------------------------------
/tests/fixtures/feature-malta.geojson:
--------------------------------------------------------------------------------
1 | {"type": "Feature", "geometry": {"coordinates": [[[14.401600308089286, 35.98854348048188],[14.401600308089286,35.9033309465853],[14.495720596358126,35.9033309465853],[14.495720596358126,35.98854348048188],[14.401600308089286,35.98854348048188]]], "type": "Polygon"}}
2 |
--------------------------------------------------------------------------------
/tests/integrationtests/__init__.py:
--------------------------------------------------------------------------------
1 | from ohsome_quality_api.config import configure_logging
2 |
3 | configure_logging()
4 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/api/__init__.py
--------------------------------------------------------------------------------
/tests/integrationtests/api/response_schema.py:
--------------------------------------------------------------------------------
1 | """This module defines schemata for API responses.
2 |
3 | Every API response must adhere to the general schema.
4 | Additionally, API responses need to adhere to one additional schema depending on which
5 | endpoint is used.
6 | """
7 |
8 | from schema import Optional as Opt
9 | from schema import Or, Schema
10 |
11 |
12 | def get_indicator_properties_template():
13 | return {
14 | "metadata": {
15 | "name": str,
16 | "description": str,
17 | },
18 | "topic": {
19 | "name": str,
20 | "description": str,
21 | },
22 | "result": {
23 | "timestamp": str,
24 | "timestampOSM": Or(str),
25 | "value": Or(float, str, int, None),
26 | "label": str,
27 | "description": str,
28 | },
29 | }
30 |
31 |
32 | def get_general_schema() -> Schema:
33 | """General response schema.
34 |
35 | Every response should have this schema
36 | """
37 | return Schema(
38 | {
39 | "apiVersion": str,
40 | "attribution": {
41 | "url": str,
42 | Opt("text"): str,
43 | },
44 | },
45 | ignore_extra_keys=True,
46 | )
47 |
48 |
49 | def get_result_schema() -> Schema:
50 | """Response schema for all endpoints.
51 |
52 | Excluded is the endpoint `/indicator`.
53 | """
54 | return Schema({"result": list}, ignore_extra_keys=True)
55 |
56 |
57 | def get_featurecollection_schema() -> Schema:
58 | """Response schema for responses of type FeatureCollection"""
59 | return Schema(
60 | {"type": "FeatureCollection", "features": list}, ignore_extra_keys=True
61 | )
62 |
63 |
64 | def get_indicator_feature_schema() -> Schema:
65 | properties = get_indicator_properties_template()
66 | return Schema(
67 | {
68 | "type": "Feature",
69 | "geometry": dict,
70 | Opt("id"): Or(str, int),
71 | "properties": properties,
72 | },
73 | ignore_extra_keys=True,
74 | )
75 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_indicators_land_cover_thematic_accuracy.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 | from approvaltests import Options, verify_as_json
5 |
6 | from tests.approvaltests_namers import PytestNamer
7 | from tests.conftest import FIXTURE_DIR
8 | from tests.integrationtests.api.test_indicators import (
9 | RESPONSE_SCHEMA_GEOJSON,
10 | RESPONSE_SCHEMA_JSON,
11 | )
12 | from tests.integrationtests.utils import oqapi_vcr
13 |
14 | ENDPOINT = "/indicators/land-cover-thematic-accuracy"
15 |
16 |
17 | @pytest.fixture
18 | def mock_db_fetch(monkeypatch):
19 | async def fetch(*_):
20 | with open(
21 | FIXTURE_DIR / "land-cover-thematic-accuracy-db-fetch-results.json", "r"
22 | ) as file:
23 | return json.load(file)
24 |
25 | monkeypatch.setattr(
26 | "ohsome_quality_api.indicators.land_cover_thematic_accuracy.indicator.client.fetch",
27 | fetch,
28 | )
29 |
30 |
31 | @oqapi_vcr.use_cassette
32 | @pytest.mark.parametrize(
33 | "headers,schema",
34 | [
35 | ({"accept": "application/json"}, RESPONSE_SCHEMA_JSON),
36 | ({"accept": "application/geo+json"}, RESPONSE_SCHEMA_GEOJSON),
37 | ],
38 | )
39 | def test_multi_class(client, bpolys, headers, schema, mock_db_fetch):
40 | # corine class parameter is optional (default all corine classes)
41 | parameters = {"bpolys": bpolys, "topic": "land-cover"}
42 | response = client.post(ENDPOINT, json=parameters, headers=headers)
43 | assert response.status_code == 200
44 | assert schema.is_valid(response.json())
45 |
46 |
47 | @oqapi_vcr.use_cassette
48 | @pytest.mark.parametrize(
49 | "headers,schema",
50 | [
51 | ({"accept": "application/json"}, RESPONSE_SCHEMA_JSON),
52 | ({"accept": "application/geo+json"}, RESPONSE_SCHEMA_GEOJSON),
53 | ],
54 | )
55 | def test_single_class(client, bpolys, headers, schema, mock_db_fetch):
56 | # Corine class 23 are Pastures
57 | parameters = {"bpolys": bpolys, "topic": "land-cover", "corineLandCoverClass": "23"}
58 | response = client.post(ENDPOINT, json=parameters, headers=headers)
59 | assert response.status_code == 200
60 | assert schema.is_valid(response.json())
61 |
62 |
63 | def test_invalid_topic(client, bpolys):
64 | parameters = {"bpolys": bpolys, "topic": "building-count"}
65 | response = client.post(ENDPOINT, json=parameters)
66 | assert response.status_code == 422
67 | verify_as_json(response.json(), options=Options().with_namer(PytestNamer()))
68 |
69 |
70 | def test_invalid_class(client, bpolys):
71 | parameters = {"bpolys": bpolys, "topic": "land-cover", "corineLandCoverClass": "1"}
72 | response = client.post(ENDPOINT, json=parameters)
73 | assert response.status_code == 422
74 | verify_as_json(response.json(), options=Options().with_namer(PytestNamer()))
75 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_indicators_mapping_saturation_data.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 | from schema import Optional, Or, Schema
4 |
5 | from tests.unittests.mapping_saturation.fixtures import VALUES_1 as DATA
6 |
7 | ENDPOINT = "/indicators/mapping-saturation/data"
8 |
9 |
10 | RESPONSE_SCHEMA_JSON = Schema(
11 | schema={
12 | "apiVersion": str,
13 | "attribution": {
14 | "url": str,
15 | Optional("text"): str,
16 | },
17 | "result": [
18 | {
19 | Optional("id"): Or(str, int),
20 | "metadata": {
21 | "name": str,
22 | "description": str,
23 | },
24 | "topic": {
25 | "name": str,
26 | "description": str,
27 | },
28 | "result": {
29 | "timestamp": str,
30 | "timestampOSM": Or(str),
31 | "value": Or(float, str, int, None),
32 | "label": str,
33 | "description": str,
34 | "figure": dict,
35 | },
36 | }
37 | ],
38 | },
39 | name="json",
40 | ignore_extra_keys=True,
41 | )
42 |
43 |
44 | def test_mapping_saturation_data(client, bpolys):
45 | """Test parameter Topic with custom data attached."""
46 | timestamp_objects = [
47 | datetime(2020, 7, 17, 9, 10, 0) + timedelta(days=1 * x)
48 | for x in range(DATA.size)
49 | ]
50 | timestamp_iso_string = [t.strftime("%Y-%m-%dT%H:%M:%S") for t in timestamp_objects]
51 | # Data is ohsome API response result for the topic 'building-count' and the bpolys
52 | # of for Heidelberg
53 | parameters = {
54 | "bpolys": bpolys,
55 | "topic": {
56 | "key": "foo",
57 | "name": "bar",
58 | "description": "",
59 | "data": {
60 | "result": [
61 | {"value": v, "timestamp": t}
62 | for v, t in zip(DATA, timestamp_iso_string)
63 | ]
64 | },
65 | },
66 | }
67 | response = client.post(ENDPOINT, json=parameters)
68 | assert RESPONSE_SCHEMA_JSON.is_valid(response.json())
69 |
70 |
71 | def test_mapping_saturation_data_invalid(client, bpolys):
72 | parameters = {
73 | "bpolys": bpolys,
74 | "topic": {
75 | "key": "foo",
76 | "name": "bar",
77 | "description": "",
78 | "data": {"result": [{"value": 1.0}]}, # Missing timestamp item
79 | },
80 | }
81 | response = client.post(ENDPOINT, json=parameters)
82 | assert response.status_code == 422
83 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_metadata.py:
--------------------------------------------------------------------------------
1 | from ohsome_quality_api.indicators.definitions import IndicatorEnum
2 | from ohsome_quality_api.topics.definitions import TopicEnum
3 |
4 |
5 | def test_metadata(
6 | client,
7 | response_template,
8 | metadata_project_core,
9 | metadata_topic_building_count,
10 | metadata_indicator_mapping_saturation,
11 | metadata_quality_dimension,
12 | metadata_attribute_clc_leaf_type,
13 | ):
14 | response = client.get("/metadata")
15 | assert response.status_code == 200
16 |
17 | content = response.json()
18 | result = content.pop("result")
19 | assert content == response_template
20 | # check topics result
21 | assert (
22 | metadata_topic_building_count["building-count"]
23 | == result["topics"]["building-count"]
24 | )
25 | # check quality dimensions result
26 | assert (
27 | metadata_quality_dimension["minimal"] == result["qualityDimensions"]["minimal"]
28 | )
29 | # check projects result
30 | assert metadata_project_core["core"] == result["projects"]["core"]
31 | # check indicators result
32 | assert (
33 | metadata_indicator_mapping_saturation["mapping-saturation"]
34 | == result["indicators"]["mapping-saturation"]
35 | )
36 | assert (
37 | metadata_attribute_clc_leaf_type["clc-leaf-type"]
38 | == result["attributes"]["clc-leaf-type"]
39 | )
40 |
41 |
42 | def test_project_core(
43 | client,
44 | response_template,
45 | ):
46 | response = client.get("/metadata?project=core")
47 | assert response.status_code == 200
48 |
49 | content = response.json()
50 | result = content.pop("result")
51 | assert content == response_template
52 | for k in ("topics", "indicators"):
53 | for p in result[k].values():
54 | assert "core" in p["projects"]
55 | # check topics result
56 | assert len(result["topics"]) > 0
57 | # check indicators result
58 | assert len(result["indicators"]) > 0
59 |
60 |
61 | def test_project_misc(
62 | client,
63 | response_template,
64 | ):
65 | response = client.get("/metadata?project=misc")
66 | assert response.status_code == 200
67 |
68 | content = response.json()
69 | result = content.pop("result")
70 | assert content == response_template
71 | for k in ("topics", "indicators"):
72 | for p in result[k].values():
73 | assert "misc" in p["projects"]
74 | # check topics result
75 | assert len(result["topics"]) > 0
76 | # check indicators result
77 | assert len(result["indicators"]) > 0
78 |
79 |
80 | def test_project_all(
81 | client,
82 | response_template,
83 | ):
84 | response = client.get("/metadata?project=all")
85 | assert response.status_code == 200
86 |
87 | content = response.json()
88 | result = content.pop("result")
89 | assert content == response_template
90 | # check topics result
91 | assert len(result["topics"]) == len(TopicEnum)
92 | # check indicators result
93 | assert len(result["indicators"]) == len(IndicatorEnum)
94 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_metadata_attributes.py:
--------------------------------------------------------------------------------
1 | def test_metadata_attribute(
2 | client,
3 | response_template,
4 | metadata_attribute_clc_leaf_type,
5 | ):
6 | response = client.get("/metadata/attributes")
7 | assert response.status_code == 200
8 |
9 | content = response.json()
10 | result = content.pop("result")
11 | assert content == response_template
12 | assert metadata_attribute_clc_leaf_type["clc-leaf-type"] == result["clc-leaf-type"]
13 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_metadata_indicators.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import geojson
4 | import pytest
5 |
6 | from ohsome_quality_api.indicators.definitions import IndicatorEnum
7 |
8 |
9 | def test(client, response_template, metadata_indicator_mapping_saturation):
10 | response = client.get("/metadata/indicators/")
11 | assert response.status_code == 200
12 |
13 | content = response.json()
14 | result = content.pop("result")
15 | assert content == response_template
16 | assert (
17 | metadata_indicator_mapping_saturation["mapping-saturation"]
18 | == result["mapping-saturation"]
19 | )
20 | assert "minimal" not in result.keys()
21 |
22 |
23 | def test_by_key(client, response_template, metadata_indicator_minimal):
24 | response = client.get("/metadata/indicators/minimal")
25 | assert response.status_code == 200
26 |
27 | content = response.json()
28 | result = content.pop("result")
29 | assert content == response_template
30 | assert result == metadata_indicator_minimal
31 |
32 |
33 | def test_by_key_not_found_error(client):
34 | response = client.get("/metadata/indicators/foo")
35 | assert response.status_code == 422
36 |
37 |
38 | def test_project_core(client, response_template, metadata_indicator_mapping_saturation):
39 | response = client.get("/metadata/indicators/?project=core")
40 | assert response.status_code == 200
41 |
42 | content = response.json()
43 | result = content.pop("result")
44 | assert content == response_template
45 | assert (
46 | metadata_indicator_mapping_saturation["mapping-saturation"]
47 | == result["mapping-saturation"]
48 | )
49 | assert "minimal" not in result.keys()
50 |
51 |
52 | def test_project_all(
53 | client,
54 | response_template,
55 | ):
56 | response = client.get("/metadata/indicators/?project=all")
57 | assert response.status_code == 200
58 |
59 | content = response.json()
60 | result = content.pop("result")
61 | assert content == response_template
62 | assert len(result) == len(IndicatorEnum)
63 |
64 |
65 | @pytest.mark.skip(reason="Not yet implemented")
66 | def test_project_not_found_error(client):
67 | response = client.get("/metadata/indicators/?project=foo")
68 | assert response.status_code == 404 # Not Found
69 |
70 |
71 | def test_coverage_default(client):
72 | response = client.get("metadata/indicators/mapping-saturation/coverage")
73 | assert response.status_code == 200
74 | assert response.json()["features"][0]["geometry"] == {
75 | "coordinates": [
76 | [
77 | [-180, 90],
78 | [-180, -90],
79 | [180, -90],
80 | [180, 90],
81 | [-180, 90],
82 | ]
83 | ],
84 | "type": "Polygon",
85 | }
86 |
87 |
88 | @pytest.mark.skip(reason="Depends on database")
89 | def test_coverage(client):
90 | response = client.get("metadata/indicators/building-comparison/coverage")
91 | assert response.status_code == 200
92 | result = geojson.loads(json.dumps(response.json()))
93 | assert result.is_valid
94 | assert isinstance(result, geojson.FeatureCollection)
95 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_metadata_projects.py:
--------------------------------------------------------------------------------
1 | def test_metadata_projects(client, response_template, metadata_project_core):
2 | response = client.get("/metadata/projects")
3 | assert response.status_code == 200
4 |
5 | content = response.json()
6 | result = content.pop("result")
7 | assert content == response_template
8 | assert metadata_project_core["core"] == result["core"]
9 | # result shouldn't contain the all key
10 | assert "all" not in result
11 |
12 |
13 | def test_metadata_projects_by_key(client, response_template, metadata_project_core):
14 | response = client.get("/metadata/projects/core")
15 | assert response.status_code == 200
16 |
17 | content = response.json()
18 | result = content.pop("result")
19 | assert content == response_template
20 | assert result == metadata_project_core
21 |
22 |
23 | def test_metadata_projects_by_key_not_found_error(client):
24 | response = client.get("/metadata/projects/foo")
25 | assert response.status_code == 422
26 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_metadata_quality_dimensions.py:
--------------------------------------------------------------------------------
1 | def test_metadata_quality_dimensions(
2 | client, response_template, metadata_quality_dimension
3 | ):
4 | response = client.get("/metadata/quality-dimensions")
5 | assert response.status_code == 200
6 |
7 | content = response.json()
8 | result = content.pop("result")
9 | assert content == response_template
10 | assert metadata_quality_dimension["minimal"] == result["minimal"]
11 |
12 |
13 | def test_metadata_quality_dimensions_by_key(
14 | client, response_template, metadata_quality_dimension
15 | ):
16 | response = client.get("/metadata/quality-dimensions/minimal")
17 | assert response.status_code == 200
18 |
19 | content = response.json()
20 | result = content.pop("result")
21 | assert content == response_template
22 | assert result == metadata_quality_dimension
23 |
24 |
25 | def test_metadata_quality_dimensions_by_key_not_found_error(client):
26 | response = client.get("/metadata/quality-dimensions/foo")
27 | assert response.status_code == 422
28 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_metadata_topics.py:
--------------------------------------------------------------------------------
1 | from ohsome_quality_api.topics.definitions import TopicEnum
2 |
3 |
4 | def test_metadata_topic(
5 | client,
6 | response_template,
7 | metadata_topic_building_count,
8 | ):
9 | response = client.get("/metadata/topics")
10 | assert response.status_code == 200
11 |
12 | content = response.json()
13 | result = content.pop("result")
14 | assert content == response_template
15 | assert metadata_topic_building_count["building-count"] == result["building-count"]
16 | assert "minimal" not in result.keys()
17 |
18 |
19 | def test_metadata_topic_project_core(
20 | client,
21 | response_template,
22 | metadata_topic_building_count,
23 | ):
24 | response = client.get("/metadata/topics/?project=core")
25 | assert response.status_code == 200
26 |
27 | content = response.json()
28 | result = content.pop("result")
29 | assert content == response_template
30 | assert metadata_topic_building_count["building-count"] == result["building-count"]
31 | assert "minimal" not in result.keys()
32 |
33 |
34 | def test_metadata_topic_project_experimental(client, response_template):
35 | response = client.get("/metadata/topics/?project=experimental")
36 | assert response.status_code == 200
37 |
38 | content = response.json()
39 | result = content.pop("result")
40 | assert content == response_template
41 | assert "building-count" not in result.keys()
42 | assert "minimal" not in result.keys()
43 |
44 |
45 | def test_project_all(
46 | client,
47 | response_template,
48 | ):
49 | response = client.get("/metadata/topics/?project=all")
50 | assert response.status_code == 200
51 |
52 | content = response.json()
53 | result = content.pop("result")
54 | assert len(result) == len(TopicEnum)
55 |
56 |
57 | def test_metadata_topic_project_not_found_error(client):
58 | response = client.get("/metadata/topics/?project=foo")
59 | assert response.status_code == 422
60 |
61 |
62 | def test_metadata_topic_by_key(
63 | client,
64 | response_template,
65 | metadata_topic_building_count,
66 | ):
67 | response = client.get("/metadata/topics/building-count")
68 | assert response.status_code == 200
69 |
70 | content = response.json()
71 | result = content.pop("result")
72 | assert content == response_template
73 | assert result == metadata_topic_building_count
74 |
75 |
76 | def test_metadata_topic_by_key_not_found_error(client):
77 | response = client.get("/metadata/topics/foo")
78 | assert response.status_code == 422
79 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_ohsome_timeout.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing FastAPI Applications:
3 | https://fastapi.tiangolo.com/tutorial/testing/
4 |
5 | Shared tests for `/indicator` endpoints using the `bpolys` parameter.
6 | Tests for the individual endpoints and using the `bpolys` parameter please see:
7 | - `test_api_indicator_geojson_io.py`
8 | """
9 |
10 | import os
11 | from unittest import mock
12 |
13 | import httpx
14 |
15 | from tests.integrationtests.utils import AsyncMock
16 |
17 |
18 | # TODO: could/should this be converted to a parameterized test?
19 | def test_ohsome_timeout(client, bpolys):
20 | path = os.path.join(
21 | os.path.dirname(os.path.abspath(__file__)),
22 | "..",
23 | "..",
24 | "unittests",
25 | "fixtures",
26 | "ohsome-response-200-invalid.geojson",
27 | )
28 | with open(path, "r") as f:
29 | invalid_response = f.read()
30 | with mock.patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_request:
31 | mock_request.return_value = httpx.Response(
32 | 200,
33 | content=invalid_response,
34 | request=httpx.Request("POST", "https://www.example.org/"),
35 | )
36 |
37 | endpoint = "/indicators/minimal"
38 | parameters = {
39 | "bpolys": bpolys,
40 | "topic": "minimal",
41 | }
42 | response = client.post(endpoint, json=parameters)
43 | assert response.status_code == 422
44 | content = response.json()
45 | assert content["type"] == "OhsomeApiError"
46 |
--------------------------------------------------------------------------------
/tests/integrationtests/api/test_response_models.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import pytest
4 |
5 | from ohsome_quality_api.api.response_models import (
6 | CompIndicator,
7 | IndicatorGeoJSONResponse,
8 | IndicatorJSONResponse,
9 | )
10 | from ohsome_quality_api.indicators.minimal.indicator import Minimal
11 | from tests.integrationtests.utils import oqapi_vcr
12 |
13 |
14 | class TestIndicatorResponseModels:
15 | @pytest.fixture(scope="class")
16 | @oqapi_vcr.use_cassette
17 | def indicator(self, topic_minimal, feature_germany_heidelberg):
18 | indicator = Minimal(topic_minimal, feature_germany_heidelberg)
19 | asyncio.run(indicator.preprocess())
20 | indicator.calculate()
21 | indicator.create_figure()
22 | return indicator
23 |
24 | def test_indicator_property(self, indicator):
25 | raw_dict = indicator.as_dict(exclude_label=True)
26 | CompIndicator(**raw_dict)
27 |
28 | def test_indicator_json_response(self, indicator):
29 | raw_dict = indicator.as_dict(exclude_label=True)
30 | result_dict = {"result": [raw_dict]}
31 | IndicatorJSONResponse(**result_dict)
32 |
33 | def test_indicator_geojson_response(self, indicator):
34 | feature = indicator.as_feature(exclude_label=True)
35 | result_dict = {"type": "FeatureCollection", "features": [feature]}
36 | IndicatorGeoJSONResponse(**result_dict)
37 |
--------------------------------------------------------------------------------
/tests/integrationtests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tests.integrationtests.utils import get_geojson_fixture
4 |
5 |
6 | @pytest.fixture
7 | def europe():
8 | return get_geojson_fixture("europe.geojson")
9 |
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/algeria-touggourt-hexcells.geojson:
--------------------------------------------------------------------------------
1 | {"type": "FeatureCollection", "features": [{"type": "Feature", "id": 4171694, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.932918, 33.019152], [5.874573, 32.995934], [5.81307, 33.024295], [5.809855, 33.075939], [5.868222, 33.0992], [5.929781, 33.070774], [5.932918, 33.019152]]]]}, "properties": {"area": 95930452.2125547}}, {"type": "Feature", "id": 4172685, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.868222, 33.0992], [5.809855, 33.075939], [5.748282, 33.104265], [5.74502, 33.155916], [5.803411, 33.179219], [5.865039, 33.150829], [5.868222, 33.0992]]]]}, "properties": {"area": 95932018.963268}}, {"type": "Feature", "id": 4171692, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.04363, 33.168571], [5.98511, 33.145517], [5.923495, 33.174009], [5.920345, 33.225623], [5.978889, 33.248721], [6.040559, 33.220161], [6.04363, 33.168571]]]]}, "properties": {"area": 95933529.0075574}}, {"type": "Feature", "id": 4170334, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.163817, 33.1629], [6.105231, 33.139974], [6.04363, 33.168571], [6.040559, 33.220161], [6.099167, 33.243133], [6.160824, 33.214467], [6.163817, 33.1629]]]]}, "properties": {"area": 95933383.7868543}}, {"type": "Feature", "id": 4170335, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.108257, 33.088391], [6.049759, 33.065381], [5.988213, 33.09391], [5.98511, 33.145517], [6.04363, 33.168571], [6.105231, 33.139974], [6.108257, 33.088391]]]]}, "properties": {"area": 95931857.5037036}}, {"type": "Feature", "id": 4172686, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.81307, 33.024295], [5.75479, 33.000954], [5.693273, 33.029215], [5.689979, 33.080882], [5.748282, 33.104265], [5.809855, 33.075939], [5.81307, 33.024295]]]]}, "properties": {"area": 95930542.1162593}}, {"type": "Feature", "id": 4171693, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.988213, 33.09391], [5.929781, 33.070774], [5.868222, 33.0992], [5.865039, 33.150829], [5.923495, 33.174009], [5.98511, 33.145517], [5.988213, 33.09391]]]]}, "properties": {"area": 95931910.750078}}, {"type": "Feature", "id": 4170336, "geometry": {"type": "MultiPolygon", "coordinates": [[[[6.052817, 33.013782], [5.994408, 32.99069], [5.932918, 33.019152], [5.929781, 33.070774], [5.988213, 33.09391], [6.049759, 33.065381], [6.052817, 33.013782]]]]}, "properties": {"area": 95930343.2759635}}, {"type": "Feature", "id": 4172684, "geometry": {"type": "MultiPolygon", "coordinates": [[[[5.923495, 33.174009], [5.865039, 33.150829], [5.803411, 33.179219], [5.800182, 33.230855], [5.858661, 33.254078], [5.920345, 33.225623], [5.923495, 33.174009]]]]}, "properties": {"area": 95933668.7242033}}]}
2 |
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/europe.geojson:
--------------------------------------------------------------------------------
1 | {"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-15.468749999999998,37.16031654673677],[45.703125,37.16031654673677],[45.703125,70.61261423801925],[-15.468749999999998,70.61261423801925],[-15.468749999999998,37.16031654673677]]]}}]}
2 |
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/heidelberg-altstadt-feature.geojson:
--------------------------------------------------------------------------------
1 | {"type": "Feature", "geometry": {"type":"Polygon","coordinates":[[[8.674092292785645,49.40427147224242],[8.695850372314453,49.40427147224242],[8.695850372314453,49.415552187316095],[8.674092292785645,49.415552187316095],[8.674092292785645,49.40427147224242]]]}}
2 |
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/niger-kanan-bakache.geojson:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Feature",
3 | "geometry": {
4 | "type": "MultiPolygon",
5 | "coordinates": [
6 | [
7 | [
8 | [
9 | 7.818124294281006,
10 | 13.859122207420464
11 | ],
12 | [
13 | 7.835311889648438,
14 | 13.859122207420464
15 | ],
16 | [
17 | 7.835311889648438,
18 | 13.872538264248497
19 | ],
20 | [
21 | 7.818124294281006,
22 | 13.872538264248497
23 | ],
24 | [
25 | 7.818124294281006,
26 | 13.859122207420464
27 | ]
28 | ]
29 | ]
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/rasters/GHS_BUILT_LDS2014_GLOBE_R2018A_54009_1K_V2_0.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/GHS_BUILT_LDS2014_GLOBE_R2018A_54009_1K_V2_0.tif
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/rasters/GHS_POP_E2015_GLOBE_R2019A_54009_1K_V1_0.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/GHS_POP_E2015_GLOBE_R2019A_54009_1K_V1_0.tif
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/rasters/GHS_SMOD_POP2015_GLOBE_R2019A_54009_1K_V2_0.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/GHS_SMOD_POP2015_GLOBE_R2019A_54009_1K_V2_0.tif
--------------------------------------------------------------------------------
/tests/integrationtests/fixtures/rasters/VNL_v2_npp_2020_global_vcmslcfg_c202102150000.average_masked.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/fixtures/rasters/VNL_v2_npp_2020_global_vcmslcfg_c202102150000.average_masked.tif
--------------------------------------------------------------------------------
/tests/integrationtests/indicators/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/integrationtests/indicators/__init__.py
--------------------------------------------------------------------------------
/tests/integrationtests/indicators/test_minimal.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import plotly.graph_objects as pgo
4 | import plotly.io as pio
5 | import pytest
6 |
7 | from ohsome_quality_api.indicators.minimal.indicator import Minimal
8 | from tests.integrationtests.utils import oqapi_vcr
9 |
10 |
11 | class TestAttribution:
12 | @oqapi_vcr.use_cassette
13 | def test_attribution(self, topic_minimal, feature_germany_heidelberg):
14 | indicator = Minimal(topic_minimal, feature_germany_heidelberg)
15 | asyncio.run(indicator.preprocess())
16 | assert indicator.attribution() is not None
17 |
18 |
19 | class TestPreprocess:
20 | @oqapi_vcr.use_cassette
21 | def test_preprocess(self, topic_minimal, feature_germany_heidelberg):
22 | indicator = Minimal(topic_minimal, feature_germany_heidelberg)
23 | asyncio.run(indicator.preprocess())
24 | assert indicator.count is not None
25 |
26 |
27 | class TestCalculate:
28 | @pytest.fixture(scope="class")
29 | @oqapi_vcr.use_cassette
30 | def indicator(self, topic_minimal, feature_germany_heidelberg):
31 | i = Minimal(topic_minimal, feature_germany_heidelberg)
32 | asyncio.run(i.preprocess())
33 | i.calculate()
34 | return i
35 |
36 | def test_calculate(self, indicator):
37 | assert indicator.result.value is not None
38 | assert indicator.result.label is not None
39 | assert indicator.result.description is not None
40 | assert indicator.result.timestamp is not None
41 | assert indicator.result.timestamp_osm is not None
42 |
43 |
44 | class TestFigure:
45 | @pytest.fixture(scope="class")
46 | @oqapi_vcr.use_cassette
47 | def indicator(self, topic_minimal, feature_germany_heidelberg):
48 | i = Minimal(topic_minimal, feature_germany_heidelberg)
49 | asyncio.run(i.preprocess())
50 | i.calculate()
51 | return i
52 |
53 | @pytest.mark.skip(reason="Only for manual testing.") # comment for manual test
54 | def test_create_figure_manual(self, indicator):
55 | indicator.create_figure()
56 | pio.show(indicator.result.figure)
57 |
58 | def test_create_figure(self, indicator):
59 | indicator.create_figure()
60 | assert isinstance(indicator.result.figure, dict)
61 | pgo.Figure(indicator.result.figure) # test for valid Plotly figure
62 |
--------------------------------------------------------------------------------
/tests/integrationtests/test_ohsome_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from datetime import datetime
3 |
4 | import pytest
5 |
6 | from ohsome_quality_api.ohsome import client as ohsome_client
7 | from ohsome_quality_api.utils.exceptions import OhsomeApiError
8 |
9 | from .utils import oqapi_vcr
10 |
11 |
12 | @oqapi_vcr.use_cassette()
13 | def test_get_latest_ohsome_timestamp():
14 | time = asyncio.run(ohsome_client.get_latest_ohsome_timestamp())
15 | assert isinstance(time, datetime)
16 |
17 |
18 | @oqapi_vcr.use_cassette()
19 | def test_query_ohsome_api_exceptions_404():
20 | url = "https://api.ohsome.org/v1/elements/lenght" # length is misspelled
21 | with pytest.raises(OhsomeApiError, match="Not Found.*"):
22 | asyncio.run(ohsome_client.query_ohsome_api(url, {}))
23 |
24 |
25 | @oqapi_vcr.use_cassette
26 | def test_query_ohsome_api_exceptions_400():
27 | url = "https://api.ohsome.org/v1/elements/length"
28 | with pytest.raises(OhsomeApiError, match="Invalid filter syntax."):
29 | asyncio.run(
30 | ohsome_client.query_ohsome_api(
31 | url, {"bboxes": "8.67,49.39,8.71,49.42", "filter": "geometry:lie"}
32 | )
33 | )
34 |
--------------------------------------------------------------------------------
/tests/integrationtests/test_postgres.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import unittest
3 | from types import MappingProxyType
4 | from unittest import mock
5 |
6 | import asyncpg
7 | import pytest
8 |
9 | from ohsome_quality_api.geodatabase import client as pg_client
10 |
11 | pytestmark = pytest.mark.skip("dependency on database setup.")
12 |
13 |
14 | async def get_connection_context_manager():
15 | async with pg_client.get_connection() as conn:
16 | return type(conn)
17 |
18 |
19 | class TestPostgres(unittest.TestCase):
20 | def test_connection(self):
21 | instance_type = asyncio.run(get_connection_context_manager())
22 | self.assertEqual(instance_type, asyncpg.connection.Connection)
23 |
24 | @mock.patch(
25 | "ohsome_quality_api.config.get_config",
26 | )
27 | def test_connection_fails(self, mock_get_config):
28 | """Test connection failure error due to wrong credentials"""
29 | mock_get_config.return_value = MappingProxyType(
30 | {
31 | "postgres_host": "foo",
32 | "postgres_port": "9999",
33 | "postgres_db": "bar",
34 | "postgres_user": "tis",
35 | "postgres_password": "fas",
36 | }
37 | )
38 | with self.assertRaises(OSError):
39 | asyncio.run(get_connection_context_manager())
40 |
41 |
42 | if __name__ == "__main__":
43 | unittest.main()
44 |
--------------------------------------------------------------------------------
/tests/integrationtests/utils.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import os
3 | from unittest.mock import MagicMock
4 |
5 | import geojson
6 | import vcr
7 |
8 | from ohsome_quality_api.topics.definitions import get_topic_preset
9 | from ohsome_quality_api.topics.models import TopicDefinition
10 |
11 | TEST_DIR = os.path.dirname(os.path.abspath(__file__))
12 | FIXTURE_DIR = os.path.join(TEST_DIR, "fixtures")
13 | VCR_DIR = os.path.join(FIXTURE_DIR, "vcr_cassettes")
14 |
15 |
16 | class AsyncMock(MagicMock):
17 | async def __call__(self, *args, **kwargs):
18 | return super().__call__(*args, **kwargs)
19 |
20 |
21 | def filename_generator(function):
22 | """Return filename of function source file with the appropriate file-ending."""
23 | test_path = inspect.getsourcefile(function)
24 | # path relative to TEST_DIR
25 | rel_test_path = os.path.relpath(os.path.dirname(test_path), TEST_DIR)
26 | filename_base = os.path.splitext(os.path.basename(test_path))[0]
27 | return os.path.join(rel_test_path, filename_base + "." + oqapi_vcr.serializer)
28 |
29 |
30 | def get_current_dir():
31 | return os.path.dirname(os.path.abspath(__file__))
32 |
33 |
34 | def get_fixture_dir():
35 | return os.path.join(get_current_dir(), "fixtures")
36 |
37 |
38 | def get_geojson_fixture(name):
39 | path = os.path.join(get_fixture_dir(), name)
40 | with open(path, "r") as f:
41 | return geojson.load(f)
42 |
43 |
44 | def get_topic_fixture(name: str) -> TopicDefinition:
45 | return get_topic_preset(name)
46 |
47 |
48 | # usage example:
49 | # add this as parameter to vcr.VCR:
50 | # before_record_response=replace_body(["image/png"], dummy_png),
51 | def replace_body(content_types, replacement):
52 | def before_record_response(response):
53 | # header keys are sometimes in camel case
54 | response["headers"] = {k.lower(): v for k, v in response["headers"].items()}
55 | if any(ct in content_types for ct in response["headers"]["content-type"]):
56 | response["body"]["string"] = replacement
57 | return response
58 |
59 | return before_record_response
60 |
61 |
62 | # dummy_png created with PIL and this command:
63 | # >>> output = io.BytesIO()
64 | # >>> Image.new("RGB", (1, 1)).save(output, format="PNG")
65 | # >>> output.getvalue()
66 | dummy_png = (
67 | b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\02\x00"
68 | b"\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xf6\x178U"
69 | b"\x00\x00\x00\x00IEND\xaeB`\x82"
70 | )
71 |
72 | oqapi_vcr = vcr.VCR(
73 | cassette_library_dir=VCR_DIR,
74 | # details see https://vcrpy.readthedocs.io/en/latest/usage.html#record-modes
75 | record_mode=os.getenv("VCR_RECORD_MODE", default="new_episodes"),
76 | match_on=["method", "scheme", "host", "port", "path", "query", "body"],
77 | func_path_generator=filename_generator,
78 | before_record_response=replace_body(["image/png"], dummy_png),
79 | # ignore github.com. ApprovalTests
80 | ignore_hosts=["testserver", "github"],
81 | ignore_localhost=True, # do not record HTTP requests to local FastAPI test instance
82 | )
83 |
--------------------------------------------------------------------------------
/tests/unittests/__init__.py:
--------------------------------------------------------------------------------
1 | from ohsome_quality_api.config import configure_logging
2 |
3 | configure_logging()
4 |
--------------------------------------------------------------------------------
/tests/unittests/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/api/__init__.py
--------------------------------------------------------------------------------
/tests/unittests/api/test_request_models_data.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ohsome_quality_api.api.request_models import (
4 | IndicatorDataRequest,
5 | )
6 |
7 |
8 | def test_indicator_data(bpolys):
9 | topic = {"key": "foo", "name": "bar", "description": "buz", "data": {}}
10 | IndicatorDataRequest(
11 | bpolys=bpolys,
12 | topic=topic,
13 | )
14 | IndicatorDataRequest(bpolys=bpolys, topic=topic)
15 |
16 |
17 | def test_topic_data_valid(bpolys):
18 | topic = {
19 | "key": "foo",
20 | "name": "bar",
21 | "description": "buz",
22 | "data": {},
23 | }
24 | IndicatorDataRequest(bpolys=bpolys, topic=topic)
25 |
26 |
27 | def test_topic_data_invalid(bpolys):
28 | for topic in (
29 | {"key": "foo", "name": "bar", "data": {}},
30 | {"key": "foo", "description": "bar", "data": {}},
31 | {"key": "foo", "name": "bar", "description": "buz"},
32 | {"key": "foo", "name": "bar", "description": "buz", "data": "fis"},
33 | ):
34 | with pytest.raises(ValueError):
35 | IndicatorDataRequest(bpolys=bpolys, topic=topic)
36 |
--------------------------------------------------------------------------------
/tests/unittests/fixtures/GHS_BUILT_R2018A-Heidelberg.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/fixtures/GHS_BUILT_R2018A-Heidelberg.tif
--------------------------------------------------------------------------------
/tests/unittests/fixtures/config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Database connection parameters;
3 | postgres_host: localhost
4 | postgres_port: 5445
5 | postgres_db: oqapi
6 | postgres_user: oqapi
7 | postgres_password: oqapi
8 | # Restrict size of input geometry
9 | geom_size_limit: 100
10 | # Python logging level
11 | log_level: INFO
12 | # ohsome API URL
13 | ohsome_api: https://api.ohsome.org/v1
14 | # Limit number of concurrent Indicator computations
15 | concurrent_computations: 4
16 | # User-Agent header for request to the ohsome API
17 | # Default: 'ohsome-quality-api/{version}'
18 | user_agent: ohsome-quality-api
19 | # Datasets and Feature IDs available in the database
20 | datasets:
21 | regions: # Name of relation with GEOM
22 | default: ogc_fid # Default unique id field
23 | other: [name] # More unique id fields (optional)
24 |
--------------------------------------------------------------------------------
/tests/unittests/fixtures/heidelberg-altstadt-feature.geojson:
--------------------------------------------------------------------------------
1 | {"type": "Feature", "geometry": {"type":"Polygon","coordinates":[[[8.674092292785645,49.40427147224242],[8.695850372314453,49.40427147224242],[8.695850372314453,49.415552187316095],[8.674092292785645,49.415552187316095],[8.674092292785645,49.40427147224242]]]}}
2 |
--------------------------------------------------------------------------------
/tests/unittests/fixtures/heidelberg-altstadt-featurecollection.geojson:
--------------------------------------------------------------------------------
1 | {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type":"Polygon","coordinates":[[[8.674092292785645,49.40427147224242],[8.695850372314453,49.40427147224242],[8.695850372314453,49.415552187316095],[8.674092292785645,49.415552187316095],[8.674092292785645,49.40427147224242]]]}}]}
2 |
--------------------------------------------------------------------------------
/tests/unittests/fixtures/heidelberg-altstadt-wsf2019.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/fixtures/heidelberg-altstadt-wsf2019.tif
--------------------------------------------------------------------------------
/tests/unittests/fixtures/nodata.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/fixtures/nodata.tif
--------------------------------------------------------------------------------
/tests/unittests/fixtures/ohsome-response-200-invalid.geojson:
--------------------------------------------------------------------------------
1 | {
2 | "attribution" : {
3 | "url" : "https://ohsome.org/copyrights",
4 | "text" : "© OpenStreetMap contributors"
5 | },
6 | "apiVersion" : "1.3.2",
7 | "metadata" : {
8 | "description" : "OSM data as GeoJSON features.",
9 | "requestUrl" : "https://api.ohsome.org/v1/elements/geometry?bboxes=-38.53%2C-13.01%2C-38.30%2C-12.91&clipGeometry=true&filter=type%3Away&properties=tags%2Cmetadata&showMetadata=yes&time=2008-01-01"
10 | },
11 | "type" : "FeatureCollection",
12 | "features" : [{
13 | "type" : "Feature",
14 | "geometry" : {
15 | "type" : "Polygon",
16 | "coordinates" : [
17 | [
18 | [
19 | -38.336482,
20 | -12.913311199999999
21 | ],
22 | [
23 | -38.3353886,
24 | -12.9130802
25 | ],
26 | [
27 | -38.3351615,
28 | -12.913180299999999
29 | ],
30 | [
31 | -38.3357467,
32 | -12.914635599999999
33 | ],
34 | [
35 | -38.3358737,
36 | -12.9146472
37 | ],
38 | [
39 | -38.336597499999996,
40 | -12.9136192
41 | ],
42 | [
43 | -38.336482,
44 | -12.913311199999999
45 | ]
46 | ]
47 | ]
48 | },
49 | "properties" : {
50 | "@changesetId" : 502107,
51 | "@lastEdit" : "2007-11-13T15:20:26Z",
52 | "@osmId" : "way/12331799",
53 | "@osmType" : "WAY",
54 | "@snapshotTimestamp" : "2008-01-01T00:00:00Z",
55 | "@version" : 1,
56 | "amenity" : "parking",
57 | "created_by" : "JOSM"
58 | }
59 | }{
60 | "timestamp" : "2021-02-27T13:41:27.384738",
61 | "status" : 413,
62 | "message" : "The given query is too large in respect to the given timeout. Please use a smaller region and/or coarser time period.",
63 | "requestUrl" : "https://api.ohsome.org/v1/elements/geometry?bboxes=-38.53%2C-13.01%2C-38.30%2C-12.91&clipGeometry=true&filter=type%3Away&properties=tags%2Cmetadata&showMetadata=yes&time=2008-01-01"
64 | }
65 |
--------------------------------------------------------------------------------
/tests/unittests/fixtures/ohsome-response-200-valid.geojson:
--------------------------------------------------------------------------------
1 | {
2 | "attribution": {
3 | "url": "https://ohsome.org/copyrights",
4 | "text": "© OpenStreetMap contributors"
5 | },
6 | "apiVersion": "1.6.2",
7 | "result": [
8 | {
9 | "timestamp": "2014-01-01T00:00:00Z",
10 | "value": 42
11 | },
12 | {
13 | "timestamp": "2015-01-01T00:00:00Z",
14 | "value": 42
15 | },
16 | {
17 | "timestamp": "2016-01-01T00:00:00Z",
18 | "value": 43
19 | },
20 | {
21 | "timestamp": "2017-01-01T00:00:00Z",
22 | "value": 43
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/tests/unittests/fixtures/ohsome-response-400-time.json:
--------------------------------------------------------------------------------
1 | {
2 | "timestamp" : "2021-06-08T08:19:49.333591",
3 | "status" : 400,
4 | "message" : "The provided time parameter is not ISO-8601 conform.",
5 | "requestUrl" : "https://api.ohsome.org/v1/contributions/count?bboxes=8.67%2C49.39%2C8.71%2C49.42&filter=type%3Away%20and%20natural%3D*&format=json&time=20140101%2C2015010"
6 | }
--------------------------------------------------------------------------------
/tests/unittests/mapping_saturation/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/mapping_saturation/__init__.py
--------------------------------------------------------------------------------
/tests/unittests/mapping_saturation/test_models.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest import mock
3 |
4 | import numpy as np
5 | from rpy2.rinterface_lib.embedded import RRuntimeError
6 |
7 | from ohsome_quality_api.indicators.mapping_saturation import models
8 |
9 | from . import fixtures
10 |
11 |
12 | class TestModels(unittest.TestCase):
13 | def setUp(self):
14 | self.ydata_1 = fixtures.VALUES_1
15 | self.xdata_1 = np.array(range(len(self.ydata_1)))
16 | self.ydata_2 = fixtures.VALUES_2
17 | self.xdata_2 = np.array(range(len(self.ydata_2)))
18 | self.expected_keys = [
19 | "name",
20 | "function_formula",
21 | "asymptote",
22 | "mae",
23 | "xdata",
24 | "ydata",
25 | "coefficients",
26 | "fitted_values",
27 | ]
28 |
29 | def run_tests(self, model):
30 | self.assertIsNotNone(model.name)
31 | self.assertNotEqual(model.name, "")
32 | self.assertIsNotNone(model.function_formula)
33 | self.assertNotEqual(model.function_formula, "")
34 |
35 | self.assertTrue(model.coefficients)
36 | self.assertNotEqual(model.fitted_values.size, 0)
37 | self.assertFalse(np.isnan(np.sum(model.fitted_values)))
38 | self.assertTrue(np.isfinite(np.sum(model.fitted_values)))
39 |
40 | self.assertIsNotNone(model.asymptote)
41 | self.assertIsNotNone(model.mae)
42 | self.assertIsNotNone(model.asym_conf_int)
43 |
44 | md = model.as_dict()
45 | self.assertListEqual(self.expected_keys, list(md.keys()))
46 |
47 | def test_sigmoid(self):
48 | model = models.Sigmoid(self.xdata_1, self.ydata_1)
49 | self.run_tests(model)
50 | model = models.Sigmoid(self.xdata_2, self.ydata_2)
51 | self.run_tests(model)
52 |
53 | def test_sslogis(self):
54 | model = models.SSlogis(self.xdata_1, self.ydata_1)
55 | self.run_tests(model)
56 | model = models.SSlogis(self.xdata_2, self.ydata_2)
57 | self.run_tests(model)
58 |
59 | def test_ssdoubles(self):
60 | with self.assertRaises(RRuntimeError):
61 | models.SSdoubleS(self.xdata_1, self.ydata_1)
62 | model = models.SSdoubleS(self.xdata_2, self.ydata_2)
63 | self.run_tests(model)
64 |
65 | def test_ssfpl(self):
66 | model = models.SSfpl(self.xdata_1, self.ydata_1)
67 | self.run_tests(model)
68 | with self.assertRaises(RRuntimeError):
69 | models.SSfpl(self.xdata_2, self.ydata_2)
70 |
71 | def test_ssasymp(self):
72 | model = models.SSasymp(self.xdata_1, self.ydata_1)
73 | self.run_tests(model)
74 | model = models.SSasymp(self.xdata_2, self.ydata_2)
75 | self.run_tests(model)
76 |
77 | def test_ssmicmen(self):
78 | model = models.SSmicmen(self.xdata_1, self.ydata_1)
79 | self.run_tests(model)
80 | model = models.SSmicmen(self.xdata_2, self.ydata_2)
81 | self.run_tests(model)
82 |
83 | @mock.patch.multiple(models.BaseStatModel, __abstractmethods__=set())
84 | def test_mae(self):
85 | model = models.BaseStatModel(np.array([2, 4]), np.array([2, 4]))
86 | model.fitted_values = np.array([4, 4]) # Mock
87 | result = model.mae
88 | self.assertEqual(result, 1.0)
89 | self.assertIsInstance(result, np.float64)
90 |
91 |
92 | if __name__ == "__main__":
93 | unittest.main()
94 |
--------------------------------------------------------------------------------
/tests/unittests/test_api.py:
--------------------------------------------------------------------------------
1 | from ohsome_quality_api import __version__ as version
2 | from ohsome_quality_api.api.api import empty_api_response
3 |
4 |
5 | def test_empty_api_response():
6 | response_template = {
7 | "apiVersion": version,
8 | "attribution": {
9 | "url": (
10 | "https://github.com/GIScience/ohsome-quality-api/blob/main/"
11 | + "COPYRIGHTS.md"
12 | ),
13 | },
14 | }
15 | assert response_template == empty_api_response()
16 |
--------------------------------------------------------------------------------
/tests/unittests/test_attribute_definitions.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ohsome_quality_api.attributes import definitions
4 | from ohsome_quality_api.attributes.models import Attribute
5 |
6 |
7 | @pytest.fixture()
8 | def attribute_key_string():
9 | return "height"
10 |
11 |
12 | def test_get_attributes():
13 | attributes = definitions.get_attributes()
14 | assert isinstance(attributes, dict)
15 | for key, value in attributes.items():
16 | for k, v in value.items():
17 | assert isinstance(k, str)
18 | assert isinstance(v, Attribute)
19 |
20 |
21 | def test_get_attribute(attribute_key_string, topic_key_building_count):
22 | attribute = definitions.get_attribute(
23 | topic_key_building_count, attribute_key_string
24 | )
25 | assert isinstance(attribute, Attribute)
26 |
27 |
28 | def test_get_attribute_wrong_key():
29 | with pytest.raises(KeyError):
30 | definitions.get_attribute("foo", "bar")
31 |
32 |
33 | def test_build_attribute_filter(attribute_key, topic_key_building_count):
34 | attribute = definitions.build_attribute_filter(
35 | attribute_key, topic_key_building_count
36 | )
37 | assert isinstance(attribute, str)
38 | assert (
39 | attribute == "building=* and building!=no and geometry:polygon"
40 | " and (height=* or building:levels=*)"
41 | )
42 |
43 |
44 | def test_build_attribute_filter_multiple_attributes(
45 | attribute_key_multiple, topic_key_building_count
46 | ):
47 | attribute = definitions.build_attribute_filter(
48 | attribute_key_multiple, topic_key_building_count
49 | )
50 | assert isinstance(attribute, str)
51 |
52 |
53 | def test_build_attribute_filter_wrong_key():
54 | with pytest.raises(KeyError):
55 | definitions.build_attribute_filter("foo", "bar")
56 |
57 |
58 | def test_get_attribute_preset(topic_key_building_count):
59 | attribute = definitions.get_attribute_preset(topic_key_building_count)
60 | assert isinstance(attribute, dict)
61 | for key, value in attribute.items():
62 | assert isinstance(value, Attribute)
63 |
--------------------------------------------------------------------------------
/tests/unittests/test_attributes.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pydantic import ValidationError
3 |
4 | from ohsome_quality_api.attributes.models import Attribute
5 |
6 |
7 | def test_attributes():
8 | Attribute(
9 | name="My fancy Attribute", description="some description", filter="testfilter"
10 | )
11 |
12 |
13 | def test_parameter_missing():
14 | with pytest.raises(ValidationError):
15 | Attribute(name="some name", description="some description")
16 | with pytest.raises(ValidationError):
17 | Attribute(description="description")
18 |
19 |
20 | def test_extra_parameter():
21 | with pytest.raises(ValidationError):
22 | Attribute(
23 | name="My fancy Attribute",
24 | description="some description",
25 | foo="bar",
26 | )
27 |
--------------------------------------------------------------------------------
/tests/unittests/test_definitions.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from ohsome_quality_api import definitions
4 | from ohsome_quality_api.indicators.models import (
5 | IndicatorMetadata as IndicatorMetadata,
6 | )
7 |
8 |
9 | class TestDefinitions(unittest.TestCase):
10 | def test_get_attribution(self):
11 | attribution = definitions.get_attribution(["OSM"])
12 | self.assertEqual(attribution, "© OpenStreetMap contributors")
13 |
14 | attributions = definitions.get_attribution(["OSM", "GHSL", "VNL"])
15 | self.assertEqual(
16 | attributions,
17 | (
18 | "© OpenStreetMap contributors; © European Union, 1995-2022, "
19 | "Global Human Settlement Topic Data; "
20 | "Earth Observation Group Nighttime Light Data"
21 | ),
22 | )
23 |
24 | self.assertRaises(AssertionError, definitions.get_attribution, ["MSO"])
25 |
--------------------------------------------------------------------------------
/tests/unittests/test_helper_asyncio.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import unittest
3 |
4 | from ohsome_quality_api.utils import helper_asyncio
5 |
6 |
7 | class TestHelper(unittest.TestCase):
8 | async def bar(self):
9 | raise ValueError()
10 |
11 | async def foo(self):
12 | return "OK"
13 |
14 | def setUp(self):
15 | self.tasks = [
16 | self.foo(),
17 | self.foo(),
18 | self.foo(),
19 | self.foo(),
20 | self.foo(),
21 | ]
22 |
23 | def test_gather_with_semaphore(self):
24 | results = asyncio.run(helper_asyncio.gather_with_semaphore(self.tasks))
25 | assert results == ["OK", "OK", "OK", "OK", "OK"]
26 |
27 | def test_gather_with_semaphore_return_exceptions(self):
28 | results = asyncio.run(
29 | helper_asyncio.gather_with_semaphore(self.tasks, return_exceptions=True)
30 | )
31 | assert results == ["OK", "OK", "OK", "OK", "OK"]
32 |
33 | def test_gather_with_semaphore_return_exceptions_error(self):
34 | tasks = self.tasks + [self.bar()]
35 | results = asyncio.run(
36 | helper_asyncio.gather_with_semaphore(tasks, return_exceptions=True)
37 | )
38 | assert len(results) == 6
39 | assert isinstance(results[-1], ValueError)
40 |
41 | def test_filter_exceptions(self):
42 | tasks = self.tasks + [self.bar()]
43 | results = asyncio.run(
44 | helper_asyncio.gather_with_semaphore(tasks, return_exceptions=True)
45 | )
46 | exceptions = helper_asyncio.filter_exceptions(results)
47 | assert len(exceptions) == 1
48 | assert isinstance(exceptions[0], ValueError)
49 |
--------------------------------------------------------------------------------
/tests/unittests/test_helper_geo.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ohsome_quality_api.utils.helper_geo import calculate_area
4 |
5 |
6 | def test_calculate_area(feature_germany_heidelberg):
7 | expected = 108852960.62891776 # derived from PostGIS ST_AREA
8 | result = calculate_area(feature_germany_heidelberg)
9 | assert result == pytest.approx(expected, abs=1e-3)
10 |
--------------------------------------------------------------------------------
/tests/unittests/test_indicators_definitions.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from unittest.mock import AsyncMock
3 |
4 | import geojson
5 | import pytest
6 | from geojson import Feature, Polygon
7 |
8 | from ohsome_quality_api.indicators import definitions, models
9 |
10 |
11 | @pytest.fixture(scope="class")
12 | def mock_get_reference_coverage(class_mocker):
13 | async_mock = AsyncMock(
14 | return_value=Feature(
15 | geometry=Polygon(
16 | coordinates=[
17 | [
18 | (-180, 90),
19 | (-180, -90),
20 | (180, -90),
21 | (180, 90),
22 | (-180, 90),
23 | ]
24 | ]
25 | )
26 | )
27 | )
28 | class_mocker.patch(
29 | "ohsome_quality_api.indicators.building_comparison.indicator.db_client.get_reference_coverage",
30 | side_effect=async_mock,
31 | )
32 |
33 |
34 | def test_get_indicator_keys():
35 | names = definitions.get_indicator_keys()
36 | assert isinstance(names, list)
37 |
38 |
39 | def test_get_valid_indicators():
40 | indicators = definitions.get_valid_indicators("building-count")
41 | assert indicators == ("mapping-saturation", "currentness", "attribute-completeness")
42 |
43 |
44 | def test_get_indicator_metadata():
45 | indicators = definitions.get_indicator_metadata()
46 | assert isinstance(indicators, dict)
47 | for indicator in indicators.values():
48 | assert isinstance(indicator, models.IndicatorMetadata)
49 |
50 |
51 | def test_get_indicator_metadata_filtered_by_project():
52 | indicators = definitions.get_indicator_metadata("core")
53 | assert isinstance(indicators, dict)
54 | for indicator in indicators.values():
55 | assert isinstance(indicator, models.IndicatorMetadata)
56 | assert indicator.projects == ["core"]
57 |
58 |
59 | def test_get_indicator(metadata_indicator_minimal):
60 | indicator = definitions.get_indicator("minimal")
61 | assert indicator == metadata_indicator_minimal["minimal"]
62 |
63 |
64 | def test_get_coverage(mock_get_reference_coverage):
65 | coverage = asyncio.run(
66 | definitions.get_coverage("building-comparison", inverse=False)
67 | )
68 | assert coverage.is_valid
69 | assert isinstance(coverage, geojson.FeatureCollection)
70 |
71 | coverage = asyncio.run(definitions.get_coverage("building-comparison"))
72 | assert coverage.is_valid
73 | assert isinstance(coverage, geojson.FeatureCollection)
74 |
75 | coverage = asyncio.run(
76 | definitions.get_coverage("building-comparison", inverse=True)
77 | )
78 | assert coverage.is_valid
79 | assert isinstance(coverage, geojson.FeatureCollection)
80 |
81 | coverage = asyncio.run(definitions.get_coverage("mapping-saturation"))
82 | assert coverage.is_valid
83 | assert isinstance(coverage, geojson.FeatureCollection)
84 |
--------------------------------------------------------------------------------
/tests/unittests/test_load_metadata.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GIScience/ohsome-quality-api/a8e3cd72e3d6ef86710aa483d9fa40fd4a0c26a7/tests/unittests/test_load_metadata.py
--------------------------------------------------------------------------------
/tests/unittests/test_logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 | import unittest
5 |
6 | from ohsome_quality_api.config import configure_logging
7 |
8 |
9 | class TestLogging(unittest.TestCase):
10 | def setUp(self):
11 | self.log_level = os.environ.pop("OQAPI_LOG_LEVEL", None)
12 |
13 | def tearDown(self):
14 | if self.log_level is not None:
15 | os.environ["OQAPI_LOG_LEVEL"] = self.log_level
16 | else:
17 | os.environ.pop("OQAPI_LOG_LEVEL", None)
18 |
19 | def test_logging(self):
20 | configure_logging()
21 | with self.assertLogs(level="INFO") as captured:
22 | logging.info("Test info logging message")
23 | logging.debug("Test debug logging message")
24 | # Test that there is only one log message
25 | self.assertEqual(len(captured.records), 1)
26 | # Test not-formatted logging output message
27 | self.assertEqual(captured.records[0].getMessage(), "Test info logging message")
28 |
29 | def test_level(self):
30 | if "pydevd" in sys.modules or "pdb" in sys.modules:
31 | level = "DEBUG"
32 | else:
33 | level = "INFO"
34 | configure_logging()
35 | self.assertEqual(getattr(logging, level), logging.root.level)
36 |
37 | os.environ["OQAPI_LOG_LEVEL"] = "DEBUG"
38 | configure_logging()
39 | self.assertEqual(getattr(logging, "DEBUG"), logging.root.level)
40 |
41 |
42 | if __name__ == "__main__":
43 | unittest.main()
44 |
--------------------------------------------------------------------------------
/tests/unittests/test_project.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pydantic import ValidationError
3 |
4 | from ohsome_quality_api.projects.models import Project
5 |
6 |
7 | def test_project():
8 | Project(name="My fancy Project", description="some description")
9 |
10 |
11 | def test_parameter_missing():
12 | with pytest.raises(ValidationError):
13 | Project(name="some name")
14 | with pytest.raises(ValidationError):
15 | Project(description="description")
16 |
17 |
18 | def test_extra_parameter():
19 | with pytest.raises(ValidationError):
20 | Project(
21 | name="My fancy Project",
22 | description="some description",
23 | foo="bar",
24 | )
25 |
--------------------------------------------------------------------------------
/tests/unittests/test_project_definitions.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ohsome_quality_api.projects import definitions
4 | from ohsome_quality_api.projects.models import Project
5 |
6 |
7 | @pytest.fixture(params=["misc", "core", "experimental"])
8 | def valid_project_keys(request):
9 | return request.param
10 |
11 |
12 | def test_get_projects():
13 | qds = definitions.get_project_metadata()
14 | assert isinstance(qds, dict)
15 | for key, qd in qds.items():
16 | assert isinstance(key, str)
17 | assert isinstance(qd, Project)
18 |
19 |
20 | def test_get_project(valid_project_keys):
21 | qd = definitions.get_project(valid_project_keys)
22 | assert isinstance(qd, Project)
23 |
24 |
25 | def test_get_project_wrong_key():
26 | with pytest.raises(KeyError):
27 | definitions.get_project("foo")
28 |
29 |
30 | def test_get_project_keys_type():
31 | keys = definitions.get_project_keys()
32 | assert isinstance(keys, list)
33 | for key in keys:
34 | assert isinstance(key, str)
35 |
36 |
37 | def test_get_project_keys_valid(valid_project_keys):
38 | keys = definitions.get_project_keys()
39 | assert valid_project_keys in keys
40 |
--------------------------------------------------------------------------------
/tests/unittests/test_quality_dimension.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pydantic import ValidationError
3 |
4 | from ohsome_quality_api.quality_dimensions.models import QualityDimension
5 |
6 |
7 | def test_quality_dimension():
8 | QualityDimension(name="My fancy QualityDimension", description="some description")
9 |
10 |
11 | def test_parameter_missing():
12 | with pytest.raises(ValidationError):
13 | QualityDimension(name="some name")
14 | with pytest.raises(ValidationError):
15 | QualityDimension(description="description")
16 |
17 |
18 | def test_extra_parameter():
19 | with pytest.raises(ValidationError):
20 | QualityDimension(
21 | name="My fancy QualityDimension",
22 | description="some description",
23 | foo="bar",
24 | )
25 |
--------------------------------------------------------------------------------
/tests/unittests/test_quality_dimension_definitions.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ohsome_quality_api.quality_dimensions import definitions
4 | from ohsome_quality_api.quality_dimensions.models import QualityDimension
5 |
6 |
7 | @pytest.fixture(params=["minimal", "completeness", "currentness"])
8 | def valid_quality_dimension_keys(request):
9 | return request.param
10 |
11 |
12 | def test_get_quality_dimensions():
13 | qds = definitions.get_quality_dimensions()
14 | assert isinstance(qds, dict)
15 | for key, qd in qds.items():
16 | assert isinstance(key, str)
17 | assert isinstance(qd, QualityDimension)
18 |
19 |
20 | def test_get_quality_dimension(valid_quality_dimension_keys):
21 | qd = definitions.get_quality_dimension(valid_quality_dimension_keys)
22 | assert isinstance(qd, QualityDimension)
23 |
24 |
25 | def test_get_quality_dimension_wrong_key():
26 | with pytest.raises(KeyError):
27 | definitions.get_quality_dimension("foo")
28 |
29 |
30 | def test_get_quality_dimension_keys_type():
31 | keys = definitions.get_quality_dimension_keys()
32 | assert isinstance(keys, list)
33 | for key in keys:
34 | assert isinstance(key, str)
35 |
36 |
37 | def test_get_quality_dimension_keys_valid(valid_quality_dimension_keys):
38 | keys = definitions.get_quality_dimension_keys()
39 | assert valid_quality_dimension_keys in keys
40 |
--------------------------------------------------------------------------------
/tests/unittests/test_topics_definitions.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ohsome_quality_api.topics import definitions, models
4 |
5 |
6 | def test_get_topic_keys():
7 | names = definitions.get_topic_keys()
8 | assert isinstance(names, list)
9 |
10 |
11 | def test_get_valid_topics():
12 | topics = definitions.get_valid_topics("minimal")
13 | assert topics == ("minimal",)
14 |
15 |
16 | def test_load_topic_definition():
17 | topics = definitions.load_topic_presets()
18 | for topic in topics:
19 | assert isinstance(topics[topic], models.TopicDefinition)
20 |
21 |
22 | def test_get_topic_definition():
23 | topic = definitions.get_topic_preset("minimal")
24 | assert isinstance(topic, models.TopicDefinition)
25 |
26 |
27 | def test_get_topic_definition_not_found_error():
28 | with pytest.raises(KeyError):
29 | definitions.get_topic_preset("foo")
30 | with pytest.raises(KeyError):
31 | definitions.get_topic_preset(None)
32 |
33 |
34 | def test_get_topic_definitions():
35 | topics = definitions.get_topic_presets()
36 | assert isinstance(topics, dict)
37 | for topic in topics.values():
38 | assert isinstance(topic, models.TopicDefinition)
39 |
40 |
41 | def test_get_topic_definitions_with_project():
42 | topics = definitions.get_topic_presets("core")
43 | assert isinstance(topics, dict)
44 | for topic in topics.values():
45 | assert isinstance(topic, models.TopicDefinition)
46 | assert topic.project == "core"
47 |
--------------------------------------------------------------------------------
/tests/unittests/test_validators.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | import pytest
4 |
5 | from ohsome_quality_api.utils.exceptions import (
6 | SizeRestrictionError,
7 | )
8 | from ohsome_quality_api.utils.validators import (
9 | validate_area,
10 | )
11 |
12 |
13 | @mock.patch.dict(
14 | "os.environ",
15 | {"OQAPI_GEOM_SIZE_LIMIT": "1000"},
16 | clear=True,
17 | )
18 | def test_validate_area(feature_germany_heidelberg):
19 | # expect not exceptions
20 | validate_area(feature_germany_heidelberg)
21 |
22 |
23 | @mock.patch.dict(
24 | "os.environ",
25 | {"OQAPI_GEOM_SIZE_LIMIT": "1"},
26 | clear=True,
27 | )
28 | def test_validate_area_exception(feature_germany_heidelberg):
29 | with pytest.raises(SizeRestrictionError):
30 | validate_area(feature_germany_heidelberg)
31 |
--------------------------------------------------------------------------------
/tests/unittests/test_version.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tomllib
3 | import unittest
4 |
5 | from ohsome_quality_api import __version__ as version
6 |
7 |
8 | class TestVersion(unittest.TestCase):
9 | def test_version(self):
10 | infile = os.path.join(
11 | os.path.dirname(os.path.abspath(__file__)), "..", "..", "pyproject.toml"
12 | )
13 | with open(infile, "rb") as fo:
14 | project_file = tomllib.load(fo)
15 | pyproject_version = project_file["project"]["version"]
16 | self.assertEqual(pyproject_version, version)
17 |
18 |
19 | if __name__ == "__main__":
20 | unittest.main()
21 |
--------------------------------------------------------------------------------
/tests/unittests/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import geojson
4 |
5 | from ohsome_quality_api.attributes.definitions import get_attribute
6 | from ohsome_quality_api.attributes.models import Attribute
7 | from ohsome_quality_api.topics.definitions import get_topic_preset
8 | from ohsome_quality_api.topics.models import TopicDefinition
9 |
10 |
11 | def get_geojson_fixture(name):
12 | path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures", name)
13 | with open(path, "r") as f:
14 | return geojson.load(f)
15 |
16 |
17 | def get_topic_fixture(name: str) -> TopicDefinition:
18 | return get_topic_preset(name)
19 |
20 |
21 | def get_attribute_fixture(a_key: str, topic_key: str) -> Attribute:
22 | return get_attribute(a_key, topic_key)
23 |
--------------------------------------------------------------------------------