aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgdamms <damguillotin@gmail.com>2026-07-24 12:44:20 +0200
committergdamms <damguillotin@gmail.com>2026-07-24 12:44:20 +0200
commit77aed43a99a295f3202063edefcb7ce01e011958 (patch)
treee34a042a75cdb52e2d30afc11dc82660c22f36d5
downloaddog-friendly-data-77aed43a99a295f3202063edefcb7ce01e011958.tar.gz
dog-friendly-data-77aed43a99a295f3202063edefcb7ce01e011958.zip
first commit
-rw-r--r--.python-version1
-rw-r--r--README.md29
-rw-r--r--pyproject.toml9
-rw-r--r--src/geojson2kml.py61
-rw-r--r--src/n2000.py96
-rw-r--r--src/rn.py81
-rw-r--r--src/split_kml.py73
-rw-r--r--src/test_export.py100
-rw-r--r--uv.lock91
9 files changed, 541 insertions, 0 deletions
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.
+
+<iframe src="https://www.google.com/maps/d/u/0/embed?mid=1CxFX_yBhNfWjP3vAlOlEF-kZTWBncik&ehbc=2E312F&noprof=1" width="640" height="480"></iframe>
+
+Yellow : Leached only<br/>
+Red : Prohibited / Not allowed<br/>
+Blue : Protected area (check rules)<br/>
+Unmarked : No specific regional restrictions found (or data is incomplete)<br/>
+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("""<?xml version="1.0" encoding="UTF-8"?>
+<kml xmlns="http://www.opengis.net/kml/2.2">
+ <Document>
+ <name>Natura 2000</name>
+""")
+
+
+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"<title>(.*?) \| Natura 2000</title>"
+ 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"""
+ <Placemark>
+ <name>{name}</name>
+ <description>
+ {name}<br/>{site_url}
+ </description>
+ <MultiGeometry>
+ """)
+
+ for polygon in polygons:
+ kml_file.write("""
+ <Polygon>
+ <outerBoundaryIs>
+ <LinearRing>
+ <coordinates>
+ """)
+ for point in polygon[0]:
+ kml_file.write(f"{point['lon']},{point['lat']},0\n")
+ kml_file.write("""
+ </coordinates>
+ </LinearRing>
+ </outerBoundaryIs>
+ </Polygon>
+ """)
+ kml_file.write(f"""
+ </MultiGeometry>
+ </Placemark>
+ """)
+
+ print(f"Processed: {name} ({i+1: 5d}/{len(matches)})")
+
+kml_file.write(""" </Document>
+</kml>""")
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"<li class=\"reserve\"[\s\S]*?</li>"
+matches = re.findall(regex, response.text)
+
+kml_file = open("reserves_naturelles.kml", "w", encoding="utf-8")
+kml_file.write("""<?xml version="1.0" encoding="UTF-8"?>
+<kml xmlns="http://www.opengis.net/kml/2.2">
+ <Document>
+ <name>RN</name>
+""")
+
+
+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"""
+ <Placemark>
+ <name>{name}</name>
+ <description>
+ {full_name}<br/>{url}
+ </description>
+ <Polygon>
+ <outerBoundaryIs>
+ <LinearRing>
+ <coordinates>
+""")
+ for coordinate in coordinates:
+ kml_file.write(f" {coordinate[0]},{coordinate[1]},0\n")
+ kml_file.write("""
+ </coordinates>
+ </LinearRing>
+ </outerBoundaryIs>
+ </Polygon>
+ </Placemark>
+""")
+
+ print(f"Processed: {name} ({i+1: 3d}/{len(matches)})")
+
+
+kml_file.write("""
+ </Document>
+</kml>""")
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"<name>(<!\[CDATA\[)?(.*?)(\]\]>)?</name>"
+ 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"<name>(<!\[CDATA\[)?(.*?)(\]\]>)?</name>"
+ 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" },
+]