├── ruff.toml ├── docs ├── authors.rst ├── history.rst ├── readme.rst ├── contributing.rst ├── quickstart.rst ├── index.rst ├── Makefile ├── make.bat ├── requirements.txt ├── usage.rst ├── installation.rst └── conf.py ├── tests ├── __init__.py ├── test_fetchers_wiki.py ├── test_graph_from_source_faker.py ├── test_graph_from_source_osm.py ├── test_fetchers_flights.py └── test_cli.py ├── requirements.txt ├── requirements_dev.txt ├── AUTHORS.rst ├── graphfaker ├── enums.py ├── __init__.py ├── utils.py ├── logger.py ├── fetchers │ ├── wiki.py │ ├── osm.py │ └── flights.py ├── cli.py └── core.py ├── MANIFEST.in ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── python-publish.yml ├── HISTORY.rst ├── tox.ini ├── .readthedocs.yaml ├── LICENSE ├── pyproject.toml ├── .gitignore ├── Proposal.md ├── Makefile ├── examples ├── osm_network.py ├── demo_graphfaker.py ├── example_osm_faker_visualization.py ├── wikipedia_wrapper.py ├── sqarql.py ├── experiment_wikidata.py └── flight_demo.py ├── CODE_OF_CONDUCT.rst ├── CONTRIBUTING.rst ├── README.md ├── README.rst └── uv.lock /ruff.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for graphfaker.""" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphgeeks-lab/graphfaker/HEAD/requirements.txt -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphgeeks-lab/graphfaker/HEAD/requirements_dev.txt -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start Guide 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :caption: Quick Start Guide 7 | 8 | notebooks/osm_quickstart 9 | 10 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Dennis Irorere 9 | 10 | Contributors 11 | ------------ 12 | * Emmanuel Jolaiya 13 | 14 | -------------------------------------------------------------------------------- /graphfaker/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class FetcherType(str, Enum): 5 | """Enum for different fetcher types.""" 6 | 7 | OSM = "osm" 8 | FLIGHTS = "flights" 9 | FAKER = "faker" 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /graphfaker/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for graphfaker.""" 2 | 3 | __author__ = """Dennis Irorere""" 4 | __email__ = "denironyx@gmail.com" 5 | __version__ = "0.2.0" 6 | 7 | from .core import GraphFaker 8 | from .fetchers.wiki import WikiFetcher 9 | from .logger import configure_logging, logger 10 | 11 | __all__ = ["GraphFaker", "logger", "configure_logging", "add_file_logging"] 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * graphfaker version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to graphfaker's documentation! 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | quickstart 11 | usage 12 | modules 13 | contributing 14 | authors 15 | history 16 | 17 | Indices and tables 18 | ================== 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /tests/test_fetchers_wiki.py: -------------------------------------------------------------------------------- 1 | # tests/test_graph_from_source_osm.py 2 | 3 | import pytest 4 | from graphfaker.fetchers.wiki import WikiFetcher 5 | 6 | wiki = WikiFetcher() 7 | 8 | def test_wiki_fetch_page(): 9 | page = wiki.fetch_page("Graph Theory") 10 | 11 | # check url 12 | assert page['url'] == 'https://en.wikipedia.org/wiki/Graph_theory' 13 | # check title 14 | assert page['title'] == "Graph theory" 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/test_graph_from_source_faker.py: -------------------------------------------------------------------------------- 1 | # tests/test_graph_from_source_faker 2 | import pytest 3 | import networkx as nx 4 | from graphfaker.core import GraphFaker 5 | 6 | def test_graph_from_source_faker(): 7 | gf = GraphFaker() 8 | # source faker 9 | G = gf.generate_graph(source="faker", total_nodes=10, total_edges=50) 10 | # Node count matches requested total_nodes 11 | assert G.number_of_nodes() == 10 12 | # Each we are calculating the edges based on the nodes. 13 | assert G.number_of_edges() >= 10 14 | # is_directed() should be True 15 | assert G.is_directed() == True 16 | 17 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.1.0 (2025-04-02) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | 10 | 0.2.0 (2025-06-08) 11 | ------------------ 12 | GraphFaker v0.2.0 – June 2025 13 | 14 | This release expands GraphFaker’s scope with a new data sources to support graph construction and entity recognition tutorials: 15 | 16 | * Wikipedia fetcher (WikiFetcher) 17 | - Retrieve raw page data (title, summary, content, sections, links, references) via the wikipedia package 18 | - Export JSON dumps of article fields 19 | 20 | Upgrade now to effortlessly pull in unstructured Wikipedia data -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, flake8 3 | 4 | [travis] 5 | python = 6 | 3.8: py38 7 | 3.7: py37 8 | 3.6: py36 9 | 10 | [testenv:flake8] 11 | basepython = python 12 | deps = flake8 13 | commands = flake8 graphfaker tests 14 | 15 | [testenv] 16 | setenv = 17 | PYTHONPATH = {toxinidir} 18 | deps = 19 | -r{toxinidir}/requirements_dev.txt 20 | ; If you want to make tox run the tests with the same versions, create a 21 | ; requirements.txt with the pinned versions and uncomment the following line: 22 | ; -r{toxinidir}/requirements.txt 23 | commands = 24 | pip install -U pip 25 | pytest --basetemp={envtmpdir} 26 | 27 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = graphfaker 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version, and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.13" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Optionally, but recommended, 18 | # declare the Python requirements required to build your documentation 19 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - method: pip 23 | path: . 24 | - requirements: docs/requirements.txt 25 | 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=graphfaker 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /graphfaker/utils.py: -------------------------------------------------------------------------------- 1 | def parse_date_range(date_range: str) -> tuple: 2 | """ 3 | Validate and parse a date range string in the format 'YYYY-MM-DD,YYYY-MM-DD'. 4 | 5 | Args: 6 | date_range (str): The date range string to validate. 7 | 8 | Returns: 9 | tuple: A tuple containing the start and end dates as strings. 10 | 11 | Raises: 12 | ValueError: If the date range format is invalid. 13 | """ 14 | try: 15 | start_date, end_date = date_range.split(",") 16 | if len(start_date) != 10 or len(end_date) != 10: 17 | raise ValueError("Date range must be in YYYY-MM-DD format.") 18 | return start_date, end_date 19 | except ValueError as e: 20 | raise ValueError(f"Invalid date range format: {e}") from e 21 | except AttributeError: 22 | raise ValueError( 23 | "Date range must contain exactly two dates separated by a comma." 24 | ) 25 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2025.1.31 2 | charset-normalizer==3.4.1 3 | click==8.1.8 4 | colorama==0.4.6 5 | coverage==7.8.0 6 | faker==37.1.0 7 | idna==3.10 8 | iniconfig==2.1.0 9 | markdown-it-py==3.0.0 10 | mdurl==0.1.2 11 | mypy==1.15.0 12 | mypy-extensions==1.0.0 13 | networkx==3.4.2 14 | numpy==2.2.4 15 | osmnx==2.0.2 16 | packaging==24.2 17 | pandas==2.2.3 18 | pluggy==1.5.0 19 | pygments==2.19.1 20 | pyparsing==3.2.3 21 | pytest==8.3.5 22 | python-dateutil==2.9.0.post0 23 | pytz==2025.2 24 | requests==2.32.3 25 | rich==14.0.0 26 | ruff==0.11.2 27 | shellingham==1.5.4 28 | six==1.17.0 29 | typer==0.15.2 30 | typing-extensions==4.13.0 31 | tzdata==2025.2 32 | urllib3==2.3.0 33 | tqdm==4.67.1 34 | sphinx==8.2.3 35 | graphfaker==0.3.1 36 | sphinxcontrib-applehelp==2.0.0 37 | sphinxcontrib-devhelp==2.0.0 38 | sphinxcontrib-htmlhelp==2.1.0 39 | sphinxcontrib-jsmath==1.0.1 40 | sphinxcontrib-qthelp==2.0.0 41 | sphinxcontrib-serializinghtml==2.0.0 42 | myst_nb==1.3.0 43 | wikipedia==1.4.0 44 | 45 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use graphfaker in a project:: 6 | 7 | import graphfaker 8 | 9 | Flight Data 10 | ========== 11 | 12 | GraphFaker supports generating airline flight network graphs using real-world airline, airport, and flight data. 13 | 14 | To generate a flight network graph for the United States in January 2024 from the command line:: 15 | 16 | python -m graphfaker.cli gen --mode flights --country "United States" --year 2024 --month 1 --export flights.graphml 17 | 18 | To use the Python API:: 19 | 20 | from graphfaker import GraphFaker 21 | gf = GraphFaker() 22 | G = gf.generate_graph(source="flights", country="United States", year=2024, month=1) 23 | gf.visualize_graph(title="US Flight Network (Jan 2024)") 24 | gf.export_graph("flights.graphml") 25 | 26 | You can also specify a date range:: 27 | 28 | G = gf.generate_graph(source="flights", country="United States", date_range=("2024-01-01", "2024-01-15")) 29 | 30 | See the documentation for more details on available options. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025, GraphGeeks Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install graphfaker, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install graphfaker 16 | 17 | This is the preferred method to install graphfaker, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io 23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | The sources for graphfaker can be downloaded from the `Github repo`_. 30 | 31 | You can either clone the public repository: 32 | 33 | .. code-block:: console 34 | 35 | $ git clone git://github.com/denironyx/graphfaker 36 | 37 | Or download the `tarball`_: 38 | 39 | .. code-block:: console 40 | 41 | $ curl -OJL https://github.com/denironyx/graphfaker/tarball/master 42 | 43 | Once you have a copy of the source, you can install it with: 44 | 45 | .. code-block:: console 46 | 47 | $ python setup.py install 48 | 49 | 50 | .. _Github repo: https://github.com/denironyx/graphfaker 51 | .. _tarball: https://github.com/denironyx/graphfaker/tarball/master 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "graphfaker" 7 | version = "0.3.0" 8 | description = "an open-source python library for generating, and loading both synthetic and real-world graph datasets" 9 | readme = "README.md" 10 | authors = [ 11 | {name = "Dennis Irorere", email = "denironyx@gmail.com"} 12 | ] 13 | maintainers = [ 14 | {name = "Dennis Irorere", email = "denironyx@gmail.com"} 15 | ] 16 | classifiers = [ 17 | 18 | ] 19 | license = "MIT" 20 | keywords = ["faker", "graph-data", "flights", "osmnx", "graphs", "graphfaker"] 21 | dependencies = [ 22 | "faker>=37.1.0", 23 | "networkx>=3.4.2", 24 | "osmnx==2.0.2", 25 | "pandas>=2.2.2", 26 | "requests>=2.32.3", 27 | "typer", 28 | "wikipedia>=1.4.0", 29 | ] 30 | 31 | [project.optional-dependencies] 32 | dev = [ 33 | "coverage", # testing 34 | "mypy", # linting 35 | "pytest", # testing 36 | "ruff" # linting 37 | ] 38 | 39 | [project.urls] 40 | 41 | bugs = "https://github.com/graphgeeks-lab/graphfaker/issues" 42 | changelog = "https://github.com/graphgeeks-lab/graphfaker/blob/master/changelog.md" 43 | homepage = "https://github.com/graphgeeks-lab/graphfaker/" 44 | 45 | [tool.setuptools] 46 | package-dir = {"graphfaker" = "graphfaker"} 47 | 48 | [tool.setuptools.package-data] 49 | "*" = ["*.*"] 50 | 51 | 52 | 53 | 54 | # Mypy 55 | # ---- 56 | 57 | [tool.mypy] 58 | files = "." 59 | 60 | # Use strict defaults 61 | strict = true 62 | warn_unreachable = true 63 | warn_no_return = true 64 | 65 | 66 | -------------------------------------------------------------------------------- /tests/test_graph_from_source_osm.py: -------------------------------------------------------------------------------- 1 | # tests/test_graph_from_source_osm.py 2 | 3 | import pytest 4 | import networkx as nx 5 | import osmnx as ox 6 | from graphfaker.fetchers.osm import OSMGraphFetcher 7 | 8 | of = OSMGraphFetcher() 9 | def test_graph_from_source_osm_address(): 10 | #Fetch address data 11 | G = of.fetch_network(address="1600 Amphitheatre Parkway, Mountain View, CA", dist=1000) 12 | 13 | assert G.is_directed() == True, "Value was False, should be True" 14 | 15 | assert G.is_multigraph() == True, "Value was False, should be True" 16 | 17 | assert G.graph['created_with'] == 'OSMnx 2.0.2', "Value wasn't 'OSMnx 2.0.2', osmnx much have been updated" 18 | 19 | assert G.number_of_nodes() <= 100, "Number of nodes should be less than or equal to 100, except it have changed" 20 | 21 | assert G.number_of_edges() >= 195, "Number of edges should be equal or greater than 94, except something changed" 22 | 23 | def test_graph_from_source_osm_place(): 24 | G = of.fetch_network(place="Chinatown, San Francisco, California", network_type="drive") 25 | 26 | assert G.is_directed() == True, "Value was False, should be True" 27 | 28 | assert G.is_multigraph() == True, "Value was False, should be True" 29 | 30 | assert G.graph['created_with'] == 'OSMnx 2.0.2', "Value wasn't 'OSMnx 2.0.2', osmnx much have been updated" 31 | 32 | assert G.number_of_nodes() == 50, "Number of nodes should be 50, except it have changed" 33 | 34 | assert G.number_of_edges() >= 94, "Number of edges should be equal or greater than 94, except something changed" 35 | 36 | -------------------------------------------------------------------------------- /graphfaker/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | PACKAGE_NAME = "graphfaker" 5 | LOGGING_FORMAT = f"{PACKAGE_NAME}:%(asctime)s - %(name)s - %(levelname)s - %(message)s" 6 | 7 | logger = logging.getLogger(PACKAGE_NAME) 8 | 9 | 10 | # Set the default logging level to INFO. 11 | logger.setLevel(logging.INFO) 12 | 13 | 14 | console_handler = logging.StreamHandler() 15 | 16 | console_formatter = logging.Formatter(LOGGING_FORMAT) 17 | console_handler.setFormatter(console_formatter) 18 | 19 | logger.addHandler(console_handler) 20 | 21 | 22 | def add_file_logging(file_path=f"{PACKAGE_NAME}.log"): 23 | """ 24 | Add file logging to the logger. 25 | 26 | Args: 27 | file_path (str): Path to the log file. Defaults to `graphfaker.log`. 28 | 29 | Example: 30 | from graphfaker import add_file_logging 31 | # Configure custom file log 32 | add_file_logging("my_log.log") 33 | """ 34 | file_handler = logging.FileHandler(file_path) 35 | file_formatter = logging.Formatter(LOGGING_FORMAT) 36 | file_handler.setFormatter(file_formatter) 37 | logger.addHandler(file_handler) 38 | 39 | 40 | def configure_logging(level=logging.WARNING): 41 | """ 42 | Configure the logging level for the package. 43 | 44 | Args: 45 | level (int): Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR) 46 | 47 | Example: 48 | # Set logging level to INFO 49 | from graphfaker import configure_logging 50 | import logging 51 | # Configure logging to INFO level 52 | configure_logging(logging.INFO) 53 | """ 54 | logger.setLevel(level) 55 | for handler in logger.handlers: 56 | handler.setLevel(level) 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # Dask worker cache 75 | dask-worker-space/ 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # IDE settings 108 | .vscode/ 109 | .idea/ 110 | 111 | cache/ 112 | -------------------------------------------------------------------------------- /Proposal.md: -------------------------------------------------------------------------------- 1 | # graphfaker 2 | ### Problem Statement 3 | Graph data is essential for solving complex problems in various fields, including social network analysis, transportation modeling, recommendation systems, and fraud detection. However, many professionals, researchers, and students face a common challenge: a lack of easily accessible, realistic graph datasets for testing, learning, and benchmarking. Real-world graph data is often restricted due to privacy concerns, complexity, or large size, making experimentation difficult. 4 | 5 | ### Solution: GraphFaker 6 | GraphFaker is an open-source Python library designed to generate, load, and export synthetic graph datasets in a user-friendly and configurable way. It enables users to create realistic yet customizable graph structures tailored to their specific needs, allowing for better experimentation and learning without relying on sensitive or proprietary data. 7 | 8 | #### Key Features 9 | - Synthetic Graph Generation 10 | - Create graphs for social networks, transportation systems, knowledge graphs, and more. 11 | - Configurable number of nodes, edges, and relationships. 12 | - Support for weighted, directed, and attributed graphs. 13 | 14 | - Prebuilt Benchmark Graphs 15 | - Load small, structured datasets for graph learning and algorithm testing. 16 | - Support for loading into NetworkX, Pandas, Kuzu, and Neo4j. 17 | - Export to formats like CSV, JSON, GraphML, and RDF. 18 | 19 | - Knowledge Graph Creation 20 | - Generate knowledge graphs with predefined schemas (people, organizations, locations, etc.). 21 | - Randomized entity and relationship generation. 22 | - Output in JSON-LD, RDF, or Neo4j formats. 23 | 24 | ### Impact 25 | GraphFaker simplifies graph data experimentation by providing an accessible, open-source solution for professionals and students alike. It helps researchers test algorithms, developers prototype applications, and educators teach graph concepts without dealing with data access barriers. 26 | 27 | References: 28 | - https://arxiv.org/pdf/2203.00112 29 | - https://research.google/pubs/graphworld-fake-graphs-bring-real-insights-for-gnns/ 30 | -------------------------------------------------------------------------------- /tests/test_fetchers_flights.py: -------------------------------------------------------------------------------- 1 | # tests/test_fetchers_flights.py 2 | import pytest 3 | import pandas as pd 4 | import networkx as nx 5 | from graphfaker.fetchers.flights import FlightGraphFetcher 6 | 7 | @pytest.fixture 8 | def sample_airlines_df(): 9 | return pd.DataFrame({ 10 | 'carrier': ['AA', 'DL'], 11 | 'airline_name': ['American Airlines', 'Delta Airlines'] 12 | }) 13 | 14 | @pytest.fixture 15 | def sample_airports_df(): 16 | return pd.DataFrame({ 17 | 'faa': ['JFK', 'LAX'], 18 | 'name': ['JFK Intl', 'LAX Intl'], 19 | 'city': ['New York', 'Los Angeles'], 20 | 'country': ['USA', 'USA'], 21 | 'lat': [40.6413, 33.9416], 22 | 'lon': [-73.7781, -118.4085] 23 | }) 24 | 25 | @pytest.fixture 26 | def sample_flights_df(): 27 | return pd.DataFrame({ 28 | 'year': [2024], 29 | 'month': [1], 30 | 'day': [1], 31 | 'carrier': ['AA'], 32 | 'flight': [100], 33 | 'origin': ['JFK'], 34 | 'dest': ['LAX'], 35 | 'cancelled': [False], 36 | 'delayed': [True] 37 | }) 38 | 39 | def test_build_graph_structure(sample_airlines_df, sample_airports_df, sample_flights_df): 40 | G = FlightGraphFetcher.build_graph(sample_airlines_df, sample_airports_df, sample_flights_df) 41 | # Check nodes 42 | expected_nodes = set(['AA', 'DL', 'JFK', 'LAX', 'New York', 'Los Angeles', 'AA100_JFK_LAX_2024-01-01']) 43 | assert expected_nodes.issubset(set(G.nodes)), "Missing expected nodes" 44 | # Check node attributes 45 | flight_node = 'AA100_JFK_LAX_2024-01-01' 46 | assert G.nodes[flight_node]['type'] == 'Flight' 47 | assert G.nodes[flight_node]['flight_number'] == 100 48 | # Check edges and relationships 49 | # Flight -> Airline 50 | assert G.has_edge(flight_node, 'AA') 51 | assert G.edges[flight_node, 'AA']['relationship'] == 'OPERATED_BY' 52 | # Flight -> Origin 53 | assert G.has_edge(flight_node, 'JFK') 54 | assert G.edges[flight_node, 'JFK']['relationship'] == 'DEPARTS_FROM' 55 | # Flight -> Destination 56 | assert G.has_edge(flight_node, 'LAX') 57 | assert G.edges[flight_node, 'LAX']['relationship'] == 'ARRIVES_AT' 58 | # Airport -> City 59 | assert G.has_edge('JFK', 'New York') 60 | assert G.edges['JFK', 'New York']['relationship'] == 'LOCATED_IN' 61 | assert G.has_edge('LAX', 'Los Angeles') 62 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: graphfaker Package build 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | release-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.x" 28 | 29 | - name: Build release distributions 30 | run: | 31 | # NOTE: put your own distribution build steps here. 32 | python -m pip install build 33 | python -m build 34 | 35 | - name: Upload distributions 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: release-dists 39 | path: dist/ 40 | 41 | # pypi-publish: 42 | # runs-on: ubuntu-latest 43 | # needs: 44 | # - release-build 45 | # permissions: 46 | # # IMPORTANT: this permission is mandatory for trusted publishing 47 | # id-token: write 48 | 49 | # # Dedicated environments with protections for publishing are strongly recommended. 50 | # # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules 51 | # environment: 52 | # name: pypi 53 | # # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: 54 | # url: https://pypi.org/p/graphfaker 55 | # # 56 | # # ALTERNATIVE: if your GitHub Release name is the PyPI project version string 57 | # # ALTERNATIVE: exactly, uncomment the following line instead: 58 | # # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} 59 | 60 | # steps: 61 | # - name: Retrieve release distributions 62 | # uses: actions/download-artifact@v4 63 | # with: 64 | # name: release-dists 65 | # path: dist/ 66 | 67 | # - name: Publish release distributions to PyPI 68 | # uses: pypa/gh-action-pypi-publish@release/v1 69 | # with: 70 | # packages-dir: dist/ 71 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | define BROWSER_PYSCRIPT 6 | import os, webbrowser, sys 7 | 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | 25 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 26 | 27 | help: 28 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 29 | 30 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 31 | 32 | clean-build: ## remove build artifacts 33 | rm -fr build/ 34 | rm -fr dist/ 35 | rm -fr .eggs/ 36 | find . -name '*.egg-info' -exec rm -fr {} + 37 | find . -name '*.egg' -exec rm -f {} + 38 | 39 | clean-pyc: ## remove Python file artifacts 40 | find . -name '*.pyc' -exec rm -f {} + 41 | find . -name '*.pyo' -exec rm -f {} + 42 | find . -name '*~' -exec rm -f {} + 43 | find . -name '__pycache__' -exec rm -fr {} + 44 | 45 | clean-test: ## remove test and coverage artifacts 46 | rm -fr .tox/ 47 | rm -f .coverage 48 | rm -fr htmlcov/ 49 | rm -fr .pytest_cache 50 | 51 | lint/flake8: ## check style with flake8 52 | flake8 graphfaker tests 53 | 54 | 55 | lint: lint/flake8 ## check style 56 | 57 | test: ## run tests quickly with the default Python 58 | pytest 59 | 60 | test-all: ## run tests on every Python version with tox 61 | tox 62 | 63 | coverage: ## check code coverage quickly with the default Python 64 | coverage run --source graphfaker -m pytest 65 | coverage report -m 66 | coverage html 67 | $(BROWSER) htmlcov/index.html 68 | 69 | docs: ## generate Sphinx HTML documentation, including API docs 70 | rm -f docs/graphfaker.rst 71 | rm -f docs/modules.rst 72 | sphinx-apidoc -o docs/ graphfaker 73 | $(MAKE) -C docs clean 74 | $(MAKE) -C docs html 75 | $(BROWSER) docs/_build/html/index.html 76 | 77 | servedocs: docs ## compile the docs watching for changes 78 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 79 | 80 | release: dist ## package and upload a release 81 | twine upload dist/* 82 | 83 | dist: clean ## builds source and wheel package 84 | python setup.py sdist 85 | python setup.py bdist_wheel 86 | ls -l dist 87 | 88 | install: clean ## install the package to the active Python's site-packages 89 | python setup.py install 90 | -------------------------------------------------------------------------------- /examples/osm_network.py: -------------------------------------------------------------------------------- 1 | import marimo 2 | 3 | __generated_with = "0.13.1" 4 | app = marimo.App(width="medium") 5 | 6 | 7 | @app.cell 8 | def _(): 9 | from graphfaker.core import GraphFaker 10 | 11 | return (GraphFaker,) 12 | 13 | 14 | @app.cell 15 | def _(GraphFaker): 16 | gf = GraphFaker() 17 | return (gf,) 18 | 19 | 20 | @app.cell 21 | def _(gf): 22 | G_rand = gf.generate_graph(mode="random", total_nodes=50, total_edges=200) 23 | return (G_rand,) 24 | 25 | 26 | @app.cell 27 | def _(G_rand, gf): 28 | gf.visualize_graph(G_rand) 29 | return 30 | 31 | 32 | @app.cell 33 | def _(gf): 34 | G_osm = gf.generate_graph( 35 | mode="osm", place="Chinatown, San Francisco, California", network_type="drive" 36 | ) 37 | return (G_osm,) 38 | 39 | 40 | @app.cell 41 | def _(G_osm): 42 | G_osm 43 | return 44 | 45 | 46 | @app.cell 47 | def _(G_osm, gf): 48 | gf.visualize_graph(G_osm) 49 | return 50 | 51 | 52 | @app.cell 53 | def _(): 54 | return 55 | 56 | 57 | @app.cell 58 | def _(G_osm, gf): 59 | gf.visualize_osm(G_osm, node_size=100) 60 | return 61 | 62 | 63 | @app.cell 64 | def _(): 65 | from graphfaker.fetchers.osm import OSMGraphFetcher 66 | 67 | return 68 | 69 | 70 | @app.cell 71 | def _(G_osm, gf): 72 | gf.basic_stats(G_osm) 73 | return 74 | 75 | 76 | @app.cell 77 | def _(): 78 | import networkx as nx 79 | 80 | def basic_stats(G: nx.Graph) -> dict: 81 | """ 82 | Compute basic statistics of the OSM network 83 | """ 84 | stats = { 85 | "nodes": G.number_of_nodes(), 86 | "edges": G.number_of_edges(), 87 | "avg_degree": sum(dict(G.degree()).values()) / float(G.number_of_nodes()), 88 | } 89 | return stats 90 | 91 | return (basic_stats,) 92 | 93 | 94 | @app.cell 95 | def _(G_osm, basic_stats): 96 | basic_stats(G_osm) 97 | return 98 | 99 | 100 | @app.cell 101 | def _(): 102 | import osmnx as ox 103 | 104 | # Example: Download street network for "Birmingham, B16, UK" 105 | G = ox.graph_from_place( 106 | "Chinatown, San Francisco, California", network_type="drive" 107 | ) 108 | 109 | # Visualize the graph using a static map 110 | fig, ax = ox.plot_graph( 111 | G, 112 | node_size=50, 113 | node_color="red", 114 | edge_color="black", 115 | edge_linewidth=2, 116 | bgcolor="white", 117 | ) 118 | 119 | # Customize the plot (optional) 120 | ax.set_title("Birmingham, B16 Street Network (Driving)") 121 | return G, fig, ox 122 | 123 | 124 | @app.cell 125 | def _(G, fig, ox): 126 | 127 | # Or visualize using an interactive web map 128 | m = ox.plot_graph_folium(G, popup_attribute="name", weight=3, color="blue") 129 | 130 | # Display the plot 131 | fig.show() 132 | m.show() 133 | return 134 | 135 | 136 | if __name__ == "__main__": 137 | app.run() 138 | -------------------------------------------------------------------------------- /examples/demo_graphfaker.py: -------------------------------------------------------------------------------- 1 | from graphfaker.logger import logger 2 | import marimo 3 | 4 | __generated_with = "0.13.1" 5 | app = marimo.App(width="medium") 6 | 7 | 8 | @app.cell 9 | def _(): 10 | import os 11 | import io 12 | import zipfile 13 | import warnings 14 | from datetime import datetime, timedelta 15 | from typing import Tuple, Optional 16 | from io import StringIO 17 | 18 | import requests 19 | import pandas as pd 20 | from tqdm.auto import tqdm 21 | import networkx as nx 22 | import time 23 | 24 | # suppress only the single warning from unverified HTTPS 25 | import urllib3 26 | from urllib3.exceptions import InsecureRequestWarning 27 | 28 | urllib3.disable_warnings(InsecureRequestWarning) 29 | return 30 | 31 | 32 | @app.cell 33 | def _(): 34 | from graphfaker.fetchers.flights import FlightGraphFetcher 35 | 36 | return (FlightGraphFetcher,) 37 | 38 | 39 | @app.cell 40 | def _(FlightGraphFetcher): 41 | airlines = FlightGraphFetcher.fetch_airlines() 42 | airlines.head() 43 | return 44 | 45 | 46 | @app.cell 47 | def _(FlightGraphFetcher): 48 | airports = FlightGraphFetcher.fetch_airports() 49 | airports.head() 50 | return 51 | 52 | 53 | @app.cell 54 | def _(FlightGraphFetcher): 55 | flights = FlightGraphFetcher.fetch_flights(year=2024, month=1) 56 | return 57 | 58 | 59 | @app.cell 60 | def _(): 61 | from graphfaker import GraphFaker 62 | 63 | return (GraphFaker,) 64 | 65 | 66 | @app.cell 67 | def _(GraphFaker): 68 | gf = GraphFaker() 69 | return (gf,) 70 | 71 | 72 | @app.cell 73 | def _(gf): 74 | G_flight = gf.generate_graph(source="flights", year=2024, month=1) 75 | return (G_flight,) 76 | 77 | 78 | @app.cell 79 | def _(G_flight): 80 | G_flight.number_of_edges() 81 | return 82 | 83 | 84 | @app.cell 85 | def _(): 86 | import scipy 87 | 88 | return 89 | 90 | 91 | @app.cell 92 | def _(): 93 | # gf.visualize_graph(G_flight) 94 | return 95 | 96 | 97 | @app.cell 98 | def _(G_flight): 99 | G_flight.number_of_nodes() 100 | return 101 | 102 | 103 | @app.cell 104 | def _(G_flight): 105 | import random 106 | 107 | # suppose G_flight is your big graph and you want at most 1000 nodes 108 | N = 500 109 | all_nodes = list(G_flight.nodes()) 110 | if len(all_nodes) > N: 111 | sampled = random.sample(all_nodes, N) 112 | G_small = G_flight.subgraph(sampled).copy() 113 | else: 114 | G_small = G_flight.copy() 115 | logger.info( 116 | f"Original: {G_flight.number_of_nodes()} nodes, {G_flight.number_of_edges()} edges" 117 | ) 118 | logger.info( 119 | f"Small : {G_small.number_of_nodes()} nodes, {G_small.number_of_edges()} edges" 120 | ) 121 | 122 | return (G_small,) 123 | 124 | 125 | @app.cell 126 | def _(G_small, gf): 127 | gf.visualize_graph(G_small) 128 | return 129 | 130 | 131 | if __name__ == "__main__": 132 | app.run() 133 | -------------------------------------------------------------------------------- /graphfaker/fetchers/wiki.py: -------------------------------------------------------------------------------- 1 | # graphfaker/fetchers/wiki.py 2 | 3 | """ 4 | WikipediaFetcher: Fetch unstructured content from Wikipedia that can be used for graph generation. 5 | 6 | Provides methods to retrieve articles by title, capture key fields (title, summary, content, sections, links, references), 7 | and export as JSON. Designed to give users raw data they can use to build their own graphs, tranform in Knowledge Graphs, 8 | peform entity resolutions, and more. 9 | 10 | Usage: 11 | from graphfaker import WikiFetcher 12 | wiki = WikiFetcher() 13 | 14 | # Fetch raw page data 15 | page = wiki.fetch_page("Graph Theory") 16 | 17 | # Access and Print key fields 18 | print(page['title']) 19 | print(page['summary']) 20 | print(page['content']) 21 | print(page['sections'], page['links'][:5], page['references'][:5]) 22 | wiki.export_page_json(page, "graph_theory.json") 23 | """ 24 | import os 25 | import json 26 | from typing import Dict, Any, List, Optional 27 | import wikipedia 28 | 29 | class WikiFetcher: 30 | """ 31 | Fetch and prepare unstructured Wikipedia content for graph construction. 32 | """ 33 | 34 | @staticmethod 35 | def fetch_page(title: str) -> Dict[str, Any]: 36 | """ 37 | Retrieve a Wikipedia page by title and return core fields. 38 | 39 | Args: 40 | title (str): The title of the Wikipedia page to fetch. 41 | 42 | Returns: 43 | Dict with keys: 44 | - title: str 45 | - url: str 46 | - summary: str 47 | - content: str 48 | - images: List[Dict] 49 | - links: List[str] 50 | - references: List[str] 51 | 52 | """ 53 | page = wikipedia.page(title) 54 | data = { 55 | "title": page.title, 56 | "url": page.url, 57 | "summary": page.summary, 58 | "content": page.content, 59 | "images": page.images, 60 | "links": page.links, 61 | } 62 | # references attributes may not exist in older wikipedia module 63 | refs = getattr(page, "references", None) 64 | data["references"] = refs if isinstance(refs, list) else [] 65 | return data 66 | 67 | @staticmethod 68 | def export_page_json(page: Dict[str, Any], filename: str) -> None: 69 | """ 70 | Write the fetched Wikipedia page data to a JSON file. 71 | 72 | Args: 73 | page_data: Dict returned by `fetch_page`. 74 | filename: Destination JSON file path. 75 | """ 76 | 77 | abs_path = os.path.abspath(filename) 78 | os.makedirs(os.path.dirname(abs_path) or ".", exist_ok=True) 79 | 80 | with open(abs_path, 'w', encoding='utf-8') as f: 81 | json.dump(page, f, ensure_ascii=False, indent=2) 82 | print(f"✅ Exported Wikipedia page data to '{abs_path}'") 83 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from typer.testing import CliRunner 2 | from unittest.mock import patch 3 | from graphfaker.cli import app 4 | 5 | runner = CliRunner() 6 | 7 | 8 | def test_faker_mode_generates_graph(): 9 | result = runner.invoke( 10 | app, 11 | ["--fetcher", "faker", "--total-nodes", "10", "--total-edges", "20"], 12 | ) 13 | 14 | assert result.exit_code == 0 15 | assert "Graph" in result.output or "nodes" in result.output 16 | 17 | 18 | @patch("graphfaker.fetchers.osm.OSMGraphFetcher.fetch_network") 19 | def test_osm_mode_with_place(mock_fetch): 20 | mock_fetch.return_value = "Mocked OSM Graph" 21 | 22 | result = runner.invoke( 23 | app, 24 | ["--fetcher", "osm", "--place", "Soho Square, London, UK"], 25 | ) 26 | assert result.exit_code == 0 27 | assert "Mocked OSM Graph" in result.output 28 | mock_fetch.assert_called_once() 29 | 30 | 31 | @patch("graphfaker.fetchers.flights.FlightGraphFetcher.fetch_airlines") 32 | @patch("graphfaker.fetchers.flights.FlightGraphFetcher.fetch_airports") 33 | @patch("graphfaker.fetchers.flights.FlightGraphFetcher.fetch_flights") 34 | @patch("graphfaker.fetchers.flights.FlightGraphFetcher.build_graph") 35 | def test_flight_mode_valid_inputs( 36 | mock_build_graph, mock_fetch_flights, mock_fetch_airports, mock_fetch_airlines 37 | ): 38 | mock_fetch_airlines.return_value = ["airline1"] 39 | mock_fetch_airports.return_value = ["airport1"] 40 | mock_fetch_flights.return_value = ["flight1"] 41 | mock_build_graph.return_value = "Mocked Flight Graph" 42 | 43 | result = runner.invoke( 44 | app, ["--fetcher", "flights", "--year", "2024", "--month", "1"] 45 | ) 46 | 47 | assert result.exit_code == 0 48 | assert "Mocked Flight Graph" in result.output 49 | mock_fetch_airlines.assert_called_once() 50 | mock_fetch_airports.assert_called_once() 51 | mock_fetch_flights.assert_called_once() 52 | mock_build_graph.assert_called_once() 53 | 54 | 55 | def test_invalid_month(): 56 | result = runner.invoke( 57 | app, ["--fetcher", "flights", "--year", "2024", "--month", "13"] 58 | ) 59 | 60 | assert result.exit_code != 0 61 | 62 | 63 | def test_invalid_year(): 64 | result = runner.invoke( 65 | app, ["--fetcher", "flights", "--year", "2200", "--month", "1"] 66 | ) 67 | assert result.exit_code != 0 68 | 69 | 70 | def test_invalid_daterange(): 71 | result = runner.invoke( 72 | app, ["--fetcher", "flights", "--date-range", "2024-01-,2024-01-10"] 73 | ) 74 | assert result.exit_code != 0 75 | 76 | 77 | @patch("graphfaker.fetchers.flights.FlightGraphFetcher.fetch_flights") 78 | def test_flight_mode_with_date_range(mock_fetch_flights): 79 | mock_fetch_flights.return_value = ["flightX"] 80 | with patch( 81 | "graphfaker.fetchers.flights.FlightGraphFetcher.fetch_airlines", return_value=[] 82 | ), patch( 83 | "graphfaker.fetchers.flights.FlightGraphFetcher.fetch_airports", return_value=[] 84 | ), patch( 85 | "graphfaker.fetchers.flights.FlightGraphFetcher.build_graph", 86 | return_value="Mocked Graph", 87 | ): 88 | result = runner.invoke( 89 | app, 90 | [ 91 | "--fetcher", 92 | "flights", 93 | "--year", 94 | "2024", 95 | "--month", 96 | "1", 97 | "--date-range", 98 | "2024-01-01,2024-01-10", 99 | ], 100 | ) 101 | assert result.exit_code == 0 102 | assert "Mocked Graph" in result.output 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | Contributor Covenant Code of Conduct 3 | ==================================== 4 | 5 | Our Pledge 6 | ---------- 7 | 8 | In the interest of fostering an open and welcoming environment, we as 9 | contributors and maintainers pledge to make participation in our project and 10 | our community a harassment-free experience for everyone, regardless of age, body 11 | size, disability, ethnicity, sex characteristics, gender identity and expression, 12 | level of experience, education, socio-economic status, nationality, personal 13 | appearance, race, religion, or sexual identity and orientation. 14 | 15 | Our Standards 16 | ------------- 17 | 18 | Examples of behavior that contributes to creating a positive environment 19 | include: 20 | 21 | * Using welcoming and inclusive language 22 | * Being respectful of differing viewpoints and experiences 23 | * Gracefully accepting constructive criticism 24 | * Focusing on what is best for the community 25 | * Showing empathy towards other community members 26 | 27 | Examples of unacceptable behavior by participants include: 28 | 29 | * The use of sexualized language or imagery and unwelcome sexual attention or 30 | advances 31 | * Trolling, insulting/derogatory comments, and personal or political attacks 32 | * Public or private harassment 33 | * Publishing others' private information, such as a physical or electronic 34 | address, without explicit permission 35 | * Other conduct which could reasonably be considered inappropriate in a 36 | professional setting 37 | 38 | Our Responsibilities 39 | -------------------- 40 | 41 | Project maintainers are responsible for clarifying the standards of acceptable 42 | behavior and are expected to take appropriate and fair corrective action in 43 | response to any instances of unacceptable behavior. 44 | 45 | Project maintainers have the right and responsibility to remove, edit, or 46 | reject comments, commits, code, wiki edits, issues, and other contributions 47 | that are not aligned to this Code of Conduct, or to ban temporarily or 48 | permanently any contributor for other behaviors that they deem inappropriate, 49 | threatening, offensive, or harmful. 50 | 51 | Scope 52 | ----- 53 | 54 | This Code of Conduct applies within all project spaces, and it also applies when 55 | an individual is representing the project or its community in public spaces. 56 | Examples of representing a project or community include using an official 57 | project e-mail address, posting via an official social media account, or acting 58 | as an appointed representative at an online or offline event. Representation of 59 | a project may be further defined and clarified by project maintainers. 60 | 61 | Enforcement 62 | ----------- 63 | 64 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 65 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 66 | complaints will be reviewed and investigated and will result in a response that 67 | is deemed necessary and appropriate to the circumstances. The project team is 68 | obligated to maintain confidentiality with regard to the reporter of an incident. 69 | Further details of specific enforcement policies may be posted separately. 70 | 71 | Project maintainers who do not follow or enforce the Code of Conduct in good 72 | faith may face temporary or permanent repercussions as determined by other 73 | members of the project's leadership. 74 | 75 | Attribution 76 | ----------- 77 | 78 | This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4, 79 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 80 | 81 | For answers to common questions about this code of conduct, see 82 | https://www.contributor-covenant.org/faq 83 | 84 | .. _`Contributor Covenant`: https://www.contributor-covenant.org 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/graphgeeks-lab/graphfaker/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | graphfaker could always use more documentation, whether as part of the 42 | official graphfaker docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/graphgeeks-lab/graphfaker/issues 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `graphfaker` for local development. 61 | 62 | 1. Fork the `graphfaker` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/graphfaker.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv graphfaker 70 | $ cd graphfaker/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ make lint 83 | $ make test 84 | Or 85 | $ make test-all 86 | 87 | To get flake8 and tox, just pip install them into your virtualenv. 88 | 89 | 6. Commit your changes and push your branch to GitHub:: 90 | 91 | $ git add . 92 | $ git commit -m "Your detailed description of your changes." 93 | $ git push origin name-of-your-bugfix-or-feature 94 | 95 | 7. Submit a pull request through the GitHub website. 96 | 97 | Pull Request Guidelines 98 | ----------------------- 99 | 100 | Before you submit a pull request, check that it meets these guidelines: 101 | 102 | 1. The pull request should include tests. 103 | 2. If the pull request adds functionality, the docs should be updated. Put 104 | your new functionality into a function with a docstring, and add the 105 | feature to the list in README.rst. 106 | 3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check 107 | https://travis-ci.com/denironyx/graphfaker/pull_requests 108 | and make sure that the tests pass for all supported Python versions. 109 | 110 | Tips 111 | ---- 112 | 113 | To run a subset of tests:: 114 | 115 | $ pytest tests.test_graph_from_source_faker.py 116 | 117 | 118 | Deploying 119 | --------- 120 | 121 | A reminder for the maintainers on how to deploy. 122 | Make sure all your changes are committed (including an entry in HISTORY.rst). 123 | Then run:: 124 | 125 | $ bump2version patch # possible: major / minor / patch 126 | $ git push 127 | $ git push --tags 128 | 129 | Travis will then deploy to PyPI if tests pass. 130 | 131 | Code of Conduct 132 | --------------- 133 | 134 | Please note that this project is released with a `Contributor Code of Conduct`_. 135 | By participating in this project you agree to abide by its terms. 136 | 137 | .. _`Contributor Code of Conduct`: CODE_OF_CONDUCT.rst 138 | -------------------------------------------------------------------------------- /examples/example_osm_faker_visualization.py: -------------------------------------------------------------------------------- 1 | # run_demo.py 2 | 3 | import matplotlib 4 | 5 | matplotlib.use("TkAgg") # pick a GUI backend on desktop 6 | from graphfaker.core import GraphFaker 7 | import networkx as nx 8 | from matplotlib import pyplot as plt 9 | from graphfaker.logger import logger 10 | 11 | 12 | # visualization function for faker source 13 | def visualize_faker_graph(self, title, k=1.5, iterations=100): 14 | """Visualize the graph using Matplotlib with a more spread-out layout.""" 15 | plt.figure(figsize=(14, 12)) 16 | pos = nx.spring_layout(self.G, seed=42, k=k, iterations=iterations) 17 | # Color nodes based on their type 18 | color_map = { 19 | "Person": "lightblue", 20 | "Place": "lightgreen", 21 | "Organization": "orange", 22 | "Event": "pink", 23 | "Product": "yellow", 24 | } 25 | node_colors = [ 26 | color_map.get(data.get("type"), "gray") for _, data in self.G.nodes(data=True) 27 | ] 28 | nx.draw_networkx_nodes( 29 | self.G, pos, node_color=node_colors, node_size=500, alpha=0.9 30 | ) 31 | nx.draw_networkx_edges(self.G, pos, alpha=0.4) 32 | labels = {node: data.get("name", node) for node, data in self.G.nodes(data=True)} 33 | nx.draw_networkx_labels(self.G, pos, labels=labels, font_size=8) 34 | plt.title(title=title) 35 | plt.axis("off") 36 | plt.show() 37 | 38 | 39 | # Visualize osm data.. since it's a multigraph 40 | def visualize_osm( 41 | self, 42 | G: nx.Graph = None, 43 | show_edge_names: bool = False, 44 | show_node_ids: bool = False, 45 | node_size: int = 20, 46 | edge_linewidth: float = 1.0, 47 | ): 48 | """ 49 | Visualize an OSM-derived graph using OSMnx plotting, with optional labels. 50 | :param G: The graph to visualize (default: last generated graph). 51 | :param show_edge_names: If True, overlay edge 'name' attributes as labels. 52 | :param show_node_ids: If True, overlay node IDs as labels. 53 | :param node_size: Size of nodes in the plot. 54 | :param edge_linewidth: Width of edges in the plot. 55 | """ 56 | if G is None: 57 | G = self.G 58 | try: 59 | import osmnx as ox 60 | except ImportError: 61 | raise ImportError( 62 | "osmnx is required for visualize_osm. Install via `pip install osmnx`." 63 | ) 64 | # Plot base OSM network 65 | fig, ax = ox.plot_graph( 66 | G, node_size=node_size, edge_linewidth=edge_linewidth, show=False, close=False 67 | ) 68 | # Prepare positions for labeling 69 | pos = {node: (data.get("x"), data.get("y")) for node, data in G.nodes(data=True)} 70 | # Edge labels 71 | if show_edge_names: 72 | edge_labels = {} 73 | for u, v, data in G.edges(data=True): 74 | name = data.get("name") 75 | if name: 76 | # OSMnx 'name' can be list or string 77 | label = name if isinstance(name, str) else ",".join(name) 78 | edge_labels[(u, v)] = label 79 | nx.draw_networkx_edge_labels( 80 | G, pos, edge_labels=edge_labels, font_size=6, ax=ax 81 | ) 82 | # Node labels 83 | if show_node_ids or any("name" in d for _, d in G.nodes(data=True)): 84 | labels = {} 85 | for node, data in G.nodes(data=True): 86 | if show_node_ids: 87 | labels[node] = str(node) 88 | elif "name" in data: 89 | labels[node] = data["name"] 90 | nx.draw_networkx_labels(G, pos, labels=labels, font_size=6, ax=ax) 91 | ax.set_title("OSM Network Visualization") 92 | plt.show() 93 | 94 | 95 | # Create the graph with defaults (can be overridden by user input) 96 | if __name__ == "__main__": 97 | gf = GraphFaker() 98 | G = gf.generate_graph(total_nodes=20, total_edges=60) 99 | logger.info("-> GraphFaker graph generation started <-") 100 | logger.info("Graph summary:") 101 | logger.info(" Total nodes: %s", G.number_of_nodes()) 102 | logger.info(" Total edges: %s", G.number_of_edges()) 103 | gf.visualize_faker_graph(title="Graph Faker PoC ") 104 | # sg.export_graph(filename="social_knowledge_graph.graphml") 105 | 106 | 107 | gf = GraphFaker() 108 | 109 | # Faker social graph 110 | G_rand = gf.generate_graph(source="faker", total_nodes=50, total_edges=200) 111 | gf.visualize_faker_graph(G_rand) 112 | 113 | # OSM network 114 | G_osm = gf.generate_graph( 115 | source="osm", place="Chinatown, San Francisco, California", network_type="drive" 116 | ) 117 | gf.visualize_faker_graph(G_osm) 118 | -------------------------------------------------------------------------------- /graphfaker/fetchers/osm.py: -------------------------------------------------------------------------------- 1 | """ 2 | OSM Fetcher module: wraps OSMnx functionality to retrieve and preprocess street networks. 3 | """ 4 | 5 | from typing import Optional 6 | import networkx as nx 7 | import osmnx as ox 8 | 9 | from graphfaker.logger import logger 10 | 11 | # OSMnx settings 12 | 13 | ox.utils.settings.log_console = True 14 | 15 | 16 | class OSMGraphFetcher: 17 | @staticmethod 18 | def fetch_network( 19 | place: Optional[str] = None, 20 | address: Optional[str] = None, 21 | bbox: Optional[tuple[float, float, float, float]] = None, 22 | network_type: str = "drive", 23 | simplify: bool = True, 24 | retain_all: bool = False, 25 | dist: float = 1000, 26 | ) -> nx.MultiDiGraph: 27 | """ 28 | OSMGraphFetcher: Fetch and preprocess street networks from OpenStreetMap via OSMnx. 29 | 30 | Methods: 31 | fetch_network( 32 | place: str = None, 33 | address: str = None, 34 | bbox: tuple = None, 35 | network_type: str = "drive", 36 | simplify: bool = True, 37 | retain_all: bool = False, 38 | dist: float = 1000 39 | ) -> nx.MultiDiGraph 40 | Fetch a street network and project it to UTM for accurate spatial analysis. 41 | 42 | Parameters: 43 | place (str, optional): A geographic name (e.g., "London, UK") to geocode and fetch. 44 | address (str, optional): A specific address or point-of-interest to geocode and fetch around. 45 | bbox (tuple, optional): A bounding box as (north, south, east, west) coordinates. 46 | network_type (str): OSMnx network type: "drive", "walk", "bike", or "all". 47 | simplify (bool): If True, simplify the graph topology (merge intersections). 48 | retain_all (bool): If True, keep all connected components; else largest only. 49 | dist (float): Search radius in meters when fetching by address. 50 | 51 | Returns: 52 | nx.MultiDiGraph: Projected street network graph (UTM coordinates). 53 | 54 | Raises: 55 | ValueError: If none of place, address, or bbox is provided. 56 | ImportError: If OSMnx is not installed. 57 | 58 | Example: 59 | from graphfaker.fetchers.osm import OSMGraphFetcher 60 | # Fetch by place 61 | G1 = OSMGraphFetcher.fetch_network(place="San Francisco, CA") 62 | # Fetch by address within 500m 63 | G2 = OSMGraphFetcher.fetch_network(address="1600 Amphitheatre Parkway, Mountain View, CA", dist=500) 64 | # Fetch by bounding box 65 | bbox = (37.79, 37.77, -122.41, -122.43) 66 | G3 = OSMGraphFetcher.fetch_network(bbox=bbox, network_type="walk") 67 | """ 68 | logger.info( 69 | "Fetching OSM network with parameters: " 70 | f"place={place}, address={address}, bbox={bbox}, " 71 | f"network_type={network_type}, simplify={simplify}, " 72 | f"retain_all={retain_all}, dist={dist}" 73 | ) 74 | if address: 75 | G = ox.graph_from_address( 76 | address, 77 | dist=dist, 78 | network_type=network_type, 79 | simplify=simplify, 80 | retain_all=retain_all, 81 | ) 82 | elif place: 83 | G = ox.graph_from_place( 84 | place, 85 | network_type=network_type, 86 | simplify=simplify, 87 | retain_all=retain_all, 88 | ) 89 | elif bbox: 90 | G = ox.graph_from_bbox( 91 | bbox, 92 | network_type=network_type, 93 | simplify=simplify, 94 | retain_all=retain_all, 95 | ) 96 | else: 97 | logger.error( 98 | "No valid input provided for fetching OSM network. " 99 | "Please provide 'place', 'address', or 'bbox'." 100 | ) 101 | raise ValueError( 102 | "Either 'place', 'address', or 'bbox' must be provided to fetch OSM network." 103 | ) 104 | 105 | # Project to UTM for accurate distance-based metrics 106 | G_proj = ox.project_graph(G) 107 | 108 | return G_proj 109 | 110 | @staticmethod 111 | def basic_stats(G: nx.Graph) -> dict: 112 | """ 113 | Compute basic statistics of the OSM network 114 | """ 115 | stats = { 116 | "nodes": G.number_of_nodes(), 117 | "edges": G.number_of_edges(), 118 | "avg_degree": sum(dict(G.degree()).values()) / float(G.number_of_nodes()), # type: ignore 119 | } 120 | return stats 121 | -------------------------------------------------------------------------------- /graphfaker/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command-line interface for GraphFaker. 3 | """ 4 | 5 | from venv import logger 6 | import typer 7 | from graphfaker.core import GraphFaker 8 | from graphfaker.enums import FetcherType 9 | from graphfaker.fetchers.osm import OSMGraphFetcher 10 | from graphfaker.fetchers.flights import FlightGraphFetcher 11 | from graphfaker.utils import parse_date_range 12 | import os 13 | 14 | app = typer.Typer() 15 | 16 | 17 | @app.command(short_help="Generate a graph using GraphFaker.") 18 | def gen( 19 | fetcher: FetcherType = typer.Option(FetcherType.FAKER, help="Fetcher type to use."), 20 | # for FetcherType.FAKER source 21 | total_nodes: int = typer.Option(100, help="Total nodes for random mode."), 22 | total_edges: int = typer.Option(1000, help="Total edges for random mode."), 23 | # for FetcherType.OSM source 24 | place: str = typer.Option( 25 | None, help="OSM place name (e.g., 'Soho Square, London, UK')." 26 | ), 27 | address: str = typer.Option( 28 | None, help="OSM address (e.g., '1600 Amphitheatre Parkway, Mountain View, CA.')" 29 | ), 30 | bbox: str = typer.Option(None, help="OSM bounding box as 'north,south,east,west.'"), 31 | network_type: str = typer.Option( 32 | "drive", help="OSM network type: drive | walk | bike | all." 33 | ), 34 | simplify: bool = typer.Option(True, help="Simplify OSM graph topology."), 35 | retain_all: bool = typer.Option(False, help="Retain all components in OSM graph."), 36 | dist: int = typer.Option( 37 | 1000, help="Search radius (meters) when fetching around address." 38 | ), 39 | # for FetcherType.FLIGHT source 40 | country: str = typer.Option( 41 | "United States", 42 | help="Filter airports by country for flight data. e.g 'United States'.", 43 | ), 44 | year: int = typer.Option( 45 | 2024, help="Year (YYYY) for single-month flight fetch. e.g. 2024." 46 | ), 47 | month: int = typer.Option( 48 | 1, help="Month (1-12) for single-month flight fetch. e.g. 1 for January." 49 | ), 50 | date_range: str = typer.Option( 51 | None, 52 | help="Year, Month and day range (YYYY-MM-DD,YYYY-MM-DD) for flight data. e.g. '2024-01-01,2024-01-15'.", 53 | ), 54 | 55 | # common 56 | export: str = typer.Option("graph.graphml", help="File path to export GraphML"), 57 | ): 58 | """Generate a graph using GraphFaker.""" 59 | gf = GraphFaker() 60 | 61 | if fetcher == FetcherType.FAKER: 62 | 63 | g = gf.generate_graph(total_nodes=total_nodes, total_edges=total_edges) 64 | logger.info( 65 | f"Generated random graph with {g.number_of_nodes()} nodes and {g.number_of_edges()} edges." 66 | ) 67 | 68 | elif fetcher == FetcherType.OSM: 69 | # parse bbox string if provided 70 | bbox_tuple = None 71 | if bbox: 72 | north, south, east, west = map(float, bbox.split(",")) 73 | bbox_tuple = (north, south, east, west) 74 | g = OSMGraphFetcher.fetch_network( 75 | place=place, 76 | address=address, 77 | bbox=bbox_tuple, 78 | network_type=network_type, 79 | simplify=simplify, 80 | retain_all=retain_all, 81 | dist=dist, 82 | ) 83 | logger.info( 84 | f"Fetched OSM graph with {g.number_of_nodes()} nodes and {g.number_of_edges()} edges." 85 | ) 86 | else: 87 | # Flight fetcher 88 | parsed_date_range = parse_date_range(date_range) if date_range else None 89 | 90 | # validate year and month 91 | if not (1 <= month <= 12): 92 | raise ValueError("Month must be between 1 and 12.") 93 | if not (1900 <= year <= 2100): 94 | raise ValueError("Year must be between 1900 and 2100.") 95 | 96 | airlines_df = FlightGraphFetcher.fetch_airlines() 97 | 98 | airports_df = FlightGraphFetcher.fetch_airports(country=country) 99 | 100 | flights_df = FlightGraphFetcher.fetch_flights( 101 | year=year, month=month, date_range=parsed_date_range 102 | ) 103 | logger.info( 104 | f"Fetched {len(airlines_df)} airlines, " 105 | f"{len(airports_df)} airports, " 106 | f"{len(flights_df)} flights." 107 | ) 108 | 109 | g = FlightGraphFetcher.build_graph(airlines_df, airports_df, flights_df) 110 | 111 | logger.info( 112 | f"Generated flight graph with {g.number_of_nodes()} nodes and {g.number_of_edges()} edges." 113 | ) 114 | 115 | abs_export_path = os.path.abspath(export) 116 | os.makedirs(os.path.dirname(abs_export_path) or ".", exist_ok=True) 117 | 118 | gf.export_graph(g, source=fetcher, path=abs_export_path) 119 | logger.info(f"exported graph to {abs_export_path}, with {g.number_of_nodes()} nodes and {g.number_of_edges()} edges.") 120 | 121 | 122 | if __name__ == "__main__": 123 | app() 124 | -------------------------------------------------------------------------------- /examples/wikipedia_wrapper.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import marimo 4 | 5 | __generated_with = "0.13.1" 6 | app = marimo.App(width="medium") 7 | 8 | 9 | @app.cell 10 | def _(): 11 | import wikipedia 12 | return (wikipedia,) 13 | 14 | 15 | @app.cell 16 | def _(wikipedia): 17 | print(wikipedia.search("Bill")) 18 | return 19 | 20 | 21 | @app.cell 22 | def _(wikipedia): 23 | print(wikipedia.page("Graph Theory").content) 24 | return 25 | 26 | 27 | @app.cell 28 | def _(wikipedia): 29 | print(wikipedia.page("Brett Cooper").content) 30 | return 31 | 32 | 33 | @app.cell 34 | def _(wikipedia): 35 | print(wikipedia.page("Brett Cooper").url) 36 | return 37 | 38 | 39 | @app.cell 40 | def _(wikipedia): 41 | gt = wikipedia.WikipediaPage("graph theory") 42 | return (gt,) 43 | 44 | 45 | @app.cell 46 | def _(gt): 47 | print(gt.html()) 48 | return 49 | 50 | 51 | @app.cell 52 | def _(gt): 53 | gt.images 54 | return 55 | 56 | 57 | @app.cell 58 | def _(gt): 59 | gt.categories 60 | return 61 | 62 | 63 | @app.cell 64 | def _(gt): 65 | gt.url 66 | return 67 | 68 | 69 | @app.cell 70 | def _(gt): 71 | print(gt.summary) 72 | return 73 | 74 | 75 | @app.cell 76 | def _(gt): 77 | gt.section() 78 | 79 | return 80 | 81 | 82 | @app.cell 83 | def _(): 84 | import wikidata 85 | return 86 | 87 | 88 | @app.cell 89 | def _(): 90 | from typing import Dict, Any, List, Optional 91 | return Any, Dict 92 | 93 | 94 | @app.cell 95 | def _(Any, Dict, wikipedia): 96 | class wikiFetcher: 97 | 98 | @staticmethod 99 | def fetch_page(title: str) -> Dict[str, Any]: 100 | """ 101 | Retrieve a Wikipedia page by title and return core fields. 102 | 103 | Args: 104 | title (str): The title of the Wikipedia page to fetch. 105 | 106 | Returns: 107 | Dict with keys: 108 | - title: str 109 | - url: str 110 | - summary: str 111 | - content: str 112 | - images: List[Dict] 113 | - links: List[str] 114 | - references: List[str] 115 | 116 | """ 117 | page = wikipedia.page(title) 118 | data = { 119 | "title": page.title, 120 | "url": page.url, 121 | "summary": page.summary, 122 | "content": page.content, 123 | "images": page.images, 124 | "links": page.links, 125 | } 126 | # references attributes may not exist in older wikipedia module 127 | refs = getattr(page, "references", None) 128 | data["references"] = refs if isinstance(refs, list) else [] 129 | return data 130 | 131 | return (wikiFetcher,) 132 | 133 | 134 | @app.cell 135 | def _(wikiFetcher): 136 | wiki = wikiFetcher() 137 | return (wiki,) 138 | 139 | 140 | @app.cell 141 | def _(wiki): 142 | game_theory = wiki.fetch_page(title="Graph Theory") 143 | return (game_theory,) 144 | 145 | 146 | @app.cell 147 | def _(game_theory): 148 | game_theory 149 | return 150 | 151 | 152 | @app.cell 153 | def _(Any, Dict, wikipedia): 154 | def fetch_page(title: str) -> Dict[str, Any]: 155 | """ 156 | Retrieve a Wikipedia page by title and return core fields. 157 | 158 | Args: 159 | title (str): The title of the Wikipedia page to fetch. 160 | 161 | Returns: 162 | Dict with keys: 163 | - title: str 164 | - summary: str 165 | - content: str 166 | - images: List[Dict] 167 | - links: List[str] 168 | - references: List[str] 169 | 170 | """ 171 | page = wikipedia.page(title) 172 | data = { 173 | "title": page.title, 174 | "summary": page.summary, 175 | "content": page.content, 176 | "images": page.images, 177 | "links": page.links, 178 | } 179 | # references attributes may not exist in older wikipedia module 180 | refs = getattr(page, "references", None) 181 | data["references"] = refs if isinstance(refs, list) else [] 182 | return data 183 | return (fetch_page,) 184 | 185 | 186 | @app.cell 187 | def _(fetch_page): 188 | theory = fetch_page(title= "Graph Theory") 189 | return (theory,) 190 | 191 | 192 | @app.cell 193 | def _(fetch_page): 194 | fetch_page(title="Graph Theory") 195 | return 196 | 197 | 198 | @app.cell 199 | def _(theory): 200 | print(theory['content']) 201 | return 202 | 203 | 204 | @app.cell 205 | def _(): 206 | import graphfaker 207 | return 208 | 209 | 210 | @app.cell 211 | def _(): 212 | from graphfaker import WikiFetcher 213 | return 214 | 215 | 216 | @app.cell 217 | def _(wikiFetcher): 218 | wiki2 = wikiFetcher() 219 | return 220 | 221 | 222 | @app.cell 223 | def _(wiki): 224 | page = wiki.fetch_page("Graph Theory") 225 | return (page,) 226 | 227 | 228 | @app.cell 229 | def _(page): 230 | page['url'] 231 | return 232 | 233 | 234 | if __name__ == "__main__": 235 | app.run() 236 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # graphfaker documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another 16 | # directory, add these directories to sys.path here. If the directory is 17 | # relative to the documentation root, use os.path.abspath to make it 18 | # absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | import graphfaker 25 | 26 | # -- General configuration --------------------------------------------- 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 34 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode','myst_nb'] 35 | 36 | nb_execution_mode = 'off' 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | # source_suffix = ['.rst', '.md'] 44 | source_suffix = { 45 | '.rst': 'restructuredtext', 46 | '.ipynb': 'myst-nb', 47 | } 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = 'graphfaker' 54 | copyright = "2025, Dennis Irorere" 55 | author = "Dennis Irorere" 56 | 57 | # The version info for the project you're documenting, acts as replacement 58 | # for |version| and |release|, also used in various other places throughout 59 | # the built documents. 60 | # 61 | # The short X.Y version. 62 | version = graphfaker.__version__ 63 | # The full version, including alpha/beta/rc tags. 64 | release = graphfaker.__version__ 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = None 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This patterns also effect to html_static_path and html_extra_path 76 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # If true, `todo` and `todoList` produce output, else they produce nothing. 82 | todo_include_todos = False 83 | 84 | 85 | # -- Options for HTML output ------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | # 90 | html_theme = 'alabaster' 91 | 92 | # Theme options are theme-specific and customize the look and feel of a 93 | # theme further. For a list of options available for each theme, see the 94 | # documentation. 95 | # 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom static files (such as style sheets) here, 99 | # relative to this directory. They are copied after the builtin static files, 100 | # so a file named "default.css" will overwrite the builtin "default.css". 101 | html_static_path = ['_static'] 102 | 103 | 104 | # -- Options for HTMLHelp output --------------------------------------- 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'graphfakerdoc' 108 | 109 | 110 | # -- Options for LaTeX output ------------------------------------------ 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, author, documentclass 132 | # [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'graphfaker.tex', 135 | 'graphfaker Documentation', 136 | 'Dennis Irorere', 'manual'), 137 | ] 138 | 139 | 140 | # -- Options for manual page output ------------------------------------ 141 | 142 | # One entry per manual page. List of tuples 143 | # (source start file, name, description, authors, manual section). 144 | man_pages = [ 145 | (master_doc, 'graphfaker', 146 | 'graphfaker Documentation', 147 | [author], 1) 148 | ] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | (master_doc, 'graphfaker', 158 | 'graphfaker Documentation', 159 | author, 160 | 'graphfaker', 161 | 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /examples/sqarql.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import requests 3 | import json 4 | import random 5 | from faker import Faker 6 | from graphfaker.logger import logger 7 | 8 | fake = Faker() 9 | 10 | WIKIDATA_SPARQL_URL = "https://query.wikidata.org/sparql" 11 | OVERPASS_API_URL = "http://overpass-api.de/api/interpreter" 12 | 13 | 14 | class WikidataFetcher: 15 | @staticmethod 16 | def run_sparql_query(query): 17 | """Fetch data from Wikidata using SPARQL.""" 18 | headers = {"Accept": "application/json"} 19 | response = requests.get( 20 | WIKIDATA_SPARQL_URL, 21 | params={"query": query, "format": "json"}, 22 | headers=headers, 23 | ) 24 | if response.status_code == 200: 25 | return response.json()["results"]["bindings"] 26 | else: 27 | logger.error( 28 | "SPARQL Query Failed! Error: %s", response.status_code, exc_info=True 29 | ) 30 | 31 | return [] 32 | 33 | @staticmethod 34 | def fetch_ceos_and_companies(): 35 | """Fetch CEOs and their companies from Wikidata.""" 36 | query = """ 37 | SELECT ?ceo ?ceoLabel ?company ?companyLabel WHERE { 38 | ?company wdt:P169 ?ceo. 39 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 40 | } 41 | LIMIT 20 42 | """ 43 | data = WikidataFetcher.run_sparql_query(query) 44 | G = nx.DiGraph() 45 | 46 | for item in data: 47 | ceo_id = item["ceo"]["value"].split("/")[-1] 48 | company_id = item["company"]["value"].split("/")[-1] 49 | ceo_name = item["ceoLabel"]["value"] 50 | company_name = item["companyLabel"]["value"] 51 | 52 | G.add_node(ceo_id, type="Person", name=ceo_name) 53 | G.add_node(company_id, type="Organization", name=company_name) 54 | G.add_edge(ceo_id, company_id, relationship="CEO_of") 55 | 56 | return G 57 | 58 | @staticmethod 59 | def fetch_places(): 60 | """Fetch major cities from Wikidata.""" 61 | query = """ 62 | SELECT ?city ?cityLabel WHERE { 63 | ?city wdt:P31 wd:Q515. 64 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 65 | } 66 | LIMIT 20 67 | """ 68 | data = WikidataFetcher.run_sparql_query(query) 69 | G = nx.DiGraph() 70 | 71 | for item in data: 72 | city_id = item["city"]["value"].split("/")[-1] 73 | city_name = item["cityLabel"]["value"] 74 | 75 | G.add_node(city_id, type="Place", name=city_name) 76 | 77 | return G 78 | 79 | @staticmethod 80 | def fetch_organizations(): 81 | """Fetch major organizations from Wikidata.""" 82 | query = """ 83 | SELECT ?org ?orgLabel WHERE { 84 | ?org wdt:P31 wd:Q43229. 85 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 86 | } 87 | LIMIT 20 88 | """ 89 | data = WikidataFetcher.run_sparql_query(query) 90 | G = nx.DiGraph() 91 | 92 | for item in data: 93 | org_id = item["org"]["value"].split("/")[-1] 94 | org_name = item["orgLabel"]["value"] 95 | 96 | G.add_node(org_id, type="Organization", name=org_name) 97 | 98 | return G 99 | 100 | 101 | class OverpassFetcher: 102 | @staticmethod 103 | def fetch_transportation_network( 104 | bbox="-0.489,51.28,0.236,51.686", 105 | ): # London Bounding Box 106 | """Fetch transportation network from OpenStreetMap using Overpass API.""" 107 | query = f""" 108 | [out:json]; 109 | ( 110 | way["highway"]({bbox}); 111 | relation["route"]({bbox}); 112 | ); 113 | out body; 114 | >; 115 | out skel qt; 116 | """ 117 | response = requests.get(OVERPASS_API_URL, params={"data": query}) 118 | if response.status_code == 200: 119 | data = response.json()["elements"] 120 | G = nx.Graph() 121 | for elem in data: 122 | if elem["type"] == "node": 123 | G.add_node(elem["id"], lat=elem["lat"], lon=elem["lon"]) 124 | elif elem["type"] == "way": 125 | nodes = elem["nodes"] 126 | for i in range(len(nodes) - 1): 127 | G.add_edge(nodes[i], nodes[i + 1], type="road") 128 | return G 129 | else: 130 | logger.error( 131 | "Overpass Query Failed! Error: %s", response.status_code, exc_info=True 132 | ) 133 | 134 | return None 135 | 136 | 137 | class Graphfaker: 138 | @staticmethod 139 | def generate_people(num=10): 140 | G = nx.Graph() 141 | for i in range(num): 142 | pid = f"person_{i}" 143 | G.add_node( 144 | pid, 145 | type="Person", 146 | name=fake.name(), 147 | email=fake.email(), 148 | age=random.randint(18, 80), 149 | ) 150 | return G 151 | 152 | @staticmethod 153 | def generate_places(num=5): 154 | G = nx.Graph() 155 | for i in range(num): 156 | cid = f"city_{i}" 157 | G.add_node(cid, type="Place", name=fake.city(), country=fake.country()) 158 | return G 159 | 160 | @staticmethod 161 | def generate_organizations(num=5): 162 | G = nx.Graph() 163 | for i in range(num): 164 | oid = f"org_{i}" 165 | G.add_node( 166 | oid, type="Organization", name=fake.company(), industry=fake.job() 167 | ) 168 | return G 169 | 170 | @staticmethod 171 | def connect_people_to_organizations(G, people_nodes, org_nodes): 172 | for p in people_nodes: 173 | org = random.choice(org_nodes) 174 | G.add_edge( 175 | p, org, relationship=random.choice(["works_at", "consults_for", "owns"]) 176 | ) 177 | 178 | @staticmethod 179 | def connect_people_to_places(G, people_nodes, place_nodes): 180 | for p in people_nodes: 181 | place = random.choice(place_nodes) 182 | G.add_edge(p, place, relationship=random.choice(["lives_in", "born_in"])) 183 | 184 | 185 | # Example Usage 186 | if __name__ == "__main__": 187 | G_ceos = WikidataFetcher.fetch_ceos_and_companies() 188 | G_places = WikidataFetcher.fetch_places() 189 | G_orgs = WikidataFetcher.fetch_organizations() 190 | G_transport = OverpassFetcher.fetch_transportation_network() 191 | 192 | G_people = Graphfaker.generate_people(10) 193 | G_fake_places = Graphfaker.generate_places(5) 194 | G_fake_orgs = Graphfaker.generate_organizations(5) 195 | 196 | Graphfaker.connect_people_to_organizations( 197 | G_people, list(G_people.nodes), list(G_fake_orgs.nodes) 198 | ) 199 | Graphfaker.connect_people_to_places( 200 | G_people, list(G_people.nodes), list(G_fake_places.nodes) 201 | ) 202 | 203 | logger.info( 204 | "Graph Summary:\n" 205 | f" CEOs and Companies: {len(G_ceos.nodes())} nodes.\n" 206 | f" Places: {len(G_places.nodes())} nodes.\n" 207 | f" Organizations: {len(G_orgs.nodes())} nodes.\n" 208 | f" Transportation Network: {len(G_transport.nodes()) if G_transport else 'Failed to fetch'} nodes.\n" 209 | f" Synthetic People: {len(G_people.nodes())} nodes." 210 | ) 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphfaker 2 | 3 | graphfaker is a Python library for generating and loading synthetic and real-world datasets tailored for graph-based applications. It supports `faker` as social graph, OpenStreetMap (OSM) road networks, and real airline flight networks. Use it for data science, research, teaching, rapid prototyping, and more! 4 | 5 | *Note: The authors and graphgeeks labs do not hold any responsibility for the correctness of this generator.* 6 | 7 | [![PyPI version](https://img.shields.io/pypi/v/graphfaker.svg)](https://pypi.python.org/pypi/graphfaker) 8 | [![Docs Status](https://readthedocs.org/projects/graphfaker/badge/?version=latest)](https://graphfaker.readthedocs.io/en/latest/?version=latest) 9 | [![Dependency Status](https://pyup.io/repos/github/denironyx/graphfaker/shield.svg)](https://pyup.io/repos/github/denironyx/graphfaker/) 10 | [![image](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 11 | 12 | --- 13 | 14 | Join our Discord server 👇 15 | 16 | [![](https://dcbadge.limes.pink/api/server/https://discord.gg/mQQz9bRRpH)](https://discord.gg/mQQz9bRRpH) 17 | 18 | 19 | ### Problem Statement 20 | Graph data is essential for solving complex problems in various fields, including social network analysis, transportation modeling, recommendation systems, and fraud detection. However, many professionals, researchers, and students face a common challenge: a lack of easily accessible, realistic graph datasets for testing, learning, and benchmarking. Real-world graph data is often restricted due to privacy concerns, complexity, or large size, making experimentation difficult. 21 | 22 | ### Solution: graphfaker 23 | GraphFaker is an open-source Python library designed to generate, load, and export synthetic graph datasets in a user-friendly and configurable way. It enables users to generate graph tailored to their specific needs, allowing for better experimentation and learning without needing to think about where the data is coming from or how to fetch the data. 24 | 25 | ## Features 26 | - **Multiple Graph Sources:** 27 | - `faker`: Synthetic “social-knowledge” graphs powered by Faker (people, places, organizations, events, products with rich attributes and relationships) 28 | - `osm`: Real-world street networks directly from OpenStreetMap (by place name, address, or bounding box) 29 | - `flights`: Flight/airline networks from Bureau of Transportation Statistics (airlines ↔ airports ↔ flight legs, complete with cancellation and delay flags) 30 | - **Unstructured Data Source:** 31 | - `WikiFetcher`: Raw Wikipedia page data (title, summary, content, sections, links, references) ready for custom graph or RAG pipelines 32 | - **Easy CLI & Python Library** 33 | 34 | This removes friction around data acquisition, letting you focus on algorithms, teaching or rapid prototyping. 35 | 36 | ## ✨ Key Features 37 | 38 | | Source | What It Gives You | 39 | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 40 | | **Faker** | Synthetic social-knowledge graphs with configurable sizes, weighted and directional relationships. | 41 | | **OSM** | Real road or walking networks via OSMnx under the hood—fetch by place, address, or bounding box; simplify topology; project to UTM. | 42 | | **Flights** | Airline/airport graph from BTS on-time performance data: nodes for carriers, airports, flights; edges for OPERATED\_BY, DEPARTS\_FROM, ARRIVES\_AT; batch or date-range support; subgraph sampling. | 43 | | **WikiFetcher** | Raw page dumps (title, summary, content, sections, links, references) as JSON | 44 | 45 | 46 | --- 47 | 48 | *Disclaimer: This is still a work in progress (WIP). With logging and debugging print statement. Our goal for releasing early is to get feedback and reiterate.* 49 | 50 | ## Installation 51 | 52 | Install from PyPI: 53 | ```sh 54 | uv pip install graphfaker 55 | ``` 56 | 57 | For development: 58 | ```sh 59 | git clone https://github.com/graphgeeks-lab/graphfaker.git 60 | cd graphfaker 61 | uv pip install -e . 62 | ``` 63 | 64 | --- 65 | 66 | ## Quick Start 67 | 68 | --- 69 | 70 | ### Python Library Usage 71 | 72 | ```python 73 | from graphfaker import GraphFaker 74 | 75 | gf = GraphFaker() 76 | # Synthetic social/knowledge graph 77 | g1 = gf.generate_graph(source="faker", total_nodes=200, total_edges=800) 78 | # OSM road network 79 | g2 = gf.generate_graph(source="osm", place="Chinatown, San Francisco, California", network_type="drive") 80 | # Flight network 81 | g3 = gf.generate_graph(source="flights", year=2024, month=1) 82 | 83 | # Fetch Wikipedia page data 84 | from graphfaker import WikiFetcher 85 | page = WikiFetcher.fetch_page("Graph theory") 86 | print(page['summary']) 87 | print(page['content']) 88 | WikiFetcher.export_page_json(page, "graph_theory.json") 89 | 90 | ``` 91 | 92 | #### Advanced: Date Range for Flights 93 | 94 | Note this isn't recommended and it's still being tested. We are working on ways to make this faster. 95 | 96 | ```python 97 | g = gf.generate_graph(source="flights", date_range=("2024-01-01", "2024-01-15")) 98 | ``` 99 | 100 | 101 | ### CLI Usage (WIP) 102 | 103 | Show help: 104 | ```sh 105 | python -m graphfaker.cli --help 106 | ``` 107 | 108 | #### Generate a Synthetic Social Graph 109 | ```sh 110 | python -m graphfaker.cli \ 111 | --fetcher faker \ 112 | --total-nodes 100 \ 113 | --total-edges 500 114 | ``` 115 | 116 | #### Generate a Real-World Road Network (OSM) 117 | ```sh 118 | python -m graphfaker.cli \ 119 | --fetcher osm \ 120 | --place "Berlin, Germany" \ 121 | --network-type drive 122 | ``` 123 | 124 | #### Generate a Flight Network (Airlines/Airports/Flights) 125 | ```sh 126 | python -m graphfaker.cli \ 127 | --fetcher flights \ 128 | --country "United States" \ 129 | --year 2024 \ 130 | --month 1 131 | ``` 132 | 133 | You can also use `--date-range` for custom time spans (e.g., `--date-range "2024-01-01,2024-01-15"`). 134 | 135 | --- 136 | 137 | ## Future Plans: Graph Export Formats 138 | 139 | - **GraphML**: General graph analysis/visualization (`--export graph.graphml`) 140 | - **JSON/JSON-LD**: Knowledge graphs/web apps (`--export data.json`) 141 | - **CSV**: Tabular analysis/database imports (`--export edges.csv`) 142 | - **RDF**: Semantic web/linked data (`--export graph.ttl`) 143 | 144 | --- 145 | 146 | ## Future Plans: Integration with Graph Tools 147 | 148 | GraphFaker generates NetworkX graph objects that can be easily integrated with: 149 | - **Graph databases**: Neo4j, Kuzu, TigerGraph 150 | - **Analysis tools**: NetworkX, SNAP, graph-tool 151 | - **ML frameworks**: PyTorch Geometric, DGL, TensorFlow GNN 152 | - **Visualization**: G.V, Gephi, Cytoscape, D3.js 153 | 154 | --- 155 | 156 | ## On the Horizon: 157 | 158 | - Handling large graph -> millions of nodes 159 | - Using NLP/LLM to fetch graph data -> "Fetch flight data for Jan 2024" 160 | - Connects to any graph database/engine of choice -> "Establish connections to graph database/engine of choice" 161 | 162 | 163 | --- 164 | 165 | ## Documentation 166 | 167 | Full documentation: https://graphfaker.readthedocs.io 168 | 169 | --- 170 | ⭐ Star the Repo 171 | 172 | If you find this project valuable, star ⭐ this repository to support the work and help others discover it! 173 | 174 | --- 175 | 176 | ## License 177 | MIT License 178 | 179 | ## Credits 180 | Created with Cookiecutter and the `audreyr/cookiecutter-pypackage` project template. 181 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | graphfaker 2 | ========== 3 | 4 | graphfaker is a python library for generating and loading synthetic and real-world datasets tailored for graph-based applications. It supports `faker` as social graph, OpenStreetMap (OSM) road networks, and real airline flight networks. Use it for data science, research, teaching, rapid prototyping, and more! 5 | 6 | *Note: The authors and GraphGeeks Labs do not hold any responsibility for the correctness of this generator.* 7 | 8 | .. image:: https://img.shields.io/pypi/v/graphfaker.svg 9 | :target: https://pypi.python.org/pypi/graphfaker 10 | 11 | .. image:: https://readthedocs.org/projects/graphfaker/badge/?version=latest 12 | :target: https://graphfaker.readthedocs.io/en/latest/?version=latest 13 | 14 | .. image:: https://pyup.io/repos/github/denironyx/graphfaker/shield.svg 15 | :target: https://pyup.io/repos/github/denironyx/graphfaker/ 16 | 17 | .. image:: https://img.shields.io/badge/License-MIT-yellow.svg 18 | :target: https://opensource.org/licenses/MIT 19 | 20 | ---- 21 | 22 | Join our Discord server 23 | 24 | .. image:: https://dcbadge.limes.pink/api/server/https://discord.gg/mQQz9bRRpH 25 | :target: https://discord.gg/mQQz9bRRpH 26 | 27 | Problem Statement 28 | ----------------- 29 | 30 | Graph data is essential for solving complex problems in various fields, including social network analysis, transportation modeling, recommendation systems, and fraud detection. However, many professionals, researchers, and students face a common challenge: a lack of easily accessible, realistic graph datasets for testing, learning, and benchmarking. Real-world graph data is often restricted due to privacy concerns, complexity, or large size, making experimentation difficult. 31 | 32 | Solution: graphfaker 33 | -------------------- 34 | 35 | GraphFaker is an open-source Python library designed to generate, load, and export synthetic graph datasets in a user-friendly and configurable way. It enables users to generate graphs tailored to their specific needs, allowing for better experimentation and learning without needing to think about where the data is coming from or how to fetch the data. 36 | 37 | Features 38 | ======== 39 | 40 | - **Multiple Graph Sources:** 41 | - ``faker``: Synthetic “social-knowledge” graphs powered by Faker (people, places, organizations, events, products with rich attributes and relationships) 42 | - ``osm``: Real-world street networks directly from OpenStreetMap (by place name, address, or bounding box) 43 | - ``flights``: Flight/airline networks from Bureau of Transportation Statistics (airlines ↔ airports ↔ flight legs, complete with cancellation and delay flags) 44 | 45 | - **Unstructured Data Source:** 46 | - ``WikiFetcher``: Raw Wikipedia page data (title, summary, content, sections, links, references) ready for custom graph or entity recognition 47 | 48 | - **Easy CLI & Python Library** 49 | 50 | 51 | *Vision:: To remove friction around data acquisition, letting you focus on algorithms, teaching, or rapid prototyping.* 52 | 53 | 54 | Key Features 55 | ============ 56 | 57 | .. list-table:: 58 | :header-rows: 1 59 | :widths: 15 85 60 | 61 | * - Source 62 | - What It Gives You 63 | * - **Faker** 64 | - Synthetic social-knowledge graphs with configurable sizes, weighted and directional relationships. 65 | * - **OSM** 66 | - Real road or walking networks via OSMnx under the hood—fetch by place, address, or bounding box; simplify topology; project to UTM. 67 | * - **Flights** 68 | - Airline/airport graph from BTS on-time performance data: nodes for carriers, airports, flights; edges for OPERATED_BY, DEPARTS_FROM, ARRIVES_AT; batch or date-range support; subgraph sampling. 69 | * - **Wikipedia** 70 | - Raw page dumps (title, summary, content, sections, links, references) as JSON. 71 | 72 | .. note:: 73 | 74 | This is still a work in progress (WIP). Includes logging and debugging print statements. Our goal for releasing early is to get feedback and reiterate. 75 | 76 | Installation 77 | ------------ 78 | 79 | Install from PyPI: 80 | 81 | .. code-block:: shell 82 | 83 | uv pip install graphfaker 84 | 85 | For development: 86 | 87 | .. code-block:: shell 88 | 89 | git clone https://github.com/graphgeeks-lab/graphfaker.git 90 | cd graphfaker 91 | uv pip install -e . 92 | 93 | Quick Start 94 | ----------- 95 | 96 | Python Library Usage 97 | ^^^^^^^^^^^^^^^^^^^^ 98 | 99 | .. code-block:: python 100 | 101 | from graphfaker import GraphFaker 102 | 103 | gf = GraphFaker() 104 | # Synthetic social/knowledge graph 105 | g1 = gf.generate_graph(source="faker", total_nodes=200, total_edges=800) 106 | # OSM road network 107 | g2 = gf.generate_graph(source="osm", place="Chinatown, San Francisco, California", network_type="drive") 108 | # Flight network 109 | g3 = gf.generate_graph(source="flights", year=2024, month=1) 110 | 111 | # Fetch Wikipedia page data 112 | from graphfaker import WikiFetcher 113 | page = WikiFetcher.fetch_page("Graph theory") 114 | print(page['summary']) 115 | print(page['content']) 116 | WikiFetcher.export_page_json(page, "graph_theory.json") 117 | 118 | Advanced: Date Range for Flights 119 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 120 | 121 | Note this isn't recommended and it's still being tested. We are working on ways to make this faster. 122 | 123 | .. code-block:: python 124 | 125 | g = gf.generate_graph(source="flights", country="United States", date_range=("2024-01-01", "2024-01-15")) 126 | 127 | CLI Usage (WIP) 128 | ^^^^^^^^^^^^^^^ 129 | 130 | Show help: 131 | 132 | .. code-block:: shell 133 | 134 | python -m graphfaker.cli --help 135 | 136 | Generate a Synthetic Social Graph: 137 | 138 | .. code-block:: shell 139 | 140 | python -m graphfaker.cli gen \ 141 | --source faker \ 142 | --total-nodes 100 \ 143 | --total-edges 500 144 | 145 | Generate a Real-World Road Network (OSM): 146 | 147 | .. code-block:: shell 148 | 149 | python -m graphfaker.cli gen \ 150 | --source osm \ 151 | --place "Berlin, Germany" \ 152 | --network-type drive \ 153 | --export berlin.graphml 154 | 155 | Generate a Flight Network (Airlines/Airports/Flights): 156 | 157 | .. code-block:: shell 158 | 159 | python -m graphfaker.cli gen \ 160 | --source flights \ 161 | --country "United States" \ 162 | --year 2024 \ 163 | --month 1 164 | 165 | You can also use `--date-range` for custom time spans (e.g., `--date-range "2024-01-01,2024-01-15"`). 166 | 167 | Future Plans: Graph Export Formats 168 | ---------------------------------- 169 | 170 | - **GraphML**: General graph analysis/visualization (`--export graph.graphml`) 171 | - **JSON/JSON-LD**: Knowledge graphs/web apps (`--export data.json`) 172 | - **CSV**: Tabular analysis/database imports (`--export edges.csv`) 173 | - **RDF**: Semantic web/linked data (`--export graph.ttl`) 174 | 175 | Future Plans: Integration with Graph Tools 176 | ------------------------------------------ 177 | 178 | GraphFaker generates NetworkX graph objects that can be easily integrated with: 179 | 180 | - **Graph databases**: Neo4j, Kuzu, TigerGraph 181 | - **Analysis tools**: NetworkX, SNAP, graph-tool 182 | - **ML frameworks**: PyTorch Geometric, DGL, TensorFlow GNN 183 | - **Visualization**: G.V, Gephi, Cytoscape, D3.js 184 | 185 | 186 | What's on the Horizon? 187 | ---------------------- 188 | 189 | - Handling large graph -> millions of nodes 190 | - Using NLP/LLM to fetch graph data -> "Fetch flight data for Jan 2024" 191 | - Connects to any graph database/engine of choice -> "Establish connections to graph database/engine of choice" 192 | 193 | 194 | Documentation 195 | ------------- 196 | 197 | Full documentation: https://graphfaker.readthedocs.io 198 | 199 | Star the Repo ⭐ 200 | --------------- 201 | 202 | If you find this project valuable, star ⭐ this repository to support the work and help others discover it! 203 | 204 | License 205 | ------- 206 | 207 | MIT License 208 | 209 | Credits 210 | ------- 211 | 212 | Created with Cookiecutter and the `audreyr/cookiecutter-pypackage` project template. 213 | -------------------------------------------------------------------------------- /examples/experiment_wikidata.py: -------------------------------------------------------------------------------- 1 | import marimo 2 | from graphfaker.logger import logger 3 | 4 | __generated_with = "0.13.1" 5 | app = marimo.App() 6 | 7 | 8 | @app.cell 9 | def _(): 10 | import pandas as pd 11 | import SPARQLWrapper as sw 12 | 13 | return (pd,) 14 | 15 | 16 | @app.cell 17 | def _(): 18 | import requests 19 | 20 | return (requests,) 21 | 22 | 23 | @app.cell 24 | def _(): 25 | import networkx as nx 26 | import json 27 | import random 28 | from faker import Faker 29 | 30 | fake = Faker() 31 | 32 | WIKIDATA_SPARQL_URL = "https://query.wikidata.org/sparql" 33 | OVERPASS_API_URL = "http://overpass-api.de/api/interpreter" 34 | return Faker, WIKIDATA_SPARQL_URL, fake, nx, random 35 | 36 | 37 | @app.cell 38 | def _(WIKIDATA_SPARQL_URL, requests): 39 | def run_sparql_query(query): 40 | """Fetch data from Wikidata using SPARQL.""" 41 | headers = {"Accept": "application/json"} 42 | response = requests.get( 43 | WIKIDATA_SPARQL_URL, 44 | params={"query": query, "format": "json"}, 45 | headers=headers, 46 | ) 47 | if response.status_code == 200: 48 | return response.json()["results"]["bindings"] 49 | else: 50 | logger.error( 51 | "SPARQL Query Failed! Error: %s", response.status_code, exc_info=True 52 | ) 53 | return [] 54 | 55 | return (run_sparql_query,) 56 | 57 | 58 | @app.cell 59 | def _(nx, run_sparql_query): 60 | def fetch_ceos_and_companies(): 61 | """Fetch CEOs and their companies from Wikidata.""" 62 | query = """ 63 | SELECT ?ceo ?ceoLabel ?company ?companyLabel WHERE { 64 | ?company wdt:P169 ?ceo. 65 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 66 | } 67 | LIMIT 20 68 | """ 69 | data = run_sparql_query(query) 70 | G = nx.DiGraph() 71 | 72 | for item in data: 73 | ceo_id = item["ceo"]["value"].split("/")[-1] 74 | company_id = item["company"]["value"].split("/")[-1] 75 | ceo_name = item["ceoLabel"]["value"] 76 | company_name = item["companyLabel"]["value"] 77 | 78 | G.add_node(ceo_id, type="Person", name=ceo_name) 79 | G.add_node(company_id, type="Organization", name=company_name) 80 | G.add_edge(ceo_id, company_id, relationship="CEO_of") 81 | 82 | return G 83 | 84 | return (fetch_ceos_and_companies,) 85 | 86 | 87 | @app.cell 88 | def _(): 89 | import matplotlib.pyplot as plt 90 | 91 | return 92 | 93 | 94 | @app.cell 95 | def _(fetch_ceos_and_companies): 96 | ceos_and_companies = fetch_ceos_and_companies() 97 | return (ceos_and_companies,) 98 | 99 | 100 | @app.cell 101 | def _(ceos_and_companies, nx): 102 | nx.draw(ceos_and_companies, with_labels=False, font_weight="bold") 103 | return 104 | 105 | 106 | @app.cell 107 | def _(fake, nx, random): 108 | class Graphfaker: 109 | @staticmethod 110 | def generate_people(num=10): 111 | G = nx.Graph() 112 | for i in range(num): 113 | pid = f"person_{i}" 114 | G.add_node( 115 | pid, 116 | type="Person", 117 | name=fake.name(), 118 | email=fake.email(), 119 | age=random.randint(18, 80), 120 | ) 121 | return G 122 | 123 | @staticmethod 124 | def generate_places(num=5): 125 | G = nx.Graph() 126 | for i in range(num): 127 | cid = f"city_{i}" 128 | G.add_node(cid, type="Place", name=fake.city(), country=fake.country()) 129 | return G 130 | 131 | @staticmethod 132 | def generate_organizations(num=5): 133 | G = nx.Graph() 134 | for i in range(num): 135 | oid = f"org_{i}" 136 | G.add_node( 137 | oid, type="Organization", name=fake.company(), industry=fake.job() 138 | ) 139 | return G 140 | 141 | @staticmethod 142 | def connect_people_to_organizations(G, people_nodes, org_nodes): 143 | for p in people_nodes: 144 | org = random.choice(org_nodes) 145 | G.add_edge( 146 | p, 147 | org, 148 | relationship=random.choice(["works_at", "consults_for", "owns"]), 149 | ) 150 | 151 | @staticmethod 152 | def connect_people_to_places(G, people_nodes, place_nodes): 153 | for p in people_nodes: 154 | place = random.choice(place_nodes) 155 | G.add_edge( 156 | p, place, relationship=random.choice(["lives_in", "born_in"]) 157 | ) 158 | 159 | return (Graphfaker,) 160 | 161 | 162 | @app.cell 163 | def _(Graphfaker): 164 | G_people = Graphfaker.generate_people(10) 165 | G_fake_places = Graphfaker.generate_places(5) 166 | G_fake_orgs = Graphfaker.generate_organizations(5) 167 | 168 | Graphfaker.connect_people_to_organizations( 169 | G_people, list(G_people.nodes), list(G_fake_orgs.nodes) 170 | ) 171 | Graphfaker.connect_people_to_places( 172 | G_people, list(G_people.nodes), list(G_fake_places.nodes) 173 | ) 174 | return (G_people,) 175 | 176 | 177 | @app.cell 178 | def _(G_people): 179 | G_people.nodes() 180 | return 181 | 182 | 183 | @app.cell 184 | def _(G_people, nx): 185 | nx.draw(G_people, with_labels=True, font_weight="bold") 186 | return 187 | 188 | 189 | @app.cell 190 | def _(ceos_and_companies): 191 | logger.info(ceos_and_companies.nodes(data=True)) 192 | return 193 | 194 | 195 | @app.cell 196 | def _(WikidataFetcher, nx): 197 | def fetch_places(): 198 | """Fetch major cities from Wikidata.""" 199 | query = """ 200 | SELECT ?city ?cityLabel WHERE { 201 | ?city wdt:P31 wd:Q515. 202 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 203 | } 204 | LIMIT 20 205 | """ 206 | data = WikidataFetcher.run_sparql_query(query) 207 | G = nx.DiGraph() 208 | 209 | for item in data: 210 | city_id = item["city"]["value"].split("/")[-1] 211 | city_name = item["cityLabel"]["value"] 212 | 213 | G.add_node(city_id, type="Place", name=city_name) 214 | 215 | return G 216 | 217 | return 218 | 219 | 220 | @app.cell 221 | def _(): 222 | query = """ 223 | SELECT ?city ?cityLabel ?location ?locationLabel ?founding_date 224 | WHERE { 225 | ?city wdt:P31/wdt:P279* wd:Q515. 226 | ?city wdt:P17 wd:Q30. 227 | ?city wdt:P625 ?location. 228 | ?city wdt:P571 ?founding_date. 229 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 230 | } 231 | """ 232 | return (query,) 233 | 234 | 235 | @app.cell 236 | def _(pd): 237 | import sys 238 | from typing import List, Dict 239 | from SPARQLWrapper import SPARQLWrapper, JSON 240 | 241 | class WikiDataQueryResults: 242 | """ 243 | A class that can be used to query data from Wikidata using SPARQL and return the results as a Pandas DataFrame or a list 244 | of values for a specific key. 245 | """ 246 | 247 | def __init__(self, query: str): 248 | """ 249 | Initializes the WikiDataQueryResults object with a SPARQL query string. 250 | :param query: A SPARQL query string. 251 | """ 252 | self.user_agent = "WDQS-example Python/%s.%s" % ( 253 | sys.version_info[0], 254 | sys.version_info[1], 255 | ) 256 | self.endpoint_url = "https://query.wikidata.org/sparql" 257 | self.sparql = SPARQLWrapper(self.endpoint_url, agent=self.user_agent) 258 | self.sparql.setQuery(query) 259 | self.sparql.setReturnFormat(JSON) 260 | 261 | def __transform2dicts(self, results: List[Dict]) -> List[Dict]: 262 | """ 263 | Helper function to transform SPARQL query results into a list of dictionaries. 264 | :param results: A list of query results returned by SPARQLWrapper. 265 | :return: A list of dictionaries, where each dictionary represents a result row and has keys corresponding to the 266 | variables in the SPARQL SELECT clause. 267 | """ 268 | new_results = [] 269 | for result in results: 270 | new_result = {} 271 | for key in result: 272 | new_result[key] = result[key]["value"] 273 | new_results.append(new_result) 274 | return new_results 275 | 276 | def _load(self) -> List[Dict]: 277 | """ 278 | Helper function that loads the data from Wikidata using the SPARQLWrapper library, and transforms the results into 279 | a list of dictionaries. 280 | :return: A list of dictionaries, where each dictionary represents a result row and has keys corresponding to the 281 | variables in the SPARQL SELECT clause. 282 | """ 283 | results = self.sparql.queryAndConvert()["results"]["bindings"] 284 | results = self.__transform2dicts(results) 285 | return results 286 | 287 | def load_as_dataframe(self) -> pd.DataFrame: 288 | """ 289 | Executes the SPARQL query and returns the results as a Pandas DataFrame. 290 | :return: A Pandas DataFrame representing the query results. 291 | """ 292 | results = self._load() 293 | return pd.DataFrame.from_dict(results) 294 | 295 | return (WikiDataQueryResults,) 296 | 297 | 298 | @app.cell 299 | def _(WikiDataQueryResults, query): 300 | data_extracter = WikiDataQueryResults(query) 301 | return (data_extracter,) 302 | 303 | 304 | @app.cell 305 | def _(data_extracter): 306 | data_extracter 307 | return 308 | 309 | 310 | @app.cell 311 | def _(data_extracter): 312 | df = data_extracter.load_as_dataframe() 313 | logger.info(df.head()) 314 | return 315 | 316 | 317 | @app.cell 318 | def _(Faker): 319 | fake_1 = Faker() 320 | return (fake_1,) 321 | 322 | 323 | @app.cell 324 | def _(fake_1): 325 | fake_1.name() 326 | return 327 | 328 | 329 | @app.cell 330 | def _(fake_1): 331 | fake_1.company() 332 | return 333 | 334 | 335 | @app.cell 336 | def _(): 337 | query_1 = '\n SELECT ?ceo ?ceoLabel ?company ?companyLabel WHERE {\n ?company wdt:P169 ?ceo.\n SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }\n }\n LIMIT 20\n ' 338 | return 339 | 340 | 341 | if __name__ == "__main__": 342 | app.run() 343 | -------------------------------------------------------------------------------- /examples/flight_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | This error is typically related to the Matplotlib backend that's being used by PyCharm's console. 3 | One workaround is to explicitly switch the backend before plotting. For example, you can try switching to 4 | TkAgg. Add this at the top of your script (before any plotting commands): 5 | """ 6 | 7 | # import os 8 | # import io 9 | # import zipfile 10 | # import warnings 11 | # from datetime import datetime, timedelta 12 | # from typing import Tuple, Optional 13 | # from io import StringIO 14 | # 15 | # import requests 16 | # import pandas as pd 17 | # from tqdm.auto import tqdm 18 | # import networkx as nx 19 | # import time 20 | # 21 | # # suppress only the single warning from unverified HTTPS 22 | # import urllib3 23 | # from urllib3.exceptions import InsecureRequestWarning 24 | # 25 | # urllib3.disable_warnings(InsecureRequestWarning) 26 | 27 | 28 | import matplotlib 29 | 30 | matplotlib.use("TkAgg") 31 | from graphfaker import GraphFaker 32 | import networkx as nx 33 | from graphfaker.logger import logger 34 | 35 | # import lxml.etree as lxmletree 36 | 37 | import matplotlib.pyplot as plt 38 | 39 | # Generate graph and visualize 40 | gf = GraphFaker() 41 | # 42 | # gd = gf.generate_graph(source='faker', total_nodes=100, total_edges=500) 43 | # 44 | # gf.visualize_graph(gd) 45 | 46 | G_flight = gf.generate_graph(source="flights", year=2024, month=1) 47 | 48 | # for n, data in G_flight.nodes(data=True): 49 | # coord = data.get('coordinates') 50 | # if isinstance(coord, tuple): 51 | # data['coordinates'] = f"{coord[0]},{coord[1]}" 52 | 53 | # for n, data in G_small.nodes(data=True): 54 | # coord = data.get('coordinates') 55 | # if isinstance(coord, tuple): 56 | # data['coordinates'] = f"{coord[0]},{coord[1]}" 57 | 58 | # for n, data in G_flight.nodes(data=True): 59 | # data.pop('coordinates', None) 60 | 61 | # 62 | # import random 63 | # #import networkx as nx 64 | # 65 | # # target number of edges 66 | # E = 1000 67 | # 68 | # # list of all directed edges (u,v) 69 | # all_edges = list(G_flight.edges()) 70 | # if len(all_edges) > E: 71 | # sampled_edges = random.sample(all_edges, E) 72 | # else: 73 | # sampled_edges = all_edges 74 | # 75 | # # induced subgraph on those edges 76 | # G_small = G_flight.edge_subgraph(sampled_edges).copy() 77 | # 78 | # print("Small graph:", G_small.number_of_nodes(), "nodes;", 79 | # G_small.number_of_edges(), "edges") 80 | # 81 | # for n, data in G_small.nodes(data=True): 82 | # coord = data.get('coordinates') 83 | # if isinstance(coord, tuple): 84 | # data['coordinates'] = f"{coord[0]},{coord[1]}" 85 | # 86 | # nx.write_gexf(G_small, "smallflightGraph.gexf") 87 | # 88 | # 89 | # for n, data in G_small.nodes(data=True): 90 | # coord = data.get('coordinates') 91 | # if isinstance(coord, tuple): 92 | # data['coordinates'] = f"{coord[0]},{coord[1]}" 93 | # 94 | # for n, data in G_connected_subgraph.nodes(data=True): 95 | # coord = data.get('coordinates') 96 | # if isinstance(coord, tuple): 97 | # data['coordinates'] = f"{coord[0]},{coord[1]}" 98 | # 99 | # nx.write_gexf(G_small, "smallflightGraph.gexf") 100 | # 101 | # print("Done exporting") 102 | # 103 | # def find_fully_connected_flights_in_graph(G): 104 | # connected_flights = [] 105 | # 106 | # for node, data in G.nodes(data=True): 107 | # if data.get('type') != 'Flight': 108 | # continue # Skip non-flights 109 | # 110 | # # Check direct connections 111 | # successors = list(G.successors(node)) 112 | # relationships = {G.edges[node, succ]['relationship']: succ for succ in successors if 'relationship' in G.edges[node, succ]} 113 | # 114 | # has_airline = 'OPERATED_BY' in relationships 115 | # has_origin = 'DEPARTS_FROM' in relationships 116 | # has_dest = 'ARRIVES_AT' in relationships 117 | # 118 | # if not (has_airline and has_origin and has_dest): 119 | # continue # Missing key relationships 120 | # 121 | # # Check if origin and dest airports are themselves connected to a City 122 | # origin_airport = relationships['DEPARTS_FROM'] 123 | # dest_airport = relationships['ARRIVES_AT'] 124 | # 125 | # origin_city_connected = False 126 | # dest_city_connected = False 127 | # 128 | # # Check outgoing edges from airports 129 | # for succ in G.successors(origin_airport): 130 | # if G.edges[origin_airport, succ].get('relationship') == 'LOCATED_IN' and G.nodes[succ].get('type') == 'City': 131 | # origin_city_connected = True 132 | # 133 | # for succ in G.successors(dest_airport): 134 | # if G.edges[dest_airport, succ].get('relationship') == 'LOCATED_IN' and G.nodes[succ].get('type') == 'City': 135 | # dest_city_connected = True 136 | # 137 | # if origin_city_connected and dest_city_connected: 138 | # connected_flights.append(node) 139 | # 140 | # print(f"Found {len(connected_flights)} fully connected flights.") 141 | # return connected_flights 142 | # 143 | # 144 | # # Find fully connected flights in G_flight 145 | # connected_flights = find_fully_connected_flights_in_graph(G_flight) 146 | # 147 | # def build_connected_flights_subgraph(G, connected_flights): 148 | # nodes_to_keep = set() 149 | # 150 | # for flight in connected_flights: 151 | # if not G.has_node(flight): 152 | # continue 153 | # 154 | # nodes_to_keep.add(flight) 155 | # 156 | # # Follow outgoing edges from the flight 157 | # for succ in G.successors(flight): 158 | # nodes_to_keep.add(succ) 159 | # 160 | # # If the successor is an Airport, find the City it's located in 161 | # if G.nodes[succ].get('type') == 'Airport': 162 | # for airport_succ in G.successors(succ): 163 | # if G.edges[succ, airport_succ].get('relationship') == 'LOCATED_IN': 164 | # nodes_to_keep.add(airport_succ) 165 | # 166 | # # Create subgraph 167 | # subG = G.subgraph(nodes_to_keep).copy() 168 | # 169 | # return subG 170 | # 171 | # 172 | # # 173 | # # import matplotlib.pyplot as plt 174 | # # import networkx as nx 175 | # # 176 | # # # Color map by node 'type' 177 | # # def get_node_color(nodetype): 178 | # # if nodetype == 'Flight': 179 | # # return 'skyblue' 180 | # # elif nodetype == 'Airport': 181 | # # return 'lightgreen' 182 | # # elif nodetype == 'Airline': 183 | # # return 'orange' 184 | # # elif nodetype == 'City': 185 | # # return 'pink' 186 | # # else: 187 | # # return 'gray' 188 | # # 189 | # # # Get layout 190 | # # pos = nx.spring_layout(G_small, seed=42) # Nice looking layout 191 | # # 192 | # # # Build color list 193 | # # node_colors = [get_node_color(G_small.nodes[n].get('type', '')) for n in G_small.nodes] 194 | # # 195 | # # # Draw nodes 196 | # # plt.figure(figsize=(14,10)) 197 | # # nx.draw_networkx_nodes(G_small, pos, node_color=node_colors, node_size=300) 198 | # # 199 | # # # Draw edges 200 | # # nx.draw_networkx_edges(G_small, pos, arrows=True) 201 | # # 202 | # # # Draw node labels (optional: flight numbers, city names) 203 | # # nx.draw_networkx_labels(G_small, pos, font_size=8) 204 | # # 205 | # # # Draw edge labels (relationship types) 206 | # # edge_labels = nx.get_edge_attributes(G_small, 'relationship') 207 | # # nx.draw_networkx_edge_labels(G_small, pos, edge_labels=edge_labels, font_size=6, label_pos=0.5) 208 | # # 209 | # # plt.title("Sample Flight Graph Visualization") 210 | # # plt.axis('off') 211 | # # plt.show() 212 | # 213 | # 214 | # 215 | # # gf.visualize_graph(G_flight) 216 | # 217 | # 218 | # # 219 | # # import random 220 | # # 221 | # # # suppose G_flight is your big graph and you want at most 1000 nodes 222 | # # N = 100 223 | # # all_nodes = list(G_flight.nodes()) 224 | # # if len(all_nodes) > N: 225 | # # sampled = random.sample(all_nodes, N) 226 | # # G_small = G_flight.subgraph(sampled).copy() 227 | # # else: 228 | # # G_small = G_flight.copy() 229 | # # 230 | # # print("Original:", G_flight.number_of_nodes(), "nodes,", G_flight.number_of_edges(), "edges") 231 | # # print("Small :", G_small.number_of_nodes(), "nodes,", G_small.number_of_edges(), "edges") 232 | # # 233 | # # gf.visualize_graph(G_small) 234 | # # # # #%matplotlib inline 235 | # 236 | # 237 | # 238 | # 239 | # 240 | # # from graphfaker.core import GraphFaker 241 | # # 242 | # # gf = GraphFaker() 243 | # # G = gf.generate_graph(total_nodes=50, total_edges=200) 244 | # # gf.visualize_graph(G, title="GraphFaker in Jupyter") 245 | # # gf.export_graph(G, path="notebook.graphml") 246 | # 247 | # import random, networkx as nx 248 | # 249 | # # target number of edges 250 | # E = 1000 251 | # 252 | # # list of all directed edges (u,v) 253 | # all_edges = list(G_flight.edges()) 254 | # if len(all_edges) > E: 255 | # sampled_edges = random.sample(all_edges, E) 256 | # else: 257 | # sampled_edges = all_edges 258 | # # 259 | # # # induced subgraph on those edges 260 | # # G_small = G_flight.edge_subgraph(sampled_edges).copy() 261 | # # 262 | # # print("Small graph:", G_small.number_of_nodes(), "nodes;", 263 | # # G_small.number_of_edges(), "edges") 264 | # 265 | # 266 | 267 | # Suppose you have a flight node id like "AA100_JFK_LAX_2024-01-05" 268 | 269 | 270 | def find_fully_connected_flights_in_graph(G): 271 | connected_flights = [] 272 | 273 | for node, data in G.nodes(data=True): 274 | if data.get("type") != "Flight": 275 | continue # Only check flights 276 | 277 | successors = list(G.successors(node)) 278 | relationships = { 279 | G.edges[node, succ]["relationship"]: succ 280 | for succ in successors 281 | if "relationship" in G.edges[node, succ] 282 | } 283 | 284 | has_airline = "OPERATED_BY" in relationships 285 | has_origin = "DEPARTS_FROM" in relationships 286 | has_dest = "ARRIVES_AT" in relationships 287 | 288 | if not (has_airline and has_origin and has_dest): 289 | continue # Skip flights not properly connected 290 | 291 | # Check Airports -> Cities 292 | origin_airport = relationships["DEPARTS_FROM"] 293 | dest_airport = relationships["ARRIVES_AT"] 294 | 295 | origin_city_connected = any( 296 | G.edges[origin_airport, succ].get("relationship") == "LOCATED_IN" 297 | for succ in G.successors(origin_airport) 298 | ) 299 | 300 | dest_city_connected = any( 301 | G.edges[dest_airport, succ].get("relationship") == "LOCATED_IN" 302 | for succ in G.successors(dest_airport) 303 | ) 304 | 305 | if origin_city_connected and dest_city_connected: 306 | connected_flights.append(node) 307 | logger.info(f"Found {len(connected_flights)} fully connected flights.") 308 | 309 | return connected_flights 310 | 311 | 312 | def build_connected_flights_subgraph(G, connected_flights, limit=1000): 313 | nodes_to_keep = set() 314 | 315 | # Pick only the first N flights 316 | selected_flights = connected_flights[:limit] 317 | 318 | for flight in selected_flights: 319 | if not G.has_node(flight): 320 | continue 321 | 322 | nodes_to_keep.add(flight) 323 | 324 | # Follow outgoing edges from the flight 325 | for succ in G.successors(flight): 326 | relationship = G.edges[flight, succ].get("relationship") 327 | 328 | if relationship in ("OPERATED_BY", "DEPARTS_FROM", "ARRIVES_AT"): 329 | nodes_to_keep.add(succ) 330 | 331 | # If it's an Airport, find the City 332 | if G.nodes[succ].get("type") == "Airport": 333 | for city_succ in G.successors(succ): 334 | if G.edges[succ, city_succ].get("relationship") == "LOCATED_IN": 335 | nodes_to_keep.add(city_succ) 336 | 337 | # Create subgraph 338 | subG = G.subgraph(nodes_to_keep).copy() 339 | return subG 340 | 341 | 342 | import networkx as nx 343 | 344 | # 1. Find fully connected flights 345 | connected_flights = find_fully_connected_flights_in_graph(G_flight) 346 | 347 | # 2. Build subgraph with 1000 flights 348 | G_sub = build_connected_flights_subgraph(G_flight, connected_flights, limit=1500) 349 | 350 | ## to export because of tuple 351 | for n, data in G_sub.nodes(data=True): 352 | coord = data.get("coordinates") 353 | if isinstance(coord, tuple): 354 | data["coordinates"] = f"{coord[0]},{coord[1]}" 355 | 356 | # 3. Export to GEXF 357 | nx.write_gexf(G_sub, "fully_connected_1500_flights.gexf") 358 | logger.info( 359 | f"✅ Exported {G_sub.number_of_nodes()} nodes and {G_sub.number_of_edges()} edges to fully_connected_1500_flights.gexf" 360 | ) 361 | -------------------------------------------------------------------------------- /graphfaker/fetchers/flights.py: -------------------------------------------------------------------------------- 1 | # graphfaker/fetchers/flights.py 2 | """ 3 | Flight network fetcher for GraphFaker. 4 | 5 | Provides methods to fetch airlines, airports, and flight performance data, 6 | then build a unified NetworkX graph of Airlines, Airports, and Flights. 7 | 8 | Node Types & Key Attributes: 9 | - Airline: carrier (IATA code), airline_name 10 | - Airport: faa code, name, city, country, coordinates 11 | - Flight: flight identifier, cancelled (bool), delayed (bool), date 12 | 13 | Relationships: 14 | - (Flight) -[OPERATED_BY]-> (Airline) 15 | - (Flight) -[DEPARTS_FROM]-> (Airport) 16 | - (Flight) -[ARRIVES_AT]-> (Airport) 17 | 18 | Usage: 19 | from graphfaker.fetchers.flights import FlightGraphFetcher 20 | airlines_df = FlightGraphFetcher.fetch_airlines() 21 | airports_df = FlightGraphFetcher.fetch_airports(country="United States") 22 | flights_df = FlightGraphFetcher.fetch_flights(year=2024, month=1) 23 | G = FlightGraphFetcher.build_graph(airlines_df, airports_df, flights_df) 24 | """ 25 | import os 26 | import io 27 | import zipfile 28 | from datetime import datetime, timedelta 29 | from typing import Tuple, Optional 30 | from io import StringIO 31 | from graphfaker.logger import logger 32 | import requests 33 | import pandas as pd 34 | from tqdm.auto import tqdm 35 | import networkx as nx 36 | 37 | 38 | # suppress only the single warning from unverified HTTPS 39 | import urllib3 40 | from urllib3.exceptions import InsecureRequestWarning 41 | 42 | urllib3.disable_warnings(InsecureRequestWarning) 43 | 44 | # Data source URLs 45 | AIRLINE_LOOKUP_URL = ( 46 | "https://transtats.bts.gov/Download_Lookup.asp?Y11x72=Y_haVdhR_PNeeVRef" 47 | ) 48 | AIRPORTS_URL = ( 49 | "https://raw.githubusercontent.com/jpatokal/openflights/master/data/airports.dat" 50 | ) 51 | AIRPORT_COLS = [ 52 | "id", 53 | "name", 54 | "city", 55 | "country", 56 | "faa", 57 | "icao", 58 | "lat", 59 | "lon", 60 | "alt", 61 | "tz", 62 | "dst", 63 | "tzone", 64 | "type", 65 | "source", 66 | ] 67 | 68 | # Column mapping for BTS flight performance 69 | COLUMN_MAP = { 70 | "Year": "year", 71 | "Month": "month", 72 | "DayofMonth": "day", 73 | "DepDelay": "dep_delay", 74 | "ArrDelay": "arr_delay", 75 | "Reporting_Airline": "carrier", 76 | "Flight_Number_Reporting_Airline": "flight", 77 | "Origin": "origin", 78 | "Dest": "dest", 79 | "Tail_Number": "tail_number", 80 | } 81 | 82 | 83 | class FlightGraphFetcher: 84 | """ 85 | FlightGraphFetcher provides static methods to fetch and transform flight-related data 86 | into a NetworkX graph. 87 | 88 | Methods: 89 | fetch_airlines() -> pd.DataFrame 90 | Download the BTS airlines lookup and return a DataFrame with columns: 91 | - carrier (IATA code) 92 | - airline_name 93 | 94 | fetch_airports(country: str = None, keep_only_with_faa: bool = True) -> pd.DataFrame 95 | Download and tidy the OpenFlights airports dataset, optionally filter by country 96 | and FAA code. Returns columns: 97 | - faa, name, city, country, lat, lon 98 | 99 | fetch_flights(year: int = None, month: int = None, 100 | date_range: Optional[Tuple[Tuple[int,int], Tuple[int,int]]] = None) 101 | Fetch BTS on-time performance data for a single month or date range. Returns 102 | DataFrame with columns: 103 | - year, month, day, carrier, flight, origin, dest, cancelled, delayed 104 | 105 | build_graph(airlines_df: pd.DataFrame, 106 | airports_df: pd.DataFrame, 107 | flights_df: pd.DataFrame) -> nx.DiGraph 108 | Construct and return a directed graph with nodes and edges: 109 | • Airline nodes from airlines_df 110 | • Airport nodes from airports_df 111 | • Flight nodes from flights_df, with 'cancelled' and 'delayed' attributes 112 | • Relationships: OPERATED_BY, DEPARTS_FROM, ARRIVES_AT 113 | """ 114 | 115 | @staticmethod 116 | def fetch_airlines() -> pd.DataFrame: 117 | """ 118 | Download and tidy BTS airlines lookup table. 119 | Source: 120 | airline -> https://transtats.bts.gov/Download_Lookup.asp?Y11x72=Y_haVdhR_PNeeVRef 121 | 122 | Returns: 123 | pd.DataFrame with columns ['carrier', 'airline_name'] 124 | Raises: 125 | HTTPError if download fails. 126 | """ 127 | logger.info("Fetching airlines lookup from BTS…") 128 | resp = requests.get(AIRLINE_LOOKUP_URL, verify=False) 129 | resp.raise_for_status() 130 | df = pd.read_csv(StringIO(resp.text)) 131 | return df.rename(columns={"Code": "carrier", "Description": "airline_name"}) 132 | 133 | @staticmethod 134 | def fetch_airports( 135 | country: Optional[str] = "United States", keep_only_with_faa: bool = True 136 | ) -> pd.DataFrame: 137 | """ 138 | Download and tidy the OpenFlights airports dataset: 139 | Source: 140 | airports -> https://openflights.org/data.php 141 | 142 | Args: 143 | country: filter airports by country name (optional). 144 | keep_only_with_faa: drop records without FAA code if True. 145 | 146 | Returns: 147 | pd.DataFrame with columns ['faa','name','city','country','lat','lon'] 148 | """ 149 | logger.info("Fetching airports dataset from OpenFlights…") 150 | df = pd.read_csv( 151 | AIRPORTS_URL, 152 | header=None, 153 | names=AIRPORT_COLS, 154 | na_values=["", "NA", r"\N"], 155 | keep_default_na=True, 156 | dtype={ 157 | "id": int, 158 | "name": str, 159 | "city": str, 160 | "country": str, 161 | "faa": str, 162 | "icao": str, 163 | "lat": float, 164 | "lon": float, 165 | "alt": float, 166 | "tz": float, 167 | "dst": str, 168 | "tzone": str, 169 | "type": str, 170 | "source": str, 171 | }, 172 | ) 173 | 174 | if country: 175 | df = df[df["country"] == country] 176 | if keep_only_with_faa: 177 | df = df[df["faa"].notna() & (df["faa"] != "")] 178 | 179 | df = df.sort_values("id").drop_duplicates(subset="faa", keep="first") 180 | return df[["faa", "name", "city", "country", "lat", "lon"]].reset_index( 181 | drop=True 182 | ) 183 | 184 | @staticmethod 185 | def _download_extract_csv(url: str) -> io.BytesIO: 186 | """Stream-download a BTS zip file and return CSV data as BytesIO.""" 187 | resp = requests.get(url, stream=True, verify=False) 188 | resp.raise_for_status() 189 | buf = io.BytesIO() 190 | total = int(resp.headers.get("content-length", 0)) 191 | with tqdm.wrapattr( 192 | resp.raw, "read", total=total, desc=os.path.basename(url), leave=False 193 | ) as r: 194 | buf.write(r.read()) 195 | buf.seek(0) 196 | with zipfile.ZipFile(buf) as z: 197 | name = next(f for f in z.namelist() if f.lower().endswith(".csv")) 198 | return io.BytesIO(z.read(name)) 199 | 200 | @staticmethod 201 | def fetch_flights( 202 | year: Optional[int] = None, 203 | month: Optional[int] = None, 204 | date_range: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None, 205 | ) -> pd.DataFrame: 206 | """ 207 | Fetch BTS on-time performance data for a given month or a range of months. 208 | source: 209 | - https://www.transtats.bts.gov/TableInfo.asp?gnoyr_VQ=FGJ&QO_fu146_anzr=b0-gvzr&V0s1_b0yB=D 210 | 211 | 212 | Args: 213 | year: calendar year for single-month fetch. 214 | month: month (1-12) for single-month fetch. 215 | date_range: ((year0, month0), (year1, month1)) to fetch multiple months. 216 | 217 | Returns: 218 | pd.DataFrame with columns: 219 | ['year','month','day','carrier','flight','origin','dest', 220 | 'cancelled','delayed'] 221 | 222 | Raises: 223 | ValueError if neither valid year/month nor date_range provided. 224 | """ 225 | logger.info( 226 | f"Fetching flight performance data for {year}-{month:02d} " 227 | f"or date range {date_range}…" 228 | ) 229 | 230 | def load_month(y, m): 231 | url = f"https://transtats.bts.gov/PREZIP/On_Time_Reporting_Carrier_On_Time_Performance_1987_present_{y}_{m}.zip" 232 | buf = FlightGraphFetcher._download_extract_csv(url) 233 | df = pd.read_csv(buf, usecols=list(COLUMN_MAP.keys())) 234 | return df.rename(columns=COLUMN_MAP) 235 | 236 | if date_range: 237 | (y0, m0), (y1, m1) = date_range 238 | cur, last = datetime(y0, m0, 1), datetime(y1, m1, 1) 239 | dfs = [] 240 | while cur <= last: 241 | dfs.append(load_month(cur.year, cur.month)) 242 | nxt = cur + timedelta(days=32) 243 | cur = datetime(nxt.year, nxt.month, 1) 244 | df = pd.concat(dfs, ignore_index=True) 245 | else: 246 | if year is None or month is None: 247 | raise ValueError("Provide year & month or date_range.") 248 | df = load_month(year, month) 249 | # derive flags 250 | df["cancelled"] = df["dep_delay"].isna() 251 | df["delayed"] = df["arr_delay"] > 15 252 | 253 | return df 254 | 255 | @staticmethod 256 | def build_graph( 257 | airlines_df: pd.DataFrame, airports_df: pd.DataFrame, flights_df: pd.DataFrame 258 | ) -> nx.DiGraph: 259 | import time 260 | 261 | t0 = time.time() 262 | G = nx.DiGraph() 263 | 264 | # 1) Airlines 265 | logger.info(f"Adding {len(airlines_df)} airlines…") 266 | 267 | for _, r in airlines_df.iterrows(): 268 | G.add_node(r["carrier"], type="Airline", name=r["airline_name"]) 269 | 270 | # 2) Airports + City relationships 271 | logger.info(f"Adding {len(airports_df)} airports + city nodes…") 272 | 273 | for _, r in airports_df.iterrows(): 274 | code = r["faa"] 275 | city = r["city"] 276 | 277 | # Add Airport node 278 | G.add_node( 279 | code, 280 | type="Airport", 281 | name=r["name"], 282 | country=r["country"], 283 | coordinates=(r["lat"], r["lon"]), 284 | ) 285 | 286 | # Add City node if missing 287 | if not G.has_node(city): 288 | G.add_node(city, type="City", name=city) 289 | 290 | # Connect Airport -> City 291 | G.add_edge(code, city, relationship="LOCATED_IN") 292 | 293 | # 3) Flights + edges 294 | logger.info(f"Adding {len(flights_df)} flights + edges…") 295 | 296 | for _, r in flights_df.iterrows(): 297 | fn = f"{r['carrier']}{r['flight']}_{r['origin']}_{r['dest']}_{r['year']}-{r['month']:02d}-{r['day']:02d}" 298 | 299 | # Check if required nodes exist first 300 | carrier_exists = G.has_node(r["carrier"]) 301 | origin_exists = G.has_node(r["origin"]) 302 | dest_exists = G.has_node(r["dest"]) 303 | 304 | if not (carrier_exists and origin_exists and dest_exists): 305 | #logger.warning(f"⚠️ Skipping flight {fn}: missing carrier or airport(s)") 306 | 307 | continue # Skip this flight 308 | 309 | # Add Flight node 310 | G.add_node( 311 | fn, 312 | type="Flight", 313 | year=int(r["year"]), 314 | month=int(r["month"]), 315 | day=int(r["day"]), 316 | carrier=r["carrier"], 317 | flight_number=r["flight"], 318 | tail_number=r.get("tail_number", None), 319 | origin=r["origin"], 320 | dest=r["dest"], 321 | cancelled=bool(r["cancelled"]), 322 | delayed=bool(r["delayed"]), 323 | ) 324 | 325 | # Now safely add edges (no missing targets) 326 | G.add_edge(fn, r["carrier"], relationship="OPERATED_BY") 327 | G.add_edge(fn, r["origin"], relationship="DEPARTS_FROM") 328 | G.add_edge(fn, r["dest"], relationship="ARRIVES_AT") 329 | 330 | elapsed = time.time() - t0 331 | logger.info( 332 | f"✅ Graph built in {elapsed:.2f}s — " 333 | f"{G.number_of_nodes()} nodes, {G.number_of_edges()} edges" 334 | ) 335 | 336 | return G 337 | -------------------------------------------------------------------------------- /graphfaker/core.py: -------------------------------------------------------------------------------- 1 | """Social Knowledge Graph module. 2 | A multi-domain network connecting entities across social, geographical, and commercial dimensions. 3 | """ 4 | 5 | import os 6 | from typing import Optional 7 | import networkx as nx 8 | import random 9 | from faker import Faker 10 | from graphfaker.fetchers.osm import OSMGraphFetcher 11 | from graphfaker.fetchers.flights import FlightGraphFetcher 12 | from graphfaker.logger import logger 13 | 14 | fake = Faker() 15 | 16 | # Define subtypes for each node category 17 | PERSON_SUBTYPES = ["Student", "Professional", "Retiree", "Unemployed"] 18 | PLACE_SUBTYPES = ["City", "Park", "Restaurant", "Airport", "University"] 19 | ORG_SUBTYPES = ["TechCompany", "Hospital", "NGO", "University", "RetailChain"] 20 | EVENT_SUBTYPES = ["Concert", "Conference", "Protest", "SportsGame"] 21 | PRODUCT_SUBTYPES = ["Electronics", "Apparel", "Book", "Vehicle"] 22 | 23 | # Define relationship possibilities 24 | REL_PERSON_PERSON = ["FRIENDS_WITH", "COLLEAGUES", "MENTORS"] 25 | REL_PERSON_PLACE = ["LIVES_IN", "VISITED", "BORN_IN"] 26 | REL_PERSON_ORG = ["WORKS_AT", "STUDIED_AT", "OWNS"] 27 | REL_ORG_PLACE = ["HEADQUARTERED_IN", "HAS_BRANCH"] 28 | REL_PERSON_EVENT = ["ATTENDED", "ORGANIZED"] 29 | REL_ORG_PRODUCT = ["MANUFACTURES", "SELLS"] 30 | REL_PERSON_PRODUCT = ["PURCHASED", "REVIEWED"] 31 | 32 | # Connection probability distribution (as percentages) 33 | EDGE_DISTRIBUTION = { 34 | ("Person", "Person"): (REL_PERSON_PERSON, 0.40), 35 | ("Person", "Place"): (REL_PERSON_PLACE, 0.20), 36 | ("Person", "Organization"): (REL_PERSON_ORG, 0.15), 37 | ("Organization", "Place"): (REL_ORG_PLACE, 0.10), 38 | ("Person", "Event"): (REL_PERSON_EVENT, 0.08), 39 | ("Organization", "Product"): (REL_ORG_PRODUCT, 0.05), 40 | ("Person", "Product"): (REL_PERSON_PRODUCT, 0.02), 41 | } 42 | 43 | 44 | class GraphFaker: 45 | def __init__(self): 46 | # We'll use a directed graph for directional relationships. 47 | self.G = nx.DiGraph() 48 | 49 | def generate_nodes(self, total_nodes=100): 50 | """ 51 | Generates nodes split into: 52 | - People (50%) 53 | - Places (20%) 54 | - Organizations (15%) 55 | - Events (10%) 56 | - Products (5%) 57 | """ 58 | counts = { 59 | "Person": int(total_nodes * 0.50), 60 | "Place": int(total_nodes * 0.20), 61 | "Organization": int(total_nodes * 0.15), 62 | "Event": int(total_nodes * 0.10), 63 | } 64 | # Remaining nodes will be Products 65 | counts["Product"] = total_nodes - sum(counts.values()) 66 | 67 | # Generate People 68 | for i in range(counts["Person"]): 69 | node_id = f"person_{i}" 70 | subtype = random.choice(PERSON_SUBTYPES) 71 | self.G.add_node( 72 | node_id, 73 | type="Person", 74 | name=fake.name(), 75 | age=random.randint(18, 80), 76 | occupation=fake.job(), 77 | email=fake.email(), 78 | education_level=random.choice( 79 | ["High School", "Bachelor", "Master", "PhD"] 80 | ), 81 | skills=", ".join(fake.words(nb=3)), 82 | subtype=subtype, 83 | ) 84 | # Generate Places 85 | for i in range(counts["Place"]): 86 | node_id = f"place_{i}" 87 | subtype = random.choice(PLACE_SUBTYPES) 88 | self.G.add_node( 89 | node_id, 90 | type="Place", 91 | name=fake.city(), 92 | place_type=subtype, 93 | population=random.randint(10000, 1000000), 94 | coordinates=(fake.latitude(), fake.longitude()), 95 | ) 96 | # Generate Organizations 97 | for i in range(counts["Organization"]): 98 | node_id = f"org_{i}" 99 | subtype = random.choice(ORG_SUBTYPES) 100 | self.G.add_node( 101 | node_id, 102 | type="Organization", 103 | name=fake.company(), 104 | industry=fake.job(), 105 | revenue=round(random.uniform(1e6, 1e9), 2), 106 | employee_count=random.randint(50, 5000), 107 | subtype=subtype, 108 | ) 109 | # Generate Events 110 | for i in range(counts["Event"]): 111 | node_id = f"event_{i}" 112 | subtype = random.choice(EVENT_SUBTYPES) 113 | self.G.add_node( 114 | node_id, 115 | type="Event", 116 | name=fake.catch_phrase(), 117 | event_type=subtype, 118 | start_date=fake.date(), 119 | duration=random.randint(1, 5), 120 | ) # days 121 | # Generate Products 122 | for i in range(counts["Product"]): 123 | node_id = f"product_{i}" 124 | subtype = random.choice(PRODUCT_SUBTYPES) 125 | self.G.add_node( 126 | node_id, 127 | type="Product", 128 | name=fake.word().capitalize(), 129 | category=subtype, 130 | price=round(random.uniform(10, 1000), 2), 131 | release_date=fake.date(), 132 | ) 133 | 134 | def add_relationship( 135 | self, source, target, rel_type, attributes=None, bidirectional=False 136 | ): 137 | """ 138 | Adds a relationship edge from source to target. 139 | If bidirectional, also adds the reverse edge. 140 | """ 141 | if attributes is None: 142 | attributes = {} 143 | self.G.add_edge(source, target, relationship=rel_type, **attributes) 144 | if bidirectional: 145 | self.G.add_edge(target, source, relationship=rel_type, **attributes) 146 | 147 | def generate_edges(self, total_edges=1000): 148 | """ 149 | Generate edges based on the EDGE_DISTRIBUTION probabilities. 150 | The number of edges for each relationship category is determined by the weight. 151 | """ 152 | # Get node lists by type 153 | nodes_by_type = { 154 | "Person": [], 155 | "Place": [], 156 | "Organization": [], 157 | "Event": [], 158 | "Product": [], 159 | } 160 | for node, data in self.G.nodes(data=True): 161 | t = data.get("type") 162 | if t in nodes_by_type: 163 | nodes_by_type[t].append(node) 164 | 165 | # For each category in EDGE_DISTRIBUTION, calculate the number of edges 166 | for (src_type, tgt_type), (possible_rels, weight) in EDGE_DISTRIBUTION.items(): 167 | num_edges = int(total_edges * weight) 168 | src_nodes = nodes_by_type.get(src_type, []) 169 | tgt_nodes = nodes_by_type.get(tgt_type, []) 170 | if not src_nodes or not tgt_nodes: 171 | continue 172 | 173 | for _ in range(num_edges): 174 | source = random.choice(src_nodes) 175 | target = random.choice(tgt_nodes) 176 | # Avoid self-loop in same category if not desired 177 | if src_type == tgt_type and source == target: 178 | continue 179 | rel = random.choice(possible_rels) 180 | attr = {} 181 | # Add additional attributes for specific relationships 182 | if rel == "VISITED": 183 | attr["visit_count"] = random.randint(1, 20) 184 | elif rel == "WORKS_AT": 185 | attr["position"] = fake.job() 186 | elif rel == "PURCHASED": 187 | attr["date"] = fake.date() 188 | attr["amount"] = round(random.uniform(1, 500), 2) 189 | elif rel == "REVIEWED": 190 | attr["rating"] = random.randint(1, 5) 191 | 192 | # Define directionality and bidirectionality 193 | # For Person-Person FRIENDS_WITH and COLLEAGUES, treat as bidirectional 194 | bidir = False 195 | if ( 196 | src_type == "Person" 197 | and tgt_type == "Person" 198 | and rel in ["FRIENDS_WITH", "COLLEAGUES"] 199 | ): 200 | bidir = True 201 | 202 | self.add_relationship( 203 | source, target, rel, attributes=attr, bidirectional=bidir 204 | ) 205 | 206 | def _generate_osm( 207 | self, 208 | place: Optional[str] = None, 209 | address: Optional[str] = None, 210 | bbox: Optional[tuple] = None, 211 | network_type: str = "drive", 212 | simplify: bool = True, 213 | retain_all: bool = False, 214 | dist: float = 1000, 215 | ) -> nx.DiGraph: 216 | """Fetch an OSM network via OSMFetcher""" 217 | try: 218 | if bbox and len(bbox) != 4: 219 | raise ValueError("Bounding box (bbox) must be a tuple of 4 values: (minx, miny, maxx, maxy).") 220 | if dist <= 0: 221 | raise ValueError("Distance (dist) must be greater than 0.") 222 | G = OSMGraphFetcher.fetch_network( 223 | place=place, 224 | address=address, 225 | bbox=bbox, 226 | network_type=network_type, 227 | simplify=simplify, 228 | retain_all=retain_all, 229 | dist=dist, 230 | ) 231 | self.G = G 232 | return G 233 | except Exception as e: 234 | logger.error(f"Failed to generate OSM graph: {e}") 235 | raise 236 | 237 | def _generate_flights( 238 | self, 239 | country: str = "United States", 240 | year: Optional[int] = None, 241 | month: Optional[int] = None, 242 | date_range: Optional[tuple] = None, 243 | ): 244 | """ 245 | Fetch flights, airport, and airline via FlightFetcher 246 | """ 247 | try: 248 | if year and (year < 1900 or year > 2100): 249 | raise ValueError("Year must be between 1900 and 2100.") 250 | if month and (month < 1 or month > 12): 251 | raise ValueError("Month must be between 1 and 12.") 252 | if date_range and len(date_range) != 2: 253 | raise ValueError("Date range must be a tuple of two dates: (start_date, end_date).") 254 | 255 | # 1) Fetch airline and airport tables 256 | airlines_df = FlightGraphFetcher.fetch_airlines() 257 | airports_df = FlightGraphFetcher.fetch_airports(country=country) 258 | 259 | # Fetch flight transit on-time performance data 260 | flights_df = FlightGraphFetcher.fetch_flights( 261 | year=year, month=month, date_range=date_range 262 | ) 263 | logger.info( 264 | f"Fetched {len(airlines_df)} airlines, " 265 | f"{len(airports_df)} airports, " 266 | f"{len(flights_df)} flights." 267 | ) 268 | 269 | G = FlightGraphFetcher.build_graph(airlines_df, airports_df, flights_df) 270 | self.G = G 271 | 272 | # Inform users of which span was downloaded 273 | if date_range: 274 | start, end = date_range 275 | logger.info(f"Flight data covers {start} -> {end}") 276 | 277 | else: 278 | logger.info(f"Flight data for {year}-{month:02d}") 279 | return G 280 | except Exception as e: 281 | logger.error(f"Failed to generate flight graph: {e}") 282 | raise 283 | 284 | 285 | def _generate_faker(self, total_nodes=100, total_edges=1000): 286 | """Generates the complete Social Knowledge Graph.""" 287 | self.G = nx.DiGraph() # Reset the graph to a new instance 288 | self.generate_nodes(total_nodes=total_nodes) 289 | self.generate_edges(total_edges=total_edges) 290 | return self.G 291 | 292 | def generate_graph( 293 | self, 294 | source: str = "faker", 295 | total_nodes: int = 100, 296 | total_edges: int = 1000, 297 | place: Optional[str] = None, 298 | address: Optional[str] = None, 299 | bbox: Optional[tuple] = None, 300 | network_type: str = "drive", 301 | simplify: bool = True, 302 | retain_all: bool = False, 303 | dist: float = 1000, 304 | country: str = "United States", 305 | year: int = 2024, 306 | month: int = 1, 307 | date_range: Optional[tuple] = None, 308 | ) -> nx.DiGraph: 309 | """ 310 | Unified entrypoint: choose 'random' or 'osm'. 311 | Pass kwargs depending on source. 312 | """ 313 | 314 | if source == "faker": 315 | return self._generate_faker( 316 | total_nodes=total_nodes, total_edges=total_edges 317 | ) 318 | elif source == "osm": 319 | logger.info( 320 | f"Generating OSM graph with source={source}, " 321 | f"place={place}, address={address}, bbox={bbox}, " 322 | f"network_type={network_type}, simplify={simplify}, " 323 | f"retain_all={retain_all}, dist={dist}" 324 | ) 325 | return self._generate_osm( 326 | place=place, 327 | address=address, 328 | bbox=bbox, 329 | network_type=network_type, 330 | simplify=simplify, 331 | retain_all=retain_all, 332 | dist=dist, 333 | ) 334 | elif source == "flights": 335 | return self._generate_flights( 336 | country=country, 337 | year=year, 338 | month=month, 339 | date_range=date_range, 340 | ) 341 | else: 342 | raise ValueError(f"Unknown source '{source}'. Use 'random' or 'osm'.") 343 | 344 | def export_graph(self, G: nx.Graph = None, source: str = None, path: str = "graph.graphml"): 345 | """ 346 | Export the graph to GraphML format. 347 | 348 | Args: 349 | G: Optional NetworkX graph. If None, uses self.G. 350 | source: Optional string, if "osm" uses osmnx for export. 351 | path: Destination file path for .graphml output. 352 | 353 | Notes: 354 | GraphML is useful for visualization in tools like Gephi or Cytoscape. 355 | Node/edge attributes should be simple types (str, int, float). 356 | """ 357 | import os 358 | 359 | abs_path = os.path.abspath(path) 360 | os.makedirs(os.path.dirname(abs_path) or ".", exist_ok=True) 361 | 362 | if G is None: 363 | G = self.G 364 | if G is None: 365 | raise ValueError("No graph available to export.") 366 | 367 | # Sanitize attributes that are not GraphML-friendly 368 | for _, data in G.nodes(data=True): 369 | if 'coordinates' in data and isinstance(data['coordinates'], tuple): 370 | lat, lon = data['coordinates'] 371 | data['coordinates'] = f"{lat},{lon}" 372 | 373 | if source == "osm": 374 | try: 375 | import osmnx as ox 376 | ox.io.save_graphml(G, filepath=abs_path) 377 | except ImportError: 378 | raise ImportError("osmnx is required to export OSM graphs.") 379 | else: 380 | nx.write_graphml(G, abs_path) 381 | 382 | print(f"✅ Graph exported to: {abs_path}") 383 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.12" 4 | 5 | [[package]] 6 | name = "beautifulsoup4" 7 | version = "4.13.4" 8 | source = { registry = "https://pypi.org/simple" } 9 | dependencies = [ 10 | { name = "soupsieve" }, 11 | { name = "typing-extensions" }, 12 | ] 13 | sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } 14 | wheels = [ 15 | { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, 16 | ] 17 | 18 | [[package]] 19 | name = "certifi" 20 | version = "2025.1.31" 21 | source = { registry = "https://pypi.org/simple" } 22 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 25 | ] 26 | 27 | [[package]] 28 | name = "charset-normalizer" 29 | version = "3.4.1" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, 34 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 35 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 36 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 37 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 38 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 39 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 40 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 41 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 42 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 43 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 44 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, 45 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, 46 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 47 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 48 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 49 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 50 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 51 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 52 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 53 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 54 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 55 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 56 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 57 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 58 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 59 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 60 | ] 61 | 62 | [[package]] 63 | name = "click" 64 | version = "8.1.8" 65 | source = { registry = "https://pypi.org/simple" } 66 | dependencies = [ 67 | { name = "colorama", marker = "sys_platform == 'win32'" }, 68 | ] 69 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 70 | wheels = [ 71 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 72 | ] 73 | 74 | [[package]] 75 | name = "colorama" 76 | version = "0.4.6" 77 | source = { registry = "https://pypi.org/simple" } 78 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 79 | wheels = [ 80 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 81 | ] 82 | 83 | [[package]] 84 | name = "coverage" 85 | version = "7.8.0" 86 | source = { registry = "https://pypi.org/simple" } 87 | sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } 88 | wheels = [ 89 | { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, 90 | { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, 91 | { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, 92 | { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, 93 | { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, 94 | { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, 95 | { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, 96 | { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, 97 | { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, 98 | { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, 99 | { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, 100 | { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, 101 | { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, 102 | { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, 103 | { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, 104 | { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, 105 | { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, 106 | { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, 107 | { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, 108 | { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, 109 | { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, 110 | { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, 111 | { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, 112 | { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, 113 | { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, 114 | { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, 115 | { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, 116 | { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, 117 | { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, 118 | { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, 119 | { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, 120 | ] 121 | 122 | [[package]] 123 | name = "faker" 124 | version = "37.1.0" 125 | source = { registry = "https://pypi.org/simple" } 126 | dependencies = [ 127 | { name = "tzdata" }, 128 | ] 129 | sdist = { url = "https://files.pythonhosted.org/packages/ba/a6/b77f42021308ec8b134502343da882c0905d725a4d661c7adeaf7acaf515/faker-37.1.0.tar.gz", hash = "sha256:ad9dc66a3b84888b837ca729e85299a96b58fdaef0323ed0baace93c9614af06", size = 1875707 } 130 | wheels = [ 131 | { url = "https://files.pythonhosted.org/packages/d7/a1/8936bc8e79af80ca38288dd93ed44ed1f9d63beb25447a4c59e746e01f8d/faker-37.1.0-py3-none-any.whl", hash = "sha256:dc2f730be71cb770e9c715b13374d80dbcee879675121ab51f9683d262ae9a1c", size = 1918783 }, 132 | ] 133 | 134 | [[package]] 135 | name = "geopandas" 136 | version = "1.0.1" 137 | source = { registry = "https://pypi.org/simple" } 138 | dependencies = [ 139 | { name = "numpy" }, 140 | { name = "packaging" }, 141 | { name = "pandas" }, 142 | { name = "pyogrio" }, 143 | { name = "pyproj" }, 144 | { name = "shapely" }, 145 | ] 146 | sdist = { url = "https://files.pythonhosted.org/packages/39/08/2cf5d85356e45b10b8d066cf4c3ba1e9e3185423c48104eed87e8afd0455/geopandas-1.0.1.tar.gz", hash = "sha256:b8bf70a5534588205b7a56646e2082fb1de9a03599651b3d80c99ea4c2ca08ab", size = 317736 } 147 | wheels = [ 148 | { url = "https://files.pythonhosted.org/packages/c4/64/7d344cfcef5efddf9cf32f59af7f855828e9d74b5f862eddf5bfd9f25323/geopandas-1.0.1-py3-none-any.whl", hash = "sha256:01e147d9420cc374d26f51fc23716ac307f32b49406e4bd8462c07e82ed1d3d6", size = 323587 }, 149 | ] 150 | 151 | [[package]] 152 | name = "graphfaker" 153 | version = "0.1.2" 154 | source = { editable = "." } 155 | dependencies = [ 156 | { name = "faker" }, 157 | { name = "networkx" }, 158 | { name = "osmnx" }, 159 | { name = "pandas" }, 160 | { name = "requests" }, 161 | { name = "typer" }, 162 | { name = "wikipedia" }, 163 | ] 164 | 165 | [package.optional-dependencies] 166 | dev = [ 167 | { name = "coverage" }, 168 | { name = "mypy" }, 169 | { name = "pytest" }, 170 | { name = "ruff" }, 171 | ] 172 | 173 | [package.metadata] 174 | requires-dist = [ 175 | { name = "coverage", marker = "extra == 'dev'" }, 176 | { name = "faker", specifier = ">=37.1.0" }, 177 | { name = "mypy", marker = "extra == 'dev'" }, 178 | { name = "networkx", specifier = ">=3.4.2" }, 179 | { name = "osmnx", specifier = "==2.0.2" }, 180 | { name = "pandas", specifier = ">=2.2.2" }, 181 | { name = "pytest", marker = "extra == 'dev'" }, 182 | { name = "requests", specifier = ">=2.32.3" }, 183 | { name = "ruff", marker = "extra == 'dev'" }, 184 | { name = "typer" }, 185 | { name = "wikipedia", specifier = ">=1.4.0" }, 186 | ] 187 | provides-extras = ["dev"] 188 | 189 | [[package]] 190 | name = "idna" 191 | version = "3.10" 192 | source = { registry = "https://pypi.org/simple" } 193 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 194 | wheels = [ 195 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 196 | ] 197 | 198 | [[package]] 199 | name = "iniconfig" 200 | version = "2.1.0" 201 | source = { registry = "https://pypi.org/simple" } 202 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } 203 | wheels = [ 204 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, 205 | ] 206 | 207 | [[package]] 208 | name = "markdown-it-py" 209 | version = "3.0.0" 210 | source = { registry = "https://pypi.org/simple" } 211 | dependencies = [ 212 | { name = "mdurl" }, 213 | ] 214 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 215 | wheels = [ 216 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 217 | ] 218 | 219 | [[package]] 220 | name = "mdurl" 221 | version = "0.1.2" 222 | source = { registry = "https://pypi.org/simple" } 223 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 224 | wheels = [ 225 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 226 | ] 227 | 228 | [[package]] 229 | name = "mypy" 230 | version = "1.15.0" 231 | source = { registry = "https://pypi.org/simple" } 232 | dependencies = [ 233 | { name = "mypy-extensions" }, 234 | { name = "typing-extensions" }, 235 | ] 236 | sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } 237 | wheels = [ 238 | { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, 239 | { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, 240 | { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, 241 | { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, 242 | { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, 243 | { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, 244 | { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, 245 | { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, 246 | { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, 247 | { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, 248 | { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, 249 | { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, 250 | { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, 251 | ] 252 | 253 | [[package]] 254 | name = "mypy-extensions" 255 | version = "1.0.0" 256 | source = { registry = "https://pypi.org/simple" } 257 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } 258 | wheels = [ 259 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, 260 | ] 261 | 262 | [[package]] 263 | name = "networkx" 264 | version = "3.4.2" 265 | source = { registry = "https://pypi.org/simple" } 266 | sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } 267 | wheels = [ 268 | { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, 269 | ] 270 | 271 | [[package]] 272 | name = "numpy" 273 | version = "2.2.4" 274 | source = { registry = "https://pypi.org/simple" } 275 | sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 } 276 | wheels = [ 277 | { url = "https://files.pythonhosted.org/packages/a2/30/182db21d4f2a95904cec1a6f779479ea1ac07c0647f064dea454ec650c42/numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4", size = 20947156 }, 278 | { url = "https://files.pythonhosted.org/packages/24/6d/9483566acfbda6c62c6bc74b6e981c777229d2af93c8eb2469b26ac1b7bc/numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854", size = 14133092 }, 279 | { url = "https://files.pythonhosted.org/packages/27/f6/dba8a258acbf9d2bed2525cdcbb9493ef9bae5199d7a9cb92ee7e9b2aea6/numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24", size = 5163515 }, 280 | { url = "https://files.pythonhosted.org/packages/62/30/82116199d1c249446723c68f2c9da40d7f062551036f50b8c4caa42ae252/numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee", size = 6696558 }, 281 | { url = "https://files.pythonhosted.org/packages/0e/b2/54122b3c6df5df3e87582b2e9430f1bdb63af4023c739ba300164c9ae503/numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba", size = 14084742 }, 282 | { url = "https://files.pythonhosted.org/packages/02/e2/e2cbb8d634151aab9528ef7b8bab52ee4ab10e076509285602c2a3a686e0/numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592", size = 16134051 }, 283 | { url = "https://files.pythonhosted.org/packages/8e/21/efd47800e4affc993e8be50c1b768de038363dd88865920439ef7b422c60/numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb", size = 15578972 }, 284 | { url = "https://files.pythonhosted.org/packages/04/1e/f8bb88f6157045dd5d9b27ccf433d016981032690969aa5c19e332b138c0/numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f", size = 17898106 }, 285 | { url = "https://files.pythonhosted.org/packages/2b/93/df59a5a3897c1f036ae8ff845e45f4081bb06943039ae28a3c1c7c780f22/numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00", size = 6311190 }, 286 | { url = "https://files.pythonhosted.org/packages/46/69/8c4f928741c2a8efa255fdc7e9097527c6dc4e4df147e3cadc5d9357ce85/numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146", size = 12644305 }, 287 | { url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 }, 288 | { url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 }, 289 | { url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 }, 290 | { url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 }, 291 | { url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 }, 292 | { url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 }, 293 | { url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 }, 294 | { url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 }, 295 | { url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 }, 296 | { url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 }, 297 | { url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 }, 298 | { url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 }, 299 | { url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 }, 300 | { url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 }, 301 | { url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 }, 302 | { url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 }, 303 | { url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 }, 304 | { url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 }, 305 | { url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 }, 306 | { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 }, 307 | ] 308 | 309 | [[package]] 310 | name = "osmnx" 311 | version = "2.0.2" 312 | source = { registry = "https://pypi.org/simple" } 313 | dependencies = [ 314 | { name = "geopandas" }, 315 | { name = "networkx" }, 316 | { name = "numpy" }, 317 | { name = "pandas" }, 318 | { name = "requests" }, 319 | { name = "shapely" }, 320 | ] 321 | sdist = { url = "https://files.pythonhosted.org/packages/56/3e/ebc1e200fd8780de0939e1e9c88e59a9cb4b3605ad66858679aa1d6350f4/osmnx-2.0.2.tar.gz", hash = "sha256:90d13cdfc6214d35214cf04736426c20dfa325ba6da473b212c839b25c795320", size = 86721 } 322 | wheels = [ 323 | { url = "https://files.pythonhosted.org/packages/e6/1a/b44f004e1be244678b4c822c442a488f0493f4b55d0a0cbc910cf14671e1/osmnx-2.0.2-py3-none-any.whl", hash = "sha256:1f7de2af6f22a6f802f459958b7fe07aad242f62db39c651bf57da2b942d2ba8", size = 99865 }, 324 | ] 325 | 326 | [[package]] 327 | name = "packaging" 328 | version = "24.2" 329 | source = { registry = "https://pypi.org/simple" } 330 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 331 | wheels = [ 332 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 333 | ] 334 | 335 | [[package]] 336 | name = "pandas" 337 | version = "2.2.3" 338 | source = { registry = "https://pypi.org/simple" } 339 | dependencies = [ 340 | { name = "numpy" }, 341 | { name = "python-dateutil" }, 342 | { name = "pytz" }, 343 | { name = "tzdata" }, 344 | ] 345 | sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } 346 | wheels = [ 347 | { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, 348 | { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, 349 | { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, 350 | { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, 351 | { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, 352 | { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, 353 | { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, 354 | { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, 355 | { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, 356 | { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, 357 | { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, 358 | { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, 359 | { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, 360 | { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, 361 | { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, 362 | { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, 363 | { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, 364 | { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, 365 | { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, 366 | { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, 367 | ] 368 | 369 | [[package]] 370 | name = "pluggy" 371 | version = "1.5.0" 372 | source = { registry = "https://pypi.org/simple" } 373 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 374 | wheels = [ 375 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 376 | ] 377 | 378 | [[package]] 379 | name = "pygments" 380 | version = "2.19.1" 381 | source = { registry = "https://pypi.org/simple" } 382 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 383 | wheels = [ 384 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 385 | ] 386 | 387 | [[package]] 388 | name = "pyogrio" 389 | version = "0.10.0" 390 | source = { registry = "https://pypi.org/simple" } 391 | dependencies = [ 392 | { name = "certifi" }, 393 | { name = "numpy" }, 394 | { name = "packaging" }, 395 | ] 396 | sdist = { url = "https://files.pythonhosted.org/packages/a5/8f/5a784595524a79c269f2b1c880f4fdb152867df700c97005dda51997da02/pyogrio-0.10.0.tar.gz", hash = "sha256:ec051cb568324de878828fae96379b71858933413e185148acb6c162851ab23c", size = 281950 } 397 | wheels = [ 398 | { url = "https://files.pythonhosted.org/packages/b5/b5/3c5dfd0b50cbce6f3d4e42c0484647feb1809dbe20e225c4c6abd067e69f/pyogrio-0.10.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d6558b180e020f71ab7aa7f82d592ed3305c9f698d98f6d0a4637ec7a84c4ce", size = 15079211 }, 399 | { url = "https://files.pythonhosted.org/packages/b8/9a/1ba9c707a094976f343bd0177741eaba0e842fa05ecd8ab97192db4f2ec1/pyogrio-0.10.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:a99102037eead8ba491bc57825c1e395ee31c9956d7bff7b4a9e4fdbff3a13c2", size = 16442782 }, 400 | { url = "https://files.pythonhosted.org/packages/5e/bb/b4250746c2c85fea5004cae93e9e25ad01516e9e94e04de780a2e78139da/pyogrio-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a4c373281d7cbf560c5b61f8f3c7442103ad7f1c7ac4ef3a84572ed7a5dd2f6", size = 23899832 }, 401 | { url = "https://files.pythonhosted.org/packages/bd/4c/79e47e40a8e54e79a45133786a0a58209534f580591c933d40c5ed314fe7/pyogrio-0.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:19f18411bdf836d24cdc08b9337eb3ec415e4ac4086ba64516b36b73a2e88622", size = 23081469 }, 402 | { url = "https://files.pythonhosted.org/packages/47/78/2b62c8a340bcb0ea56b9ddf2ef5fd3d1f101dc0e98816b9e6da87c5ac3b7/pyogrio-0.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1abbcdd9876f30bebf1df8a0273f6cdeb29d03259290008275c7fddebe139f20", size = 24024758 }, 403 | { url = "https://files.pythonhosted.org/packages/43/97/34605480f06b0ad9611bf58a174eccc6f3673275f3d519cf763391892881/pyogrio-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a3e09839590d71ff832aa95c4f23fa00a2c63c3de82c1fbd4fb8d265792acfc", size = 16160294 }, 404 | { url = "https://files.pythonhosted.org/packages/14/4a/4c8e4f5b9edbca46e0f8d6c1c0b56c0d4af0900c29f4bea22d37853c07f3/pyogrio-0.10.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c90478209537a31dcc65664a87a04c094bb0e08efe502908a6682b8cec0259bf", size = 15076879 }, 405 | { url = "https://files.pythonhosted.org/packages/5f/be/7db0644eef9ef3382518399aaf3332827c43018112d2a74f78784fd496ec/pyogrio-0.10.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:fec45e1963b7058e5a1aa98598aed07c0858512c833d6aad2c672c3ec98bbf04", size = 16440405 }, 406 | { url = "https://files.pythonhosted.org/packages/96/77/f199230ba86fe88b1f57e71428c169ed982de68a32d6082cd7c12d0f5d55/pyogrio-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28cb139f8a5d0365ede602230104b407ae52bb6b55173c8d5a35424d28c4a2c5", size = 23871511 }, 407 | { url = "https://files.pythonhosted.org/packages/25/ac/ca483bec408b59c54f7129b0244cc9de21d8461aefe89ece7bd74ad33807/pyogrio-0.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:cea0187fcc2d574e52af8cfab041fa0a7ad71d5ef6b94b49a3f3d2a04534a27e", size = 23048830 }, 408 | { url = "https://files.pythonhosted.org/packages/d7/3e/c35f2d8dad95b24e568c468f09ff60fb61945065465e0ec7868400596566/pyogrio-0.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7c02b207ea8cf09c501ea3e95d29152781a00d3c32267286bc36fa457c332205", size = 23996873 }, 409 | { url = "https://files.pythonhosted.org/packages/27/5d/0deb16d228362a097ee3258d0a887c9c0add4b9678bb4847b08a241e124d/pyogrio-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:02e54bcfb305af75f829044b0045f74de31b77c2d6546f7aaf96822066147848", size = 16158260 }, 410 | ] 411 | 412 | [[package]] 413 | name = "pyproj" 414 | version = "3.7.1" 415 | source = { registry = "https://pypi.org/simple" } 416 | dependencies = [ 417 | { name = "certifi" }, 418 | ] 419 | sdist = { url = "https://files.pythonhosted.org/packages/67/10/a8480ea27ea4bbe896c168808854d00f2a9b49f95c0319ddcbba693c8a90/pyproj-3.7.1.tar.gz", hash = "sha256:60d72facd7b6b79853f19744779abcd3f804c4e0d4fa8815469db20c9f640a47", size = 226339 } 420 | wheels = [ 421 | { url = "https://files.pythonhosted.org/packages/e6/c9/876d4345b8d17f37ac59ebd39f8fa52fc6a6a9891a420f72d050edb6b899/pyproj-3.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:2781029d90df7f8d431e29562a3f2d8eafdf233c4010d6fc0381858dc7373217", size = 6264087 }, 422 | { url = "https://files.pythonhosted.org/packages/ff/e6/5f8691f8c90e7f402cc80a6276eb19d2ec1faa150d5ae2dd9c7b0a254da8/pyproj-3.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d61bf8ab04c73c1da08eedaf21a103b72fa5b0a9b854762905f65ff8b375d394", size = 4669628 }, 423 | { url = "https://files.pythonhosted.org/packages/42/ec/16475bbb79c1c68845c0a0d9c60c4fb31e61b8a2a20bc18b1a81e81c7f68/pyproj-3.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04abc517a8555d1b05fcee768db3280143fe42ec39fdd926a2feef31631a1f2f", size = 9721415 }, 424 | { url = "https://files.pythonhosted.org/packages/b3/a3/448f05b15e318bd6bea9a32cfaf11e886c4ae61fa3eee6e09ed5c3b74bb2/pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084c0a475688f934d386c2ab3b6ce03398a473cd48adfda70d9ab8f87f2394a0", size = 9556447 }, 425 | { url = "https://files.pythonhosted.org/packages/6a/ae/bd15fe8d8bd914ead6d60bca7f895a4e6f8ef7e3928295134ff9a7dad14c/pyproj-3.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a20727a23b1e49c7dc7fe3c3df8e56a8a7acdade80ac2f5cca29d7ca5564c145", size = 10758317 }, 426 | { url = "https://files.pythonhosted.org/packages/9d/d9/5ccefb8bca925f44256b188a91c31238cae29ab6ee7f53661ecc04616146/pyproj-3.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bf84d766646f1ebd706d883755df4370aaf02b48187cedaa7e4239f16bc8213d", size = 10771259 }, 427 | { url = "https://files.pythonhosted.org/packages/2a/7d/31dedff9c35fa703162f922eeb0baa6c44a3288469a5fd88d209e2892f9e/pyproj-3.7.1-cp312-cp312-win32.whl", hash = "sha256:5f0da2711364d7cb9f115b52289d4a9b61e8bca0da57f44a3a9d6fc9bdeb7274", size = 5859914 }, 428 | { url = "https://files.pythonhosted.org/packages/3e/47/c6ab03d6564a7c937590cff81a2742b5990f096cce7c1a622d325be340ee/pyproj-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:aee664a9d806612af30a19dba49e55a7a78ebfec3e9d198f6a6176e1d140ec98", size = 6273196 }, 429 | { url = "https://files.pythonhosted.org/packages/ef/01/984828464c9960036c602753fc0f21f24f0aa9043c18fa3f2f2b66a86340/pyproj-3.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:5f8d02ef4431dee414d1753d13fa82a21a2f61494737b5f642ea668d76164d6d", size = 6253062 }, 430 | { url = "https://files.pythonhosted.org/packages/68/65/6ecdcdc829811a2c160cdfe2f068a009fc572fd4349664f758ccb0853a7c/pyproj-3.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0b853ae99bda66cbe24b4ccfe26d70601d84375940a47f553413d9df570065e0", size = 4660548 }, 431 | { url = "https://files.pythonhosted.org/packages/67/da/dda94c4490803679230ba4c17a12f151b307a0d58e8110820405ca2d98db/pyproj-3.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83db380c52087f9e9bdd8a527943b2e7324f275881125e39475c4f9277bdeec4", size = 9662464 }, 432 | { url = "https://files.pythonhosted.org/packages/6f/57/f61b7d22c91ae1d12ee00ac4c0038714e774ebcd851b9133e5f4f930dd40/pyproj-3.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b35ed213892e211a3ce2bea002aa1183e1a2a9b79e51bb3c6b15549a831ae528", size = 9497461 }, 433 | { url = "https://files.pythonhosted.org/packages/b7/f6/932128236f79d2ac7d39fe1a19667fdf7155d9a81d31fb9472a7a497790f/pyproj-3.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8b15b0463d1303bab113d1a6af2860a0d79013c3a66fcc5475ce26ef717fd4f", size = 10708869 }, 434 | { url = "https://files.pythonhosted.org/packages/1d/0d/07ac7712994454a254c383c0d08aff9916a2851e6512d59da8dc369b1b02/pyproj-3.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:87229e42b75e89f4dad6459200f92988c5998dfb093c7c631fb48524c86cd5dc", size = 10729260 }, 435 | { url = "https://files.pythonhosted.org/packages/b0/d0/9c604bc72c37ba69b867b6df724d6a5af6789e8c375022c952f65b2af558/pyproj-3.7.1-cp313-cp313-win32.whl", hash = "sha256:d666c3a3faaf3b1d7fc4a544059c4eab9d06f84a604b070b7aa2f318e227798e", size = 5855462 }, 436 | { url = "https://files.pythonhosted.org/packages/98/df/68a2b7f5fb6400c64aad82d72bcc4bc531775e62eedff993a77c780defd0/pyproj-3.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3caac7473be22b6d6e102dde6c46de73b96bc98334e577dfaee9886f102ea2e", size = 6266573 }, 437 | ] 438 | 439 | [[package]] 440 | name = "pytest" 441 | version = "8.3.5" 442 | source = { registry = "https://pypi.org/simple" } 443 | dependencies = [ 444 | { name = "colorama", marker = "sys_platform == 'win32'" }, 445 | { name = "iniconfig" }, 446 | { name = "packaging" }, 447 | { name = "pluggy" }, 448 | ] 449 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 450 | wheels = [ 451 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 452 | ] 453 | 454 | [[package]] 455 | name = "python-dateutil" 456 | version = "2.9.0.post0" 457 | source = { registry = "https://pypi.org/simple" } 458 | dependencies = [ 459 | { name = "six" }, 460 | ] 461 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 462 | wheels = [ 463 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, 464 | ] 465 | 466 | [[package]] 467 | name = "pytz" 468 | version = "2025.2" 469 | source = { registry = "https://pypi.org/simple" } 470 | sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } 471 | wheels = [ 472 | { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, 473 | ] 474 | 475 | [[package]] 476 | name = "requests" 477 | version = "2.32.3" 478 | source = { registry = "https://pypi.org/simple" } 479 | dependencies = [ 480 | { name = "certifi" }, 481 | { name = "charset-normalizer" }, 482 | { name = "idna" }, 483 | { name = "urllib3" }, 484 | ] 485 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 486 | wheels = [ 487 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 488 | ] 489 | 490 | [[package]] 491 | name = "rich" 492 | version = "14.0.0" 493 | source = { registry = "https://pypi.org/simple" } 494 | dependencies = [ 495 | { name = "markdown-it-py" }, 496 | { name = "pygments" }, 497 | ] 498 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } 499 | wheels = [ 500 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, 501 | ] 502 | 503 | [[package]] 504 | name = "ruff" 505 | version = "0.11.2" 506 | source = { registry = "https://pypi.org/simple" } 507 | sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } 508 | wheels = [ 509 | { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, 510 | { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, 511 | { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, 512 | { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, 513 | { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, 514 | { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, 515 | { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, 516 | { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, 517 | { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, 518 | { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, 519 | { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, 520 | { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, 521 | { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, 522 | { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, 523 | { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, 524 | { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, 525 | { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, 526 | ] 527 | 528 | [[package]] 529 | name = "shapely" 530 | version = "2.1.0" 531 | source = { registry = "https://pypi.org/simple" } 532 | dependencies = [ 533 | { name = "numpy" }, 534 | ] 535 | sdist = { url = "https://files.pythonhosted.org/packages/fb/fe/3b0d2f828ffaceadcdcb51b75b9c62d98e62dd95ce575278de35f24a1c20/shapely-2.1.0.tar.gz", hash = "sha256:2cbe90e86fa8fc3ca8af6ffb00a77b246b918c7cf28677b7c21489b678f6b02e", size = 313617 } 536 | wheels = [ 537 | { url = "https://files.pythonhosted.org/packages/4e/d1/6a9371ec39d3ef08e13225594e6c55b045209629afd9e6d403204507c2a8/shapely-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53e7ee8bd8609cf12ee6dce01ea5affe676976cf7049315751d53d8db6d2b4b2", size = 1830732 }, 538 | { url = "https://files.pythonhosted.org/packages/32/87/799e3e48be7ce848c08509b94d2180f4ddb02e846e3c62d0af33da4d78d3/shapely-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cab20b665d26dbec0b380e15749bea720885a481fa7b1eedc88195d4a98cfa4", size = 1638404 }, 539 | { url = "https://files.pythonhosted.org/packages/85/00/6665d77f9dd09478ab0993b8bc31668aec4fd3e5f1ddd1b28dd5830e47be/shapely-2.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a38b39a09340273c3c92b3b9a374272a12cc7e468aeeea22c1c46217a03e5c", size = 2945316 }, 540 | { url = "https://files.pythonhosted.org/packages/34/49/738e07d10bbc67cae0dcfe5a484c6e518a517f4f90550dda2adf3a78b9f2/shapely-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edaec656bdd9b71278b98e6f77c464b1c3b2daa9eace78012ff0f0b4b5b15b04", size = 3063099 }, 541 | { url = "https://files.pythonhosted.org/packages/88/b8/138098674559362ab29f152bff3b6630de423378fbb0324812742433a4ef/shapely-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c8a732ddd9b25e7a54aa748e7df8fd704e23e5d5d35b7d376d80bffbfc376d04", size = 3887873 }, 542 | { url = "https://files.pythonhosted.org/packages/67/a8/fdae7c2db009244991d86f4d2ca09d2f5ccc9d41c312c3b1ee1404dc55da/shapely-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9c93693ad8adfdc9138a5a2d42da02da94f728dd2e82d2f0f442f10e25027f5f", size = 4067004 }, 543 | { url = "https://files.pythonhosted.org/packages/ed/78/17e17d91b489019379df3ee1afc4bd39787b232aaa1d540f7d376f0280b7/shapely-2.1.0-cp312-cp312-win32.whl", hash = "sha256:d8ac6604eefe807e71a908524de23a37920133a1729fe3a4dfe0ed82c044cbf4", size = 1527366 }, 544 | { url = "https://files.pythonhosted.org/packages/b8/bd/9249bd6dda948441e25e4fb14cbbb5205146b0fff12c66b19331f1ff2141/shapely-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f47e631aa4f9ec5576eac546eb3f38802e2f82aeb0552f9612cb9a14ece1db", size = 1708265 }, 545 | { url = "https://files.pythonhosted.org/packages/8d/77/4e368704b2193e74498473db4461d697cc6083c96f8039367e59009d78bd/shapely-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b64423295b563f43a043eb786e7a03200ebe68698e36d2b4b1c39f31dfb50dfb", size = 1830029 }, 546 | { url = "https://files.pythonhosted.org/packages/71/3c/d888597bda680e4de987316b05ca9db07416fa29523beff64f846503302f/shapely-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1b5578f45adc25b235b22d1ccb9a0348c8dc36f31983e57ea129a88f96f7b870", size = 1637999 }, 547 | { url = "https://files.pythonhosted.org/packages/03/8d/ee0e23b7ef88fba353c63a81f1f329c77f5703835db7b165e7c0b8b7f839/shapely-2.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a7e83d383b27f02b684e50ab7f34e511c92e33b6ca164a6a9065705dd64bcb", size = 2929348 }, 548 | { url = "https://files.pythonhosted.org/packages/d1/a7/5c9cb413e4e2ce52c16be717e94abd40ce91b1f8974624d5d56154c5d40b/shapely-2.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:942031eb4d8f7b3b22f43ba42c09c7aa3d843aa10d5cc1619fe816e923b66e55", size = 3048973 }, 549 | { url = "https://files.pythonhosted.org/packages/84/23/45b90c0bd2157b238490ca56ef2eedf959d3514c7d05475f497a2c88b6d9/shapely-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2843c456a2e5627ee6271800f07277c0d2652fb287bf66464571a057dbc00b3", size = 3873148 }, 550 | { url = "https://files.pythonhosted.org/packages/c0/bc/ed7d5d37f5395166042576f0c55a12d7e56102799464ba7ea3a72a38c769/shapely-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8c4b17469b7f39a5e6a7cfea79f38ae08a275427f41fe8b48c372e1449147908", size = 4052655 }, 551 | { url = "https://files.pythonhosted.org/packages/c0/8f/a1dafbb10d20d1c569f2db3fb1235488f624dafe8469e8ce65356800ba31/shapely-2.1.0-cp313-cp313-win32.whl", hash = "sha256:30e967abd08fce49513d4187c01b19f139084019f33bec0673e8dbeb557c45e4", size = 1526600 }, 552 | { url = "https://files.pythonhosted.org/packages/e3/f0/9f8cdf2258d7aed742459cea51c70d184de92f5d2d6f5f7f1ded90a18c31/shapely-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:1dc8d4364483a14aba4c844b7bd16a6fa3728887e2c33dfa1afa34a3cf4d08a5", size = 1707115 }, 553 | { url = "https://files.pythonhosted.org/packages/75/ed/32952df461753a65b3e5d24c8efb361d3a80aafaef0b70d419063f6f2c11/shapely-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:673e073fea099d1c82f666fb7ab0a00a77eff2999130a69357ce11941260d855", size = 1824847 }, 554 | { url = "https://files.pythonhosted.org/packages/ff/b9/2284de512af30b02f93ddcdd2e5c79834a3cf47fa3ca11b0f74396feb046/shapely-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d1513f915a56de67659fe2047c1ad5ff0f8cbff3519d1e74fced69c9cb0e7da", size = 1631035 }, 555 | { url = "https://files.pythonhosted.org/packages/35/16/a59f252a7e736b73008f10d0950ffeeb0d5953be7c0bdffd39a02a6ba310/shapely-2.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d6a7043178890b9e028d80496ff4c79dc7629bff4d78a2f25323b661756bab8", size = 2968639 }, 556 | { url = "https://files.pythonhosted.org/packages/a5/0a/6a20eca7b0092cfa243117e8e145a58631a4833a0a519ec9b445172e83a0/shapely-2.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb638378dc3d76f7e85b67d7e2bb1366811912430ac9247ac00c127c2b444cdc", size = 3055713 }, 557 | { url = "https://files.pythonhosted.org/packages/fb/44/eeb0c7583b1453d1cf7a319a1d738e08f98a5dc993fa1ef3c372983e4cb5/shapely-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:737124e87d91d616acf9a911f74ac55e05db02a43a6a7245b3d663817b876055", size = 3890478 }, 558 | { url = "https://files.pythonhosted.org/packages/5d/6e/37ff3c6af1d408cacb0a7d7bfea7b8ab163a5486e35acb08997eae9d8756/shapely-2.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e6c229e7bb87aae5df82fa00b6718987a43ec168cc5affe095cca59d233f314", size = 4036148 }, 559 | { url = "https://files.pythonhosted.org/packages/c8/6a/8c0b7de3aeb5014a23f06c5e9d3c7852ebcf0d6b00fe660b93261e310e24/shapely-2.1.0-cp313-cp313t-win32.whl", hash = "sha256:a9580bda119b1f42f955aa8e52382d5c73f7957e0203bc0c0c60084846f3db94", size = 1535993 }, 560 | { url = "https://files.pythonhosted.org/packages/a8/91/ae80359a58409d52e4d62c7eacc7eb3ddee4b9135f1db884b6a43cf2e174/shapely-2.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e8ff4e5cfd799ba5b6f37b5d5527dbd85b4a47c65b6d459a03d0962d2a9d4d10", size = 1717777 }, 561 | ] 562 | 563 | [[package]] 564 | name = "shellingham" 565 | version = "1.5.4" 566 | source = { registry = "https://pypi.org/simple" } 567 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 568 | wheels = [ 569 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 570 | ] 571 | 572 | [[package]] 573 | name = "six" 574 | version = "1.17.0" 575 | source = { registry = "https://pypi.org/simple" } 576 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 577 | wheels = [ 578 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, 579 | ] 580 | 581 | [[package]] 582 | name = "soupsieve" 583 | version = "2.7" 584 | source = { registry = "https://pypi.org/simple" } 585 | sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } 586 | wheels = [ 587 | { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, 588 | ] 589 | 590 | [[package]] 591 | name = "typer" 592 | version = "0.15.2" 593 | source = { registry = "https://pypi.org/simple" } 594 | dependencies = [ 595 | { name = "click" }, 596 | { name = "rich" }, 597 | { name = "shellingham" }, 598 | { name = "typing-extensions" }, 599 | ] 600 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } 601 | wheels = [ 602 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, 603 | ] 604 | 605 | [[package]] 606 | name = "typing-extensions" 607 | version = "4.13.0" 608 | source = { registry = "https://pypi.org/simple" } 609 | sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } 610 | wheels = [ 611 | { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, 612 | ] 613 | 614 | [[package]] 615 | name = "tzdata" 616 | version = "2025.2" 617 | source = { registry = "https://pypi.org/simple" } 618 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } 619 | wheels = [ 620 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, 621 | ] 622 | 623 | [[package]] 624 | name = "urllib3" 625 | version = "2.3.0" 626 | source = { registry = "https://pypi.org/simple" } 627 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 628 | wheels = [ 629 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 630 | ] 631 | 632 | [[package]] 633 | name = "wikipedia" 634 | version = "1.4.0" 635 | source = { registry = "https://pypi.org/simple" } 636 | dependencies = [ 637 | { name = "beautifulsoup4" }, 638 | { name = "requests" }, 639 | ] 640 | sdist = { url = "https://files.pythonhosted.org/packages/67/35/25e68fbc99e672127cc6fbb14b8ec1ba3dfef035bf1e4c90f78f24a80b7d/wikipedia-1.4.0.tar.gz", hash = "sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2", size = 27748 } 641 | --------------------------------------------------------------------------------