diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index fa04e128..4e47acd1 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -70,7 +70,7 @@ jobs: pip install -e ".[tests, lxml, docs]" - name: test the pythoncode in the documentation run: | - python -m doctest docs/*.rst -v + python -m doctest docs/*.rst - name: Run the pythoncode in the examples run: | python examples/read_kml.py diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 00000000..c22b0e71 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,20 @@ +name: SonarBuild +on: + push: + branches: + - develop + pull_request: + types: [opened, synchronize, reopened] +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2fcf01b..ccb8bba9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,11 @@ --- +default_language_version: + python: python3.12 repos: + - repo: https://github.com/pre-commit-ci/pre-commit-ci-config + rev: v1.6.1 + hooks: + - id: check-pre-commit-ci-config - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: @@ -36,18 +42,11 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports - - repo: https://github.com/psf/black - rev: 24.10.0 - hooks: - - id: black - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.7.2' hooks: - id: ruff + - id: ruff-format - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: diff --git a/README.rst b/README.rst index d96255f5..b52bcde9 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Fastkml is continually tested :target: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml :alt: Test -.. |cov| image:: http://codecov.io/github/cleder/fastkml/coverage.svg?branch=main +.. |cov| image:: https://codecov.io/gh/cleder/fastkml/branch/main/graph/badge.svg?token=VIuhPHq0ow :target: http://codecov.io/github/cleder/fastkml?branch=main :alt: codecov.io @@ -108,7 +108,9 @@ Requirements Optional --------- -* lxml_:: +* lxml_: + +.. code-block:: bash pip install --pre "fastkml[lxml]" diff --git a/docs/conf.py b/docs/conf.py index 63233c5c..53caa139 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,6 +176,14 @@ # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True +html_context = { + "display_github": True, # Integrate GitHub + "github_user": "cleder", # Username + "github_repo": "fastkml", # Repo name + "github_version": "main", # Version + "conf_py_path": "/docs/", # Path in the checkout to the docs root +} + # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True diff --git a/docs/configuration.rst b/docs/configuration.rst index a0dbf585..a006a548 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -11,7 +11,9 @@ as its parser, but you can change this by setting the implementation. E.g. if you have lxml installed, but you want to use the -standard ``xml.etree.ElementTree``, you can do this:: +standard ``xml.etree.ElementTree``, you can do this: + +.. code-block:: pycon >>> import fastkml.config >>> import xml.etree.ElementTree diff --git a/docs/create_kml_files.rst b/docs/create_kml_files.rst index 5fff34db..0b6938a6 100644 --- a/docs/create_kml_files.rst +++ b/docs/create_kml_files.rst @@ -15,43 +15,43 @@ Our World in Data, and the Small scale data (1:110m) shapefile from First we import the necessary modules: -.. code-block:: python - - import csv - import pathlib - import random - - import shapefile - from pygeoif.factories import force_3d - from pygeoif.factories import shape - - import fastkml - import fastkml.containers - import fastkml.features - import fastkml.styles - from fastkml.enums import AltitudeMode - from fastkml.geometry import create_kml_geometry +.. code-block:: pycon + + >>> import csv + >>> import pathlib + >>> import random + >>> import shapefile + >>> from pygeoif.factories import force_3d + >>> from pygeoif.factories import shape + >>> import fastkml + >>> import fastkml.containers + >>> import fastkml.features + >>> import fastkml.styles + >>> from fastkml.enums import AltitudeMode + >>> from fastkml.geometry import create_kml_geometry Read the shapefile: -.. code-block:: python +.. code-block:: pycon - shp = shapefile.Reader("ne_110m_admin_0_countries.shp") + >>> shp = shapefile.Reader("examples/ne_110m_admin_0_countries.shp") Read the CSV file and store the CO2 data for 2020: -.. code-block:: python +.. code-block:: pycon - co2_csv = pathlib.Path("owid-co2-data.csv") - co2_data = {} - with co2_csv.open() as csvfile: - reader = csv.DictReader(csvfile) - for row in reader: - if row["year"] == "2020": - co2_data[row["iso_code"]] = ( - float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 - ) + >>> shp = shapefile.Reader("examples/ne_110m_admin_0_countries.shp") + >>> co2_csv = pathlib.Path("examples/owid-co2-data.csv") + >>> co2_data = {} + >>> with co2_csv.open() as csvfile: + ... reader = csv.DictReader(csvfile) + ... for row in reader: + ... if row["year"] == "2020": + ... co2_data[row["iso_code"]] = ( + ... float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 + ... ) + ... We prepare the styles and placemarks for the KML file, using random colors for each @@ -59,50 +59,52 @@ country and the CO2 emissions as the height of the geometry. The shapefile offer a handy ``__geo_interface__`` attribute that we can use to iterate over the features, just like we would with a ``GeoJSON`` object, and extract the necessary information: -.. code-block:: python - - placemarks = [] - for feature in shp.__geo_interface__["features"]: - iso3_code = feature["properties"]["ADM0_A3"] - geometry = shape(feature["geometry"]) - co2_emission = co2_data.get(iso3_code, 0) - geometry = force_3d(geometry, co2_emission * 100_000) - kml_geometry = create_kml_geometry( - geometry, - extrude=True, - altitude_mode=AltitudeMode.relative_to_ground, - ) - color = random.randint(0, 0xFFFFFF) - style = fastkml.styles.Style( - id=iso3_code, - styles=[ - fastkml.styles.LineStyle(color=f"33{color:06X}", width=2), - fastkml.styles.PolyStyle( - color=f"88{color:06X}", - fill=True, - outline=True, - ), - ], - ) - placemark = fastkml.features.Placemark( - name=feature["properties"]["NAME"], - description=feature["properties"]["FORMAL_EN"], - kml_geometry=kml_geometry, - styles=[style], - ) - placemarks.append(placemark) +.. code-block:: pycon + + >>> placemarks = [] + >>> for feature in shp.__geo_interface__["features"]: + ... iso3_code = feature["properties"]["ADM0_A3"] + ... geometry = shape(feature["geometry"]) + ... co2_emission = co2_data.get(iso3_code, 0) + ... geometry = force_3d(geometry, co2_emission * 100_000) + ... kml_geometry = create_kml_geometry( + ... geometry, + ... extrude=True, + ... altitude_mode=AltitudeMode.relative_to_ground, + ... ) + ... color = random.randint(0, 0xFFFFFF) + ... style = fastkml.styles.Style( + ... id=iso3_code, + ... styles=[ + ... fastkml.styles.LineStyle(color=f"33{color:06X}", width=2), + ... fastkml.styles.PolyStyle( + ... color=f"88{color:06X}", + ... fill=True, + ... outline=True, + ... ), + ... ], + ... ) + ... placemark = fastkml.features.Placemark( + ... name=feature["properties"]["NAME"], + ... description=feature["properties"]["FORMAL_EN"], + ... kml_geometry=kml_geometry, + ... styles=[style], + ... ) + ... placemarks.append(placemark) + ... Finally, we create the KML object and write it to a file: -.. code-block:: python - - document = fastkml.containers.Document(features=placemarks, styles=styles) - kml = fastkml.KML(features=[document]) +.. code-block:: pycon - outfile = pathlib.Path("co2_per_capita_2020.kml") - with outfile.open("w") as f: - f.write(kml.to_string(prettyprint=True, precision=6)) + >>> document = fastkml.containers.Document(features=placemarks) + >>> kml = fastkml.KML(features=[document]) + >>> outfile = pathlib.Path("co2_per_capita_2020.kml") + >>> with outfile.open("w") as f: + ... f.write(kml.to_string(prettyprint=True, precision=3)) # doctest: +ELLIPSIS + ... + 4... The resulting KML file can be opened in Google Earth or any other KML viewer. @@ -122,38 +124,37 @@ create a KML file that shows the CO2 emissions accumulating from 1995 to 2022. First we import the necessary modules: -.. code-block:: python - - import csv - import pathlib - import random - import datetime - import shapefile - from pygeoif.factories import force_3d - from pygeoif.factories import shape - - import fastkml - import fastkml.containers - import fastkml.features - import fastkml.styles - import fastkml.times - from fastkml.enums import AltitudeMode, DateTimeResolution - from fastkml.geometry import create_kml_geometry +.. code-block:: pycon + + >>> import csv + >>> import pathlib + >>> import random + >>> import datetime + >>> import shapefile + >>> from pygeoif.factories import force_3d + >>> from pygeoif.factories import shape + >>> import fastkml + >>> import fastkml.containers + >>> import fastkml.features + >>> import fastkml.styles + >>> import fastkml.times + >>> from fastkml.enums import AltitudeMode, DateTimeResolution + >>> from fastkml.geometry import create_kml_geometry Read the shapefile, the CSV file and store the CO2 data for each year: -.. code-block:: python +.. code-block:: pycon - shp = shapefile.Reader("ne_110m_admin_0_countries.shp") - co2_csv = pathlib.Path("owid-co2-data.csv") - co2_pa = {str(i): {} for i in range(1995, 2023)} - with co2_csv.open() as csvfile: - reader = csv.DictReader(csvfile) - for row in reader: - if row["year"] >= "1995": - co2_pa[row["year"]][row["iso_code"]] = ( - float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 - ) + >>> co2_csv = pathlib.Path("examples/owid-co2-data.csv") + >>> co2_pa = {str(i): {} for i in range(1995, 2023)} + >>> with co2_csv.open() as csvfile: + ... reader = csv.DictReader(csvfile) + ... for row in reader: + ... if row["year"] >= "1995": + ... co2_pa[row["year"]][row["iso_code"]] = ( + ... float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 + ... ) + ... @@ -163,67 +164,67 @@ We will also create a style for each country, which we store at the document lev prevent creating duplicate styles. Each placemark will have a time-span that covers the whole year: -.. code-block:: python - - styles = [] - folders = [] - for feature in shp.__geo_interface__["features"]: - iso3_code = feature["properties"]["ADM0_A3"] - geometry = shape(feature["geometry"]) - color = random.randint(0, 0xFFFFFF) - styles.append( - fastkml.styles.Style( - id=iso3_code, - styles=[ - fastkml.styles.LineStyle(color=f"33{color:06X}", width=2), - fastkml.styles.PolyStyle( - color=f"88{color:06X}", - fill=True, - outline=True, - ), - ], - ), - ) - style_url = fastkml.styles.StyleUrl(url=f"#{iso3_code}") - folder = fastkml.containers.Folder(name=feature["properties"]["NAME"]) - co2_growth = 0 - for year in range(1995, 2023): - co2_year = co2_pa[str(year)].get(iso3_code, 0) - co2_growth += co2_year - - kml_geometry = create_kml_geometry( - force_3d(geometry, co2_growth * 5_000), - extrude=True, - altitude_mode=AltitudeMode.relative_to_ground, - ) - timespan = fastkml.times.TimeSpan( - begin=fastkml.times.KmlDateTime( - datetime.date(year, 1, 1), resolution=DateTimeResolution.year_month - ), - end=fastkml.times.KmlDateTime( - datetime.date(year, 12, 31), resolution=DateTimeResolution.year_month - ), - ) - placemark = fastkml.features.Placemark( - name=f"{feature['properties']['NAME']} - {year}", - description=feature["properties"]["FORMAL_EN"], - kml_geometry=kml_geometry, - style_url=style_url, - times=timespan, - ) - folder.features.append(placemark) - folders.append(folder) +.. code-block:: pycon + +>>> styles = [] +>>> folders = [] +>>> for feature in shp.__geo_interface__["features"]: +... iso3_code = feature["properties"]["ADM0_A3"] +... geometry = shape(feature["geometry"]) +... color = random.randint(0, 0xFFFFFF) +... styles.append( +... fastkml.styles.Style( +... id=iso3_code, +... styles=[ +... fastkml.styles.LineStyle(color=f"33{color:06X}", width=2), +... fastkml.styles.PolyStyle( +... color=f"88{color:06X}", +... fill=True, +... outline=True, +... ), +... ], +... ), +... ) +... style_url = fastkml.styles.StyleUrl(url=f"#{iso3_code}") +... folder = fastkml.containers.Folder(name=feature["properties"]["NAME"]) +... co2_growth = 0 +... for year in range(1995, 2023): +... co2_year = co2_pa[str(year)].get(iso3_code, 0) +... co2_growth += co2_year +... kml_geometry = create_kml_geometry( +... force_3d(geometry, co2_growth * 5_000), +... extrude=True, +... altitude_mode=AltitudeMode.relative_to_ground, +... ) +... timespan = fastkml.times.TimeSpan( +... begin=fastkml.times.KmlDateTime( +... datetime.date(year, 1, 1), resolution=DateTimeResolution.year_month +... ), +... end=fastkml.times.KmlDateTime( +... datetime.date(year, 12, 31), resolution=DateTimeResolution.year_month +... ), +... ) +... placemark = fastkml.features.Placemark( +... name=f"{feature['properties']['NAME']} - {year}", +... description=feature["properties"]["FORMAL_EN"], +... kml_geometry=kml_geometry, +... style_url=style_url, +... times=timespan, +... ) +... folder.features.append(placemark) +... folders.append(folder) Finally, we create the KML object and write it to a file: -.. code-block:: python - - document = fastkml.containers.Document(features=folders, styles=styles) - kml = fastkml.KML(features=[document]) +.. code-block:: pycon - outfile = pathlib.Path("co2_growth_1995_2022.kml") - with outfile.open("w") as f: - f.write(kml.to_string(prettyprint=True, precision=3)) + >>> document = fastkml.containers.Document(features=folders, styles=styles) + >>> kml = fastkml.KML(features=[document]) + >>> outfile = pathlib.Path("co2_growth_1995_2022.kml") + >>> with outfile.open("w") as f: + ... f.write(kml.to_string(prettyprint=True, precision=3)) # doctest: +ELLIPSIS + ... + 1... You can open the resulting KML file in Google Earth Desktop and use the time slider to diff --git a/docs/fastkml.rst b/docs/fastkml.rst index 67c325ed..e752b965 100644 --- a/docs/fastkml.rst +++ b/docs/fastkml.rst @@ -2,15 +2,10 @@ Reference Guide =============== -.. toctree:: - :maxdepth: 4 - - - .. automodule:: fastkml -fastkml.about module +fastkml.about -------------------- .. automodule:: fastkml.about @@ -18,7 +13,7 @@ fastkml.about module :undoc-members: :show-inheritance: -fastkml.atom module +fastkml.atom ------------------- .. automodule:: fastkml.atom @@ -26,7 +21,7 @@ fastkml.atom module :undoc-members: :show-inheritance: -fastkml.base module +fastkml.base ------------------- .. automodule:: fastkml.base @@ -34,7 +29,7 @@ fastkml.base module :undoc-members: :show-inheritance: -fastkml.config module +fastkml.config --------------------- .. automodule:: fastkml.config @@ -42,7 +37,7 @@ fastkml.config module :undoc-members: :show-inheritance: -fastkml.containers module +fastkml.containers ------------------------- .. automodule:: fastkml.containers @@ -50,7 +45,7 @@ fastkml.containers module :undoc-members: :show-inheritance: -fastkml.data module +fastkml.data ------------------- .. automodule:: fastkml.data @@ -58,7 +53,7 @@ fastkml.data module :undoc-members: :show-inheritance: -fastkml.enums module +fastkml.enums -------------------- .. automodule:: fastkml.enums @@ -66,7 +61,7 @@ fastkml.enums module :undoc-members: :show-inheritance: -fastkml.exceptions module +fastkml.exceptions ------------------------- .. automodule:: fastkml.exceptions @@ -74,7 +69,7 @@ fastkml.exceptions module :undoc-members: :show-inheritance: -fastkml.features module +fastkml.features ----------------------- .. automodule:: fastkml.features @@ -82,7 +77,7 @@ fastkml.features module :undoc-members: :show-inheritance: -fastkml.geometry module +fastkml.geometry ----------------------- .. automodule:: fastkml.geometry @@ -90,7 +85,7 @@ fastkml.geometry module :undoc-members: :show-inheritance: -fastkml.gx module +fastkml.gx ----------------- .. automodule:: fastkml.gx @@ -98,7 +93,7 @@ fastkml.gx module :undoc-members: :show-inheritance: -fastkml.helpers module +fastkml.helpers ---------------------- .. automodule:: fastkml.helpers @@ -106,7 +101,7 @@ fastkml.helpers module :undoc-members: :show-inheritance: -fastkml.kml module +fastkml.kml ------------------ .. automodule:: fastkml.kml @@ -114,7 +109,7 @@ fastkml.kml module :undoc-members: :show-inheritance: -fastkml.kml\_base module +fastkml.kml\_base ------------------------ .. automodule:: fastkml.kml_base @@ -122,7 +117,7 @@ fastkml.kml\_base module :undoc-members: :show-inheritance: -fastkml.links module +fastkml.links -------------------- .. automodule:: fastkml.links @@ -130,7 +125,7 @@ fastkml.links module :undoc-members: :show-inheritance: -fastkml.mixins module +fastkml.mixins --------------------- .. automodule:: fastkml.mixins @@ -138,7 +133,7 @@ fastkml.mixins module :undoc-members: :show-inheritance: -fastkml.overlays module +fastkml.overlays ----------------------- .. automodule:: fastkml.overlays @@ -146,7 +141,7 @@ fastkml.overlays module :undoc-members: :show-inheritance: -fastkml.registry module +fastkml.registry ----------------------- .. automodule:: fastkml.registry @@ -154,7 +149,7 @@ fastkml.registry module :undoc-members: :show-inheritance: -fastkml.styles module +fastkml.styles --------------------- .. automodule:: fastkml.styles @@ -162,7 +157,7 @@ fastkml.styles module :undoc-members: :show-inheritance: -fastkml.times module +fastkml.times -------------------- .. automodule:: fastkml.times @@ -170,7 +165,7 @@ fastkml.times module :undoc-members: :show-inheritance: -fastkml.types module +fastkml.types -------------------- .. automodule:: fastkml.types @@ -178,7 +173,7 @@ fastkml.types module :undoc-members: :show-inheritance: -fastkml.utils module +fastkml.utils -------------------- .. automodule:: fastkml.utils @@ -186,7 +181,16 @@ fastkml.utils module :undoc-members: :show-inheritance: -fastkml.views module +fastkml.validator +-------------------- + +.. automodule:: fastkml.validator + :members: + :undoc-members: + :show-inheritance: + + +fastkml.views -------------------- .. automodule:: fastkml.views diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index d3cafeb8..00000000 --- a/docs/modules.rst +++ /dev/null @@ -1 +0,0 @@ - fastkml diff --git a/docs/working_with_kml.rst b/docs/working_with_kml.rst index c227bb15..172ad5bc 100644 --- a/docs/working_with_kml.rst +++ b/docs/working_with_kml.rst @@ -29,6 +29,12 @@ type and returns an iterator of all matching elements found in the document tree ... POINT Z (-123.93563168 49.16716103 5.0) POLYGON Z ((-123.940449937288 49.16927524669021 17.0, ... + +We could also search for all Points, which will also return the Points inside the +``MultiGeometries``. + +.. code-block:: pycon + >>> pts = list(find_all(k, of_type=Point)) >>> for point in pts: ... print(point.geometry) @@ -75,6 +81,8 @@ For more targeted searches, we can combine multiple search criteria: Point(-123.93563168, 49.16716103, 5.0) >>> pm.style_url.url '#khStyle712' + >>> pm.name + 'HBC Bastion' Extending FastKML @@ -156,6 +164,8 @@ Now we can create a new KML object and confirm that the new element is parsed co To be able to open the KML file in Google Earth Pro, we need to transform the CascadingStyle element into a supported Style element. +To achieve this we copy the styles into the document styles and adjust their id +to match the id of the CascadingStyle. .. code-block:: pycon @@ -165,6 +175,11 @@ CascadingStyle element into a supported Style element. ... kml_style.id = cascading_style.id ... document.styles.append(kml_style) ... + +Now we can remove the CascadingStyle from the document and have a look at the result. + +.. code-block:: pycon + >>> document.gx_cascading_style = [] >>> print(document.to_string(prettyprint=True)) @@ -180,9 +195,6 @@ CascadingStyle element into a supported Style element. - - hide - 1.2 @@ -196,11 +208,11 @@ CascadingStyle element into a supported Style element. 80000000 - - hide + + https://earth.google.com/earth/rpc/cc/icon?color=1976d2&id=2000&scale=4 @@ -213,6 +225,9 @@ CascadingStyle element into a supported Style element. 80000000 + + hide + Ort1 @@ -222,8 +237,8 @@ CascadingStyle element into a supported Style element. 13.96486261382906 0.0 0.0 - absolute 632.584179697442 + absolute #__managed_style_0D301BCC0014827EFCCB diff --git a/fastkml/about.py b/fastkml/about.py index c3030e31..c99d0c99 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -18,4 +18,7 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.0.0b1" + +__version__ = "1.0.0b2" + +__all__ = ["__version__"] diff --git a/fastkml/atom.py b/fastkml/atom.py index fde10d0b..55548144 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -258,9 +258,9 @@ class _Person(_AtomObject): """ - name: str - uri: str - email: str + name: Optional[str] + uri: Optional[str] + email: Optional[str] def __init__( self, @@ -285,9 +285,9 @@ def __init__( """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) - self.name = name.strip() if name else "" - self.uri = uri.strip() if uri else "" - self.email = email.strip() if email else "" + self.name = name.strip() or None if name else None + self.uri = uri.strip() or None if uri else None + self.email = email.strip() or None if email else None def __repr__(self) -> str: """ diff --git a/fastkml/data.py b/fastkml/data.py index 9d02a80b..49c1169f 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -18,6 +18,7 @@ https://developers.google.com/kml/documentation/extendeddata """ + import logging from typing import Any from typing import Dict @@ -26,6 +27,7 @@ from typing import Optional from typing import Union +from fastkml.base import _XMLObject from fastkml.enums import DataType from fastkml.exceptions import KMLSchemaError from fastkml.helpers import attribute_enum_kwarg @@ -53,7 +55,7 @@ logger = logging.getLogger(__name__) -class SimpleField(_BaseObject): +class SimpleField(_XMLObject): """ A SimpleField always has both name and type attributes. @@ -76,18 +78,18 @@ class SimpleField(_BaseObject): HTML markup. """ + _default_nsid = "kml" + name: Optional[str] - type: Optional[DataType] + type_: Optional[DataType] display_name: Optional[str] def __init__( self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, - target_id: Optional[str] = None, name: Optional[str] = None, - type: Optional[DataType] = None, + type_: Optional[DataType] = None, display_name: Optional[str] = None, **kwargs: Any, ) -> None: @@ -99,10 +101,8 @@ def __init__( ns (Optional[str]): The namespace of the data. name_spaces (Optional[Dict[str, str]]): The dictionary of namespace prefixes and URIs. - id (Optional[str]): The ID of the data. - target_id (Optional[str]): The target ID of the data. name (Optional[str]): The name of the data. - type (Optional[DataType]): The type of the data. + type_ (Optional[DataType]): The type of the data. display_name (Optional[str]): The display name of the data. **kwargs (Any): Additional keyword arguments. @@ -114,13 +114,11 @@ def __init__( super().__init__( ns=ns, name_spaces=name_spaces, - id=id, - target_id=target_id, **kwargs, ) - self.name = name - self.type = type - self.display_name = display_name + self.name = name.strip() or None if name else None + self.type_ = type_ or None + self.display_name = display_name.strip() or None if display_name else None def __repr__(self) -> str: """ @@ -135,10 +133,8 @@ def __repr__(self) -> str: f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " f"name={self.name!r}, " - f"type={self.type}, " + f"type_={self.type_}, " f"display_name={self.display_name!r}, " f"**{self._get_splat()!r}," ")" @@ -153,13 +149,13 @@ def __bool__(self) -> bool: bool: True if both the name and type are non-empty, False otherwise. """ - return bool(self.name) and bool(self.type) + return bool(self.name) and bool(self.type_) registry.register( SimpleField, RegistryItem( - ns_ids=("kml",), + ns_ids=("kml", ""), attr_name="display_name", node_name="displayName", classes=(str,), @@ -182,7 +178,7 @@ def __bool__(self) -> bool: SimpleField, RegistryItem( ns_ids=("", "kml"), - attr_name="type", + attr_name="type_", node_name="type", classes=(DataType,), get_kwarg=attribute_enum_kwarg, @@ -191,7 +187,7 @@ def __bool__(self) -> bool: ) -class Schema(_BaseObject): +class Schema(_XMLObject): """ Specifies a custom KML schema that is used to add custom data to KML Features. @@ -202,6 +198,8 @@ class Schema(_BaseObject): """ + _default_nsid = "kml" + name: Optional[str] fields: List[SimpleField] @@ -210,7 +208,6 @@ def __init__( ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, - target_id: Optional[str] = None, name: Optional[str] = None, fields: Optional[Iterable[SimpleField]] = None, **kwargs: Any, @@ -241,18 +238,17 @@ def __init__( If the id is not provided. """ - if id is None: + if not id: msg = "Id is required for schema" raise KMLSchemaError(msg) super().__init__( ns=ns, name_spaces=name_spaces, - id=id, - target_id=target_id, **kwargs, ) - self.name = name + self.name = name.strip() or None if name else None self.fields = list(fields) if fields else [] + self.id = id.strip() or None if id else None def __repr__(self) -> str: """ @@ -269,7 +265,6 @@ def __repr__(self) -> str: f"ns={self.ns!r}, " f"name_spaces={self.name_spaces!r}, " f"id={self.id!r}, " - f"target_id={self.target_id!r}, " f"name={self.name!r}, " f"fields={self.fields!r}, " f"**{self._get_splat()!r}," @@ -289,6 +284,17 @@ def append(self, field: SimpleField) -> None: self.fields.append(field) +registry.register( + Schema, + RegistryItem( + ns_ids=("kml", ""), + attr_name="id", + node_name="id", + classes=(str,), + get_kwarg=attribute_text_kwarg, + set_element=text_attribute, + ), +) registry.register( Schema, RegistryItem( @@ -358,9 +364,9 @@ def __init__( target_id=target_id, **kwargs, ) - self.name = name - self.value = value - self.display_name = display_name + self.name = name.strip() or None if name else None + self.value = value.strip() or None if value else None + self.display_name = display_name.strip() or None if display_name else None def __repr__(self) -> str: """ @@ -395,26 +401,26 @@ def __bool__(self) -> bool: True if the Data object has a name and a non-None value, False otherwise. """ - return bool(self.name) and self.value is not None + return bool(self.name) and bool(self.value) registry.register( Data, RegistryItem( - ns_ids=("", "kml"), - attr_name="name", - node_name="name", + ns_ids=("kml", ""), + attr_name="display_name", + node_name="displayName", classes=(str,), - get_kwarg=attribute_text_kwarg, - set_element=text_attribute, + get_kwarg=subelement_text_kwarg, + set_element=text_subelement, ), ) registry.register( Data, RegistryItem( - ns_ids=("kml",), - attr_name="display_name", - node_name="displayName", + ns_ids=("kml", ""), + attr_name="value", + node_name="value", classes=(str,), get_kwarg=subelement_text_kwarg, set_element=text_subelement, @@ -424,16 +430,18 @@ def __bool__(self) -> bool: Data, RegistryItem( ns_ids=("", "kml"), - attr_name="value", - node_name="value", + attr_name="name", + node_name="name", classes=(str,), - get_kwarg=subelement_text_kwarg, - set_element=text_subelement, + get_kwarg=attribute_text_kwarg, + set_element=text_attribute, ), ) -class SimpleData(_BaseObject): +class SimpleData(_XMLObject): + _default_nsid = "kml" + name: Optional[str] value: Optional[str] @@ -441,8 +449,6 @@ def __init__( self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, - target_id: Optional[str] = None, name: Optional[str] = None, value: Optional[str] = None, **kwargs: Any, @@ -455,8 +461,6 @@ def __init__( ns (Optional[str]): The namespace of the object. name_spaces (Optional[Dict[str, str]]): The dictionary of namespace prefixes and URIs. - id (Optional[str]): The ID of the object. - target_id (Optional[str]): The target ID of the object. name (Optional[str]): The name of the object. value (Optional[str]): The value of the object. **kwargs: Additional keyword arguments. @@ -465,12 +469,10 @@ def __init__( super().__init__( ns=ns, name_spaces=name_spaces, - id=id, - target_id=target_id, **kwargs, ) - self.name = name - self.value = value + self.name = name.strip() or None if name else None + self.value = value.strip() or None if value else None def __repr__(self) -> str: """ @@ -485,8 +487,6 @@ def __repr__(self) -> str: f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " f"name={self.name!r}, " f"value={self.value!r}, " f"**{self._get_splat()!r}," @@ -502,13 +502,13 @@ def __bool__(self) -> bool: bool: True if the name and the value attribute are set, False otherwise. """ - return bool(self.name) and self.value is not None + return bool(self.name) and bool(self.value) registry.register( SimpleData, RegistryItem( - ns_ids=("kml",), + ns_ids=("kml", ""), attr_name="value", node_name="value", classes=(str,), @@ -579,7 +579,7 @@ def __init__( target_id=target_id, **kwargs, ) - self.schema_url = schema_url + self.schema_url = schema_url.strip() or None if schema_url else None self.data = list(data) if data else [] def __repr__(self) -> str: @@ -644,17 +644,16 @@ def append_data(self, data: SimpleData) -> None: ) -class ExtendedData(_BaseObject): +class ExtendedData(_XMLObject): """Represents a list of untyped name/value pairs.""" + _default_nsid = "kml" elements: List[Union[Data, SchemaData]] def __init__( self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, - target_id: Optional[str] = None, elements: Optional[Iterable[Union[Data, SchemaData]]] = None, **kwargs: Any, ) -> None: @@ -666,8 +665,6 @@ def __init__( ns (Optional[str]): The namespace for the data. name_spaces (Optional[Dict[str, str]]): The dictionary of namespace prefixes and URIs. - id (Optional[str]): The ID of the data. - target_id (Optional[str]): The target ID of the data. elements (Optional[Iterable[Union[Data, SchemaData]]]): The iterable of data elements. **kwargs (Any): Additional keyword arguments. @@ -680,11 +677,9 @@ def __init__( super().__init__( ns=ns, name_spaces=name_spaces, - id=id, - target_id=target_id, **kwargs, ) - self.elements = list(elements) if elements else [] + self.elements = [e for e in elements if e] if elements else [] def __repr__(self) -> str: """ @@ -699,8 +694,6 @@ def __repr__(self) -> str: f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " f"elements={self.elements!r}, " f"**{self._get_splat()!r}," ")" diff --git a/fastkml/features.py b/fastkml/features.py index 08e6ed24..1a204ad8 100644 --- a/fastkml/features.py +++ b/fastkml/features.py @@ -31,6 +31,7 @@ from pygeoif.types import GeoType from fastkml import atom +from fastkml import config from fastkml import gx from fastkml.base import _XMLObject from fastkml.data import ExtendedData @@ -97,6 +98,8 @@ class Snippet(_XMLObject): maximum number of lines to display. """ + _default_nsid = config.KML + text: Optional[str] max_lines: Optional[int] = None @@ -109,7 +112,7 @@ def __init__( **kwargs: Any, ) -> None: """ - Initialize a Feature object. + Initialize a Snippet object. Args: ---- @@ -130,7 +133,7 @@ def __init__( """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) - self.text = text + self.text = text.strip() or None if text else None self.max_lines = max_lines def __repr__(self) -> str: @@ -168,9 +171,9 @@ def __bool__(self) -> bool: registry.register( Snippet, RegistryItem( - ns_ids=("kml",), + ns_ids=("kml", ""), attr_name="text", - node_name="", + node_name="Snippet", classes=(str,), get_kwarg=node_text_kwarg, set_element=node_text, @@ -284,7 +287,7 @@ def __init__( **kwargs, ) self.name = name - self.description = description + self.description = description.strip() or None if description else None self.style_url = style_url self.styles = list(styles) if styles else [] self.view = view diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 51d8791e..c2104a6d 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -645,8 +645,9 @@ def subelement_text_kwarg( """ node = element.find(f"{ns}{node_name}") - if node is None: + if node is None or node.text is None: return {} + assert isinstance(node.text, str) # noqa: S101 return {kwarg: node.text.strip()} if node.text and node.text.strip() else {} diff --git a/fastkml/links.py b/fastkml/links.py index 62bd4bc8..32222b14 100644 --- a/fastkml/links.py +++ b/fastkml/links.py @@ -14,6 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Link and Icon elements.""" + from typing import Any from typing import Dict from typing import Optional @@ -77,7 +78,7 @@ def __init__( target_id=target_id, **kwargs, ) - self.href = href or "" + self.href = href.strip() if href else "" self.refresh_mode = refresh_mode self.refresh_interval = refresh_interval self.view_refresh_mode = view_refresh_mode diff --git a/fastkml/styles.py b/fastkml/styles.py index c70de2c7..763f0888 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -62,7 +62,7 @@ logger = logging.getLogger(__name__) -class StyleUrl(_BaseObject): +class StyleUrl(_XMLObject): """ URL of a