pax_global_header00006660000000000000000000000064144503427440014521gustar00rootroot0000000000000052 comment=8a07ab86373fb289c6a4ce34c2105a777debe380 geojson-pydantic-0.6.3/000077500000000000000000000000001445034274400150045ustar00rootroot00000000000000geojson-pydantic-0.6.3/.bumpversion.cfg000066400000000000000000000003341445034274400201140ustar00rootroot00000000000000[bumpversion] current_version = 0.6.3 commit = True tag = True tag_name = {new_version} [bumpversion:file:geojson_pydantic/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" geojson-pydantic-0.6.3/.github/000077500000000000000000000000001445034274400163445ustar00rootroot00000000000000geojson-pydantic-0.6.3/.github/codecov.yml000066400000000000000000000002041445034274400205050ustar00rootroot00000000000000comment: off coverage: status: project: default: target: auto threshold: 5 geojson-pydantic-0.6.3/.github/workflows/000077500000000000000000000000001445034274400204015ustar00rootroot00000000000000geojson-pydantic-0.6.3/.github/workflows/ci.yml000066400000000000000000000042601445034274400215210ustar00rootroot00000000000000name: CI on: push: branches: - main tags: - '*' pull_request: env: LATEST_PY_VERSION: '3.10' jobs: tests: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install .["test"] - name: Run pre-commit if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} run: | python -m pip install pre-commit pre-commit run --all-files - name: Run tests run: python -m pytest --cov geojson_pydantic --cov-report xml --cov-report term-missing - name: Upload Results if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} uses: codecov/codecov-action@v1 with: file: ./coverage.xml flags: unittests name: ${{ matrix.python-version }} fail_ci_if_error: false publish: needs: [tests] runs-on: ubuntu-latest if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.LATEST_PY_VERSION }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flit python -m pip install . - name: Set tag version id: tag run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} - name: Set module version id: module run: echo ::set-output name=version::$(python -c 'from importlib.metadata import version; print(version("geojson_pydantic"))') - name: Build and publish if: steps.tag.outputs.tag == steps.module.outputs.version env: FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }} FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: flit publish geojson-pydantic-0.6.3/.gitignore000066400000000000000000000022251445034274400167750ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .pytest_cache geojson-pydantic-0.6.3/.pre-commit-config.yaml000066400000000000000000000014351445034274400212700ustar00rootroot00000000000000repos: - repo: https://github.com/abravalheri/validate-pyproject rev: v0.12.1 hooks: - id: validate-pyproject - repo: https://github.com/psf/black rev: 22.12.0 hooks: - id: black language_version: python - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort language_version: python - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.0.238 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.991 hooks: - id: mypy language_version: python # No reason to run if only tests have changed. They intentionally break typing. exclude: tests/.* additional_dependencies: - pydantic~=1.0 geojson-pydantic-0.6.3/CHANGELOG.md000066400000000000000000000275101445034274400166220ustar00rootroot00000000000000 # Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). Note: Minor version `0.X.0` update might break the API, It's recommanded to pin geojson-pydantic to minor version: `geojson-pydantic>=0.6,<0.7` ## [0.6.3] - 2023-07-02 * limit pydantic requirement to `~=1.0`` ## [0.6.2] - 2023-05-16 ### Added * Additional bbox validation (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/122) ## [0.6.1] - 2023-05-12 ### Fixed * Fix issue with null bbox validation (author @bmschmidt, https://github.com/developmentseed/geojson-pydantic/pull/119) ## [0.6.0] - 2023-05-09 No change since 0.6.0a0 ## [0.6.0a0] - 2023-04-04 ### Added - Validate order of bounding box values. (author @moradology, https://github.com/developmentseed/geojson-pydantic/pull/114) - Enforce required keys and avoid defaults. This aim to follow the geojson specification to the letter. ```python # Before Feature(geometry=Point(coordinates=(0,0))) # Now Feature( type="Feature", geometry=Point( type="Point", coordinates=(0,0) ), properties=None, ) ``` - Add has_z function to Geometries (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/103) - Add optional bbox to geometries. (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/108) - Add support for nested GeometryCollection and a corresponding warning. (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/111) ### Changed - Refactor and simplify WKT construction (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/97) - Support empty geometry coordinates (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/100) - Refactored `__geo_interface__` to be a Mixin which returns `self.dict` (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/105) - GeometryCollection containing a single geometry or geometries of only one type will now produce a warning. (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/111) ### Fixed - Do not validates arbitrary dictionaries. Make `Type` a mandatory key for objects (author @vincentsarago, https://github.com/developmentseed/geojson-pydantic/pull/94) - Add Geometry discriminator when parsing geometry objects (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/101) - Mixed Dimensionality WKTs (make sure the coordinates are either all 2D or 3D) (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/107) - Allow Feature's **id** to be either a String or a Number (author @vincentsarago, https://github.com/developmentseed/geojson-pydantic/pull/91) ### Removed - Python 3.7 support (author @vincentsarago, https://github.com/developmentseed/geojson-pydantic/pull/94) - Unused `LinearRing` Model (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/106) ## [0.5.0] - 2022-12-16 ### Added - python 3.11 support ### Fixed - Derive WKT type from Geometry's type instead of class name (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/81) ### Changed - Replace `NumType` with `float` throughout (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/83) - `__geo_interface__` definition to not use pydantic `BaseModel.dict()` method and better match the specification - Adjusted mypy configuration and updated type definitions to satisfy all rules (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/87) - Updated pre-commit config to run mypy on the whole library instead of individual changed files. - Defaults are more explicit. This keeps pyright from thinking they are required. ### Removed - Remove `validate` classmethods used to implicitly load json strings (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/88) ## [0.4.3] - 2022-07-18 ### Fixed - The bbox key should not be in a `__geo_interface__` object if the bbox is None (author @yellowcap, https://github.com/developmentseed/geojson-pydantic/pull/77) ## [0.4.2] - 2022-06-13 ### Added - `GeometryCollection` as optional input to geometry field in `Feature` (author @davidraleigh, https://github.com/developmentseed/geojson-pydantic/pull/72) ## [0.4.1] - 2022-06-10 ### Added - `Geometry` and `GeometryCollection` validation from dict or string (author @Vikka, https://github.com/developmentseed/geojson-pydantic/pull/69) ```python Point.validate('{"coordinates": [1.0, 2.0], "type": "Point"}') >> Point(coordinates=(1.0, 2.0), type='Point') ``` - `Feature` and `FeatureCollection` validation from dict or string ```python FeatureCollection.validate('{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"coordinates": [1.0, 2.0], "type": "Point"}}]}') >> FeatureCollection(type='FeatureCollection', features=[Feature(type='Feature', geometry=Point(coordinates=(1.0, 2.0), type='Point'), properties=None, id=None, bbox=None)], bbox=None) ``` ## [0.4.0] - 2022-06-03 ### Added - `.wkt` property for Geometry object ```python from geojson_pydantic.geometries import Point Point(coordinates=(1, 2)).wkt >> 'POINT (1.0 2.0)' ``` - `.exterior` and `.interiors` properties for `geojson_pydantic.geometries.Polygon` object. ```python from geojson_pydantic.geometries import Polygon polygon = Polygon( coordinates=[ [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], [(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)], ] ) polygon.exterior >> [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)] list(polygon.interiors) >> [[(2.0, 2.0), (2.0, 4.0), (4.0, 4.0), (4.0, 2.0), (2.0, 2.0)]] ``` - `__geo_interface__` to `geojson_pydantic.geometries.GeometryCollection` object - `__geo_interface__` to `geojson_pydantic.feature.Feature` and `geojson_pydantic.feature.FeatureCollection` object - `geojson_pydantic.__all__` to declaring public objects (author @farridav, https://github.com/developmentseed/geojson-pydantic/pull/52) ### Changed - switch to `pyproject.toml` - rename `geojson_pydantic.version` to `geojson_pydantic.__version__` ### Fixed - changelog compare links ## [0.3.4] - 2022-04-28 - Fix optional geometry and bbox fields on `Feature`; allowing users to pass in `None` or even omit either field (author @moradology, https://github.com/developmentseed/geojson-pydantic/pull/56) - Fix `Polygon.from_bounds` to respect geojson specification and return counterclockwise linear ring (author @jmfee-usgs, https://github.com/developmentseed/geojson-pydantic/pull/49) ## [0.3.3] - 2022-03-04 - Follow geojson specification and make feature geometry optional (author @yellowcap, https://github.com/developmentseed/geojson-pydantic/pull/47) ```python from geojson_pydantic import Feature # Before feature = Feature(type="Feature", geometry=None, properties={}) >> ValidationError: 1 validation error for Feature geometry none is not an allowed value (type=type_error.none.not_allowed) # Now feature = Feature(type="Feature", geometry=None, properties={}) assert feature.geometry is None ``` ## [0.3.2] - 2022-02-21 - fix `parse_geometry_obj` potential bug (author @geospatial-jeff, https://github.com/developmentseed/geojson-pydantic/pull/45) - improve type definition (and validation) for geometry coordinate arrays (author @geospatial-jeff, https://github.com/developmentseed/geojson-pydantic/pull/44) ## [0.3.1] - 2021-08-04 ### Added - `Polygon.from_bounds` class method to create a Polygon geometry from a bounding box. ```python from geojson_pydantic import Polygon print(Polygon.from_bounds((1, 2, 3, 4)).dict(exclude_none=True)) >> {'coordinates': [[(1.0, 2.0), (1.0, 4.0), (3.0, 4.0), (3.0, 2.0), (1.0, 2.0)]], 'type': 'Polygon'} ``` ### Fixed - Added validation for Polygons with zero size. ## [0.3.0] - 2021-05-25 ### Added - `Feature` and `FeatureCollection` model generics to support custom geometry and/or properties validation (author @iwpnd, https://github.com/developmentseed/geojson-pydantic/pull/29) ```python from pydantic import BaseModel from geojson_pydantic.features import Feature from geojson_pydantic.geometries import Polygon class MyFeatureProperties(BaseModel): name: str value: int feature = Feature[Polygon, MyFeatureProperties]( **{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [13.38272,52.46385], [13.42786,52.46385], [13.42786,52.48445], [13.38272,52.48445], [13.38272,52.46385] ] ] }, "properties": { "name": "test", "value": 1 } } ) ``` - Top level export (https://github.com/developmentseed/geojson-pydantic/pull/34) ```python # before from geojson_pydantic.features import Feature, FeatureCollection from geojson_pydantic.geometries import Polygon # now from geojson_pydantic import Feature, Polygon ``` ### Removed - Drop python 3.6 support - Renamed `utils.py` to `types.py` - Removed `Coordinate` type in `geojson_pydantic.features` (replaced by `Position`) ## [0.2.3] - 2021-05-05 ### Fixed - incorrect version number set in `__init__.py` ## [0.2.2] - 2020-12-29 ### Added - Made collections iterable (#12) - Added `parse_geometry_obj` function (#9) ## [0.2.1] - 2020-08-07 Although the type file was added in `0.2.0` it wasn't included in the distributed package. Use this version `0.2.1` for type annotations. ### Fixed - Correct package type information files ## [0.2.0] - 2020-08-06 ### Added - Added documentation on locally running tests (#3) - Publish type information (#6) ### Changed - Removed geojson dependency (#4) ### Fixed - Include MultiPoint as a valid geometry for a Feature (#1) ## [0.1.0] - 2020-05-21 ### Added - Initial Release [unreleased]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.3...HEAD [0.6.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.2...0.6.3 [0.6.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.1...0.6.2 [0.6.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.0...0.6.1 [0.6.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.6.0a0...0.6.0 [0.6.0a]: https://github.com/developmentseed/geojson-pydantic/compare/0.5.0...0.6.0a0 [0.5.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.3...0.5.0 [0.4.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.2...0.4.3 [0.4.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.1...0.4.2 [0.4.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.4.0...0.4.1 [0.4.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.4...0.4.0 [0.3.4]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.3...0.3.4 [0.3.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.2...0.3.3 [0.3.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.1...0.3.2 [0.3.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.3.0...0.3.1 [0.3.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.3...0.3.0 [0.2.3]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.2...0.2.3 [0.2.2]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.1...0.2.2 [0.2.1]: https://github.com/developmentseed/geojson-pydantic/compare/0.2.0...0.2.1 [0.2.0]: https://github.com/developmentseed/geojson-pydantic/compare/0.1.0...0.2.0 [0.1.0]: https://github.com/developmentseed/geojson-pydantic/compare/005f3e57ad07272c99c54302decc63eec12175c9...0.1.0 geojson-pydantic-0.6.3/CONTRIBUTING.md000066400000000000000000000015371445034274400172430ustar00rootroot00000000000000# Contributing To run the tests, first install the package in a virtual environment: ```sh virtualenv venv source venv/bin/activate python -m pip install -e '.[test]' ``` You can then run the tests with the following command: ```sh python -m pytest --cov geojson_pydantic --cov-report term-missing ``` This repo is set to use pre-commit to run `isort`, `flake8`, `pydocstring`, `black` ("uncompromising Python code formatter") and `mypy` when committing new code. ``` sh pre-commit install ``` ## Release we use https://github.com/c4urself/bump2version to update the package version. ``` # Install bump2version $ pip install --upgrade bump2version # Update version (edit files, commit and create tag) # this will do `0.2.1 -> 0.2.2` because we use the `patch` tag $ bump2version patch # Push change and tag to github $ git push origin main --tags ``` geojson-pydantic-0.6.3/LICENSE000066400000000000000000000020611445034274400160100ustar00rootroot00000000000000MIT License Copyright (c) 2020 Development Seed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. geojson-pydantic-0.6.3/README.md000066400000000000000000000173511445034274400162720ustar00rootroot00000000000000# geojson-pydantic

Pydantic models for GeoJSON.

Test Coverage Package version Downloads License Conda

## Description `geojson_pydantic` provides a suite of Pydantic models matching the GeoJSON specification [rfc7946](https://datatracker.ietf.org/doc/html/rfc7946). Those models can be used for creating or validating geojson data. ## Install ```bash $ python -m pip install -U pip $ python -m pip install geojson-pydantic ``` Or install from source: ```bash $ python -m pip install -U pip $ python -m pip install git+https://github.com/developmentseed/geojson-pydantic.git ``` Install with conda from [`conda-forge`](https://anaconda.org/conda-forge/geojson-pydantic): ```bash $ conda install -c conda-forge geojson-pydantic ``` ## Usage ```python from geojson_pydantic import Feature, FeatureCollection, Point geojson_feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.38272, 52.46385], }, "properties": { "name": "jeff", }, } feat = Feature(**geojson_feature) assert feat.type == "Feature" assert type(feat.geometry) == Point assert feat.properties["name"] == "jeff" fc = FeatureCollection(type="FeatureCollection", features=[geojson_feature, geojson_feature]) assert fc.type == "FeatureCollection" assert len(fc) == 2 assert type(fc.features[0].geometry) == Point assert fc.features[0].properties["name"] == "jeff" ``` ### Advanced usage In `geojson_pydantic` we've implemented pydantic's [Generic Models](https://pydantic-docs.helpmanual.io/usage/models/#generic-models) which allow the creation of more advanced models to validate either the geometry type or the properties. In order to make use of this generic typing, there are two steps: first create a new model, then use that model to validate your data. To create a model using a `Generic` type, you **HAVE TO** pass `Type definitions` to the `Feature` model in form of `Feature[Geometry Type, Properties Type]`. Then pass your data to this constructor. By default `Feature` and `FeatureCollections` are defined using `geojson_pydantic.geometries.Geometry` for the geometry and `typing.Dict` for the properties. Here's an example where we want to validate that GeoJSON features have Polygon types, but don't do any specific property validation. ```python from typing import Dict from geojson_pydantic import Feature, Polygon from pydantic import BaseModel geojson_feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.38272, 52.46385], }, "properties": { "name": "jeff", }, } # Define a Feature model with Geometry as `Polygon` and Properties as `Dict` MyPolygonFeatureModel = Feature[Polygon, Dict] feat = MyPolygonFeatureModel(**geojson_feature) # should raise Validation Error because `geojson_feature` is a point >>> ValidationError: 3 validation errors for Feature[Polygon, Dict] ... geometry -> type unexpected value; permitted: 'Polygon' (type=value_error.const; given=Point; permitted=['Polygon']) geojson_feature = { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [13.38272, 52.46385], [13.42786, 52.46385], [13.42786, 52.48445], [13.38272, 52.48445], [13.38272, 52.46385], ] ], }, "properties": { "name": "jeff", }, } feat = MyPolygonFeatureModel(**geojson_feature) assert type(feature.geometry) == Polygon ``` Or with optional geometry ```python from geojson_pydantic import Feature, Point from typing import Optional MyPointFeatureModel = Feature[Optional[Point], Dict] assert MyPointFeatureModel(type="Feature", geometry=None, properties={}).geometry is None assert MyPointFeatureModel(type="Feature", geometry=Point(coordinates=(0,0)), properties={}).geometry is not None ``` And now with constrained properties ```python from geojson_pydantic import Feature, Point from pydantic import BaseModel, constr # Define a Feature model with Geometry as `Point` and Properties as a constrained Model class MyProps(BaseModel): name: constr(regex=r'^(drew|vincent)$') MyPointFeatureModel = Feature[Point, MyProps] geojson_feature = { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.38272, 52.46385], }, "properties": { "name": "jeff", }, } feat = MyPointFeatureModel(**geojson_feature) >>> ValidationError: 1 validation error for Feature[Point, MyProps] properties -> name string does not match regex "^(drew|vincent)$" (type=value_error.str.regex; pattern=^(drew|vincent)$) geojson_feature["properties"]["name"] = "drew" feat = MyPointFeatureModel(**geojson_feature) assert feat.properties.name == "drew" ``` ## Enforced Keys Starting with version `0.6.0`, geojson-pydantic's classes will not define default keys such has `type`, `geometry` or `properties`. This is to make sure the library does well its first goal, which is `validating` GeoJSON object based on the [specification](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1) o A GeoJSON object has a member with the name "type". The value of the member MUST be one of the GeoJSON types. o A Feature object HAS a "type" member with the value "Feature". o A Feature object HAS a member with the name "geometry". The value of the geometry member SHALL be either a Geometry object as defined above or, in the case that the Feature is unlocated, a JSON null value. o A Feature object HAS a member with the name "properties". The value of the properties member is an object (any JSON object or a JSON null value). ```python from geojson_pydantic import Point ## Before 0.6 Point(coordinates=(0,0)) >> Point(type='Point', coordinates=(0.0, 0.0), bbox=None) ## After 0.6 Point(coordinates=(0,0)) >> ValidationError: 1 validation error for Point type field required (type=value_error.missing) Point(type="Point", coordinates=(0,0)) >> Point(type='Point', coordinates=(0.0, 0.0), bbox=None) ``` ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). ## Changes See [CHANGES.md](https://github.com/developmentseed/geojson-pydantic/blob/main/CHANGELOG.md). ## Authors Initial implementation by @geospatial-jeff; taken liberally from https://github.com/arturo-ai/stac-pydantic/ See [contributors](hhttps://github.com/developmentseed/geojson-pydantic/graphs/contributors) for a listing of individual contributors. ## License See [LICENSE](https://github.com/developmentseed/geojson-pydantic/blob/main/LICENSE) geojson-pydantic-0.6.3/geojson_pydantic/000077500000000000000000000000001445034274400203435ustar00rootroot00000000000000geojson-pydantic-0.6.3/geojson_pydantic/__init__.py000066400000000000000000000007001445034274400224510ustar00rootroot00000000000000"""geojson-pydantic.""" from .features import Feature, FeatureCollection # noqa from .geometries import ( # noqa GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) __version__ = "0.6.3" __all__ = [ "Feature", "FeatureCollection", "GeometryCollection", "LineString", "MultiLineString", "MultiPoint", "MultiPolygon", "Point", "Polygon", ] geojson-pydantic-0.6.3/geojson_pydantic/features.py000066400000000000000000000035621445034274400225410ustar00rootroot00000000000000"""pydantic models for GeoJSON Feature objects.""" from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypeVar, Union from pydantic import BaseModel, Field, StrictInt, StrictStr, validator from pydantic.generics import GenericModel from geojson_pydantic.geo_interface import GeoInterfaceMixin from geojson_pydantic.geometries import Geometry, GeometryCollection from geojson_pydantic.types import BBox, validate_bbox Props = TypeVar("Props", bound=Union[Dict[str, Any], BaseModel]) Geom = TypeVar("Geom", bound=Union[Geometry, GeometryCollection]) class Feature(GenericModel, Generic[Geom, Props], GeoInterfaceMixin): """Feature Model""" type: Literal["Feature"] geometry: Union[Geom, None] = Field(...) properties: Union[Props, None] = Field(...) id: Optional[Union[StrictInt, StrictStr]] = None bbox: Optional[BBox] = None _validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox) @validator("geometry", pre=True, always=True) def set_geometry(cls, geometry: Any) -> Any: """set geometry from geo interface or input""" if hasattr(geometry, "__geo_interface__"): return geometry.__geo_interface__ return geometry class FeatureCollection(GenericModel, Generic[Geom, Props], GeoInterfaceMixin): """FeatureCollection Model""" type: Literal["FeatureCollection"] features: List[Feature[Geom, Props]] bbox: Optional[BBox] = None def __iter__(self) -> Iterator[Feature]: # type: ignore [override] """iterate over features""" return iter(self.features) def __len__(self) -> int: """return features length""" return len(self.features) def __getitem__(self, index: int) -> Feature: """get feature at a given index""" return self.features[index] _validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox) geojson-pydantic-0.6.3/geojson_pydantic/geo_interface.py000066400000000000000000000013011445034274400235020ustar00rootroot00000000000000"""Mixin for __geo_interface__ on GeoJSON objects.""" from typing import Any, Dict, Protocol class _DictProtocol(Protocol): """Protocol for use as the type of self in the mixin.""" def dict(self, *, exclude_unset: bool, **args: Any) -> Dict[str, Any]: """Define a dict function so the mixin knows it exists.""" ... class GeoInterfaceMixin: """Mixin for __geo_interface__ on GeoJSON objects.""" @property def __geo_interface__(self: _DictProtocol) -> Dict[str, Any]: """GeoJSON-like protocol for geo-spatial (GIS) vector data. ref: https://gist.github.com/sgillies/2217756#__geo_interface """ return self.dict(exclude_unset=True) geojson-pydantic-0.6.3/geojson_pydantic/geometries.py000066400000000000000000000253401445034274400230640ustar00rootroot00000000000000"""pydantic models for GeoJSON Geometry objects.""" from __future__ import annotations import abc import warnings from typing import Any, Iterator, List, Literal, Optional, Union from pydantic import BaseModel, Field, ValidationError, validator from pydantic.error_wrappers import ErrorWrapper from typing_extensions import Annotated from geojson_pydantic.geo_interface import GeoInterfaceMixin from geojson_pydantic.types import ( BBox, LinearRing, LineStringCoords, MultiLineStringCoords, MultiPointCoords, MultiPolygonCoords, PolygonCoords, Position, validate_bbox, ) def _position_wkt_coordinates(coordinates: Position, force_z: bool = False) -> str: """Converts a Position to WKT Coordinates.""" wkt_coordinates = " ".join(str(number) for number in coordinates) if force_z and len(coordinates) < 3: wkt_coordinates += " 0.0" return wkt_coordinates def _position_has_z(position: Position) -> bool: return len(position) == 3 def _position_list_wkt_coordinates( coordinates: List[Position], force_z: bool = False ) -> str: """Converts a list of Positions to WKT Coordinates.""" return ", ".join( _position_wkt_coordinates(position, force_z) for position in coordinates ) def _position_list_has_z(positions: List[Position]) -> bool: """Checks if any position in a list has a Z.""" return any(_position_has_z(position) for position in positions) def _lines_wtk_coordinates( coordinates: List[LineStringCoords], force_z: bool = False ) -> str: """Converts lines to WKT Coordinates.""" return ", ".join( f"({_position_list_wkt_coordinates(line, force_z)})" for line in coordinates ) def _lines_has_z(lines: List[LineStringCoords]) -> bool: """Checks if any position in a list has a Z.""" return any( _position_has_z(position) for positions in lines for position in positions ) def _polygons_wkt_coordinates( coordinates: List[PolygonCoords], force_z: bool = False ) -> str: return ",".join( f"({_lines_wtk_coordinates(polygon, force_z)})" for polygon in coordinates ) class _GeometryBase(BaseModel, abc.ABC, GeoInterfaceMixin): """Base class for geometry models""" type: str coordinates: Any bbox: Optional[BBox] = None @abc.abstractmethod def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" ... @property @abc.abstractmethod def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" ... @property def wkt(self) -> str: """Return the Well Known Text representation.""" # Start with the WKT Type wkt = self.type.upper() has_z = self.has_z if self.coordinates: # If any of the coordinates have a Z add a "Z" to the WKT wkt += " Z " if has_z else " " # Add the rest of the WKT inside parentheses wkt += f"({self.__wkt_coordinates__(self.coordinates, force_z=has_z)})" else: # Otherwise it will be "EMPTY" wkt += " EMPTY" return wkt _validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox) class Point(_GeometryBase): """Point Model""" type: Literal["Point"] coordinates: Position def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _position_wkt_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_has_z(self.coordinates) class MultiPoint(_GeometryBase): """MultiPoint Model""" type: Literal["MultiPoint"] coordinates: MultiPointCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _position_list_wkt_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_list_has_z(self.coordinates) class LineString(_GeometryBase): """LineString Model""" type: Literal["LineString"] coordinates: LineStringCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _position_list_wkt_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_list_has_z(self.coordinates) class MultiLineString(_GeometryBase): """MultiLineString Model""" type: Literal["MultiLineString"] coordinates: MultiLineStringCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _lines_wtk_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _lines_has_z(self.coordinates) class Polygon(_GeometryBase): """Polygon Model""" type: Literal["Polygon"] coordinates: PolygonCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _lines_wtk_coordinates(coordinates, force_z) @validator("coordinates") def check_closure(cls, coordinates: List) -> List: """Validate that Polygon is closed (first and last coordinate are the same).""" if any(ring[-1] != ring[0] for ring in coordinates): raise ValueError("All linear rings have the same start and end coordinates") return coordinates @property def exterior(self) -> Union[LinearRing, None]: """Return the exterior Linear Ring of the polygon.""" return self.coordinates[0] if self.coordinates else None @property def interiors(self) -> Iterator[LinearRing]: """Interiors (Holes) of the polygon.""" yield from ( interior for interior in self.coordinates[1:] if len(self.coordinates) > 1 ) @property def has_z(self) -> bool: """Checks if any coordinates have a Z value.""" return _lines_has_z(self.coordinates) @classmethod def from_bounds( cls, xmin: float, ymin: float, xmax: float, ymax: float ) -> "Polygon": """Create a Polygon geometry from a boundingbox.""" return cls( type="Polygon", coordinates=[ [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax), (xmin, ymin)] ], ) class MultiPolygon(_GeometryBase): """MultiPolygon Model""" type: Literal["MultiPolygon"] coordinates: MultiPolygonCoords def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str: """return WKT coordinates.""" return _polygons_wkt_coordinates(coordinates, force_z) @property def has_z(self) -> bool: """Checks if any coordinates have a Z value.""" return any(_lines_has_z(polygon) for polygon in self.coordinates) @validator("coordinates") def check_closure(cls, coordinates: List) -> List: """Validate that Polygon is closed (first and last coordinate are the same).""" if any(ring[-1] != ring[0] for polygon in coordinates for ring in polygon): raise ValueError("All linear rings have the same start and end coordinates") return coordinates Geometry = Annotated[ Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon], Field(discriminator="type"), ] class GeometryCollection(BaseModel, GeoInterfaceMixin): """GeometryCollection Model""" type: Literal["GeometryCollection"] geometries: List[Union[Geometry, GeometryCollection]] bbox: Optional[BBox] = None def __iter__(self) -> Iterator[Union[Geometry, GeometryCollection]]: # type: ignore [override] """iterate over geometries""" return iter(self.geometries) def __len__(self) -> int: """return geometries length""" return len(self.geometries) def __getitem__(self, index: int) -> Union[Geometry, GeometryCollection]: """get geometry at a given index""" return self.geometries[index] @property def wkt(self) -> str: """Return the Well Known Text representation.""" # Each geometry will check its own coordinates for Z and include "Z" in the wkt # if necessary. Rather than looking at the coordinates for each of the geometries # again, we can just get the wkt from each of them and check if there is a Z # anywhere in the text. # Get the wkt from each of the geometries in the collection geometries = ( f'({", ".join(geom.wkt for geom in self.geometries)})' if self.geometries else "EMPTY" ) # If any of them contain `Z` add Z to the output wkt z = " Z " if "Z" in geometries else " " return f"{self.type.upper()}{z}{geometries}" _validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox) @validator("geometries") def check_geometries(cls, geometries: List) -> List: """Add warnings for conditions the spec does not explicitly forbid.""" if len(geometries) == 1: warnings.warn( "GeometryCollection should not be used for single geometries." ) if any(geom.type == "GeometryCollection" for geom in geometries): warnings.warn( "GeometryCollection should not be used for nested GeometryCollections." ) if len({geom.type for geom in geometries}) == 1: warnings.warn( "GeometryCollection should not be used for homogeneous collections." ) return geometries def parse_geometry_obj(obj: Any) -> Geometry: """ `obj` is an object that is supposed to represent a GeoJSON geometry. This method returns the reads the `"type"` field and returns the correct pydantic Geometry model. """ if "type" not in obj: raise ValidationError( errors=[ ErrorWrapper(ValueError("Missing 'type' field in geometry"), loc="type") ], model=_GeometryBase, ) if obj["type"] == "Point": return Point.parse_obj(obj) elif obj["type"] == "MultiPoint": return MultiPoint.parse_obj(obj) elif obj["type"] == "LineString": return LineString.parse_obj(obj) elif obj["type"] == "MultiLineString": return MultiLineString.parse_obj(obj) elif obj["type"] == "Polygon": return Polygon.parse_obj(obj) elif obj["type"] == "MultiPolygon": return MultiPolygon.parse_obj(obj) raise ValidationError( errors=[ErrorWrapper(ValueError("Unknown type"), loc="type")], model=_GeometryBase, ) geojson-pydantic-0.6.3/geojson_pydantic/py.typed000066400000000000000000000000001445034274400220300ustar00rootroot00000000000000geojson-pydantic-0.6.3/geojson_pydantic/types.py000066400000000000000000000032401445034274400220600ustar00rootroot00000000000000"""Types for geojson_pydantic models""" from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union from pydantic import conlist T = TypeVar("T") BBox = Union[ Tuple[float, float, float, float], # 2D bbox Tuple[float, float, float, float, float, float], # 3D bbox ] def validate_bbox(bbox: Optional[BBox]) -> Optional[BBox]: """Validate BBox values are ordered correctly.""" # If bbox is None, there is nothing to validate. if bbox is None: return None # A list to store any errors found so we can raise them all at once. errors: List[str] = [] # Determine where the second position starts. 2 for 2D, 3 for 3D. offset = len(bbox) // 2 # Check X if bbox[0] > bbox[offset]: errors.append(f"Min X ({bbox[0]}) must be <= Max X ({bbox[offset]}).") # Check Y if bbox[1] > bbox[1 + offset]: errors.append(f"Min Y ({bbox[1]}) must be <= Max Y ({bbox[1 + offset]}).") # If 3D, check Z values. if offset > 2 and bbox[2] > bbox[2 + offset]: errors.append(f"Min Z ({bbox[2]}) must be <= Max Z ({bbox[2 + offset]}).") # Raise any errors found. if errors: raise ValueError("Invalid BBox. Error(s): " + " ".join(errors)) return bbox Position = Union[Tuple[float, float], Tuple[float, float, float]] # Coordinate arrays if TYPE_CHECKING: LineStringCoords = List[Position] LinearRing = List[Position] else: LineStringCoords = conlist(Position, min_items=2) LinearRing = conlist(Position, min_items=4) MultiPointCoords = List[Position] MultiLineStringCoords = List[LineStringCoords] PolygonCoords = List[LinearRing] MultiPolygonCoords = List[PolygonCoords] geojson-pydantic-0.6.3/pyproject.toml000066400000000000000000000043371445034274400177270ustar00rootroot00000000000000[project] name = "geojson-pydantic" description = "Pydantic data models for the GeoJSON spec." readme = "README.md" requires-python = ">=3.8" license = {file = "LICENSE"} authors = [ {name = "Drew Bollinger", email = "drew@developmentseed.org"}, ] keywords = ["geojson", "Pydantic"] classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering :: GIS", "Typing :: Typed", ] dynamic = ["version"] dependencies = ["pydantic~=1.0"] [project.optional-dependencies] test = ["pytest", "pytest-cov", "shapely"] dev = ["pre-commit"] [project.urls] Source = "https://github.com/developmentseed/geojson-pydantic" [build-system] requires = ["flit>=3.2,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "geojson_pydantic" [tool.flit.sdist] exclude = [ "tests/", "docs/", ".github/", "CHANGELOG.md", "CONTRIBUTING.md", ] [tool.coverage.run] branch = true parallel = true [tool.coverage.report] exclude_lines = [ "no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] [tool.isort] profile = "black" known_first_party = ["geojson_pydantic"] known_third_party = ["pydantic"] default_section = "THIRDPARTY" [tool.mypy] plugins = ["pydantic.mypy"] disallow_untyped_calls = true disallow_untyped_defs = true disallow_incomplete_defs = true warn_redundant_casts = true warn_unused_ignores = true no_implicit_optional = true show_error_codes = true [tool.ruff] select = [ "D1", # pydocstyle errors "E", # pycodestyle errors "W", # pycodestyle warnings "F", # flake8 "C", # flake8-comprehensions "B", # flake8-bugbear ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults "B905", # ignore zip() without an explicit strict= parameter, only support with python >3.10 ] [tool.ruff.per-file-ignores] "tests/test_geometries.py" = ["D1"] "tests/test_features.py" = ["D1"] "tests/test_package.py" = ["D1"] geojson-pydantic-0.6.3/tests/000077500000000000000000000000001445034274400161465ustar00rootroot00000000000000geojson-pydantic-0.6.3/tests/test_features.py000066400000000000000000000176251445034274400214100ustar00rootroot00000000000000from random import randint from typing import Any, Dict from uuid import uuid4 import pytest from pydantic import BaseModel, ValidationError from geojson_pydantic.features import Feature, FeatureCollection from geojson_pydantic.geometries import ( Geometry, GeometryCollection, MultiPolygon, Polygon, ) class GenericProperties(BaseModel): id: str description: str size: int properties: Dict[str, Any] = { "id": str(uuid4()), "description": str(uuid4()), "size": randint(0, 1000), } coordinates = [ [ [13.38272, 52.46385], [13.42786, 52.46385], [13.42786, 52.48445], [13.38272, 52.48445], [13.38272, 52.46385], ] ] polygon: Dict[str, Any] = { "type": "Polygon", "coordinates": coordinates, } multipolygon: Dict[str, Any] = { "type": "MultiPolygon", "coordinates": [coordinates], } geom_collection: Dict[str, Any] = { "type": "GeometryCollection", "geometries": [polygon, multipolygon], } test_feature: Dict[str, Any] = { "type": "Feature", "geometry": polygon, "properties": properties, "bbox": [13.38272, 52.46385, 13.42786, 52.48445], } test_feature_geom_null: Dict[str, Any] = { "type": "Feature", "geometry": None, "properties": properties, } test_feature_geometry_collection: Dict[str, Any] = { "type": "Feature", "geometry": geom_collection, "properties": properties, } def test_feature_collection_iteration(): """test if feature collection is iterable""" gc = FeatureCollection( type="FeatureCollection", features=[test_feature, test_feature] ) assert hasattr(gc, "__geo_interface__") iter(gc) def test_geometry_collection_iteration(): """test if feature collection is iterable""" gc = FeatureCollection( type="FeatureCollection", features=[test_feature_geometry_collection] ) assert hasattr(gc, "__geo_interface__") iter(gc) def test_generic_properties_is_dict(): feature = Feature(**test_feature) assert hasattr(feature, "__geo_interface__") assert feature.properties["id"] == test_feature["properties"]["id"] assert type(feature.properties) == dict assert not hasattr(feature.properties, "id") def test_generic_properties_is_dict_collection(): feature = Feature(**test_feature_geometry_collection) assert hasattr(feature, "__geo_interface__") assert ( feature.properties["id"] == test_feature_geometry_collection["properties"]["id"] ) assert type(feature.properties) == dict assert not hasattr(feature.properties, "id") def test_generic_properties_is_object(): feature = Feature[Geometry, GenericProperties](**test_feature) assert feature.properties.id == test_feature["properties"]["id"] assert type(feature.properties) == GenericProperties assert hasattr(feature.properties, "id") def test_generic_geometry(): feature = Feature[Polygon, GenericProperties](**test_feature) assert feature.properties.id == test_feature_geometry_collection["properties"]["id"] assert type(feature.geometry) == Polygon assert type(feature.properties) == GenericProperties assert hasattr(feature.properties, "id") feature = Feature[Polygon, Dict](**test_feature) assert type(feature.geometry) == Polygon assert feature.properties["id"] == test_feature["properties"]["id"] assert type(feature.properties) == dict assert not hasattr(feature.properties, "id") with pytest.raises(ValidationError): Feature[MultiPolygon, Dict](**({"type": "Feature", "geometry": polygon})) def test_generic_geometry_collection(): feature = Feature[GeometryCollection, GenericProperties]( **test_feature_geometry_collection ) assert feature.properties.id == test_feature_geometry_collection["properties"]["id"] assert type(feature.geometry) == GeometryCollection assert feature.geometry.wkt.startswith("GEOMETRYCOLLECTION (POLYGON ") assert type(feature.properties) == GenericProperties assert hasattr(feature.properties, "id") feature = Feature[GeometryCollection, Dict](**test_feature_geometry_collection) assert type(feature.geometry) == GeometryCollection assert ( feature.properties["id"] == test_feature_geometry_collection["properties"]["id"] ) assert type(feature.properties) == dict assert not hasattr(feature.properties, "id") with pytest.raises(ValidationError): Feature[MultiPolygon, Dict](**({"type": "Feature", "geometry": polygon})) def test_generic_properties_should_raise_for_string(): with pytest.raises(ValidationError): Feature( **({"type": "Feature", "geometry": polygon, "properties": "should raise"}) ) def test_feature_collection_generic(): fc = FeatureCollection[Polygon, GenericProperties]( type="FeatureCollection", features=[test_feature, test_feature] ) assert len(fc) == 2 assert type(fc[0].properties) == GenericProperties assert type(fc[0].geometry) == Polygon def test_geo_interface_protocol(): class Pointy: __geo_interface__ = {"type": "Point", "coordinates": (0.0, 0.0)} feat = Feature(type="Feature", geometry=Pointy(), properties={}) assert feat.geometry.dict(exclude_unset=True) == Pointy.__geo_interface__ def test_feature_with_null_geometry(): feature = Feature(**test_feature_geom_null) assert feature.geometry is None def test_feature_geo_interface_with_null_geometry(): feature = Feature(**test_feature_geom_null) assert "bbox" not in feature.__geo_interface__ def test_feature_collection_geo_interface_with_null_geometry(): fc = FeatureCollection( type="FeatureCollection", features=[test_feature_geom_null, test_feature] ) assert "bbox" not in fc.__geo_interface__ assert "bbox" not in fc.__geo_interface__["features"][0] assert "bbox" in fc.__geo_interface__["features"][1] @pytest.mark.parametrize("id", ["a", 1, "1"]) def test_feature_id(id): """Test if a string stays a string and if an int stays an int.""" feature = Feature(**test_feature, id=id) assert feature.id == id @pytest.mark.parametrize("id", [True, 1.0]) def test_bad_feature_id(id): """make sure it raises error.""" with pytest.raises(ValidationError): Feature(**test_feature, id=id) def test_feature_validation(): """Test default.""" assert Feature(type="Feature", properties=None, geometry=None) assert Feature(type="Feature", properties=None, geometry=None, bbox=None) with pytest.raises(ValidationError): # should be type=Feature Feature(type="feature", properties=None, geometry=None) with pytest.raises(ValidationError): # missing type Feature(properties=None, geometry=None) with pytest.raises(ValidationError): # missing properties Feature(type="Feature", geometry=None) with pytest.raises(ValidationError): # missing geometry Feature(type="Feature", properties=None) assert Feature( type="Feature", properties=None, bbox=(0, 0, 100, 100), geometry=None ) assert Feature( type="Feature", properties=None, bbox=(0, 0, 0, 100, 100, 100), geometry=None ) with pytest.raises(ValidationError): # bad bbox2d Feature(type="Feature", properties=None, bbox=(100, 100, 0, 0), geometry=None) with pytest.raises(ValidationError): # bad bbox3d Feature( type="Feature", properties=None, bbox=(100, 100, 100, 0, 0, 0), geometry=None, ) def test_bbox_validation(): # Some attempts at generic validation did not validate the types within # bbox before passing them to the function and resulted in TypeErrors. # This test exists to ensure that doesn't happen in the future. with pytest.raises(ValidationError): Feature( type="Feature", properties=None, bbox=(0, "a", 0, 1, 1, 1), geometry=None, ) geojson-pydantic-0.6.3/tests/test_geometries.py000066400000000000000000000464371445034274400217400ustar00rootroot00000000000000import re from typing import Union import pytest from pydantic import ValidationError from shapely.geometry import shape from geojson_pydantic.geometries import ( Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, parse_geometry_obj, ) def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]): """Assert WKT equivalence with Shapely.""" # Remove any trailing `.0` to match Shapely format clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt) assert shape(geom).wkt == clean_wkt @pytest.mark.parametrize("coordinates", [(1.01, 2.01), (1.0, 2.0, 3.0), (1.0, 2.0)]) def test_point_valid_coordinates(coordinates): """ Two or three number elements as coordinates should be okay """ p = Point(type="Point", coordinates=coordinates) assert p.type == "Point" assert p.coordinates == coordinates assert hasattr(p, "__geo_interface__") assert_wkt_equivalence(p) @pytest.mark.parametrize( "coordinates", [(1.0,), (1.0, 2.0, 3.0, 4.0), "Foo", (None, 2.0), (1.0, (2.0,)), (), [], None], ) def test_point_invalid_coordinates(coordinates): """ Too few or to many elements should not, nor weird data types """ with pytest.raises(ValidationError): Point(type="Point", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # No Z [(1.0, 2.0)], [(1.0, 2.0), (1.0, 2.0)], # Has Z [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)], # Mixed [(1.0, 2.0), (1.0, 2.0, 3.0)], ], ) def test_multi_point_valid_coordinates(coordinates): """ Two or three number elements as coordinates should be okay, as well as an empty array. """ p = MultiPoint(type="MultiPoint", coordinates=coordinates) assert p.type == "MultiPoint" assert p.coordinates == coordinates assert hasattr(p, "__geo_interface__") assert_wkt_equivalence(p) @pytest.mark.parametrize( "coordinates", [[(1.0,)], [(1.0, 2.0, 3.0, 4.0)], ["Foo"], [(None, 2.0)], [(1.0, (2.0,))], None], ) def test_multi_point_invalid_coordinates(coordinates): """ Too few or to many elements should not, nor weird data types """ with pytest.raises(ValidationError): MultiPoint(type="MultiPoint", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Two Points, no Z [(1.0, 2.0), (3.0, 4.0)], # Three Points, no Z [(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)], # Two Points, has Z [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)], # Shapely doesn't like mixed here ], ) def test_line_string_valid_coordinates(coordinates): """ A list of two coordinates or more should be okay """ linestring = LineString(type="LineString", coordinates=coordinates) assert linestring.type == "LineString" assert linestring.coordinates == coordinates assert hasattr(linestring, "__geo_interface__") assert_wkt_equivalence(linestring) @pytest.mark.parametrize("coordinates", [None, "Foo", [], [(1.0, 2.0)], ["Foo", "Bar"]]) def test_line_string_invalid_coordinates(coordinates): """ But we don't accept non-list inputs, too few coordinates, or bogus coordinates """ with pytest.raises(ValidationError): LineString(type="LineString", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # One line, two points, no Z [[(1.0, 2.0), (3.0, 4.0)]], # One line, two points, has Z [[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]], # One line, three points, no Z [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]], # Two lines, two points each, no Z [[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0), (1.0, 1.0)]], # Two lines, two points each, has Z [[(1.0, 2.0, 0.0), (3.0, 4.0, 1.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]], # Mixed [[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]], ], ) def test_multi_line_string_valid_coordinates(coordinates): """ A list of two coordinates or more should be okay """ multilinestring = MultiLineString(type="MultiLineString", coordinates=coordinates) assert multilinestring.type == "MultiLineString" assert multilinestring.coordinates == coordinates assert hasattr(multilinestring, "__geo_interface__") assert_wkt_equivalence(multilinestring) @pytest.mark.parametrize( "coordinates", [None, [None], ["Foo"], [[]], [[(1.0, 2.0)]], [["Foo", "Bar"]]] ) def test_multi_line_string_invalid_coordinates(coordinates): """ But we don't accept non-list inputs, too few coordinates, or bogus coordinates """ with pytest.raises(ValidationError): MultiLineString(type="MultiLineString", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # Polygon, no Z [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]], # Polygon, has Z [[(0.0, 0.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 0.0)]], ], ) def test_polygon_valid_coordinates(coordinates): """ Should accept lists of linear rings """ polygon = Polygon(type="Polygon", coordinates=coordinates) assert polygon.type == "Polygon" assert polygon.coordinates == coordinates assert hasattr(polygon, "__geo_interface__") if polygon.coordinates: assert polygon.exterior == coordinates[0] else: assert polygon.exterior is None assert not list(polygon.interiors) assert_wkt_equivalence(polygon) @pytest.mark.parametrize( "coordinates", [ # Polygon with holes, no Z [ [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)], [(2.0, 2.0), (2.0, 4.0), (4.0, 4.0), (4.0, 2.0), (2.0, 2.0)], ], # Polygon with holes, has Z [ [ (0.0, 0.0, 0.0), (0.0, 10.0, 0.0), (10.0, 10.0, 0.0), (10.0, 0.0, 0.0), (0.0, 0.0, 0.0), ], [ (2.0, 2.0, 1.0), (2.0, 4.0, 1.0), (4.0, 4.0, 1.0), (4.0, 2.0, 1.0), (2.0, 2.0, 1.0), ], ], # Mixed [ [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)], [ (2.0, 2.0, 2.0), (2.0, 4.0, 0.0), (4.0, 4.0, 0.0), (4.0, 2.0, 0.0), (2.0, 2.0, 2.0), ], ], ], ) def test_polygon_with_holes(coordinates): """Check interior and exterior rings.""" polygon = Polygon(type="Polygon", coordinates=coordinates) assert polygon.type == "Polygon" assert hasattr(polygon, "__geo_interface__") assert polygon.exterior == polygon.coordinates[0] assert list(polygon.interiors) == [polygon.coordinates[1]] assert_wkt_equivalence(polygon) @pytest.mark.parametrize( "coordinates", [ "foo", None, [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)], "foo", None], [[(1.0, 2.0), (3.0, 4.0), (1.0, 2.0)]], [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (7.0, 8.0)]], ], ) def test_polygon_invalid_coordinates(coordinates): """ Should not accept when: - Coordinates is not a list - Not all elements in coordinates are lists - If not all elements have four or more coordinates - If not all elements are linear rings """ with pytest.raises(ValidationError): Polygon(type="Polygon", coordinates=coordinates) @pytest.mark.parametrize( "coordinates", [ # Empty array [], # Multipolygon, no Z [ [ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], [(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 2.2), (2.1, 2.1)], ] ], # Multipolygon, has Z [ [ [ (0.0, 0.0, 4.0), (1.0, 0.0, 4.0), (1.0, 1.0, 4.0), (0.0, 1.0, 4.0), (0.0, 0.0, 4.0), ], [ (2.1, 2.1, 4.0), (2.2, 2.1, 4.0), (2.2, 2.2, 4.0), (2.1, 2.2, 4.0), (2.1, 2.1, 4.0), ], ] ], # Mixed [ [ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], [ (2.1, 2.1, 2.1), (2.2, 2.1, 2.0), (2.2, 2.2, 2.2), (2.1, 2.2, 2.3), (2.1, 2.1, 2.1), ], ] ], ], ) def test_multi_polygon(coordinates): """Should accept sequence of polygons.""" multi_polygon = MultiPolygon(type="MultiPolygon", coordinates=coordinates) assert multi_polygon.type == "MultiPolygon" assert hasattr(multi_polygon, "__geo_interface__") assert_wkt_equivalence(multi_polygon) @pytest.mark.parametrize( "coordinates", [ "foo", None, [ [ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], ], [ [(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 4.2)], ], ], ], ) def test_multipolygon_invalid_coordinates(coordinates): with pytest.raises(ValidationError): MultiPolygon(type="MultiPolygon", coordinates=coordinates) def test_parse_geometry_obj_point(): assert parse_geometry_obj({"type": "Point", "coordinates": [102.0, 0.5]}) == Point( type="Point", coordinates=(102.0, 0.5) ) def test_parse_geometry_obj_multi_point(): assert parse_geometry_obj( {"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]} ) == MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]) def test_parse_geometry_obj_line_string(): assert parse_geometry_obj( { "type": "LineString", "coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]], } ) == LineString( type="LineString", coordinates=[(102.0, 0.0), (103.0, 1.0), (104.0, 0.0), (105.0, 1.0)], ) def test_parse_geometry_obj_multi_line_string(): assert parse_geometry_obj( { "type": "MultiLineString", "coordinates": [[[100.0, 0.0], [101.0, 1.0]], [[102.0, 2.0], [103.0, 3.0]]], } ) == MultiLineString( type="MultiLineString", coordinates=[[(100.0, 0.0), (101.0, 1.0)], [(102.0, 2.0), (103.0, 3.0)]], ) def test_parse_geometry_obj_polygon(): assert parse_geometry_obj( { "type": "Polygon", "coordinates": [ [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]] ], } ) == Polygon( type="Polygon", coordinates=[ [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)] ], ) def test_parse_geometry_obj_multi_polygon(): assert parse_geometry_obj( { "type": "MultiPolygon", "coordinates": [ [ [ [102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0], ] ], [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0], ], [ [100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2], ], ], ], } ) == MultiPolygon( type="MultiPolygon", coordinates=[ [[(102.0, 2.0), (103.0, 2.0), (103.0, 3.0), (102.0, 3.0), (102.0, 2.0)]], [ [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)], [(100.2, 0.2), (100.8, 0.2), (100.8, 0.8), (100.2, 0.8), (100.2, 0.2)], ], ], ) def test_parse_geometry_obj_invalid_type(): with pytest.raises(ValidationError): parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"}) with pytest.raises(ValidationError): parse_geometry_obj({"type": "", "obviously": "doesn't exist"}) with pytest.raises(ValidationError): parse_geometry_obj({}) def test_parse_geometry_obj_invalid_point(): """ litmus test that invalid geometries don't get parsed """ with pytest.raises(ValidationError): parse_geometry_obj( {"type": "Point", "coordinates": ["not", "valid", "coordinates"]} ) @pytest.mark.parametrize( "coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]] ) def test_geometry_collection_iteration(coordinates): """test if geometry collection is iterable""" polygon = Polygon(type="Polygon", coordinates=coordinates) multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates]) gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) assert hasattr(gc, "__geo_interface__") assert_wkt_equivalence(gc) iter(gc) @pytest.mark.parametrize( "coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]] ) def test_len_geometry_collection(coordinates): """test if GeometryCollection return self leng""" polygon = Polygon(type="Polygon", coordinates=coordinates) multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates]) gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) assert_wkt_equivalence(gc) assert len(gc) == 2 @pytest.mark.parametrize( "coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]] ) def test_getitem_geometry_collection(coordinates): """test if GeometryCollection is subscriptable""" polygon = Polygon(type="Polygon", coordinates=coordinates) multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates]) gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) assert_wkt_equivalence(gc) assert polygon == gc[0] assert multipolygon == gc[1] def test_wkt_mixed_geometry_collection(): point = Point(type="Point", coordinates=(0.0, 0.0, 0.0)) line_string = LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]) gc = GeometryCollection(type="GeometryCollection", geometries=[point, line_string]) assert_wkt_equivalence(gc) def test_wkt_empty_geometry_collection(): gc = GeometryCollection(type="GeometryCollection", geometries=[]) assert_wkt_equivalence(gc) def test_polygon_from_bounds(): """Result from `from_bounds` class method should be the same.""" coordinates = [[(1.0, 2.0), (3.0, 2.0), (3.0, 4.0), (1.0, 4.0), (1.0, 2.0)]] assert Polygon(type="Polygon", coordinates=coordinates) == Polygon.from_bounds( 1.0, 2.0, 3.0, 4.0 ) def test_wkt_name(): """Make sure WKT name is derived from geometry Type.""" class PointType(Point): ... assert ( PointType(type="Point", coordinates=(1.01, 2.01)).wkt == Point(type="Point", coordinates=(1.01, 2.01)).wkt ) @pytest.mark.parametrize( "coordinates,expected", [ ((0, 0), False), ((0, 0, 0), True), ], ) def test_point_has_z(coordinates, expected): assert Point(type="Point", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([(0, 0)], False), ([(0, 0), (1, 1)], False), ([(0, 0), (1, 1, 1)], True), ([(0, 0, 0)], True), ([(0, 0, 0), (1, 1)], True), ], ) def test_multipoint_has_z(coordinates, expected): assert MultiPoint(type="MultiPoint", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([(0, 0), (1, 1)], False), ([(0, 0), (1, 1, 1)], True), ([(0, 0, 0), (1, 1, 1)], True), ([(0, 0, 0), (1, 1)], True), ], ) def test_linestring_has_z(coordinates, expected): assert LineString(type="LineString", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([[(0, 0), (1, 1)]], False), ([[(0, 0), (1, 1)], [(0, 0), (1, 1)]], False), ([[(0, 0), (1, 1)], [(0, 0, 0), (1, 1)]], True), ([[(0, 0), (1, 1)], [(0, 0), (1, 1, 1)]], True), ([[(0, 0), (1, 1, 1)]], True), ([[(0, 0, 0), (1, 1, 1)]], True), ([[(0, 0, 0), (1, 1)]], True), ([[(0, 0, 0), (1, 1, 1)], [(0, 0, 0), (1, 1, 1)]], True), ], ) def test_multilinestring_has_z(coordinates, expected): assert ( MultiLineString(type="MultiLineString", coordinates=coordinates).has_z == expected ) @pytest.mark.parametrize( "coordinates,expected", [ ([[(0, 0), (1, 1), (2, 2), (0, 0)]], False), ([[(0, 0), (1, 1), (2, 2, 2), (0, 0)]], True), ([[(0, 0), (1, 1), (2, 2), (0, 0)], [(0, 0), (1, 1), (2, 2), (0, 0)]], False), ( [[(0, 0), (1, 1), (2, 2), (0, 0)], [(0, 0), (1, 1), (2, 2, 2), (0, 0)]], True, ), ([[(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)]], True), ( [ [(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)], [(0, 0), (1, 1), (2, 2), (0, 0)], ], True, ), ], ) def test_polygon_has_z(coordinates, expected): assert Polygon(type="Polygon", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "coordinates,expected", [ ([[[(0, 0), (1, 1), (2, 2), (0, 0)]]], False), ([[[(0, 0), (1, 1), (2, 2, 2), (0, 0)]]], True), ( [[[(0, 0), (1, 1), (2, 2), (0, 0)]], [[(0, 0), (1, 1), (2, 2), (0, 0)]]], False, ), ( [ [[(0, 0), (1, 1), (2, 2), (0, 0)]], [ [(0, 0), (1, 1), (2, 2), (0, 0)], [(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)], ], ], True, ), ( [[[(0, 0), (1, 1), (2, 2), (0, 0)]], [[(0, 0), (1, 1), (2, 2, 2), (0, 0)]]], True, ), ([[[(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)]]], True), ( [ [[(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 0, 0)]], [[(0, 0), (1, 1), (2, 2), (0, 0)]], ], True, ), ], ) def test_multipolygon_has_z(coordinates, expected): assert MultiPolygon(type="MultiPolygon", coordinates=coordinates).has_z == expected @pytest.mark.parametrize( "shape", [ MultiPoint, MultiLineString, Polygon, MultiPolygon, ], ) def test_wkt_empty(shape): assert shape(type=shape.__name__, coordinates=[]).wkt.endswith(" EMPTY") def test_wkt_empty_geometrycollection(): assert GeometryCollection(type="GeometryCollection", geometries=[]).wkt.endswith( " EMPTY" ) geojson-pydantic-0.6.3/tests/test_package.py000066400000000000000000000005771445034274400211630ustar00rootroot00000000000000import geojson_pydantic def test_import_namespace(): """We have exposed all of the public objects via __all__""" assert sorted(geojson_pydantic.__all__) == [ "Feature", "FeatureCollection", "GeometryCollection", "LineString", "MultiLineString", "MultiPoint", "MultiPolygon", "Point", "Polygon", ]