From 77aed43a99a295f3202063edefcb7ce01e011958 Mon Sep 17 00:00:00 2001 From: gdamms Date: Fri, 24 Jul 2026 12:44:20 +0200 Subject: first commit --- .python-version | 1 + README.md | 29 ++++++++++++++++ pyproject.toml | 9 +++++ src/geojson2kml.py | 61 ++++++++++++++++++++++++++++++++ src/n2000.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/rn.py | 81 +++++++++++++++++++++++++++++++++++++++++++ src/split_kml.py | 73 ++++++++++++++++++++++++++++++++++++++ src/test_export.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++ uv.lock | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 541 insertions(+) create mode 100644 .python-version create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/geojson2kml.py create mode 100644 src/n2000.py create mode 100644 src/rn.py create mode 100644 src/split_kml.py create mode 100644 src/test_export.py create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3e099b --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Dog Friendly Data +This repository contains scripts I used to scrape and process data related to dog-friendly locations. + +## Data + +I collected the data to create a map of dog-unfriendly locations. The collected data are acutally meant to identify possible dog-prohibited areas. Only areas that are known to me to be dog-restricted are then moved to the concerned category. I always add a justification for each area, and I try to provide a link to the official source of information. If you find any errors or have additional information, please feel free contact me. + + + +Yellow : Leached only
+Red : Prohibited / Not allowed
+Blue : Protected area (check rules)
+Unmarked : No specific regional restrictions found (or data is incomplete)
+Dog-friendly areas are intentionally not marked, as regulations may change over time. Always verify local rules before visiting with a dog. + +And here is the Google My Maps raw link to the map: +https://www.google.com/maps/d/edit?mid=1CxFX_yBhNfWjP3vAlOlEF-kZTWBncik&usp=sharing + + +## Setup + +```sh +uv init +uv install +``` + +## Scripts + +`test_export.py`: Tests the export of data to KML format for Google Maps. I used this to make sure I didn't make any mistakes while importing multiple data sources. Example: `uv run src/test_export.py exported_google_layer.kml 'source_files_*.kml'` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aac7e12 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "dog-friendly-data" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "requests>=2.34.2", +] diff --git a/src/geojson2kml.py b/src/geojson2kml.py new file mode 100644 index 0000000..b923d98 --- /dev/null +++ b/src/geojson2kml.py @@ -0,0 +1,61 @@ +import json +import xml.etree.ElementTree as ET + + +KML_NS = "http://www.opengis.net/kml/2.2" +ET.register_namespace("", KML_NS) + + +def geojson_to_kml(geojson: dict) -> ET.ElementTree[ET.Element]: + root = ET.Element(f"kml", xmlns=KML_NS) + document = ET.SubElement(root, f"Document") + + ET.SubElement(document, f"name").text = geojson.get("name", "Unnamed") + + for feature in geojson.get("features", []): + placemark = ET.SubElement(document, f"Placemark") + + ET.SubElement(placemark, f"name").text = "" + ET.SubElement(placemark, f"description").text = "" + + # TODO: Handle other geometry types if needed + if feature["geometry"]["type"] != "MultiPolygon": + print(f"Skipping feature with unsupported geometry type: {feature['geometry']['type']}") + continue + + for polygon in feature["geometry"]["coordinates"][0]: + polygon_el = ET.SubElement(placemark, f"Polygon") + + outer = ET.SubElement(polygon_el, f"outerBoundaryIs") + ring = ET.SubElement(outer, f"LinearRing") + + coords = ET.SubElement(ring, f"coordinates") + coords.text = "\n".join( + f"{lon},{lat}" + for lon, lat in polygon + ) + + ET.indent(root) + + return ET.ElementTree(root) + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description="Convert GeoJSON to KML") + parser.add_argument("input_file", help="Path to the input GeoJSON file") + parser.add_argument("-o", "--output_file", help="Path to the output KML file") + args = parser.parse_args() + + with open(args.input_file, "r") as geojson_file: + geojson = json.load(geojson_file) + + output_file = args.output_file or args.input_file.rsplit(".", 1)[0] + ".kml" + + kml = geojson_to_kml(geojson) + kml.write(output_file, encoding="utf-8", xml_declaration=True) + + +if __name__ == "__main__": + main() diff --git a/src/n2000.py b/src/n2000.py new file mode 100644 index 0000000..357f744 --- /dev/null +++ b/src/n2000.py @@ -0,0 +1,96 @@ +import re +import requests + + +reponse = requests.get("https://www.natura2000.fr/carte-natura2000") +regex = r"\"entity_id\":\"(\d+)\"" +matches = re.findall(regex, reponse.text) + + +kml_file = open("natura_2000.kml", "w", encoding="utf-8") +kml_file.write(""" + + + Natura 2000 +""") + + +for i, match in enumerate(matches): + url = f"https://www.natura2000.fr/leaflet-ajax-popup/node/{match}/pop_up_n2000_field/und?_wrapper_format=drupal_ajax" + response = requests.get(url) + + regex = r"href=\\u0022\\/site-natura\\/(.*?)\\u0022" + match = re.search(regex, response.text) + + if not match: + print(f"No match found for entity_id {match}.") + continue + + site_url = f"https://www.natura2000.fr/site-natura/{match.group(1)}" + response = requests.get(site_url) + + regex = r"(.*?) \| Natura 2000" + match = re.search(regex, response.text) + if not match: + print(f"No name found for {site_url}.") + continue + name = match.group(1) + + regex = r"\"type\":\"(multipolygon|polygon)\"" + match = re.search(regex, response.text) + if not match: + print(f"No geometry type found for {site_url}.") + continue + geometry_type = match.group(1) + + if geometry_type == "polygon": + regex = r"\"points\":(\[\[.*?\]\])" + elif geometry_type == "multipolygon": + regex = r"\"points\":(\[\[\[.*?\]\]\])" + else: + print(f"Unsupported geometry type {geometry_type} for {site_url}.") + continue + + match = re.search(regex, response.text) + if not match: + print(f"No coordinates found for {site_url}.") + continue + polygons = eval(match.group(1)) + + if geometry_type == "polygon": + # Wrap single polygon in a list to treat it as multipolygon + polygons = [polygons] + + kml_file.write(f""" + + {name} + + {name}
{site_url} +
+ + """) + + for polygon in polygons: + kml_file.write(""" + + + + + """) + for point in polygon[0]: + kml_file.write(f"{point['lon']},{point['lat']},0\n") + kml_file.write(""" + + + + + """) + kml_file.write(f""" + +
+ """) + + print(f"Processed: {name} ({i+1: 5d}/{len(matches)})") + +kml_file.write("""
+
""") diff --git a/src/rn.py b/src/rn.py new file mode 100644 index 0000000..0c6589c --- /dev/null +++ b/src/rn.py @@ -0,0 +1,81 @@ +import requests +import re + + +def get_coordinates_from_url(url): + response = requests.get(url) + regex = r"\[\[-?\d+\.\d+,-?\d+\.\d+\](?:,\[-?\d+\.\d+,-?\d+\.\d+\])+\]" + matche = re.search(regex, response.text) + if not matche: + return None + coordinates = eval(matche.group(0)) + return coordinates + + +response = requests.get("https://reserves-naturelles.org/reserves-naturelles/") +regex = r"
  • " +matches = re.findall(regex, response.text) + +kml_file = open("reserves_naturelles.kml", "w", encoding="utf-8") +kml_file.write(""" + + + RN +""") + + +for i, match in enumerate(matches): + + regex = r"title=\"([^\"]+)\"" + full_name = re.search(regex, match) + if not full_name: + print(f"Full name not found in the match: {match}") + continue + full_name = full_name.group(1) + + regex = r"class=\"rsv_nom\">([^<]+)<" + name = re.search(regex, match) + if not name: + print(f"Name not found in the match: {match}") + continue + name = name.group(1) + + regex = r"href=\"([^\"]+)\"" + url = re.search(regex, match) + if not url: + print(f"URL not found in the match: {match}") + continue + url = url.group(1) + + coordinates = get_coordinates_from_url(url) + if not coordinates: + print(f"Coordinates not found for URL: {url}") + continue + + kml_file.write(f""" + + {name} + + {full_name}
    {url} +
    + + + + +""") + for coordinate in coordinates: + kml_file.write(f" {coordinate[0]},{coordinate[1]},0\n") + kml_file.write(""" + + + + +
    +""") + + print(f"Processed: {name} ({i+1: 3d}/{len(matches)})") + + +kml_file.write(""" +
    +
    """) diff --git a/src/split_kml.py b/src/split_kml.py new file mode 100644 index 0000000..931704b --- /dev/null +++ b/src/split_kml.py @@ -0,0 +1,73 @@ +import copy +import os +import shutil +import xml.etree.ElementTree as ET +import pathlib +import tempfile + + +KML_NS = "http://www.opengis.net/kml/2.2" +ET.register_namespace("", KML_NS) + + +def split_kml(input_file: str, split_size: int = 5) -> list[str]: + input_path = pathlib.Path(input_file) + input_name = input_path.stem + + kml = ET.parse(input_path) + root = kml.getroot() + prefix = f"{{{root.tag.split('}')[0][1:]}}}" + document = root.find(f"{prefix}Document") + if document is None: + raise ValueError("No Document element found in the KML file.") + placemarks = document.findall(f"{prefix}Placemark") + + current_placemark = placemarks.pop(0) + + split_files = [] + + new_root = copy.deepcopy(root) + new_document = new_root.find(f"{prefix}Document") + if new_document is None: + raise ValueError("No Document element found in the new KML file.") + for placemark in new_document.findall(f"{prefix}Placemark"): + new_document.remove(placemark) + new_document.append(copy.deepcopy(current_placemark)) + current_tmp_file = tempfile.NamedTemporaryFile(suffix=".kml") + ET.ElementTree(new_root).write(current_tmp_file.name, encoding="utf-8", xml_declaration=True) + + while placemarks: + current_placemark = placemarks.pop(0) + + new_document.append(copy.deepcopy(current_placemark)) + + new_tmp_file = tempfile.NamedTemporaryFile(suffix=".kml") + ET.ElementTree(new_root).write(new_tmp_file.name, encoding="utf-8", xml_declaration=True) + + if os.path.getsize(new_tmp_file.name) < split_size * 1024 * 1024: + current_tmp_file = new_tmp_file + else: + n = len(split_files) + 1 + output_path = input_path.parent / f"{input_name}_{n}.kml" + shutil.copy(current_tmp_file.name, output_path) + split_files.append(str(output_path)) + + new_root = copy.deepcopy(root) + new_document = new_root.find(f"{prefix}Document") + if new_document is None: + raise ValueError("No Document element found in the new KML file.") + for placemark in new_document.findall(f"{prefix}Placemark"): + new_document.remove(placemark) + new_document.append(copy.deepcopy(current_placemark)) + + current_tmp_file = tempfile.NamedTemporaryFile(suffix=".kml") + ET.ElementTree(new_root).write(current_tmp_file.name, encoding="utf-8", xml_declaration=True) + + n = len(split_files) + 1 + output_path = input_path.parent / f"{input_name}_{n}.kml" + shutil.copy(current_tmp_file.name, output_path) + split_files.append(str(output_path)) + + return split_files + + diff --git a/src/test_export.py b/src/test_export.py new file mode 100644 index 0000000..8f5ff38 --- /dev/null +++ b/src/test_export.py @@ -0,0 +1,100 @@ +import re +import pathlib + + +def check_duplicate_names(kml_file: str, quiet: bool = False) -> dict: + """Check for duplicate names in the exported KML file. + + Args: + kml_file (str): Path to the KML file exported from Google My Maps. + quiet (bool, optional): If True, suppress output. Defaults to False. + Returns: + dict: A dictionary with names as keys and their counts as values. + """ + if not quiet: + print("\nChecking for duplicate names in the exported KML file...") + + with open(kml_file, "r", encoding="utf-8") as f: + content = f.read() + + regex = r"()?" + matches = re.findall(regex, content) + + names = {} + for match in matches: + name = match[1] + if name in names: + names[name] += 1 + else: + names[name] = 1 + + if not quiet: + if any(count > 1 for count in names.values()): + print("Duplicate names found in the exported KML file:") + for name, count in names.items(): + if count > 1: + print(f" - {name}: {count} times") + else: + print("No duplicate names found in the exported KML file.") + + return names + + +def check_missing_names(kml_file: str, kml_glob: str, quiet: bool = False) -> dict: + """Check for missing names in the exported KML file compared to other KML files. + + Args: + kml_file (str): Path to the KML file exported from Google My Maps. + kml_glob (str): Glob pattern to match KML files to check against the exported KML file. + quiet (bool, optional): If True, suppress output. Defaults to False. + Returns: + { 'file_name': { 'missing': [list of missing names], 'total': total number of names in the file } } + """ + if not quiet: + print("\nChecking for missing names in the exported KML file compared to other KML files...") + + names = check_duplicate_names(kml_file, quiet=True) + + missing_files = {} + + for file in pathlib.Path().glob(kml_glob): + with open(file, "r", encoding="utf-8") as f: + content = f.read() + + regex = r"()?" + matches = re.findall(regex, content) + + current_names = set(match[1] for match in matches) + + for name in current_names: + if not name in names: + if file.name not in missing_files: + missing_files[file.name] = {'missing': [], 'total': len( + current_names)-1} # because of the name of the document + missing_files[file.name]['missing'].append(name) + + if not quiet: + if not missing_files: + print("All names in the KML files matching the glob pattern were found in the exported KML file.") + else: + print("Files with names not found in the exported KML file:") + for file, count in missing_files.items(): + print(f" - {file}: missing {len(count['missing'])}/{count['total']}") + for name in count['missing']: + print(f" - {name}") + + return missing_files + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Check for duplicate or missing names in the KML file exported from Google My Maps.") + parser.add_argument("kml_file", type=str, help="Path to the KML file exported from Google My Maps.") + parser.add_argument("kml_glob", type=str, + help="Glob pattern to match KML files to check against the exported KML file.") + args = parser.parse_args() + + check_duplicate_names(args.kml_file) + check_missing_names(args.kml_file, args.kml_glob) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fc8583a --- /dev/null +++ b/uv.lock @@ -0,0 +1,91 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "dog-friendly-data" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "requests" }, +] + +[package.metadata] +requires-dist = [{ name = "requests", specifier = ">=2.34.2" }] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] -- cgit v1.3.1