├── .github
└── workflows
│ ├── codspeed.yml
│ └── python-package.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
├── Backend-Caches.md
├── ImplementationNotes.md
├── README.md
├── What-About-Neptune-Dramatic-Retelling.md
├── What-About-Neptune.md
├── grand.png
└── reference
│ ├── backends
│ ├── _dataframe.py.md
│ ├── _dynamodb.py.md
│ ├── _gremlin.py.md
│ ├── _igraph.py.md
│ ├── _networkit.py.md
│ ├── _networkx.py.md
│ ├── _sqlbackend.py.md
│ ├── backend.py.md
│ ├── backends.md
│ ├── dynamodb.py.md
│ ├── gremlin.py.md
│ ├── igraph.py.md
│ ├── metadatastore.py.md
│ ├── networkit.py.md
│ ├── networkx.py.md
│ └── sqlbackend.py.md
│ ├── dialects
│ └── dialects.md
│ ├── grand
│ └── grand.md
│ └── setup.py.md
├── grand
├── __init__.py
├── backends
│ ├── __init__.py
│ ├── _dataframe.py
│ ├── _dynamodb.py
│ ├── _gremlin.py
│ ├── _igraph.py
│ ├── _networkit.py
│ ├── _networkx.py
│ ├── _sqlbackend.py
│ ├── backend.py
│ ├── metadatastore.py
│ ├── test_backends.py
│ ├── test_cached_backend.py
│ └── test_metadatastore.py
├── dialects
│ ├── __init__.py
│ └── test_dialect.py
└── test_graph.py
├── pyproject.toml
└── uv.lock
/.github/workflows/codspeed.yml:
--------------------------------------------------------------------------------
1 | name: codspeed-benchmarks
2 |
3 | on:
4 | push:
5 | branches:
6 | - "master"
7 | pull_request:
8 | # `workflow_dispatch` allows CodSpeed to trigger backtest
9 | # performance analysis in order to generate initial data.
10 | workflow_dispatch:
11 |
12 | jobs:
13 | benchmarks:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-python@v3
18 | with:
19 | python-version: "3.11"
20 |
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install pytest pytest-cov pytest-codspeed
25 | # Install "cmake", "cython" for networkit. Must happen first:
26 | pip install --upgrade cmake cython
27 | pip install -e ".[sql]"
28 |
29 | - name: Run benchmarks
30 | uses: CodSpeedHQ/action@v3
31 | with:
32 | # token: ${{ secrets.CODSPEED_TOKEN }}
33 | run: pytest . --codspeed
34 |
--------------------------------------------------------------------------------
/.github/workflows/python-package.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python package
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | python-version: ['3.10', '3.11', '3.12', '3.13']
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v5
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | pip install flake8 pytest pytest-cov
30 | # Install "cmake", "cython" for networkit. Must happen first:
31 | pip install --upgrade cmake cython
32 | pip install -e ".[sql,networkit,igraph]"
33 | - name: Lint with flake8
34 | run: |
35 | # stop the build if there are Python syntax errors or undefined names
36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
39 | - name: Test with pytest
40 | run: |
41 | pytest --cov=./ --cov-report=xml
42 | - name: Codecov
43 | uses: codecov/codecov-action@v1.0.13
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | notebooks/
3 | # Created by https://www.toptal.com/developers/gitignore/api/macos,python,windows,jupyternotebooks,visualstudiocode
4 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,python,windows,jupyternotebooks,visualstudiocode
5 |
6 | ### JupyterNotebooks ###
7 | # gitignore template for Jupyter Notebooks
8 | # website: http://jupyter.org/
9 |
10 | .ipynb_checkpoints
11 | */.ipynb_checkpoints/*
12 |
13 | # IPython
14 | profile_default/
15 | ipython_config.py
16 |
17 | # Remove previous ipynb_checkpoints
18 | # git rm -r .ipynb_checkpoints/
19 |
20 | ### macOS ###
21 | # General
22 | .DS_Store
23 | .AppleDouble
24 | .LSOverride
25 |
26 | # Icon must end with two \r
27 | Icon
28 |
29 | # Thumbnails
30 | ._*
31 |
32 | # Files that might appear in the root of a volume
33 | .DocumentRevisions-V100
34 | .fseventsd
35 | .Spotlight-V100
36 | .TemporaryItems
37 | .Trashes
38 | .VolumeIcon.icns
39 | .com.apple.timemachine.donotpresent
40 |
41 | # Directories potentially created on remote AFP share
42 | .AppleDB
43 | .AppleDesktop
44 | Network Trash Folder
45 | Temporary Items
46 | .apdisk
47 |
48 | ### Python ###
49 | # Byte-compiled / optimized / DLL files
50 | __pycache__/
51 | *.py[cod]
52 | *$py.class
53 |
54 | # C extensions
55 | *.so
56 |
57 | # Distribution / packaging
58 | .Python
59 | build/
60 | develop-eggs/
61 | dist/
62 | downloads/
63 | eggs/
64 | .eggs/
65 | lib/
66 | lib64/
67 | parts/
68 | sdist/
69 | var/
70 | wheels/
71 | pip-wheel-metadata/
72 | share/python-wheels/
73 | *.egg-info/
74 | .installed.cfg
75 | *.egg
76 | MANIFEST
77 |
78 | # PyInstaller
79 | # Usually these files are written by a python script from a template
80 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
81 | *.manifest
82 | *.spec
83 |
84 | # Installer logs
85 | pip-log.txt
86 | pip-delete-this-directory.txt
87 |
88 | # Unit test / coverage reports
89 | htmlcov/
90 | .tox/
91 | .nox/
92 | .coverage
93 | .coverage.*
94 | .cache
95 | nosetests.xml
96 | coverage.xml
97 | *.cover
98 | *.py,cover
99 | .hypothesis/
100 | .pytest_cache/
101 | pytestdebug.log
102 |
103 | # Translations
104 | *.mo
105 | *.pot
106 |
107 | # Django stuff:
108 | *.log
109 | local_settings.py
110 | db.sqlite3
111 | db.sqlite3-journal
112 |
113 | # Flask stuff:
114 | instance/
115 | .webassets-cache
116 |
117 | # Scrapy stuff:
118 | .scrapy
119 |
120 | # Sphinx documentation
121 | docs/_build/
122 | doc/_build/
123 |
124 | # PyBuilder
125 | target/
126 |
127 | # Jupyter Notebook
128 |
129 | # IPython
130 |
131 | # pyenv
132 | .python-version
133 |
134 | # pipenv
135 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
136 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
137 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
138 | # install all needed dependencies.
139 | #Pipfile.lock
140 |
141 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
142 | __pypackages__/
143 |
144 | # Celery stuff
145 | celerybeat-schedule
146 | celerybeat.pid
147 |
148 | # SageMath parsed files
149 | *.sage.py
150 |
151 | # Environments
152 | .env
153 | .venv
154 | env/
155 | venv/
156 | ENV/
157 | env.bak/
158 | venv.bak/
159 |
160 | # Spyder project settings
161 | .spyderproject
162 | .spyproject
163 |
164 | # Rope project settings
165 | .ropeproject
166 |
167 | # mkdocs documentation
168 | /site
169 |
170 | # mypy
171 | .mypy_cache/
172 | .dmypy.json
173 | dmypy.json
174 |
175 | # Pyre type checker
176 | .pyre/
177 |
178 | # pytype static type analyzer
179 | .pytype/
180 |
181 | ### VisualStudioCode ###
182 | .vscode/*
183 | !.vscode/settings.json
184 | !.vscode/tasks.json
185 | !.vscode/launch.json
186 | !.vscode/extensions.json
187 | *.code-workspace
188 |
189 | ### VisualStudioCode Patch ###
190 | # Ignore all local history of files
191 | .history
192 |
193 | ### Windows ###
194 | # Windows thumbnail cache files
195 | Thumbs.db
196 | Thumbs.db:encryptable
197 | ehthumbs.db
198 | ehthumbs_vista.db
199 |
200 | # Dump file
201 | *.stackdump
202 |
203 | # Folder config file
204 | [Dd]esktop.ini
205 |
206 | # Recycle Bin used on file shares
207 | $RECYCLE.BIN/
208 |
209 | # Windows Installer files
210 | *.cab
211 | *.msi
212 | *.msix
213 | *.msm
214 | *.msp
215 |
216 | # Windows shortcuts
217 | *.lnk
218 |
219 | # End of https://www.toptal.com/developers/gitignore/api/macos,python,windows,jupyternotebooks,visualstudiocode
220 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## **0.7.0** (May 25, 2025)
4 |
5 | - Housekeeping:
6 | - Upgrade from setup.py to pyproject.toml (#62, thanks @AdrianVollmer!)
7 | - Bugfixes:
8 | - Fix NetworkX `degree` parity (#64)
9 |
10 | ## **0.6.0** (December 8, 2024)
11 |
12 | - Features:
13 | - Upgrade codspeed CI runner to v3
14 | - Bugfixes:
15 | - Fix `get_node_by_id` method in `SQLBackend` (#59. thanks @davidmezzetti!)
16 | - Add `remove_node` to the `CachedBackend` (#59. thanks @davidmezzetti!)
17 |
18 | ## **0.5.3** (December 7, 2024)
19 |
20 | - Features:
21 | - Add `remove_node` method to SQLBackend (#58. thanks @davidmezzetti!)
22 | - Bugfixes:
23 | - Modify sqlite backend `has_node` to return a bool vs int (#58. thanks @davidmezzetti!)
24 | - Update method parameters to conform to existing naming standards (#58. thanks @davidmezzetti!)
25 |
26 | ## **0.5.2** (June 4, 2024)
27 |
28 | - Bugfixes:
29 | - Fixes SQLBackend mistakenly referencing Nodes table for enumeration of Edges (#55, #56)
30 |
31 | ## **0.5.1** (May 13, 2024)
32 |
33 | - Bugfixes:
34 | - Fixes SQLBackend bug where graphs would not commit down to disk after transactions (#51, thanks @acthecoder23!)
35 |
36 | ## **0.5.0** (April 17, 2024)
37 |
38 | - Features
39 | - https://github.com/aplbrain/grand/pull/45 (thanks @davidmezzetti!)
40 | - Improved support for SQL backend, including an index on edge.sources and edge.targets
41 | - Improved batch-adding performance for nodes and edges
42 | - Housekeeping
43 | - Removed extra arguments to Graph constructor and improved Graph kwargs handing
44 | - Added `grand.DiGraph` convenience wrapper for directed graphs
45 | - Dialects
46 | - Expanded Networkit dialect test coverage
47 | - Added support for exporting NetworkX graphs by adding `graph` attribute
48 |
49 | ## **0.4.2** (May 7, 2022)
50 |
51 | - Backends
52 | - NEW: `DataFrameBackend` supports operations on a pandas-like API.
53 | - Housekeeping
54 | - Added tests for metadata stores
55 | - Added IGraph and Networkit backends to the standard GitHub Actions CI test suite
56 |
57 | ## **0.4.1** (February 10, 2022)
58 |
59 | - Improvements
60 | - Dialects
61 | - NetworkX Dialect:
62 | - Edges are now reported as EdgeViews or other NetworkX reporting classes.
63 | - Housekeeping
64 | - Removed unused tests
65 | - Removed DotMotif dialect, which is no longer a goal of this repository (and is still possible by passing a grand.Graph#nx to the DotMotif library.)
66 |
67 | ## **0.4.0**
68 |
69 | - Improvements
70 | - Backends
71 | - SQLBackend:
72 | - Add support for user-specified edge column names, with `edge_table_source_column` and `edge_table_target_column` arguments.
73 | - Fix buggy performance when updating nodes and edges.
74 | - Caching support. You can now cache the results from backend methods by wrapping with a class that inherits from `CachedBackend`.
75 | - Housekeeping
76 | - Fix `pip install grand-graph[sql]` and `pip install grand-graph[dynamodb]`, which failed on previous versions due to a faulty setup.py key.
77 | - Rename all backend files.
78 |
79 | ## **0.3.0**
80 |
81 | > This version adds support for Gremlin-compatible graph databases, such as AWS Neptune, TinkerPop, Janus, etc, through the `GremlinBackend`, and loosens the requirements for the base installation of `grand-graph`. You can now install `grand-graph[sql]` or `grand-graph[dynamodb]` to get additional functionality (with additional dependencies).
82 |
83 | - Improvements
84 | - Backends
85 | - Add `GremlinBackend` to the list of supported backends
86 | - Housekeeping
87 | - Remove sqlalchemy and boto3 from the list of requirements for the base install. You can now install these with `pip3 install grand-graph[sql]` or `[dyanmodb]`.
88 |
89 | ## **0.2.0**
90 |
91 | > This version adds a new `IGraphBackend` (non-default install). If you have IGraph installed already, you can now import this backend with `from grand.backends.igraph import IGraphBackend`.
92 |
93 | - Improvements
94 | - Backends
95 | - Add `IGraphBackend` to the list of supported backends
96 |
97 | ## **0.1.0** (January 19, 2021)
98 |
99 | > This version adds dependency-install support to installations with `pip`. (Thanks @Raphtor!)
100 |
101 | - Improvements
102 | - Faster edge-indexing support for the SQLBackend, using compound primary keys rather than columnwise lookups
103 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |

4 |
5 |
6 |
7 |
8 |
9 | Graph toolkit interoperability and scalability for Python
10 |
11 | ## Installation
12 |
13 | ```shell
14 | pip install grand-graph
15 | ```
16 |
17 | ## Example use-cases
18 |
19 | - Write NetworkX commands to analyze true-serverless graph databases using DynamoDB\*
20 | - Query a host graph in SQL for subgraph isomorphisms with DotMotif
21 | - Write iGraph code to construct a graph, and then play with it in Networkit
22 | - Attach node and edge attributes to Networkit or IGraph graphs
23 |
24 | > \* [Neptune is not true-serverless.](docs/What-About-Neptune.md)
25 |
26 | ## Why it's a big deal
27 |
28 | _Grand_ is a Rosetta Stone of graph technologies. A _Grand_ graph has a "Backend," which handles the implementation-details of talking to data on disk (or in the cloud), and an "Dialect", which is your preferred way of talking to a graph.
29 |
30 | For example, here's how you make a graph that is persisted in DynamoDB (the "Backend") but that you can talk to as though it's a `networkx.DiGraph` (the "Dialect"):
31 |
32 | ```python
33 | import grand
34 |
35 | G = grand.Graph(backend=grand.DynamoDBBackend())
36 |
37 | G.nx.add_node("Jordan", type="Person")
38 | G.nx.add_node("DotMotif", type="Project")
39 |
40 | G.nx.add_edge("Jordan", "DotMotif", type="Created")
41 |
42 | assert len(G.nx.edges()) == 1
43 | assert len(G.nx.nodes()) == 2
44 | ```
45 |
46 | It doesn't stop there. If you like the way IGraph handles anonymous node insertion (ugh) but you want to handle the graph using regular NetworkX syntax, use a `IGraphDialect` and then switch to a `NetworkXDialect` halfway through:
47 |
48 | ```python
49 | import grand
50 |
51 | G = grand.Graph()
52 |
53 | # Start in igraph:
54 | G.igraph.add_vertices(5)
55 |
56 | # A little bit of networkit:
57 | G.networkit.addNode()
58 |
59 | # And switch to networkx:
60 | assert len(G.nx.nodes()) == 6
61 |
62 | # And back to igraph!
63 | assert len(G.igraph.vs) == 6
64 | ```
65 |
66 | You should be able to use the "dialect" objects the same way you'd use a real graph from the constituent libraries. For example, here is a NetworkX algorithm running on NetworkX graphs alongside Grand graphs:
67 |
68 | ```python
69 | import networkx as nx
70 |
71 | nx.algorithms.isomorphism.GraphMatcher(networkxGraph, grandGraph.nx)
72 | ```
73 |
74 | Here is an example of using Networkit, a highly performant graph library, and attaching node/edge attributes, which are not supported by the library by default:
75 |
76 | ```python
77 | import grand
78 | from grand.backends.networkit import NetworkitBackend
79 |
80 | G = grand.Graph(backend=NetworkitBackend())
81 |
82 | G.nx.add_node("Jordan", type="Person")
83 | G.nx.add_node("Grand", type="Software")
84 | G.nx.add_edge("Jordan", "Grand", weight=1)
85 |
86 | print(G.nx.edges(data=True)) # contains attributes, even though graph is stored in networkit
87 | ```
88 |
89 | ## Current Support
90 |
91 |
92 | ✅ = Fully Implemented |
93 | 🤔 = In Progress |
94 | 🔴 = Unsupported |
95 |
96 |
97 | | Dialect | Description & Notes | Status |
98 | | ------------------ | ------------------------ | ------ |
99 | | `IGraphDialect` | Python-IGraph interface | ✅ |
100 | | `NetworkXDialect` | NetworkX-like interface | ✅ |
101 | | `NetworkitDialect` | Networkit-like interface | ✅ |
102 |
103 | | Backend | Description & Notes | Status |
104 | | ------------------ | ---------------------------- | ------ |
105 | | `DataFrameBackend` | Stored in pandas-like tables | ✅ |
106 | | `DynamoDBBackend` | Edge/node tables in DynamoDB | ✅ |
107 | | `GremlinBackend` | For Gremlin datastores | ✅ |
108 | | `IGraphBackend` | An IGraph graph, in memory | ✅ |
109 | | `NetworkitBackend` | A Networkit graph, in memory | ✅ |
110 | | `NetworkXBackend` | A NetworkX graph, in memory | ✅ |
111 | | `SQLBackend` | Two SQL-queryable tables | ✅ |
112 |
113 | You can read more about usage and learn about backends and dialects in [the wiki](https://github.com/aplbrain/grand/wiki).
114 |
115 | ## Citing
116 |
117 | If this tool is helpful to your research, please consider citing it with:
118 |
119 | ```bibtex
120 | # https://doi.org/10.1038/s41598-021-91025-5
121 | @article{Matelsky_Motifs_2021,
122 | title={{DotMotif: an open-source tool for connectome subgraph isomorphism search and graph queries}},
123 | volume={11},
124 | ISSN={2045-2322},
125 | url={http://dx.doi.org/10.1038/s41598-021-91025-5},
126 | DOI={10.1038/s41598-021-91025-5},
127 | number={1},
128 | journal={Scientific Reports},
129 | publisher={Springer Science and Business Media LLC},
130 | author={Matelsky, Jordan K. and Reilly, Elizabeth P. and Johnson, Erik C. and Stiso, Jennifer and Bassett, Danielle S. and Wester, Brock A. and Gray-Roncal, William},
131 | year={2021},
132 | month={Jun}
133 | }
134 | ```
135 |
136 | ---
137 |
138 | Made with 💙 at 
139 |
--------------------------------------------------------------------------------
/docs/Backend-Caches.md:
--------------------------------------------------------------------------------
1 | # Backend Caches
2 |
3 | When a graph changes little or not at all, it may be desirable to cache the results of backend method calls to be reused on subsequent calls. This is especially useful when the backend is a remote service, such as a database or a REST API, where the cost of each method call may be substantial.
4 |
5 | To address this, we recommend the use of a class that inherits from `CachedBackend`. This class will cache the results of backend method calls, and will return the cached results if the method is called again with the same arguments.
6 |
7 | Furthermore, this cache will listen for calls to methods (such as `add_node`) that change the graph, and will automatically dirty the cache to avoid returning invalid results.
8 |
9 | ## Example Usage
10 |
11 | ```python
12 | from grand.backends import SQLBackend, InMemoryCachedBackend
13 | from grand import Graph
14 |
15 | G = Graph(backend=InMemoryCachedBackend(SQLBackend(...)))
16 |
17 | G.nx.degree(42) # May be slow if the graph is very large
18 | G.nx.degree(42) # Will be very fast, since the result is cached
19 | G.backend.clear_cache() # Clear the cache explicitly
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/ImplementationNotes.md:
--------------------------------------------------------------------------------
1 | # Implementation Notes
2 |
3 |
4 |
5 | Adding nodes is an upsert operation.
6 |
7 | When you call the `add_node` operation on a grand `backend` object, it will check to see if the node already exists in the database. If it does, it will update the node with the new data. If it does not, it will insert the node into the database. Note that there is no difference visible to the user between these two operations.
8 |
9 | If you (as the end-user of this library) want to know whether a node is already present in the graph, you can use the `has_node` operation.
10 |
11 | Alternatively, you can call the `add_node` operation with the `upsert` flag set to False.
12 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Grand: Documentation
2 |
3 | API reference documentation is available under `reference/` in this directory (https://github.com/aplbrain/grand/tree/master/docs/reference).
4 |
5 | Reference documentation was generated with [Docshund](https://github.com/fitmango/docshund).
6 |
--------------------------------------------------------------------------------
/docs/What-About-Neptune-Dramatic-Retelling.md:
--------------------------------------------------------------------------------
1 | # What about Neptune?
2 |
3 | ```
4 | WIDE SHOT: COMPUTER LAB
5 |
6 | NEPTUNE
7 |
8 | (...Emerging from the sea,
9 | which has materialized in
10 | the corner of the computer
11 | lab, much to the chagrin of
12 | the sysadmin.)
13 |
14 | Thou darest call Grand-DynamoDB the first true-serverless
15 | graph database? [BOOMING] Audacity!
16 |
17 |
18 | JORDAN
19 |
20 | (Without turning or looking
21 | away from laptop)
22 |
23 | Tell me the Neptune pricing model.
24 |
25 | NEPTUNE
26 |
27 | (Rearing from throne of
28 | seafoam, shaking seawater
29 | off of his beard with rage)
30 |
31 | Thou simply payest for thine virtual machine!
32 |
33 | JORDAN
34 |
35 | That sounds serverful to me.
36 |
37 | NEPTUNE
38 |
39 | Yet thou needn't manage thine own server!
40 |
41 | JORDAN
42 |
43 | Actually that kinda sounds like a disadvantage, if
44 | I'm going to the trouble of paying for CPU-hours anyway.
45 |
46 | NEPTUNE
47 |
48 | (Red with fury, seamounts
49 | erupting in vicinity)
50 |
51 | FOOL! If thou would to pay only per-request, thou
52 | haveth DynamoDB, among other offerings!
53 |
54 | JORDAN
55 |
56 | Yep, exactly. I don't use my graph database 24/7, so
57 | it doesn't make sense to pay for an always-on VM-equivalent.
58 |
59 | (NEPTUNE grows thoughtful)
60 |
61 | So Grand wraps DynamoDB with a graph-based API so that you
62 | can treat data stored in DynamoDB like any other graph.
63 |
64 | NEPTUNE
65 |
66 | Ah, I see thine logic.
67 |
68 | (...Detecting a potential weakness!)
69 |
70 | But havest thou a standardized API?!
71 |
72 | JORDAN
73 |
74 | Aye— I mean, yes. Grand supports interacting with a graph
75 | with standard NetworkX or IGraph APIs, among others.
76 |
77 | NEPTUNE
78 |
79 | (Washing gradually back
80 | into the ocean, calmed...
81 | SUN appears over the
82 | SERVER RACKS)
83 |
84 | I am humbled. Thou mayest proceed.
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/What-About-Neptune.md:
--------------------------------------------------------------------------------
1 | # What about Neptune?
2 |
3 | ## Background
4 |
5 | In many industry applications, graph databases are commonly used for "shallow" graph queries — in other words, queries that rely heavily on node-wise or edge-wise indexing, but not too heavily on multiple hops along graph relations. For example, "find all purchases made by this set of users," or "find all movies with actors that also acted in a movie this user has rated five stars." These queries benefit greatly from the graph structure of the database, but do not involve deep or complete graph traversals.
6 |
7 | In contrast with these common industry needs, mathematical or scientific graph algorithms are notoriously hard to profile, and predicting workload for arbitrary data science manipulations on a graph is a major challenge in many big-data graph or network-science applications. Graph queries may frequently involve traversing every node or edge in a graph, or performing some accumulative function across a graph. So-called "deep" graph queries include subgraph isomorphism search, graph matching, and pathfinding.
8 |
9 | ## AWS Neptune
10 |
11 | [AWS Neptune](https://aws.amazon.com/neptune/getting-started/) is a graph database as a service provided by Amazon Web Services. Though it says "serverless" on the packaging, there are a few considerations to be aware of:
12 |
13 | * Neptune requires that you provision a "server-equivalent" amount of compute power. For example, you can provision a single EC2 instance's worth of compute, in which case it is equivalent to running a graph database on a single node for writes. ([Reads can be parallelized across a provisioned cluster.](https://docs.aws.amazon.com/neptune/latest/userguide/intro.html))
14 | * Unlike DynamoDB, Neptune does not have an on-demand auto-scaling feature. In other words, if you rapidly double the number of queries you're running per second in DynamoDB, it meets your need. If you rapidly double the number of queries you send to Neptune, it may very likely fail. [[DynamoDB Auto-Scaling Documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html)]
15 |
16 | This means for certain sparse or bursty use-cases, Neptune may be _dramatically_ more expensive. For example, consider these cases:
17 |
18 | ### Neptune's pricing model excels:
19 |
20 | * Homogenous or predictable workload, consistent algorithmic complexity at all hours.
21 | * For example: An e-commerce site that maps users to recommended products.
22 | * Adiabatic (gradual) changes to server load that take place over many minutes or hours (to give auto-scaling software time to adapt)
23 | * For example: A social media website with traffic patterns that match common wake/sleep cycles across time-zones
24 |
25 | ### Neptune's pricing model may be disadvantageous:
26 | * A large workload runs periodically but infrequently, for short periods of time
27 | * For example: A web-crawler that runs every hour to identify and link new pharmaceutical research literature
28 | * A user can trigger an arbitrary graph database algorithm via API
29 | * For example: Users have direct control over the executiion of complex queries across a graph
30 |
31 | As a workaround, [Grand](https://github.com/aplbrain/grand) rewrites graph operations in an abstracted graph API representation, and then implements these calls as operations on a DynamoDB table. In other words, Grand acts as an abstraction layer to enable bursty workloads with arbitrary datastore backends (DynamoDB, SQLite, NetworkX in memory). This is _less efficient_ than using a graph database like Neptune, but it frees the user to pay only for the compute and resources that they are using.
32 |
33 | ## Discussion
34 |
35 | Neptune is still a relatively young product, and I'm hopeful that AWS will consider adding "true-serverless" pricing models, as they have done with "on-demand" DynamoDB pricing and, more recently, Aurora (though [this is still in preview](https://pages.awscloud.com/AmazonAuroraServerlessv2Preview.html)).
36 |
37 | In the medium-term, it is possible to engineer your own scaling software for Neptune that listens for traffic and scales up to meet demand. This capability is currently out of scope for the Grand library.
38 |
39 | ---
40 |
41 | To read a dramatic screenplay retelling of this document, click [here](What-About-Neptune-Dramatic-Retelling.md).
42 |
--------------------------------------------------------------------------------
/docs/grand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aplbrain/grand/ce815357e73c05c947b1194220f531430e6fc2cc/docs/grand.png
--------------------------------------------------------------------------------
/docs/reference/backends/_dataframe.py.md:
--------------------------------------------------------------------------------
1 | ## *Function* `is_directed(self) -> bool`
2 |
3 |
4 | Return True if the backend graph is directed.
5 |
6 | ### Arguments
7 | None
8 |
9 | ### Returns
10 | > - **bool** (`None`: `None`): True if the backend graph is directed.
11 |
12 |
13 |
14 | ## *Function* `teardown(self, yes_i_am_sure: bool = False)`
15 |
16 |
17 | Tear down this graph, deleting all evidence it once was here.
18 |
19 |
20 |
21 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict) -> Hashable`
22 |
23 |
24 | Add a new node to the graph.
25 |
26 | Insert a new document into the nodes table.
27 |
28 | ### Arguments
29 | > - **node_name** (`Hashable`: `None`): The ID of the node
30 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
31 |
32 | ### Returns
33 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
34 |
35 |
36 |
37 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False)`
38 |
39 |
40 | Get a generator of all of the nodes in this graph.
41 |
42 | ### Arguments
43 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
44 | the response
45 |
46 | ### Returns
47 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
48 |
49 |
50 |
51 | ## *Function* `has_node(self, u: Hashable) -> bool`
52 |
53 |
54 | Return true if the node exists in the graph.
55 |
56 | ### Arguments
57 | > - **u** (`Hashable`: `None`): The ID of the node to check
58 |
59 | ### Returns
60 | > - **bool** (`None`: `None`): True if the node exists
61 |
62 |
63 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
64 |
65 |
66 | Add a new edge to the graph between two nodes.
67 |
68 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
69 |
70 | ### Arguments
71 | > - **u** (`Hashable`: `None`): The source node ID
72 | > - **v** (`Hashable`: `None`): The target node ID
73 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
74 |
75 | ### Returns
76 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
77 |
78 |
79 |
80 | ## *Function* `_has_edge(self, u: Hashable, v: Hashable) -> bool`
81 |
82 |
83 | Return true if the edge exists in the graph.
84 |
85 | ### Arguments
86 | > - **u** (`Hashable`: `None`): The source node ID
87 | > - **v** (`Hashable`: `None`): The target node ID
88 |
89 | ### Returns
90 | > - **bool** (`None`: `None`): True if the edge exists
91 |
92 |
93 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
94 |
95 |
96 | Get a list of all edges in this graph, arbitrary sort.
97 |
98 | ### Arguments
99 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
100 |
101 | ### Returns
102 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
103 |
104 |
105 |
106 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
107 |
108 |
109 | Return the data associated with a node.
110 |
111 | ### Arguments
112 | > - **node_name** (`Hashable`: `None`): The node ID to look up
113 |
114 | ### Returns
115 | > - **dict** (`None`: `None`): The metadata associated with this node
116 |
117 |
118 |
119 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
120 |
121 |
122 | Get an edge by its source and target IDs.
123 |
124 | ### Arguments
125 | > - **u** (`Hashable`: `None`): The source node ID
126 | > - **v** (`Hashable`: `None`): The target node ID
127 |
128 | ### Returns
129 | > - **dict** (`None`: `None`): Metadata associated with this edge
130 |
131 |
132 |
133 | ## *Function* `get_node_neighbors(self, u: Hashable, include_metadata: bool = False)`
134 |
135 |
136 | Get a generator of all downstream nodes from this node.
137 |
138 | ### Arguments
139 | > - **u** (`Hashable`: `None`): The source node ID
140 |
141 | ### Returns
142 | Generator
143 |
144 |
145 |
146 | ## *Function* `_edge_as_dict(self, row)`
147 |
148 |
149 | Convert an edge row to a dictionary.
150 |
151 | ### Arguments
152 | > - **row** (`pandas.Series`: `None`): The edge row
153 |
154 | ### Returns
155 | > - **dict** (`None`: `None`): The edge metadata
156 |
157 |
158 |
159 | ## *Function* `get_node_predecessors(self, u: Hashable, include_metadata: bool = False)`
160 |
161 |
162 | Get a generator of all upstream nodes from this node.
163 |
164 | ### Arguments
165 | > - **u** (`Hashable`: `None`): The source node ID
166 |
167 | ### Returns
168 | Generator
169 |
170 |
171 |
172 | ## *Function* `get_node_count(self) -> int`
173 |
174 |
175 | Get an integer count of the number of nodes in this graph.
176 |
177 | ### Arguments
178 | None
179 |
180 | ### Returns
181 | > - **int** (`None`: `None`): The count of nodes
182 |
183 |
--------------------------------------------------------------------------------
/docs/reference/backends/_dynamodb.py.md:
--------------------------------------------------------------------------------
1 | ## *Function* `_dynamo_table_exists(table_name: str, client: boto3.client)`
2 |
3 |
4 | Check to see if the DynamoDB table already exists.
5 |
6 | ### Returns
7 | > - **bool** (`None`: `None`): Whether table exists
8 |
9 |
10 |
11 | ## *Class* `DynamoDBBackend(Backend)`
12 |
13 |
14 | A graph datastore that uses DynamoDB for persistance and queries.
15 |
16 |
17 |
18 | ## *Function* `is_directed(self) -> bool`
19 |
20 |
21 | Return True if the backend graph is directed.
22 |
23 | ### Arguments
24 | None
25 |
26 | ### Returns
27 | > - **bool** (`None`: `None`): True if the backend graph is directed.
28 |
29 |
30 |
31 | ## *Function* `teardown(self, yes_i_am_sure: bool = False)`
32 |
33 |
34 | Tear down this graph, deleting all evidence it once was here.
35 |
36 |
37 |
38 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict) -> Hashable`
39 |
40 |
41 | Add a new node to the graph.
42 |
43 | Insert a new document into the nodes table.
44 |
45 | ### Arguments
46 | > - **node_name** (`Hashable`: `None`): The ID of the node
47 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
48 |
49 | ### Returns
50 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
51 |
52 |
53 |
54 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection`
55 |
56 |
57 | Get a generator of all of the nodes in this graph.
58 |
59 | ### Arguments
60 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
61 | the response
62 |
63 | ### Returns
64 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
65 |
66 |
67 |
68 | ## *Function* `has_node(self, u: Hashable) -> bool`
69 |
70 |
71 | Return true if the node exists in the graph.
72 |
73 | ### Arguments
74 | > - **u** (`Hashable`: `None`): The ID of the node to check
75 |
76 | ### Returns
77 | > - **bool** (`None`: `None`): True if the node exists
78 |
79 |
80 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
81 |
82 |
83 | Add a new edge to the graph between two nodes.
84 |
85 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
86 |
87 | ### Arguments
88 | > - **u** (`Hashable`: `None`): The source node ID
89 | > - **v** (`Hashable`: `None`): The target node ID
90 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
91 |
92 | ### Returns
93 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
94 |
95 |
96 |
97 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Collection`
98 |
99 |
100 | Get a list of all edges in this graph, arbitrary sort.
101 |
102 | ### Arguments
103 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
104 |
105 | ### Returns
106 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
107 |
108 |
109 |
110 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
111 |
112 |
113 | Return the data associated with a node.
114 |
115 | ### Arguments
116 | > - **node_name** (`Hashable`: `None`): The node ID to look up
117 |
118 | ### Returns
119 | > - **dict** (`None`: `None`): The metadata associated with this node
120 |
121 |
122 |
123 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
124 |
125 |
126 | Get an edge by its source and target IDs.
127 |
128 | ### Arguments
129 | > - **u** (`Hashable`: `None`): The source node ID
130 | > - **v** (`Hashable`: `None`): The target node ID
131 |
132 | ### Returns
133 | > - **dict** (`None`: `None`): Metadata associated with this edge
134 |
135 |
136 |
137 | ## *Function* `get_node_count(self) -> int`
138 |
139 |
140 | Get an integer count of the number of nodes in this graph.
141 |
142 | ### Arguments
143 | None
144 |
145 | ### Returns
146 | > - **int** (`None`: `None`): The count of nodes
147 |
148 |
--------------------------------------------------------------------------------
/docs/reference/backends/_gremlin.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `GremlinBackend(Backend)`
2 |
3 |
4 | A backend instance for Gremlin-compatible graph databases.
5 |
6 |
7 |
8 | ## *Function* `__init__(self, graph: GraphTraversalSource, directed: bool = True)`
9 |
10 |
11 | Create a new Backend instance wrapping a Gremlin endpoint.
12 |
13 | ### Arguments
14 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
15 |
16 | ### Returns
17 | None
18 |
19 |
20 |
21 | ## *Function* `is_directed(self) -> bool`
22 |
23 |
24 | Return True if the backend graph is directed.
25 |
26 | The Gremlin-backed datastore is always directed.
27 |
28 | ### Arguments
29 | None
30 |
31 | ### Returns
32 | > - **bool** (`None`: `None`): True if the backend graph is directed.
33 |
34 |
35 |
36 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
37 |
38 |
39 | Add a new node to the graph.
40 |
41 | ### Arguments
42 | > - **node_name** (`Hashable`: `None`): The ID of the node
43 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
44 |
45 | ### Returns
46 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
47 |
48 |
49 |
50 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
51 |
52 |
53 | Return the data associated with a node.
54 |
55 | ### Arguments
56 | > - **node_name** (`Hashable`: `None`): The node ID to look up
57 |
58 | ### Returns
59 | > - **dict** (`None`: `None`): The metadata associated with this node
60 |
61 |
62 |
63 | ## *Function* `has_node(self, u: Hashable) -> bool`
64 |
65 |
66 | Return the data associated with a node.
67 |
68 | ### Arguments
69 | > - **node_name** (`Hashable`: `None`): The node ID to look up
70 |
71 | ### Returns
72 | > - **dict** (`None`: `None`): The metadata associated with this node
73 |
74 |
75 |
76 | ## *Function* `remove_node(self, node_name: Hashable)`
77 |
78 |
79 | Remove a node.
80 |
81 | ### Arguments
82 | > - **node_name** (`Hashable`: `None`): The node ID to look up
83 |
84 | ### Returns
85 | > - **dict** (`None`: `None`): The metadata associated with this node
86 |
87 |
88 |
89 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection`
90 |
91 |
92 | Get a generator of all of the nodes in this graph.
93 |
94 | ### Arguments
95 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
96 | the response
97 |
98 | ### Returns
99 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
100 |
101 |
102 |
103 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
104 |
105 |
106 | Add a new edge to the graph between two nodes.
107 |
108 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
109 |
110 | ### Arguments
111 | > - **u** (`Hashable`: `None`): The source node ID
112 | > - **v** (`Hashable`: `None`): The target node ID
113 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
114 |
115 | ### Returns
116 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
117 |
118 |
119 |
120 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Collection`
121 |
122 |
123 | Get a list of all edges in this graph, arbitrary sort.
124 |
125 | ### Arguments
126 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
127 |
128 | ### Returns
129 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
130 |
131 |
132 |
133 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
134 |
135 |
136 | Get an edge by its source and target IDs.
137 |
138 | ### Arguments
139 | > - **u** (`Hashable`: `None`): The source node ID
140 | > - **v** (`Hashable`: `None`): The target node ID
141 |
142 | ### Returns
143 | > - **dict** (`None`: `None`): Metadata associated with this edge
144 |
145 |
146 |
147 | ## *Function* `get_node_count(self) -> int`
148 |
149 |
150 | Get an integer count of the number of nodes in this graph.
151 |
152 | ### Arguments
153 | None
154 |
155 | ### Returns
156 | > - **int** (`None`: `None`): The count of nodes
157 |
158 |
--------------------------------------------------------------------------------
/docs/reference/backends/_igraph.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `IGraphBackend(Backend)`
2 |
3 |
4 | This is currently UNOPTIMIZED CODE.
5 |
6 | Recommendations for future work include improved indexing and caching of node names and metadata.
7 |
8 |
9 |
10 | ## *Function* `__init__(self, directed: bool = False)`
11 |
12 |
13 | Create a new IGraphBackend instance, using an igraph.Graph object to store and manage network structure.
14 |
15 | ### Arguments
16 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
17 |
18 | ### Returns
19 | None
20 |
21 |
22 |
23 | ## *Function* `is_directed(self) -> bool`
24 |
25 |
26 | Return True if the backend graph is directed.
27 |
28 | ### Arguments
29 | None
30 |
31 | ### Returns
32 | > - **bool** (`None`: `None`): True if the backend graph is directed.
33 |
34 |
35 |
36 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
37 |
38 |
39 | Add a new node to the graph.
40 |
41 | ### Arguments
42 | > - **node_name** (`Hashable`: `None`): The ID of the node
43 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
44 |
45 | ### Returns
46 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
47 |
48 |
49 |
50 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
51 |
52 |
53 | Return the data associated with a node.
54 |
55 | ### Arguments
56 | > - **node_name** (`Hashable`: `None`): The node ID to look up
57 |
58 | ### Returns
59 | > - **dict** (`None`: `None`): The metadata associated with this node
60 |
61 |
62 |
63 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection`
64 |
65 |
66 | Get a generator of all of the nodes in this graph.
67 |
68 | ### Arguments
69 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
70 | the response
71 |
72 | ### Returns
73 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
74 |
75 |
76 |
77 | ## *Function* `has_node(self, u: Hashable) -> bool`
78 |
79 |
80 | Return true if the node exists in the graph.
81 |
82 | ### Arguments
83 | > - **u** (`Hashable`: `None`): The ID of the node to check
84 |
85 | ### Returns
86 | > - **bool** (`None`: `None`): True if the node exists
87 |
88 |
89 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
90 |
91 |
92 | Add a new edge to the graph between two nodes.
93 |
94 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
95 |
96 | ### Arguments
97 | > - **u** (`Hashable`: `None`): The source node ID
98 | > - **v** (`Hashable`: `None`): The target node ID
99 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
100 |
101 | ### Returns
102 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
103 |
104 |
105 |
106 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Collection`
107 |
108 |
109 | Get a list of all edges in this graph, arbitrary sort.
110 |
111 | ### Arguments
112 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
113 |
114 | ### Returns
115 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
116 |
117 |
118 |
119 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
120 |
121 |
122 | Get an edge by its source and target IDs.
123 |
124 | ### Arguments
125 | > - **u** (`Hashable`: `None`): The source node ID
126 | > - **v** (`Hashable`: `None`): The target node ID
127 |
128 | ### Returns
129 | > - **dict** (`None`: `None`): Metadata associated with this edge
130 |
131 |
132 |
133 | ## *Function* `get_node_count(self) -> int`
134 |
135 |
136 | Get an integer count of the number of nodes in this graph.
137 |
138 | ### Arguments
139 | None
140 |
141 | ### Returns
142 | > - **int** (`None`: `None`): The count of nodes
143 |
144 |
--------------------------------------------------------------------------------
/docs/reference/backends/_networkit.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `NetworkitBackend(Backend)`
2 |
3 |
4 | Networkit doesn't support metadata or named nodes, so all node names and metadata must currently be stored in a parallel data structure.
5 |
6 | To solve this problem, a NodeNameManager and MetadataStore, from `grand.backends.metadatastore.NodeNameManager` and `grand.backends.metadatastore.MetadataStore` respectively, are included at the top level of this class. In order to preserve this metadata structure statefully, you must serialize both the graph as well as the data stores.
7 |
8 | This is currently UNOPTIMIZED CODE.
9 |
10 | Recommendations for future work include improved indexing and caching of node names and metadata.
11 |
12 | > - **documentation** (`None`: `None`): https://networkit.github.io/dev-docs/python_api/graph.html
13 |
14 |
15 |
16 | ## *Function* `__init__(self, directed: bool = False, metadata_store: MetadataStore = None)`
17 |
18 |
19 | Create a new NetworkitBackend instance, using a Networkit.graph.Graph object to store and manage network structure.
20 |
21 | ### Arguments
22 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
23 | > - **metadata_store** (`MetadataStore`: `None`): Optionally, a MetadataStore to use
24 | to handle node and edge attributes. If not provided, defaults to a DictMetadataStore.
25 |
26 | ### Returns
27 | None
28 |
29 |
30 |
31 | ## *Function* `is_directed(self) -> bool`
32 |
33 |
34 | Return True if the backend graph is directed.
35 |
36 | ### Arguments
37 | None
38 |
39 | ### Returns
40 | > - **bool** (`None`: `None`): True if the backend graph is directed.
41 |
42 |
43 |
44 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
45 |
46 |
47 | Add a new node to the graph.
48 |
49 | ### Arguments
50 | > - **node_name** (`Hashable`: `None`): The ID of the node
51 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
52 |
53 | ### Returns
54 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
55 |
56 |
57 |
58 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
59 |
60 |
61 | Return the data associated with a node.
62 |
63 | ### Arguments
64 | > - **node_name** (`Hashable`: `None`): The node ID to look up
65 |
66 | ### Returns
67 | > - **dict** (`None`: `None`): The metadata associated with this node
68 |
69 |
70 |
71 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
72 |
73 |
74 | Get a generator of all of the nodes in this graph.
75 |
76 | ### Arguments
77 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
78 | the response
79 |
80 | ### Returns
81 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
82 |
83 |
84 |
85 | ## *Function* `has_node(self, u: Hashable) -> bool`
86 |
87 |
88 | Return true if the node exists in the graph.
89 |
90 | ### Arguments
91 | > - **u** (`Hashable`: `None`): The ID of the node to check
92 |
93 | ### Returns
94 | > - **bool** (`None`: `None`): True if the node exists
95 |
96 |
97 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
98 |
99 |
100 | Add a new edge to the graph between two nodes.
101 |
102 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
103 |
104 | ### Arguments
105 | > - **u** (`Hashable`: `None`): The source node ID
106 | > - **v** (`Hashable`: `None`): The target node ID
107 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
108 |
109 | ### Returns
110 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
111 |
112 |
113 |
114 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
115 |
116 |
117 | Get a list of all edges in this graph, arbitrary sort.
118 |
119 | ### Arguments
120 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
121 |
122 | ### Returns
123 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
124 |
125 |
126 |
127 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
128 |
129 |
130 | Get an edge by its source and target IDs.
131 |
132 | ### Arguments
133 | > - **u** (`Hashable`: `None`): The source node ID
134 | > - **v** (`Hashable`: `None`): The target node ID
135 |
136 | ### Returns
137 | > - **dict** (`None`: `None`): Metadata associated with this edge
138 |
139 |
140 |
141 | ## *Function* `get_node_count(self) -> Iterable`
142 |
143 |
144 | Get an integer count of the number of nodes in this graph.
145 |
146 | ### Arguments
147 | None
148 |
149 | ### Returns
150 | > - **int** (`None`: `None`): The count of nodes
151 |
152 |
--------------------------------------------------------------------------------
/docs/reference/backends/_networkx.py.md:
--------------------------------------------------------------------------------
1 | ## *Function* `__init__(self, directed: bool = False)`
2 |
3 |
4 | Create a new Backend instance.
5 |
6 | ### Arguments
7 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
8 |
9 | ### Returns
10 | None
11 |
12 |
13 |
14 | ## *Function* `is_directed(self) -> bool`
15 |
16 |
17 | Return True if the backend graph is directed.
18 |
19 | ### Arguments
20 | None
21 |
22 | ### Returns
23 | > - **bool** (`None`: `None`): True if the backend graph is directed.
24 |
25 |
26 |
27 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
28 |
29 |
30 | Add a new node to the graph.
31 |
32 | ### Arguments
33 | > - **node_name** (`Hashable`: `None`): The ID of the node
34 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
35 |
36 | ### Returns
37 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
38 |
39 |
40 |
41 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
42 |
43 |
44 | Return the data associated with a node.
45 |
46 | ### Arguments
47 | > - **node_name** (`Hashable`: `None`): The node ID to look up
48 |
49 | ### Returns
50 | > - **dict** (`None`: `None`): The metadata associated with this node
51 |
52 |
53 |
54 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection`
55 |
56 |
57 | Get a generator of all of the nodes in this graph.
58 |
59 | ### Arguments
60 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
61 | the response
62 |
63 | ### Returns
64 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
65 |
66 |
67 |
68 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
69 |
70 |
71 | Add a new edge to the graph between two nodes.
72 |
73 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
74 |
75 | ### Arguments
76 | > - **u** (`Hashable`: `None`): The source node ID
77 | > - **v** (`Hashable`: `None`): The target node ID
78 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
79 |
80 | ### Returns
81 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
82 |
83 |
84 |
85 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Collection`
86 |
87 |
88 | Get a list of all edges in this graph, arbitrary sort.
89 |
90 | ### Arguments
91 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
92 |
93 | ### Returns
94 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
95 |
96 |
97 |
98 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
99 |
100 |
101 | Get an edge by its source and target IDs.
102 |
103 | ### Arguments
104 | > - **u** (`Hashable`: `None`): The source node ID
105 | > - **v** (`Hashable`: `None`): The target node ID
106 |
107 | ### Returns
108 | > - **dict** (`None`: `None`): Metadata associated with this edge
109 |
110 |
111 |
112 | ## *Function* `get_node_count(self) -> int`
113 |
114 |
115 | Get an integer count of the number of nodes in this graph.
116 |
117 | ### Arguments
118 | None
119 |
120 | ### Returns
121 | > - **int** (`None`: `None`): The count of nodes
122 |
123 |
--------------------------------------------------------------------------------
/docs/reference/backends/_sqlbackend.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `SQLBackend(Backend)`
2 |
3 |
4 | A graph datastore that uses a SQL-like store for persistance and queries.
5 |
6 |
7 |
8 | ## *Function* `is_directed(self) -> bool`
9 |
10 |
11 | Return True if the backend graph is directed.
12 |
13 | ### Arguments
14 | None
15 |
16 | ### Returns
17 | > - **bool** (`None`: `None`): True if the backend graph is directed.
18 |
19 |
20 |
21 | ## *Function* `teardown(self, yes_i_am_sure: bool = False)`
22 |
23 |
24 | Tear down this graph, deleting all evidence it once was here.
25 |
26 |
27 |
28 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict) -> Hashable`
29 |
30 |
31 | Add a new node to the graph.
32 |
33 | Insert a new document into the nodes table.
34 |
35 | ### Arguments
36 | > - **node_name** (`Hashable`: `None`): The ID of the node
37 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
38 |
39 | ### Returns
40 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
41 |
42 |
43 |
44 | ## *Function* `_upsert_node(self, node_name: Hashable, metadata: dict) -> Hashable`
45 |
46 |
47 | Add a new node to the graph, or update an existing one.
48 |
49 | ### Arguments
50 | > - **node_name** (`Hashable`: `None`): The ID of the node
51 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
52 |
53 | ### Returns
54 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
55 |
56 |
57 |
58 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
59 |
60 |
61 | Get a generator of all of the nodes in this graph.
62 |
63 | ### Arguments
64 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
65 | the response
66 |
67 | ### Returns
68 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
69 |
70 |
71 |
72 | ## *Function* `has_node(self, u: Hashable) -> bool`
73 |
74 |
75 | Return true if the node exists in the graph.
76 |
77 | ### Arguments
78 | > - **u** (`Hashable`: `None`): The ID of the node to check
79 |
80 | ### Returns
81 | > - **bool** (`None`: `None`): True if the node exists
82 |
83 |
84 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
85 |
86 |
87 | Add a new edge to the graph between two nodes.
88 |
89 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
90 |
91 | ### Arguments
92 | > - **u** (`Hashable`: `None`): The source node ID
93 | > - **v** (`Hashable`: `None`): The target node ID
94 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
95 |
96 | ### Returns
97 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
98 |
99 |
100 |
101 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
102 |
103 |
104 | Get a list of all edges in this graph, arbitrary sort.
105 |
106 | ### Arguments
107 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
108 |
109 | ### Returns
110 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
111 |
112 |
113 |
114 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
115 |
116 |
117 | Return the data associated with a node.
118 |
119 | ### Arguments
120 | > - **node_name** (`Hashable`: `None`): The node ID to look up
121 |
122 | ### Returns
123 | > - **dict** (`None`: `None`): The metadata associated with this node
124 |
125 |
126 |
127 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
128 |
129 |
130 | Get an edge by its source and target IDs.
131 |
132 | ### Arguments
133 | > - **u** (`Hashable`: `None`): The source node ID
134 | > - **v** (`Hashable`: `None`): The target node ID
135 |
136 | ### Returns
137 | > - **dict** (`None`: `None`): Metadata associated with this edge
138 |
139 |
140 |
141 | ## *Function* `get_node_count(self) -> Iterable`
142 |
143 |
144 | Get an integer count of the number of nodes in this graph.
145 |
146 | ### Arguments
147 | None
148 |
149 | ### Returns
150 | > - **int** (`None`: `None`): The count of nodes
151 |
152 |
153 |
154 | ## *Function* `out_degrees(self, nbunch=None)`
155 |
156 |
157 | Return the in-degree of each node in the graph.
158 |
159 | ### Arguments
160 | > - **nbunch** (`Iterable`: `None`): The nodes to get the in-degree of
161 |
162 | ### Returns
163 | > - **dict** (`None`: `None`): A dictionary of node: in-degree pairs
164 |
165 |
166 |
167 | ## *Function* `in_degrees(self, nbunch=None)`
168 |
169 |
170 | Return the in-degree of each node in the graph.
171 |
172 | ### Arguments
173 | > - **nbunch** (`Iterable`: `None`): The nodes to get the in-degree of
174 |
175 | ### Returns
176 | > - **dict** (`None`: `None`): A dictionary of node: in-degree pairs
177 |
178 |
--------------------------------------------------------------------------------
/docs/reference/backends/backend.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `Backend(abc.ABC)`
2 |
3 |
4 | Abstract base class for the management of persisted graph structure.
5 |
6 | Do not use this class directly.
7 |
8 |
9 |
10 | ## *Function* `__init__(self, directed: bool = False)`
11 |
12 |
13 | Create a new Backend instance.
14 |
15 | ### Arguments
16 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
17 |
18 | ### Returns
19 | None
20 |
21 |
22 |
23 | ## *Function* `is_directed(self) -> bool`
24 |
25 |
26 | Return True if the backend graph is directed.
27 |
28 | ### Arguments
29 | None
30 |
31 | ### Returns
32 | > - **bool** (`None`: `None`): True if the backend graph is directed.
33 |
34 |
35 |
36 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
37 |
38 |
39 | Add a new node to the graph.
40 |
41 | ### Arguments
42 | > - **node_name** (`Hashable`: `None`): The ID of the node
43 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
44 | > - **upsert** (`bool`: `True`): Update the node if it already exists. If this
45 | is set to False and the node already exists, a backend may choose to throw an error or proceed gracefully.
46 |
47 | ### Returns
48 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
49 |
50 |
51 |
52 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
53 |
54 |
55 | Return the data associated with a node.
56 |
57 | ### Arguments
58 | > - **node_name** (`Hashable`: `None`): The node ID to look up
59 |
60 | ### Returns
61 | > - **dict** (`None`: `None`): The metadata associated with this node
62 |
63 |
64 |
65 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection`
66 |
67 |
68 | Get a generator of all of the nodes in this graph.
69 |
70 | ### Arguments
71 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
72 | the response
73 |
74 | ### Returns
75 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
76 |
77 |
78 |
79 | ## *Function* `has_node(self, u: Hashable) -> bool`
80 |
81 |
82 | Return true if the node exists in the graph.
83 |
84 | ### Arguments
85 | > - **u** (`Hashable`: `None`): The ID of the node to check
86 |
87 | ### Returns
88 | > - **bool** (`None`: `None`): True if the node exists
89 |
90 |
91 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
92 |
93 |
94 | Add a new edge to the graph between two nodes.
95 |
96 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
97 |
98 | ### Arguments
99 | > - **u** (`Hashable`: `None`): The source node ID
100 | > - **v** (`Hashable`: `None`): The target node ID
101 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
102 |
103 | ### Returns
104 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
105 |
106 |
107 |
108 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Collection`
109 |
110 |
111 | Get a list of all edges in this graph, arbitrary sort.
112 |
113 | ### Arguments
114 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
115 |
116 | ### Returns
117 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
118 |
119 |
120 |
121 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
122 |
123 |
124 | Get an edge by its source and target IDs.
125 |
126 | ### Arguments
127 | > - **u** (`Hashable`: `None`): The source node ID
128 | > - **v** (`Hashable`: `None`): The target node ID
129 |
130 | ### Returns
131 | > - **dict** (`None`: `None`): Metadata associated with this edge
132 |
133 |
134 |
135 | ## *Function* `get_node_count(self) -> int`
136 |
137 |
138 | Get an integer count of the number of nodes in this graph.
139 |
140 | ### Arguments
141 | None
142 |
143 | ### Returns
144 | > - **int** (`None`: `None`): The count of nodes
145 |
146 |
147 |
148 | ## *Function* `degree(self, u: Hashable) -> int`
149 |
150 |
151 | Get the degree of a node.
152 |
153 | ### Arguments
154 | > - **u** (`Hashable`: `None`): The node ID
155 |
156 | ### Returns
157 | > - **int** (`None`: `None`): The degree of the node
158 |
159 |
160 |
161 | ## *Function* `in_degree(self, u: Hashable) -> int`
162 |
163 |
164 | Get the in-degree of a node.
165 |
166 | ### Arguments
167 | > - **u** (`Hashable`: `None`): The node ID
168 |
169 | ### Returns
170 | > - **int** (`None`: `None`): The in-degree of the node
171 |
172 |
173 |
174 | ## *Function* `out_degree(self, u: Hashable) -> int`
175 |
176 |
177 | Get the out-degree of a node.
178 |
179 | ### Arguments
180 | > - **u** (`Hashable`: `None`): The node ID
181 |
182 | ### Returns
183 | > - **int** (`None`: `None`): The out-degree of the node
184 |
185 |
186 |
187 | ## *Class* `CachedBackend(Backend)`
188 |
189 |
190 | A proxy Backend that serves as a cache for any other grand.Backend.
191 |
192 |
193 |
194 | ## *Class* `InMemoryCachedBackend(CachedBackend)`
195 |
196 |
197 | A proxy Backend that serves as a cache for any other grand.Backend.
198 |
199 | Wraps each call to the Backend with an LRU cache.
200 |
201 |
202 |
203 | ## *Function* `clear_cache(self)`
204 |
205 |
206 | Clear the cache.
207 |
208 |
--------------------------------------------------------------------------------
/docs/reference/backends/backends.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aplbrain/grand/ce815357e73c05c947b1194220f531430e6fc2cc/docs/reference/backends/backends.md
--------------------------------------------------------------------------------
/docs/reference/backends/dynamodb.py.md:
--------------------------------------------------------------------------------
1 | ## *Function* `_dynamo_table_exists(table_name: str, client: boto3.client)`
2 |
3 |
4 | Check to see if the DynamoDB table already exists.
5 |
6 | ### Returns
7 | > - **bool** (`None`: `None`): Whether table exists
8 |
9 |
10 |
11 | ## *Class* `DynamoDBBackend(Backend)`
12 |
13 |
14 | A graph datastore that uses DynamoDB for persistance and queries.
15 |
16 |
17 |
18 | ## *Function* `is_directed(self) -> bool`
19 |
20 |
21 | Return True if the backend graph is directed.
22 |
23 | ### Arguments
24 | None
25 |
26 | ### Returns
27 | > - **bool** (`None`: `None`): True if the backend graph is directed.
28 |
29 |
30 |
31 | ## *Function* `teardown(self, yes_i_am_sure: bool = False)`
32 |
33 |
34 | Tear down this graph, deleting all evidence it once was here.
35 |
36 |
37 |
38 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict) -> Hashable`
39 |
40 |
41 | Add a new node to the graph.
42 |
43 | Insert a new document into the nodes table.
44 |
45 | ### Arguments
46 | > - **node_name** (`Hashable`: `None`): The ID of the node
47 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
48 |
49 | ### Returns
50 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
51 |
52 |
53 |
54 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
55 |
56 |
57 | Get a generator of all of the nodes in this graph.
58 |
59 | ### Arguments
60 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
61 | the response
62 |
63 | ### Returns
64 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
65 |
66 |
67 |
68 | ## *Function* `has_node(self, u: Hashable) -> bool`
69 |
70 |
71 | Return true if the node exists in the graph.
72 |
73 | ### Arguments
74 | > - **u** (`Hashable`: `None`): The ID of the node to check
75 |
76 | ### Returns
77 | > - **bool** (`None`: `None`): True if the node exists
78 |
79 |
80 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
81 |
82 |
83 | Add a new edge to the graph between two nodes.
84 |
85 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
86 |
87 | ### Arguments
88 | > - **u** (`Hashable`: `None`): The source node ID
89 | > - **v** (`Hashable`: `None`): The target node ID
90 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
91 |
92 | ### Returns
93 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
94 |
95 |
96 |
97 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
98 |
99 |
100 | Get a list of all edges in this graph, arbitrary sort.
101 |
102 | ### Arguments
103 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
104 |
105 | ### Returns
106 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
107 |
108 |
109 |
110 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
111 |
112 |
113 | Return the data associated with a node.
114 |
115 | ### Arguments
116 | > - **node_name** (`Hashable`: `None`): The node ID to look up
117 |
118 | ### Returns
119 | > - **dict** (`None`: `None`): The metadata associated with this node
120 |
121 |
122 |
123 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
124 |
125 |
126 | Get an edge by its source and target IDs.
127 |
128 | ### Arguments
129 | > - **u** (`Hashable`: `None`): The source node ID
130 | > - **v** (`Hashable`: `None`): The target node ID
131 |
132 | ### Returns
133 | > - **dict** (`None`: `None`): Metadata associated with this edge
134 |
135 |
136 |
137 | ## *Function* `get_node_count(self) -> Iterable`
138 |
139 |
140 | Get an integer count of the number of nodes in this graph.
141 |
142 | ### Arguments
143 | None
144 |
145 | ### Returns
146 | > - **int** (`None`: `None`): The count of nodes
147 |
148 |
--------------------------------------------------------------------------------
/docs/reference/backends/gremlin.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `GremlinBackend(Backend)`
2 |
3 |
4 | A backend instance for Gremlin-compatible graph databases.
5 |
6 |
7 |
8 | ## *Function* `__init__(self, graph: GraphTraversalSource, directed: bool = True)`
9 |
10 |
11 | Create a new Backend instance wrapping a Gremlin endpoint.
12 |
13 | ### Arguments
14 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
15 |
16 | ### Returns
17 | None
18 |
19 |
20 |
21 | ## *Function* `is_directed(self) -> bool`
22 |
23 |
24 | Return True if the backend graph is directed.
25 |
26 | The Gremlin-backed datastore is always directed.
27 |
28 | ### Arguments
29 | None
30 |
31 | ### Returns
32 | > - **bool** (`None`: `None`): True if the backend graph is directed.
33 |
34 |
35 |
36 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
37 |
38 |
39 | Add a new node to the graph.
40 |
41 | ### Arguments
42 | > - **node_name** (`Hashable`: `None`): The ID of the node
43 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
44 |
45 | ### Returns
46 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
47 |
48 |
49 |
50 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
51 |
52 |
53 | Return the data associated with a node.
54 |
55 | ### Arguments
56 | > - **node_name** (`Hashable`: `None`): The node ID to look up
57 |
58 | ### Returns
59 | > - **dict** (`None`: `None`): The metadata associated with this node
60 |
61 |
62 |
63 | ## *Function* `has_node(self, u: Hashable) -> bool`
64 |
65 |
66 | Return the data associated with a node.
67 |
68 | ### Arguments
69 | > - **node_name** (`Hashable`: `None`): The node ID to look up
70 |
71 | ### Returns
72 | > - **dict** (`None`: `None`): The metadata associated with this node
73 |
74 |
75 |
76 | ## *Function* `remove_node(self, node_name: Hashable)`
77 |
78 |
79 | Remove a node.
80 |
81 | ### Arguments
82 | > - **node_name** (`Hashable`: `None`): The node ID to look up
83 |
84 | ### Returns
85 | > - **dict** (`None`: `None`): The metadata associated with this node
86 |
87 |
88 |
89 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
90 |
91 |
92 | Get a generator of all of the nodes in this graph.
93 |
94 | ### Arguments
95 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
96 | the response
97 |
98 | ### Returns
99 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
100 |
101 |
102 |
103 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
104 |
105 |
106 | Add a new edge to the graph between two nodes.
107 |
108 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
109 |
110 | ### Arguments
111 | > - **u** (`Hashable`: `None`): The source node ID
112 | > - **v** (`Hashable`: `None`): The target node ID
113 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
114 |
115 | ### Returns
116 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
117 |
118 |
119 |
120 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
121 |
122 |
123 | Get a list of all edges in this graph, arbitrary sort.
124 |
125 | ### Arguments
126 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
127 |
128 | ### Returns
129 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
130 |
131 |
132 |
133 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
134 |
135 |
136 | Get an edge by its source and target IDs.
137 |
138 | ### Arguments
139 | > - **u** (`Hashable`: `None`): The source node ID
140 | > - **v** (`Hashable`: `None`): The target node ID
141 |
142 | ### Returns
143 | > - **dict** (`None`: `None`): Metadata associated with this edge
144 |
145 |
146 |
147 | ## *Function* `get_node_count(self) -> Iterable`
148 |
149 |
150 | Get an integer count of the number of nodes in this graph.
151 |
152 | ### Arguments
153 | None
154 |
155 | ### Returns
156 | > - **int** (`None`: `None`): The count of nodes
157 |
158 |
--------------------------------------------------------------------------------
/docs/reference/backends/igraph.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `IGraphBackend(Backend)`
2 |
3 |
4 | This is currently UNOPTIMIZED CODE.
5 |
6 | Recommendations for future work include improved indexing and caching of node names and metadata.
7 |
8 |
9 |
10 | ## *Function* `__init__(self, directed: bool = False)`
11 |
12 |
13 | Create a new IGraphBackend instance, using an igraph.Graph object to store and manage network structure.
14 |
15 | ### Arguments
16 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
17 |
18 | ### Returns
19 | None
20 |
21 |
22 |
23 | ## *Function* `is_directed(self) -> bool`
24 |
25 |
26 | Return True if the backend graph is directed.
27 |
28 | ### Arguments
29 | None
30 |
31 | ### Returns
32 | > - **bool** (`None`: `None`): True if the backend graph is directed.
33 |
34 |
35 |
36 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
37 |
38 |
39 | Add a new node to the graph.
40 |
41 | ### Arguments
42 | > - **node_name** (`Hashable`: `None`): The ID of the node
43 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
44 |
45 | ### Returns
46 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
47 |
48 |
49 |
50 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
51 |
52 |
53 | Return the data associated with a node.
54 |
55 | ### Arguments
56 | > - **node_name** (`Hashable`: `None`): The node ID to look up
57 |
58 | ### Returns
59 | > - **dict** (`None`: `None`): The metadata associated with this node
60 |
61 |
62 |
63 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
64 |
65 |
66 | Get a generator of all of the nodes in this graph.
67 |
68 | ### Arguments
69 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
70 | the response
71 |
72 | ### Returns
73 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
74 |
75 |
76 |
77 | ## *Function* `has_node(self, u: Hashable) -> bool`
78 |
79 |
80 | Return true if the node exists in the graph.
81 |
82 | ### Arguments
83 | > - **u** (`Hashable`: `None`): The ID of the node to check
84 |
85 | ### Returns
86 | > - **bool** (`None`: `None`): True if the node exists
87 |
88 |
89 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
90 |
91 |
92 | Add a new edge to the graph between two nodes.
93 |
94 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
95 |
96 | ### Arguments
97 | > - **u** (`Hashable`: `None`): The source node ID
98 | > - **v** (`Hashable`: `None`): The target node ID
99 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
100 |
101 | ### Returns
102 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
103 |
104 |
105 |
106 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
107 |
108 |
109 | Get a list of all edges in this graph, arbitrary sort.
110 |
111 | ### Arguments
112 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
113 |
114 | ### Returns
115 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
116 |
117 |
118 |
119 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
120 |
121 |
122 | Get an edge by its source and target IDs.
123 |
124 | ### Arguments
125 | > - **u** (`Hashable`: `None`): The source node ID
126 | > - **v** (`Hashable`: `None`): The target node ID
127 |
128 | ### Returns
129 | > - **dict** (`None`: `None`): Metadata associated with this edge
130 |
131 |
132 |
133 | ## *Function* `get_node_count(self) -> Iterable`
134 |
135 |
136 | Get an integer count of the number of nodes in this graph.
137 |
138 | ### Arguments
139 | None
140 |
141 | ### Returns
142 | > - **int** (`None`: `None`): The count of nodes
143 |
144 |
--------------------------------------------------------------------------------
/docs/reference/backends/metadatastore.py.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aplbrain/grand/ce815357e73c05c947b1194220f531430e6fc2cc/docs/reference/backends/metadatastore.py.md
--------------------------------------------------------------------------------
/docs/reference/backends/networkit.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `NetworkitBackend(Backend)`
2 |
3 |
4 | Networkit doesn't support metadata or named nodes, so all node names and metadata must currently be stored in a parallel data structure.
5 |
6 | To solve this problem, a NodeNameManager and MetadataStore, from `grand.backends.metadatastore.NodeNameManager` and `grand.backends.metadatastore.MetadataStore` respectively, are included at the top level of this class. In order to preserve this metadata structure statefully, you must serialize both the graph as well as the data stores.
7 |
8 | This is currently UNOPTIMIZED CODE.
9 |
10 | Recommendations for future work include improved indexing and caching of node names and metadata.
11 |
12 | > - **documentation** (`None`: `None`): https://networkit.github.io/dev-docs/python_api/graph.html
13 |
14 |
15 |
16 | ## *Function* `__init__(self, directed: bool = False, metadata_store: MetadataStore = None)`
17 |
18 |
19 | Create a new NetworkitBackend instance, using a Networkit.graph.Graph object to store and manage network structure.
20 |
21 | ### Arguments
22 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
23 | > - **metadata_store** (`MetadataStore`: `None`): Optionally, a MetadataStore to use
24 | to handle node and edge attributes. If not provided, defaults to a DictMetadataStore.
25 |
26 | ### Returns
27 | None
28 |
29 |
30 |
31 | ## *Function* `is_directed(self) -> bool`
32 |
33 |
34 | Return True if the backend graph is directed.
35 |
36 | ### Arguments
37 | None
38 |
39 | ### Returns
40 | > - **bool** (`None`: `None`): True if the backend graph is directed.
41 |
42 |
43 |
44 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
45 |
46 |
47 | Add a new node to the graph.
48 |
49 | ### Arguments
50 | > - **node_name** (`Hashable`: `None`): The ID of the node
51 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
52 |
53 | ### Returns
54 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
55 |
56 |
57 |
58 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
59 |
60 |
61 | Return the data associated with a node.
62 |
63 | ### Arguments
64 | > - **node_name** (`Hashable`: `None`): The node ID to look up
65 |
66 | ### Returns
67 | > - **dict** (`None`: `None`): The metadata associated with this node
68 |
69 |
70 |
71 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
72 |
73 |
74 | Get a generator of all of the nodes in this graph.
75 |
76 | ### Arguments
77 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
78 | the response
79 |
80 | ### Returns
81 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
82 |
83 |
84 |
85 | ## *Function* `has_node(self, u: Hashable) -> bool`
86 |
87 |
88 | Return true if the node exists in the graph.
89 |
90 | ### Arguments
91 | > - **u** (`Hashable`: `None`): The ID of the node to check
92 |
93 | ### Returns
94 | > - **bool** (`None`: `None`): True if the node exists
95 |
96 |
97 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
98 |
99 |
100 | Add a new edge to the graph between two nodes.
101 |
102 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
103 |
104 | ### Arguments
105 | > - **u** (`Hashable`: `None`): The source node ID
106 | > - **v** (`Hashable`: `None`): The target node ID
107 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
108 |
109 | ### Returns
110 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
111 |
112 |
113 |
114 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
115 |
116 |
117 | Get a list of all edges in this graph, arbitrary sort.
118 |
119 | ### Arguments
120 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
121 |
122 | ### Returns
123 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
124 |
125 |
126 |
127 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
128 |
129 |
130 | Get an edge by its source and target IDs.
131 |
132 | ### Arguments
133 | > - **u** (`Hashable`: `None`): The source node ID
134 | > - **v** (`Hashable`: `None`): The target node ID
135 |
136 | ### Returns
137 | > - **dict** (`None`: `None`): Metadata associated with this edge
138 |
139 |
140 |
141 | ## *Function* `get_node_count(self) -> Iterable`
142 |
143 |
144 | Get an integer count of the number of nodes in this graph.
145 |
146 | ### Arguments
147 | None
148 |
149 | ### Returns
150 | > - **int** (`None`: `None`): The count of nodes
151 |
152 |
--------------------------------------------------------------------------------
/docs/reference/backends/networkx.py.md:
--------------------------------------------------------------------------------
1 | ## *Function* `__init__(self, directed: bool = False)`
2 |
3 |
4 | Create a new Backend instance.
5 |
6 | ### Arguments
7 | > - **directed** (`bool`: `False`): Whether to make the backend graph directed
8 |
9 | ### Returns
10 | None
11 |
12 |
13 |
14 | ## *Function* `is_directed(self) -> bool`
15 |
16 |
17 | Return True if the backend graph is directed.
18 |
19 | ### Arguments
20 | None
21 |
22 | ### Returns
23 | > - **bool** (`None`: `None`): True if the backend graph is directed.
24 |
25 |
26 |
27 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict)`
28 |
29 |
30 | Add a new node to the graph.
31 |
32 | ### Arguments
33 | > - **node_name** (`Hashable`: `None`): The ID of the node
34 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
35 |
36 | ### Returns
37 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
38 |
39 |
40 |
41 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
42 |
43 |
44 | Return the data associated with a node.
45 |
46 | ### Arguments
47 | > - **node_name** (`Hashable`: `None`): The node ID to look up
48 |
49 | ### Returns
50 | > - **dict** (`None`: `None`): The metadata associated with this node
51 |
52 |
53 |
54 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
55 |
56 |
57 | Get a generator of all of the nodes in this graph.
58 |
59 | ### Arguments
60 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
61 | the response
62 |
63 | ### Returns
64 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
65 |
66 |
67 |
68 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
69 |
70 |
71 | Add a new edge to the graph between two nodes.
72 |
73 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
74 |
75 | ### Arguments
76 | > - **u** (`Hashable`: `None`): The source node ID
77 | > - **v** (`Hashable`: `None`): The target node ID
78 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
79 |
80 | ### Returns
81 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
82 |
83 |
84 |
85 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
86 |
87 |
88 | Get a list of all edges in this graph, arbitrary sort.
89 |
90 | ### Arguments
91 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
92 |
93 | ### Returns
94 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
95 |
96 |
97 |
98 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
99 |
100 |
101 | Get an edge by its source and target IDs.
102 |
103 | ### Arguments
104 | > - **u** (`Hashable`: `None`): The source node ID
105 | > - **v** (`Hashable`: `None`): The target node ID
106 |
107 | ### Returns
108 | > - **dict** (`None`: `None`): Metadata associated with this edge
109 |
110 |
111 |
112 | ## *Function* `get_node_count(self) -> Iterable`
113 |
114 |
115 | Get an integer count of the number of nodes in this graph.
116 |
117 | ### Arguments
118 | None
119 |
120 | ### Returns
121 | > - **int** (`None`: `None`): The count of nodes
122 |
123 |
--------------------------------------------------------------------------------
/docs/reference/backends/sqlbackend.py.md:
--------------------------------------------------------------------------------
1 | ## *Class* `SQLBackend(Backend)`
2 |
3 |
4 | A graph datastore that uses a SQL-like store for persistance and queries.
5 |
6 |
7 |
8 | ## *Function* `is_directed(self) -> bool`
9 |
10 |
11 | Return True if the backend graph is directed.
12 |
13 | ### Arguments
14 | None
15 |
16 | ### Returns
17 | > - **bool** (`None`: `None`): True if the backend graph is directed.
18 |
19 |
20 |
21 | ## *Function* `teardown(self, yes_i_am_sure: bool = False)`
22 |
23 |
24 | Tear down this graph, deleting all evidence it once was here.
25 |
26 |
27 |
28 | ## *Function* `add_node(self, node_name: Hashable, metadata: dict) -> Hashable`
29 |
30 |
31 | Add a new node to the graph.
32 |
33 | Insert a new document into the nodes table.
34 |
35 | ### Arguments
36 | > - **node_name** (`Hashable`: `None`): The ID of the node
37 | > - **metadata** (`dict`: `None`): An optional dictionary of metadata
38 |
39 | ### Returns
40 | > - **Hashable** (`None`: `None`): The ID of this node, as inserted
41 |
42 |
43 |
44 | ## *Function* `all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator`
45 |
46 |
47 | Get a generator of all of the nodes in this graph.
48 |
49 | ### Arguments
50 | > - **include_metadata** (`bool`: `False`): Whether to include node metadata in
51 | the response
52 |
53 | ### Returns
54 | > - **Generator** (`None`: `None`): A generator of all nodes (arbitrary sort)
55 |
56 |
57 |
58 | ## *Function* `has_node(self, u: Hashable) -> bool`
59 |
60 |
61 | Return true if the node exists in the graph.
62 |
63 | ### Arguments
64 | > - **u** (`Hashable`: `None`): The ID of the node to check
65 |
66 | ### Returns
67 | > - **bool** (`None`: `None`): True if the node exists
68 |
69 |
70 | ## *Function* `add_edge(self, u: Hashable, v: Hashable, metadata: dict)`
71 |
72 |
73 | Add a new edge to the graph between two nodes.
74 |
75 | If the graph is directed, this edge will start (source) at the `u` node and end (target) at the `v` node.
76 |
77 | ### Arguments
78 | > - **u** (`Hashable`: `None`): The source node ID
79 | > - **v** (`Hashable`: `None`): The target node ID
80 | > - **metadata** (`dict`: `None`): Optional metadata to associate with the edge
81 |
82 | ### Returns
83 | > - **Hashable** (`None`: `None`): The edge ID, as inserted.
84 |
85 |
86 |
87 | ## *Function* `all_edges_as_iterable(self, include_metadata: bool = False) -> Generator`
88 |
89 |
90 | Get a list of all edges in this graph, arbitrary sort.
91 |
92 | ### Arguments
93 | > - **include_metadata** (`bool`: `False`): Whether to include edge metadata
94 |
95 | ### Returns
96 | > - **Generator** (`None`: `None`): A generator of all edges (arbitrary sort)
97 |
98 |
99 |
100 | ## *Function* `get_node_by_id(self, node_name: Hashable)`
101 |
102 |
103 | Return the data associated with a node.
104 |
105 | ### Arguments
106 | > - **node_name** (`Hashable`: `None`): The node ID to look up
107 |
108 | ### Returns
109 | > - **dict** (`None`: `None`): The metadata associated with this node
110 |
111 |
112 |
113 | ## *Function* `get_edge_by_id(self, u: Hashable, v: Hashable)`
114 |
115 |
116 | Get an edge by its source and target IDs.
117 |
118 | ### Arguments
119 | > - **u** (`Hashable`: `None`): The source node ID
120 | > - **v** (`Hashable`: `None`): The target node ID
121 |
122 | ### Returns
123 | > - **dict** (`None`: `None`): Metadata associated with this edge
124 |
125 |
126 |
127 | ## *Function* `get_node_count(self) -> Iterable`
128 |
129 |
130 | Get an integer count of the number of nodes in this graph.
131 |
132 | ### Arguments
133 | None
134 |
135 | ### Returns
136 | > - **int** (`None`: `None`): The count of nodes
137 |
138 |
--------------------------------------------------------------------------------
/docs/reference/dialects/dialects.md:
--------------------------------------------------------------------------------
1 | ## *Class* `NetworkXDialect(nx.Graph)`
2 |
3 |
4 | A NetworkXDialect provides a networkx-like interface for graph manipulation
5 |
6 |
7 |
8 | ## *Function* `__init__(self, parent: "Graph")`
9 |
10 |
11 | Create a new dialect to query a backend with NetworkX syntax.
12 |
13 | ### Arguments
14 | > - **parent** (`Graph`: `None`): The parent Graph object
15 |
16 | ### Returns
17 | None
18 |
19 |
20 |
21 | ## *Function* `adj(self)`
22 |
23 |
24 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
25 |
26 |
27 | ## *Function* `_adj(self)`
28 |
29 |
30 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
31 |
32 |
33 | ## *Function* `pred(self)`
34 |
35 |
36 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
37 |
38 |
39 | ## *Function* `_pred(self)`
40 |
41 |
42 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
43 |
44 |
45 | ## *Class* `IGraphDialect(nx.Graph)`
46 |
47 |
48 | An IGraphDialect provides a python-igraph-like interface
49 |
50 |
51 |
52 | ## *Function* `__init__(self, parent: "Graph")`
53 |
54 |
55 | Create a new dialect to query a backend with Python-IGraph syntax.
56 |
57 | ### Arguments
58 | > - **parent** (`Graph`: `None`): The parent Graph object
59 |
60 | ### Returns
61 | None
62 |
63 |
64 |
65 | ## *Class* `NetworkitDialect`
66 |
67 |
68 | A Networkit-like API for interacting with a Grand graph.
69 |
70 | > - **here** (`None`: `None`): https://networkit.github.io/dev-docs/python_api/graph.html
71 |
72 |
--------------------------------------------------------------------------------
/docs/reference/grand/grand.md:
--------------------------------------------------------------------------------
1 | ## *Class* `Graph`
2 |
3 |
4 | A grand.Graph enables you to manipulate a graph using multiple dialects.
5 |
6 |
--------------------------------------------------------------------------------
/docs/reference/setup.py.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aplbrain/grand/ce815357e73c05c947b1194220f531430e6fc2cc/docs/reference/setup.py.md
--------------------------------------------------------------------------------
/grand/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Grand graphs package.
3 |
4 | """
5 |
6 | from typing import Optional
7 | from .backends import Backend, NetworkXBackend
8 | from .dialects import NetworkXDialect, IGraphDialect, NetworkitDialect
9 |
10 |
11 | _DEFAULT_BACKEND = NetworkXBackend
12 |
13 | __version__ = "0.7.0"
14 |
15 |
16 | class Graph:
17 | """
18 | A grand.Graph enables you to manipulate a graph using multiple dialects.
19 |
20 | """
21 |
22 | nx: NetworkXDialect
23 | networkit: NetworkitDialect
24 | igraph: IGraphDialect
25 |
26 | def __init__(self, backend: Optional[Backend] = None, **backend_kwargs: dict):
27 | """
28 | Create a new grand.Graph.
29 |
30 | The only positional argument is the backend to use. All other arguments
31 | are passed to the backend's constructor, if a type is provided.
32 | Otherwise, kwargs are ignored.
33 |
34 | Arguments:
35 | backend (Backend): The backend to use. If none is provided, will
36 | default to _DEFAULT_BACKEND.
37 |
38 | """
39 | self.backend = backend or _DEFAULT_BACKEND
40 |
41 | # If you passed a class instead of an instance, instantiate it with
42 | # kwargs from the constructor:
43 | if isinstance(self.backend, type):
44 | self.backend = self.backend(**backend_kwargs)
45 |
46 | # Attach dialects:
47 | self.attach_dialect("nx", NetworkXDialect)
48 | self.attach_dialect("igraph", IGraphDialect)
49 | self.attach_dialect("networkit", NetworkitDialect)
50 |
51 | def attach_dialect(self, name: str, dialect: type):
52 | """
53 | Attach a dialect to the graph.
54 |
55 | Arguments:
56 | name (str): The name of the dialect.
57 | dialect (type): The dialect class to attach.
58 |
59 | """
60 | setattr(self, name, dialect(self))
61 |
62 |
63 | class DiGraph(Graph):
64 | """
65 | A grand.DiGraph enables you to manipulate a directed graph. This is a
66 | convenience class that inherits from grand.Graph.
67 |
68 | """
69 |
70 | def __init__(self, backend: Optional[Backend] = None, **backend_kwargs: dict):
71 | """
72 | Create a new grand.DiGraph.
73 |
74 | The only positional argument is the backend to use. All other arguments
75 | are passed to the backend's constructor, if a type is provided.
76 | Otherwise, kwargs are ignored.
77 |
78 | Arguments:
79 | backend (Backend): The backend to use. If none is provided, will
80 | default to _DEFAULT_BACKEND.
81 |
82 | """
83 | super().__init__(backend, **{**backend_kwargs, "directed": True})
84 |
--------------------------------------------------------------------------------
/grand/backends/__init__.py:
--------------------------------------------------------------------------------
1 | from .backend import Backend, CachedBackend, InMemoryCachedBackend
2 |
3 | try:
4 | from ._dynamodb import DynamoDBBackend
5 | except ImportError:
6 | pass
7 | from ._networkx import NetworkXBackend
8 | from ._dataframe import DataFrameBackend
9 |
10 | try:
11 | from ._sqlbackend import SQLBackend
12 | except ImportError:
13 | pass
14 |
15 | try:
16 | from ._networkit import NetworkitBackend
17 | except ImportError:
18 | pass
19 |
20 | __all__ = [
21 | "Backend",
22 | "CachedBackend",
23 | "InMemoryCachedBackend",
24 | "NetworkXBackend",
25 | "DataFrameBackend",
26 | "DynamoDBBackend",
27 | "SQLBackend",
28 | "NetworkitBackend",
29 | ]
30 |
--------------------------------------------------------------------------------
/grand/backends/_dataframe.py:
--------------------------------------------------------------------------------
1 | from typing import Hashable, Generator
2 | import time
3 |
4 | import pandas as pd
5 |
6 | from .backend import Backend
7 |
8 |
9 | class DataFrameBackend(Backend):
10 | def __init__(
11 | self,
12 | directed: bool = False,
13 | edge_df: pd.DataFrame = None,
14 | node_df: pd.DataFrame = None,
15 | edge_df_source_column: str = "Source",
16 | edge_df_target_column: str = "Target",
17 | node_df_id_column: str = "id",
18 | ):
19 | """
20 | Create a new DataFrame backend.
21 |
22 | You must pass an edgelist. A nodelist is optional.
23 |
24 | Arguments:
25 | edge_df (pd.DataFrame): An edgelist dataframe with one edge per row
26 | directed (bool: False): Whether the graph is directed
27 | node_df (pd.DataFrame): A node metadata lookup
28 | edge_df_source_column (str): The name of the column in `edge_df` to
29 | use as the source of edges
30 | edge_df_target_column (str): The name of the column in `edge_df` to
31 | use as the target of edges
32 | node_df_id_column (str): The name of the column in `node_df` to
33 | use as the node ID
34 | """
35 | self._directed = directed
36 | self._edge_df = (
37 | edge_df
38 | if edge_df is not None
39 | else pd.DataFrame(columns=[edge_df_source_column, edge_df_target_column])
40 | )
41 | self._node_df = node_df if node_df is not None else None
42 | self._edge_df_source_column = edge_df_source_column
43 | self._edge_df_target_column = edge_df_target_column
44 | self._node_df_id_column = node_df_id_column
45 |
46 | def is_directed(self) -> bool:
47 | """
48 | Return True if the backend graph is directed.
49 |
50 | Arguments:
51 | None
52 |
53 | Returns:
54 | bool: True if the backend graph is directed.
55 |
56 | """
57 | return self._directed
58 |
59 | def teardown(self, yes_i_am_sure: bool = False):
60 | """
61 | Tear down this graph, deleting all evidence it once was here.
62 |
63 | """
64 | return
65 |
66 | def add_node(self, node_name: Hashable, metadata: dict) -> Hashable:
67 | """
68 | Add a new node to the graph.
69 |
70 | Insert a new document into the nodes table.
71 |
72 | Arguments:
73 | node_name (Hashable): The ID of the node
74 | metadata (dict: None): An optional dictionary of metadata
75 |
76 | Returns:
77 | Hashable: The ID of this node, as inserted
78 |
79 | """
80 |
81 | # Add a new row to the nodes table:
82 | if self._node_df is None:
83 | self._node_df = pd.DataFrame(
84 | [
85 | {
86 | self._node_df_id_column: node_name,
87 | **metadata,
88 | }
89 | ],
90 | columns=[
91 | self._node_df_id_column,
92 | *metadata.keys(),
93 | ],
94 | )
95 | self._node_df.set_index(self._node_df_id_column, inplace=True)
96 | else:
97 | if self.has_node(node_name):
98 | existing_metadata = self.get_node_by_id(node_name)
99 | existing_metadata.update(metadata)
100 | for k, v in existing_metadata.items():
101 | self._node_df.at[node_name, k] = v
102 | else:
103 | # Insert a new row:
104 | self._node_df = pd.concat(
105 | [self._node_df, pd.DataFrame([{node_name: metadata}]).T]
106 | )
107 |
108 | return node_name
109 |
110 | def all_nodes_as_iterable(self, include_metadata: bool = False):
111 | """
112 | Get a generator of all of the nodes in this graph.
113 |
114 | Arguments:
115 | include_metadata (bool: False): Whether to include node metadata in
116 | the response
117 |
118 | Returns:
119 | Generator: A generator of all nodes (arbitrary sort)
120 |
121 | """
122 | if self._node_df is not None:
123 | return [
124 | (
125 | (
126 | node_id,
127 | row.to_dict(),
128 | )
129 | if include_metadata
130 | else node_id
131 | )
132 | for node_id, row in self._node_df.iterrows()
133 | ]
134 |
135 | else:
136 | return [
137 | (node_id, {}) if include_metadata else node_id
138 | for node_id in self._edge_df[self._edge_df_source_column]
139 | ] + [
140 | (node_id, {}) if include_metadata else node_id
141 | for node_id in self._edge_df[self._edge_df_target_column]
142 | ]
143 |
144 | def has_node(self, u: Hashable) -> bool:
145 | """
146 | Return true if the node exists in the graph.
147 |
148 | Arguments:
149 | u (Hashable): The ID of the node to check
150 |
151 | Returns:
152 | bool: True if the node exists
153 | """
154 | if self._node_df is not None:
155 | return u in self._node_df.index
156 |
157 | return u in (self._edge_df[self._edge_df_source_column]) or u in (
158 | self._edge_df[self._edge_df_target_column]
159 | )
160 |
161 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict):
162 | """
163 | Add a new edge to the graph between two nodes.
164 |
165 | If the graph is directed, this edge will start (source) at the `u` node
166 | and end (target) at the `v` node.
167 |
168 | Arguments:
169 | u (Hashable): The source node ID
170 | v (Hashable): The target node ID
171 | metadata (dict): Optional metadata to associate with the edge
172 |
173 | Returns:
174 | Hashable: The edge ID, as inserted.
175 |
176 | """
177 |
178 | if not self.has_node(u):
179 | self.add_node(u, {})
180 | if not self.has_node(v):
181 | self.add_node(v, {})
182 |
183 | if self._has_edge(u, v):
184 | # Update the existing edge:
185 | for k, m in metadata.items():
186 | if self._directed:
187 | self._edge_df.loc[
188 | (self._edge_df[self._edge_df_source_column] == u)
189 | & (self._edge_df[self._edge_df_target_column] == v),
190 | k,
191 | ] = m
192 | else:
193 | # Check for the edge in both directions:
194 | self._edge_df.loc[
195 | (self._edge_df[self._edge_df_source_column] == u)
196 | & (self._edge_df[self._edge_df_target_column] == v),
197 | k,
198 | ] = m
199 | self._edge_df.loc[
200 | (self._edge_df[self._edge_df_source_column] == v)
201 | & (self._edge_df[self._edge_df_target_column] == u),
202 | k,
203 | ] = m
204 | else:
205 | row = {
206 | self._edge_df_source_column: u,
207 | self._edge_df_target_column: v,
208 | **metadata,
209 | }
210 | self._edge_df.loc[len(self._edge_df)] = None
211 | for k, m in row.items():
212 | self._edge_df.loc[len(self._edge_df) - 1, k] = m
213 | return (u, v)
214 |
215 | def _has_edge(self, u: Hashable, v: Hashable) -> bool:
216 | """
217 | Return true if the edge exists in the graph.
218 |
219 | Arguments:
220 | u (Hashable): The source node ID
221 | v (Hashable): The target node ID
222 |
223 | Returns:
224 | bool: True if the edge exists
225 | """
226 | if self._directed:
227 | return (
228 | (self._edge_df[self._edge_df_source_column] == u)
229 | & (self._edge_df[self._edge_df_target_column] == v)
230 | ).any()
231 | else:
232 | return (
233 | (self._edge_df[self._edge_df_source_column] == u)
234 | & (self._edge_df[self._edge_df_target_column] == v)
235 | ).any() or (
236 | (self._edge_df[self._edge_df_source_column] == v)
237 | & (self._edge_df[self._edge_df_target_column] == u)
238 | ).any()
239 |
240 | def all_edges_as_iterable(self, include_metadata: bool = False) -> Generator:
241 | """
242 | Get a list of all edges in this graph, arbitrary sort.
243 |
244 | Arguments:
245 | include_metadata (bool: False): Whether to include edge metadata
246 |
247 | Returns:
248 | Generator: A generator of all edges (arbitrary sort)
249 |
250 | """
251 | for _, row in self._edge_df.iterrows():
252 | if include_metadata:
253 | yield (
254 | row[self._edge_df_source_column],
255 | row[self._edge_df_target_column],
256 | dict(row),
257 | )
258 | else:
259 | yield (
260 | row[self._edge_df_source_column],
261 | row[self._edge_df_target_column],
262 | )
263 |
264 | def get_node_by_id(self, node_name: Hashable):
265 | """
266 | Return the data associated with a node.
267 |
268 | Arguments:
269 | node_name (Hashable): The node ID to look up
270 |
271 | Returns:
272 | dict: The metadata associated with this node
273 |
274 | """
275 | if self._node_df is not None:
276 | res = (self._node_df.loc[node_name]).to_dict()
277 | return res.get(0, res)
278 |
279 | return {}
280 |
281 | def get_edge_by_id(self, u: Hashable, v: Hashable):
282 | """
283 | Get an edge by its source and target IDs.
284 |
285 | Arguments:
286 | u (Hashable): The source node ID
287 | v (Hashable): The target node ID
288 |
289 | Returns:
290 | dict: Metadata associated with this edge
291 |
292 | """
293 | if self._directed:
294 | result = self._edge_df[
295 | (self._edge_df[self._edge_df_source_column] == u)
296 | & (self._edge_df[self._edge_df_target_column] == v)
297 | ]
298 | if len(result):
299 | return self._edge_as_dict(result.iloc[0])
300 |
301 | else:
302 | left = self._edge_df[
303 | (self._edge_df[self._edge_df_source_column] == u)
304 | & (self._edge_df[self._edge_df_target_column] == v)
305 | ]
306 | if len(left):
307 | return self._edge_as_dict(left.iloc[0])
308 | right = self._edge_df[
309 | (self._edge_df[self._edge_df_source_column] == v)
310 | & (self._edge_df[self._edge_df_target_column] == u)
311 | ]
312 | if len(right):
313 | return self._edge_as_dict(right.iloc[0])
314 |
315 | def get_node_neighbors(self, u: Hashable, include_metadata: bool = False):
316 | """
317 | Get a generator of all downstream nodes from this node.
318 |
319 | Arguments:
320 | u (Hashable): The source node ID
321 |
322 | Returns:
323 | Generator
324 |
325 | """
326 |
327 | if include_metadata:
328 | if self._directed:
329 | return {
330 | (r[self._edge_df_target_column]): self._edge_as_dict(r)
331 | for _, r in self._edge_df[
332 | (self._edge_df[self._edge_df_source_column] == u)
333 | ].iterrows()
334 | }
335 | else:
336 | return {
337 | (
338 | r[self._edge_df_source_column]
339 | if r[self._edge_df_source_column] != u
340 | else r[self._edge_df_target_column]
341 | ): self._edge_as_dict(r)
342 | for _, r in self._edge_df[
343 | (self._edge_df[self._edge_df_source_column] == u)
344 | | (self._edge_df[self._edge_df_target_column] == u)
345 | ].iterrows()
346 | }
347 |
348 | if self._directed:
349 | return iter(
350 | [
351 | row[self._edge_df_target_column]
352 | for _, row in self._edge_df[
353 | (self._edge_df[self._edge_df_source_column] == u)
354 | ].iterrows()
355 | ]
356 | )
357 | else:
358 | return iter(
359 | [
360 | (
361 | row[self._edge_df_source_column]
362 | if row[self._edge_df_source_column] != u
363 | else row[self._edge_df_target_column]
364 | )
365 | for _, row in self._edge_df[
366 | (self._edge_df[self._edge_df_source_column] == u)
367 | | (self._edge_df[self._edge_df_target_column] == u)
368 | ].iterrows()
369 | ]
370 | )
371 |
372 | def _edge_as_dict(self, row):
373 | """
374 | Convert an edge row to a dictionary.
375 |
376 | Arguments:
377 | row (pandas.Series): The edge row
378 |
379 | Returns:
380 | dict: The edge metadata
381 |
382 | """
383 | r = row.to_dict()
384 | r.pop(self._edge_df_source_column)
385 | r.pop(self._edge_df_target_column)
386 | return r
387 |
388 | def get_node_predecessors(self, u: Hashable, include_metadata: bool = False):
389 | """
390 | Get a generator of all upstream nodes from this node.
391 |
392 | Arguments:
393 | u (Hashable): The source node ID
394 |
395 | Returns:
396 | Generator
397 |
398 | """
399 |
400 | if include_metadata:
401 | if self._directed:
402 | return {
403 | (
404 | r[self._edge_df_target_column]
405 | if r[self._edge_df_target_column] != u
406 | else r[self._edge_df_source_column]
407 | ): self._edge_as_dict(r)
408 | for _, r in self._edge_df[
409 | (self._edge_df[self._edge_df_target_column] == u)
410 | ].iterrows()
411 | }
412 | else:
413 | return {
414 | (
415 | r[self._edge_df_target_column]
416 | if r[self._edge_df_target_column] != u
417 | else r[self._edge_df_source_column]
418 | ): self._edge_as_dict(r)
419 | for _, r in self._edge_df[
420 | (self._edge_df[self._edge_df_target_column] == u)
421 | | (self._edge_df[self._edge_df_source_column] == u)
422 | ].iterrows()
423 | }
424 |
425 | if self._directed:
426 | return iter(
427 | [
428 | row[self._edge_df_source_column]
429 | for _, row in self._edge_df[
430 | (self._edge_df[self._edge_df_target_column] == u)
431 | ].iterrows()
432 | ]
433 | )
434 | else:
435 | return iter(
436 | [
437 | (
438 | row[self._edge_df_source_column]
439 | if row[self._edge_df_target_column] != u
440 | else row[self._edge_df_target_column]
441 | )
442 | for _, row in self._edge_df[
443 | (self._edge_df[self._edge_df_target_column] == u)
444 | | (self._edge_df[self._edge_df_source_column] == u)
445 | ].iterrows()
446 | ]
447 | )
448 |
449 | def get_node_count(self) -> int:
450 | """
451 | Get an integer count of the number of nodes in this graph.
452 |
453 | Arguments:
454 | None
455 |
456 | Returns:
457 | int: The count of nodes
458 |
459 | """
460 | if self._node_df is not None:
461 | return len(self._node_df)
462 | # Return number of unique sources intersected with number of unique targets
463 | return len(
464 | set(self._edge_df[self._edge_df_source_column]).intersection(
465 | set(self._edge_df[self._edge_df_target_column])
466 | )
467 | )
468 |
469 | def get_edge_count(self) -> int:
470 | """
471 | Get an integer count of the number of edges in this graph.
472 |
473 | Arguments:
474 | None
475 |
476 | Returns:
477 | int: The count of edges
478 |
479 | """
480 | return len(self._edge_df)
481 |
482 | def ingest_from_edgelist_dataframe(
483 | self, edgelist: pd.DataFrame, source_column: str, target_column: str
484 | ) -> dict:
485 | """
486 | Ingest an edgelist from a Pandas DataFrame.
487 |
488 | """
489 | # Produce edge list:
490 |
491 | edge_tic = time.time()
492 | self._edge_df = edgelist
493 | self._edge_df_source_column = source_column
494 | self._edge_df_target_column = target_column
495 |
496 | return {
497 | "edge_duration": time.time() - edge_tic,
498 | }
499 |
--------------------------------------------------------------------------------
/grand/backends/_gremlin.py:
--------------------------------------------------------------------------------
1 | """
2 | https://tinkerpop.apache.org/docs/current/reference/
3 | """
4 |
5 | from typing import Hashable, Collection
6 |
7 | import pandas as pd
8 | from gremlin_python.structure.graph import Graph
9 | from gremlin_python.process.graph_traversal import __, GraphTraversalSource
10 | from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
11 |
12 | from .backend import Backend
13 |
14 | ID = "__id"
15 | EDGE_NAME = "__edge"
16 | NODE_NAME = "__node"
17 |
18 |
19 | def _node_to_metadata(n):
20 | return {k if isinstance(k, str) else k.name: v for k, v in n.items()}
21 |
22 |
23 | class GremlinBackend(Backend):
24 | """
25 | A backend instance for Gremlin-compatible graph databases.
26 |
27 | """
28 |
29 | def __init__(self, graph: GraphTraversalSource, directed: bool = True):
30 | """
31 | Create a new Backend instance wrapping a Gremlin endpoint.
32 |
33 | Arguments:
34 | directed (bool: False): Whether to make the backend graph directed
35 |
36 | Returns:
37 | None
38 |
39 | """
40 | self._g = graph
41 |
42 | def is_directed(self) -> bool:
43 | """
44 | Return True if the backend graph is directed.
45 |
46 | The Gremlin-backed datastore is always directed.
47 |
48 | Arguments:
49 | None
50 |
51 | Returns:
52 | bool: True if the backend graph is directed.
53 |
54 | """
55 | return True
56 |
57 | def add_node(self, node_name: Hashable, metadata: dict):
58 | """
59 | Add a new node to the graph.
60 |
61 | Arguments:
62 | node_name (Hashable): The ID of the node
63 | metadata (dict: None): An optional dictionary of metadata
64 |
65 | Returns:
66 | Hashable: The ID of this node, as inserted
67 |
68 | """
69 | if self.has_node(node_name):
70 | # Retrieve the existing node; we will update the props.
71 | v = self._g.V().has(ID, node_name)
72 | else:
73 | v = self._g.addV().property(ID, node_name)
74 | for key, val in metadata.items():
75 | v = v.property(key, val)
76 | return v.toList()[0]
77 |
78 | def get_node_by_id(self, node_name: Hashable):
79 | """
80 | Return the data associated with a node.
81 |
82 | Arguments:
83 | node_name (Hashable): The node ID to look up
84 |
85 | Returns:
86 | dict: The metadata associated with this node
87 |
88 | """
89 | try:
90 | return _node_to_metadata(
91 | self._g.V().has(ID, node_name).valueMap(True).toList()[0]
92 | )
93 | except IndexError as e:
94 | raise KeyError() from e
95 |
96 | def has_node(self, u: Hashable) -> bool:
97 | """
98 | Return the data associated with a node.
99 |
100 | Arguments:
101 | node_name (Hashable): The node ID to look up
102 |
103 | Returns:
104 | dict: The metadata associated with this node
105 |
106 | """
107 | try:
108 | self.get_node_by_id(u)
109 | return True
110 | except KeyError:
111 | return False
112 |
113 | def remove_node(self, node_name: Hashable):
114 | """
115 | Remove a node.
116 |
117 | Arguments:
118 | node_name (Hashable): The node ID to look up
119 |
120 | Returns:
121 | dict: The metadata associated with this node
122 |
123 | """
124 | return self._g.V().has(ID, node_name).drop().toList()
125 |
126 | def all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection:
127 | """
128 | Get a generator of all of the nodes in this graph.
129 |
130 | Arguments:
131 | include_metadata (bool: False): Whether to include node metadata in
132 | the response
133 |
134 | Returns:
135 | Generator: A generator of all nodes (arbitrary sort)
136 |
137 | """
138 | if include_metadata:
139 | return iter(
140 | [
141 | {n[ID][0]: _node_to_metadata(n)}
142 | for n in self._g.V().valueMap(True).toList()
143 | ]
144 | )
145 | else:
146 | return iter([n[ID] for n in self._g.V().project(ID).by(ID).toList()])
147 |
148 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict):
149 | """
150 | Add a new edge to the graph between two nodes.
151 |
152 | If the graph is directed, this edge will start (source) at the `u` node
153 | and end (target) at the `v` node.
154 |
155 | Arguments:
156 | u (Hashable): The source node ID
157 | v (Hashable): The target node ID
158 | metadata (dict): Optional metadata to associate with the edge
159 |
160 | Returns:
161 | Hashable: The edge ID, as inserted.
162 |
163 | """
164 | try:
165 | self.get_edge_by_id(u, v)
166 | e = self._g.V().has(ID, u).outE().as_("e").inV().has(ID, v).select("e")
167 | except IndexError:
168 | if not self.has_node(u):
169 | self.add_node(u, {})
170 | if not self.has_node(v):
171 | self.add_node(v, {})
172 | e = (
173 | self._g.V()
174 | .has(ID, u)
175 | .addE(EDGE_NAME)
176 | .as_("e")
177 | .to(__.V().has(ID, v))
178 | .select("e")
179 | )
180 | for key, val in metadata.items():
181 | e = e.property(key, val)
182 | return e.toList()
183 |
184 | def all_edges_as_iterable(self, include_metadata: bool = False) -> Collection:
185 | """
186 | Get a list of all edges in this graph, arbitrary sort.
187 |
188 | Arguments:
189 | include_metadata (bool: False): Whether to include edge metadata
190 |
191 | Returns:
192 | Generator: A generator of all edges (arbitrary sort)
193 |
194 | """
195 | if include_metadata:
196 | return iter(
197 | [
198 | (e["source"], e["target"], _node_to_metadata(e["properties"]))
199 | for e in (
200 | self._g.V()
201 | .outE()
202 | .project("target", "source", "properties")
203 | .by(__.inV().values(ID))
204 | .by(__.outV().values(ID))
205 | .by(__.valueMap(True))
206 | .toList()
207 | )
208 | ]
209 | )
210 | return iter(
211 | [
212 | (e["source"], e["target"])
213 | for e in self._g.V()
214 | .outE()
215 | .project("target", "source")
216 | .by(__.inV().values(ID))
217 | .by(__.outV().values(ID))
218 | .toList()
219 | ]
220 | )
221 |
222 | def get_edge_by_id(self, u: Hashable, v: Hashable):
223 | """
224 | Get an edge by its source and target IDs.
225 |
226 | Arguments:
227 | u (Hashable): The source node ID
228 | v (Hashable): The target node ID
229 |
230 | Returns:
231 | dict: Metadata associated with this edge
232 |
233 | """
234 | return (
235 | self._g.V()
236 | .has(ID, u)
237 | .outE()
238 | .as_("e")
239 | .inV()
240 | .has(ID, v)
241 | .select("e")
242 | .properties()
243 | .toList()
244 | )[0]
245 |
246 | def get_node_neighbors(
247 | self, u: Hashable, include_metadata: bool = False
248 | ) -> Collection:
249 | """
250 | Get a generator of all downstream nodes from this node.
251 |
252 | Arguments:
253 | u (Hashable): The source node ID
254 |
255 | Returns:
256 | Generator
257 |
258 | """
259 | if include_metadata:
260 | return {
261 | e["target"]: _node_to_metadata(e["properties"])
262 | for e in (
263 | self._g.V()
264 | .has(ID, u)
265 | .outE()
266 | .project("target", "source", "properties")
267 | .by(__.inV().values(ID))
268 | .by(__.outV().values(ID))
269 | .by(__.valueMap(True))
270 | .toList()
271 | )
272 | }
273 | return self._g.V().has(ID, u).out().values(ID).toList()
274 |
275 | def get_node_predecessors(
276 | self, u: Hashable, include_metadata: bool = False
277 | ) -> Collection:
278 | """
279 | Get a generator of all downstream nodes from this node.
280 |
281 | Arguments:
282 | u (Hashable): The source node ID
283 |
284 | Returns:
285 | Generator
286 |
287 | """
288 | if include_metadata:
289 | return {
290 | e["source"]: e
291 | for e in (
292 | self._g.V()
293 | .has(ID, u)
294 | .inE()
295 | .project("target", "source", "properties")
296 | .by(__.inV().values(ID))
297 | .by(__.outV().values(ID))
298 | .by(__.valueMap(True))
299 | .toList()
300 | )
301 | }
302 | return self._g.V().out().has(ID, u).values(ID).toList()
303 |
304 | def get_node_count(self) -> int:
305 | """
306 | Get an integer count of the number of nodes in this graph.
307 |
308 | Arguments:
309 | None
310 |
311 | Returns:
312 | int: The count of nodes
313 |
314 | """
315 | return self._g.V().count().toList()[0]
316 |
317 | def get_edge_count(self) -> int:
318 | """
319 | Get an integer count of the number of edges in this graph.
320 |
321 | Arguments:
322 | None
323 |
324 | Returns:
325 | int: The count of edges
326 |
327 | """
328 | return self._g.E().count().toList()[0]
329 |
330 | def teardown(self) -> None:
331 | self._g.V().drop().toList()
332 |
--------------------------------------------------------------------------------
/grand/backends/_igraph.py:
--------------------------------------------------------------------------------
1 | from typing import Hashable, Collection
2 |
3 | from igraph import Graph, InternalError
4 | import pandas as pd
5 |
6 | from .backend import Backend
7 |
8 |
9 | def _remove_name_from_attributes(attributes: dict):
10 | attrs = attributes
11 | attrs.pop("name")
12 | return attrs
13 |
14 |
15 | class IGraphBackend(Backend):
16 | """
17 | This is currently UNOPTIMIZED CODE.
18 |
19 | Recommendations for future work include improved indexing and caching of
20 | node names and metadata.
21 |
22 | """
23 |
24 | def __init__(self, directed: bool = False):
25 | """
26 | Create a new IGraphBackend instance, using an igraph.Graph object to
27 | store and manage network structure.
28 |
29 | Arguments:
30 | directed (bool: False): Whether to make the backend graph directed
31 |
32 | Returns:
33 | None
34 |
35 | """
36 | self._directed = directed
37 | self._ig = Graph(directed=self._directed)
38 |
39 | def ingest_from_edgelist_dataframe(
40 | self, edgelist: pd.DataFrame, source_column: str, target_column: str
41 | ) -> None:
42 | """
43 | Ingest an edgelist from a Pandas DataFrame.
44 |
45 | """
46 | raise NotImplementedError()
47 |
48 | def is_directed(self) -> bool:
49 | """
50 | Return True if the backend graph is directed.
51 |
52 | Arguments:
53 | None
54 |
55 | Returns:
56 | bool: True if the backend graph is directed.
57 |
58 | """
59 | return self._directed
60 |
61 | def add_node(self, node_name: Hashable, metadata: dict):
62 | """
63 | Add a new node to the graph.
64 |
65 | Arguments:
66 | node_name (Hashable): The ID of the node
67 | metadata (dict: None): An optional dictionary of metadata
68 |
69 | Returns:
70 | Hashable: The ID of this node, as inserted
71 |
72 | """
73 | if self.has_node(node_name):
74 | # Update metadata
75 | m = self._ig.vs.find(name=node_name)
76 | m.update_attributes(metadata)
77 | return node_name
78 | self._ig.add_vertex(name=node_name, **metadata)
79 | return node_name
80 |
81 | def get_node_by_id(self, node_name: Hashable):
82 | """
83 | Return the data associated with a node.
84 |
85 | Arguments:
86 | node_name (Hashable): The node ID to look up
87 |
88 | Returns:
89 | dict: The metadata associated with this node
90 |
91 | """
92 | return _remove_name_from_attributes(
93 | self._ig.vs.find(name=node_name).attributes()
94 | )
95 |
96 | def all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection:
97 | """
98 | Get a generator of all of the nodes in this graph.
99 |
100 | Arguments:
101 | include_metadata (bool: False): Whether to include node metadata in
102 | the response
103 |
104 | Returns:
105 | Generator: A generator of all nodes (arbitrary sort)
106 |
107 | """
108 | if include_metadata:
109 | for v in self._ig.vs:
110 | yield (v["name"], _remove_name_from_attributes(v.attributes()))
111 | else:
112 | for i in self._ig.vs:
113 | yield i["name"]
114 |
115 | def has_node(self, u: Hashable) -> bool:
116 | """
117 | Return true if the node exists in the graph.
118 |
119 | Arguments:
120 | u (Hashable): The ID of the node to check
121 |
122 | Returns:
123 | bool: True if the node exists
124 | """
125 | try:
126 | self._ig.vs.find(name=u)
127 | return True
128 | except:
129 | return False
130 |
131 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict):
132 | """
133 | Add a new edge to the graph between two nodes.
134 |
135 | If the graph is directed, this edge will start (source) at the `u` node
136 | and end (target) at the `v` node.
137 |
138 | Arguments:
139 | u (Hashable): The source node ID
140 | v (Hashable): The target node ID
141 | metadata (dict): Optional metadata to associate with the edge
142 |
143 | Returns:
144 | Hashable: The edge ID, as inserted.
145 |
146 | """
147 | if self.has_edge(u, v):
148 | # Update metadata
149 | e = self._ig.get_eid(u, v)
150 | self._ig.es[e].update_attributes(metadata)
151 | return
152 | if not self.has_node(u):
153 | self.add_node(u, {})
154 | if not self.has_node(v):
155 | self.add_node(v, {})
156 | return self._ig.add_edge(source=u, target=v, **metadata)
157 |
158 | def all_edges_as_iterable(self, include_metadata: bool = False) -> Collection:
159 | """
160 | Get a list of all edges in this graph, arbitrary sort.
161 |
162 | Arguments:
163 | include_metadata (bool: False): Whether to include edge metadata
164 |
165 | Returns:
166 | Generator: A generator of all edges (arbitrary sort)
167 |
168 | """
169 | if include_metadata:
170 | for e in self._ig.es:
171 | yield (e.source_vertex["name"], e.target_vertex["name"], e.attributes())
172 | else:
173 | for e in self._ig.es:
174 | yield e.source_vertex["name"], e.target_vertex["name"]
175 |
176 | def has_edge(self, u, v):
177 | try:
178 | self._ig.get_eid(u, v)
179 | return True
180 | except (InternalError, ValueError):
181 | # InternalError means no such edge
182 | # ValueError means one vertex doesn't exist
183 | return False
184 |
185 | def get_edge_by_id(self, u: Hashable, v: Hashable):
186 | """
187 | Get an edge by its source and target IDs.
188 |
189 | Arguments:
190 | u (Hashable): The source node ID
191 | v (Hashable): The target node ID
192 |
193 | Returns:
194 | dict: Metadata associated with this edge
195 |
196 | """
197 | try:
198 | return self._ig.es[self._ig.get_eid(u, v)].attributes()
199 | except InternalError:
200 | raise IndexError(f"The edge ({u}, {v}) is not in the graph.")
201 |
202 | def get_node_successors(
203 | self, u: Hashable, include_metadata: bool = False
204 | ) -> Collection:
205 | return self.get_node_neighbors(u, include_metadata)
206 |
207 | def get_node_neighbors(
208 | self, u: Hashable, include_metadata: bool = False
209 | ) -> Collection:
210 | """
211 | Get a generator of all downstream nodes from this node.
212 |
213 | Arguments:
214 | u (Hashable): The source node ID
215 |
216 | Returns:
217 | Generator
218 |
219 | """
220 | if include_metadata:
221 | return {
222 | self._ig.vs[s]["name"]: self.get_edge_by_id(u, s)
223 | for s in self._ig.successors(u)
224 | }
225 | else:
226 | return iter([self._ig.vs[s]["name"] for s in self._ig.successors(u)])
227 |
228 | def get_node_predecessors(
229 | self, u: Hashable, include_metadata: bool = False
230 | ) -> Collection:
231 | """
232 | Get a generator of all upstream nodes from this node.
233 |
234 | Arguments:
235 | u (Hashable): The source node ID
236 |
237 | Returns:
238 | Generator
239 |
240 | """
241 | if include_metadata:
242 | return {
243 | self._ig.vs[s]["name"]: self.get_edge_by_id(s, u)
244 | for s in self._ig.predecessors(u)
245 | }
246 | else:
247 | return iter([self._ig.vs[s]["name"] for s in self._ig.predecessors(u)])
248 |
249 | def get_node_count(self) -> int:
250 | """
251 | Get an integer count of the number of nodes in this graph.
252 |
253 | Arguments:
254 | None
255 |
256 | Returns:
257 | int: The count of nodes
258 |
259 | """
260 | return self._ig.vcount()
261 |
262 | def get_edge_count(self) -> int:
263 | """
264 | Get an integer count of the number of edges in this graph.
265 |
266 | Arguments:
267 | None
268 |
269 | Returns:
270 | int: The count of edges
271 |
272 | """
273 | return self._ig.ecount()
274 |
--------------------------------------------------------------------------------
/grand/backends/_networkit.py:
--------------------------------------------------------------------------------
1 | from typing import Hashable, Generator, Iterable
2 | import abc
3 |
4 | import networkit
5 | import pandas as pd
6 |
7 | from .backend import Backend
8 | from .metadatastore import MetadataStore, DictMetadataStore, NodeNameManager
9 |
10 |
11 | class NetworkitBackend(Backend):
12 | """
13 | Networkit doesn't support metadata or named nodes, so all node names and
14 | metadata must currently be stored in a parallel data structure.
15 |
16 | To solve this problem, a NodeNameManager and MetadataStore, from
17 | `grand.backends.metadatastore.NodeNameManager` and
18 | `grand.backends.metadatastore.MetadataStore` respectively, are included at
19 | the top level of this class. In order to preserve this metadata structure
20 | statefully, you must serialize both the graph as well as the data stores.
21 |
22 | This is currently UNOPTIMIZED CODE.
23 |
24 | Recommendations for future work include improved indexing and caching of
25 | node names and metadata.
26 |
27 | Networkit.graph.Graph documentation:
28 | https://networkit.github.io/dev-docs/python_api/graph.html
29 |
30 | """
31 |
32 | def __init__(self, directed: bool = False, metadata_store: MetadataStore = None):
33 | """
34 | Create a new NetworkitBackend instance, using a Networkit.graph.Graph
35 | object to store and manage network structure.
36 |
37 | Arguments:
38 | directed (bool: False): Whether to make the backend graph directed
39 | metadata_store (MetadataStore): Optionally, a MetadataStore to use
40 | to handle node and edge attributes. If not provided, defaults
41 | to a DictMetadataStore.
42 |
43 | Returns:
44 | None
45 |
46 | """
47 | self._directed = directed
48 | self._meta = metadata_store or DictMetadataStore()
49 | self._nk_graph = networkit.graph.Graph(directed=directed)
50 | self._names = NodeNameManager()
51 |
52 | def ingest_from_edgelist_dataframe(
53 | self, edgelist: pd.DataFrame, source_column: str, target_column: str
54 | ) -> None:
55 | """
56 | Ingest an edgelist from a Pandas DataFrame.
57 |
58 | """
59 | raise NotImplementedError()
60 |
61 | def is_directed(self) -> bool:
62 | """
63 | Return True if the backend graph is directed.
64 |
65 | Arguments:
66 | None
67 |
68 | Returns:
69 | bool: True if the backend graph is directed.
70 |
71 | """
72 | return self._directed
73 |
74 | def add_node(self, node_name: Hashable, metadata: dict):
75 | """
76 | Add a new node to the graph.
77 |
78 | Arguments:
79 | node_name (Hashable): The ID of the node
80 | metadata (dict: None): An optional dictionary of metadata
81 |
82 | Returns:
83 | Hashable: The ID of this node, as inserted
84 |
85 | """
86 | # TODO: Remove metadata from lookup if insertion fails
87 | nk_id = self._nk_graph.addNode()
88 | self._names.add_node(node_name, nk_id)
89 | self._meta.add_node(node_name, metadata)
90 | return nk_id
91 |
92 | def get_node_by_id(self, node_name: Hashable):
93 | """
94 | Return the data associated with a node.
95 |
96 | Arguments:
97 | node_name (Hashable): The node ID to look up
98 |
99 | Returns:
100 | dict: The metadata associated with this node
101 |
102 | """
103 | return self._meta.get_node(node_name)
104 |
105 | def all_nodes_as_iterable(self, include_metadata: bool = False) -> Generator:
106 | """
107 | Get a generator of all of the nodes in this graph.
108 |
109 | Arguments:
110 | include_metadata (bool: False): Whether to include node metadata in
111 | the response
112 |
113 | Returns:
114 | Generator: A generator of all nodes (arbitrary sort)
115 |
116 | """
117 | if include_metadata:
118 | return [
119 | (self._names.get_name(i), self._meta.get_node(self._names.get_name(i)))
120 | for i in self._nk_graph.iterNodes()
121 | ]
122 | return [self._names.get_name(i) for i in self._nk_graph.iterNodes()]
123 |
124 | def has_node(self, u: Hashable) -> bool:
125 | """
126 | Return true if the node exists in the graph.
127 |
128 | Arguments:
129 | u (Hashable): The ID of the node to check
130 |
131 | Returns:
132 | bool: True if the node exists
133 | """
134 | return u in self._names
135 |
136 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict):
137 | """
138 | Add a new edge to the graph between two nodes.
139 |
140 | If the graph is directed, this edge will start (source) at the `u` node
141 | and end (target) at the `v` node.
142 |
143 | Arguments:
144 | u (Hashable): The source node ID
145 | v (Hashable): The target node ID
146 | metadata (dict): Optional metadata to associate with the edge
147 |
148 | Returns:
149 | Hashable: The edge ID, as inserted.
150 |
151 | """
152 | # If u doesn't exist:
153 | if self.has_node(u):
154 | x = self._names.get_id(u)
155 | else:
156 | x = self.add_node(u, None)
157 |
158 | if self.has_node(v):
159 | y = self._names.get_id(v)
160 | else:
161 | y = self.add_node(v, None)
162 |
163 | # Insert metadata for this edge, replacing the previous metadata:
164 | self._meta.add_edge(u, v, metadata)
165 |
166 | # TODO: Support multigraphs, and allow duplicate edges.
167 | if self.has_edge(u, v):
168 | return
169 | return self._nk_graph.addEdge(x, y)
170 |
171 | def all_edges_as_iterable(self, include_metadata: bool = False) -> Generator:
172 | """
173 | Get a list of all edges in this graph, arbitrary sort.
174 |
175 | Arguments:
176 | include_metadata (bool: False): Whether to include edge metadata
177 |
178 | Returns:
179 | Generator: A generator of all edges (arbitrary sort)
180 |
181 | """
182 | if include_metadata:
183 | return [
184 | (
185 | self._names.get_name(u),
186 | self._names.get_name(v),
187 | self._meta.get_edge(
188 | self._names.get_name(u), self._names.get_name(v)
189 | ),
190 | )
191 | for u, v in self._nk_graph.iterEdges()
192 | ]
193 | return [
194 | (self._names.get_name(u), self._names.get_name(v))
195 | for u, v in self._nk_graph.iterEdges()
196 | ]
197 |
198 | def has_edge(self, u, v):
199 | return self._nk_graph.hasEdge(self._names.get_id(u), self._names.get_id(v))
200 |
201 | def get_edge_by_id(self, u: Hashable, v: Hashable):
202 | """
203 | Get an edge by its source and target IDs.
204 |
205 | Arguments:
206 | u (Hashable): The source node ID
207 | v (Hashable): The target node ID
208 |
209 | Returns:
210 | dict: Metadata associated with this edge
211 |
212 | """
213 | if self.has_edge(u, v):
214 | return self._meta.get_edge(u, v)
215 | raise IndexError(f"The edge ({u}, {v}) is not in the graph.")
216 |
217 | def get_node_successors(
218 | self, u: Hashable, include_metadata: bool = False
219 | ) -> Generator:
220 | return self.get_node_neighbors(u, include_metadata)
221 |
222 | def get_node_neighbors(
223 | self, u: Hashable, include_metadata: bool = False
224 | ) -> Generator:
225 | """
226 | Get a generator of all downstream nodes from this node.
227 |
228 | Arguments:
229 | u (Hashable): The source node ID
230 |
231 | Returns:
232 | Generator
233 |
234 | """
235 | my_id = self._names.get_id(u)
236 | if include_metadata:
237 | val = {}
238 | for vid in self._nk_graph.iterNeighbors(my_id):
239 | v = self._names.get_name(vid)
240 | if self.is_directed():
241 | val[v] = self._meta.get_edge(u, v)
242 | else:
243 | try:
244 | val[v] = self._meta.get_edge(u, v)
245 | except KeyError:
246 | val[v] = self._meta.get_edge(v, u)
247 | return val
248 |
249 | return iter(
250 | [self._names.get_name(i) for i in self._nk_graph.iterNeighbors(my_id)]
251 | )
252 |
253 | def get_node_predecessors(
254 | self, u: Hashable, include_metadata: bool = False
255 | ) -> Generator:
256 | """
257 | Get a generator of all upstream nodes from this node.
258 |
259 | Arguments:
260 | u (Hashable): The source node ID
261 |
262 | Returns:
263 | Generator
264 |
265 | """
266 | my_id = self._names.get_id(u)
267 | if include_metadata:
268 | val = {}
269 | for vid in self._nk_graph.iterInNeighbors(my_id):
270 | v = self._names.get_name(vid)
271 | if self.is_directed():
272 | val[v] = self._meta.get_edge(v, u)
273 | else:
274 | try:
275 | val[v] = self._meta.get_edge(u, v)
276 | except KeyError:
277 | val[v] = self._meta.get_edge(v, u)
278 | return val
279 |
280 | return iter(
281 | [self._names.get_name(i) for i in self._nk_graph.iterInNeighbors(my_id)]
282 | )
283 |
284 | def get_node_count(self) -> int:
285 | """
286 | Get an integer count of the number of nodes in this graph.
287 |
288 | Arguments:
289 | None
290 |
291 | Returns:
292 | int: The count of nodes
293 |
294 | """
295 | return self._nk_graph.numberOfNodes()
296 |
297 | def get_edge_count(self) -> int:
298 | """
299 | Get an integer count of the number of edges in this graph.
300 |
301 | Arguments:
302 | None
303 |
304 | Returns:
305 | int: The count of edges
306 |
307 | """
308 | return self._nk_graph.numberOfEdges()
309 |
--------------------------------------------------------------------------------
/grand/backends/_networkx.py:
--------------------------------------------------------------------------------
1 | from typing import Hashable, Collection
2 | import time
3 |
4 | import pandas as pd
5 | import networkx as nx
6 |
7 | from .backend import Backend
8 |
9 |
10 | class NetworkXBackend(Backend):
11 | def __init__(self, directed: bool = False):
12 | """
13 | Create a new Backend instance.
14 |
15 | Arguments:
16 | directed (bool: False): Whether to make the backend graph directed
17 |
18 | Returns:
19 | None
20 |
21 | """
22 | self._nx_graph = nx.DiGraph() if directed else nx.Graph()
23 | self._directed = directed
24 |
25 | def is_directed(self) -> bool:
26 | """
27 | Return True if the backend graph is directed.
28 |
29 | Arguments:
30 | None
31 |
32 | Returns:
33 | bool: True if the backend graph is directed.
34 |
35 | """
36 | return self._directed
37 |
38 | def add_node(self, node_name: Hashable, metadata: dict):
39 | """
40 | Add a new node to the graph.
41 |
42 | Arguments:
43 | node_name (Hashable): The ID of the node
44 | metadata (dict: None): An optional dictionary of metadata
45 |
46 | Returns:
47 | Hashable: The ID of this node, as inserted
48 |
49 | """
50 | self._nx_graph.add_node(node_name, **metadata)
51 |
52 | def get_node_by_id(self, node_name: Hashable):
53 | """
54 | Return the data associated with a node.
55 |
56 | Arguments:
57 | node_name (Hashable): The node ID to look up
58 |
59 | Returns:
60 | dict: The metadata associated with this node
61 |
62 | """
63 | return self._nx_graph.nodes[node_name]
64 |
65 | def all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection:
66 | """
67 | Get a generator of all of the nodes in this graph.
68 |
69 | Arguments:
70 | include_metadata (bool: False): Whether to include node metadata in
71 | the response
72 |
73 | Returns:
74 | Generator: A generator of all nodes (arbitrary sort)
75 |
76 | """
77 | return list(self._nx_graph.nodes(data=include_metadata))
78 |
79 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict):
80 | """
81 | Add a new edge to the graph between two nodes.
82 |
83 | If the graph is directed, this edge will start (source) at the `u` node
84 | and end (target) at the `v` node.
85 |
86 | Arguments:
87 | u (Hashable): The source node ID
88 | v (Hashable): The target node ID
89 | metadata (dict): Optional metadata to associate with the edge
90 |
91 | Returns:
92 | Hashable: The edge ID, as inserted.
93 |
94 | """
95 | self._nx_graph.add_edge(u, v, **metadata)
96 |
97 | def all_edges_as_iterable(self, include_metadata: bool = False) -> Collection:
98 | """
99 | Get a list of all edges in this graph, arbitrary sort.
100 |
101 | Arguments:
102 | include_metadata (bool: False): Whether to include edge metadata
103 |
104 | Returns:
105 | Generator: A generator of all edges (arbitrary sort)
106 |
107 | """
108 | return self._nx_graph.edges(data=include_metadata)
109 |
110 | def get_edge_by_id(self, u: Hashable, v: Hashable):
111 | """
112 | Get an edge by its source and target IDs.
113 |
114 | Arguments:
115 | u (Hashable): The source node ID
116 | v (Hashable): The target node ID
117 |
118 | Returns:
119 | dict: Metadata associated with this edge
120 |
121 | """
122 | return self._nx_graph.edges[u, v]
123 |
124 | def get_node_neighbors(
125 | self, u: Hashable, include_metadata: bool = False
126 | ) -> Collection:
127 | """
128 | Get a generator of all downstream nodes from this node.
129 |
130 | Arguments:
131 | u (Hashable): The source node ID
132 |
133 | Returns:
134 | Generator
135 |
136 | """
137 | if include_metadata:
138 | return self._nx_graph[u]
139 | return self._nx_graph.neighbors(u)
140 |
141 | def get_node_predecessors(
142 | self, u: Hashable, include_metadata: bool = False
143 | ) -> Collection:
144 | """
145 | Get a generator of all downstream nodes from this node.
146 |
147 | Arguments:
148 | u (Hashable): The source node ID
149 |
150 | Returns:
151 | Generator
152 |
153 | """
154 | if include_metadata:
155 | return self._nx_graph.pred[u]
156 | return self._nx_graph.predecessors(u)
157 |
158 | def get_node_count(self) -> int:
159 | """
160 | Get an integer count of the number of nodes in this graph.
161 |
162 | Arguments:
163 | None
164 |
165 | Returns:
166 | int: The count of nodes
167 |
168 | """
169 | return len(self._nx_graph)
170 |
171 | def get_edge_count(self) -> int:
172 | """
173 | Get an integer count of the number of edges in this graph.
174 |
175 | Arguments:
176 | None
177 |
178 | Returns:
179 | int: The count of edges
180 |
181 | """
182 | return len(self._nx_graph.edges)
183 |
184 | def ingest_from_edgelist_dataframe(
185 | self, edgelist: pd.DataFrame, source_column: str, target_column: str
186 | ) -> dict:
187 | """
188 | Ingest an edgelist from a Pandas DataFrame.
189 |
190 | """
191 |
192 | tic = time.time()
193 | self._nx_graph.add_edges_from(
194 | [
195 | (
196 | e[source_column],
197 | e[target_column],
198 | {
199 | k: v
200 | for k, v in dict(e).items()
201 | if k not in [source_column, target_column]
202 | },
203 | )
204 | for _, e in edgelist.iterrows()
205 | ]
206 | )
207 |
208 | nodes = edgelist[source_column].append(edgelist[target_column]).unique()
209 |
210 | return {
211 | "node_count": len(nodes),
212 | "node_duration": 0,
213 | "edge_count": len(edgelist),
214 | "edge_duration": time.time() - tic,
215 | }
216 |
--------------------------------------------------------------------------------
/grand/backends/backend.py:
--------------------------------------------------------------------------------
1 | import cachetools.func
2 | from typing import Callable, Hashable, Collection
3 | import abc
4 |
5 | import pandas as pd
6 |
7 |
8 | class Backend(abc.ABC):
9 | """
10 | Abstract base class for the management of persisted graph structure.
11 |
12 | Do not use this class directly.
13 |
14 | """
15 |
16 | def __init__(self, directed: bool = False):
17 | """
18 | Create a new Backend instance.
19 |
20 | Arguments:
21 | directed (bool: False): Whether to make the backend graph directed
22 |
23 | Returns:
24 | None
25 |
26 | """
27 | ...
28 |
29 | def ingest_from_edgelist_dataframe(
30 | self, edgelist: pd.DataFrame, source_column: str, target_column: str
31 | ) -> None:
32 | """
33 | Ingest an edgelist from a Pandas DataFrame.
34 |
35 | """
36 | ...
37 |
38 | def is_directed(self) -> bool:
39 | """
40 | Return True if the backend graph is directed.
41 |
42 | Arguments:
43 | None
44 |
45 | Returns:
46 | bool: True if the backend graph is directed.
47 |
48 | """
49 | ...
50 |
51 | def add_node(self, node_name: Hashable, metadata: dict):
52 | """
53 | Add a new node to the graph.
54 |
55 | Arguments:
56 | node_name (Hashable): The ID of the node
57 | metadata (dict: None): An optional dictionary of metadata
58 | upsert (bool: True): Update the node if it already exists. If this
59 | is set to False and the node already exists, a backend may
60 | choose to throw an error or proceed gracefully.
61 |
62 | Returns:
63 | Hashable: The ID of this node, as inserted
64 |
65 | """
66 | ...
67 |
68 | def add_nodes_from(self, nodes_for_adding, **attr):
69 | """
70 | Add nodes to the graph.
71 |
72 | Arguments:
73 | nodes_for_adding: nodes to add
74 | attr: additional attributes
75 | """
76 | for node, metadata in nodes_for_adding:
77 | self.add_node(node, {**attr, **metadata})
78 |
79 | def get_node_by_id(self, node_name: Hashable):
80 | """
81 | Return the data associated with a node.
82 |
83 | Arguments:
84 | node_name (Hashable): The node ID to look up
85 |
86 | Returns:
87 | dict: The metadata associated with this node
88 |
89 | """
90 | ...
91 |
92 | def all_nodes_as_iterable(self, include_metadata: bool = False) -> Collection:
93 | """
94 | Get a generator of all of the nodes in this graph.
95 |
96 | Arguments:
97 | include_metadata (bool: False): Whether to include node metadata in
98 | the response
99 |
100 | Returns:
101 | Generator: A generator of all nodes (arbitrary sort)
102 |
103 | """
104 | ...
105 |
106 | def has_node(self, u: Hashable) -> bool:
107 | """
108 | Return true if the node exists in the graph.
109 |
110 | Arguments:
111 | u (Hashable): The ID of the node to check
112 |
113 | Returns:
114 | bool: True if the node exists
115 | """
116 | try:
117 | return self.get_node_by_id(u) is not None
118 | except KeyError:
119 | return False
120 |
121 | def has_edge(self, u: Hashable, v: Hashable) -> bool:
122 | """
123 | Return true if the edge exists in the graph.
124 |
125 | Arguments:
126 | u (Hashable): The source node ID
127 | v (Hashable): The target node ID
128 |
129 | Returns:
130 | bool: True if the edge exists
131 | """
132 | try:
133 | return self.get_edge_by_id(u, v) is not None
134 | except KeyError:
135 | return False
136 |
137 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict):
138 | """
139 | Add a new edge to the graph between two nodes.
140 |
141 | If the graph is directed, this edge will start (source) at the `u` node
142 | and end (target) at the `v` node.
143 |
144 | Arguments:
145 | u (Hashable): The source node ID
146 | v (Hashable): The target node ID
147 | metadata (dict): Optional metadata to associate with the edge
148 |
149 | Returns:
150 | Hashable: The edge ID, as inserted.
151 |
152 | """
153 | ...
154 |
155 | def add_edges_from(self, ebunch_to_add, **attr):
156 | """
157 | Add new edges to the graph.
158 |
159 | Arguments:
160 | ebunch_to_add: list of (source, target, metadata)
161 | attr: additional common attributes
162 | """
163 | for u, v, metadata in ebunch_to_add:
164 | self.add_edge(u, v, {**attr, **metadata})
165 |
166 | def all_edges_as_iterable(self, include_metadata: bool = False) -> Collection:
167 | """
168 | Get a list of all edges in this graph, arbitrary sort.
169 |
170 | Arguments:
171 | include_metadata (bool: False): Whether to include edge metadata
172 |
173 | Returns:
174 | Generator: A generator of all edges (arbitrary sort)
175 |
176 | """
177 | ...
178 |
179 | def get_edge_by_id(self, u: Hashable, v: Hashable):
180 | """
181 | Get an edge by its source and target IDs.
182 |
183 | Arguments:
184 | u (Hashable): The source node ID
185 | v (Hashable): The target node ID
186 |
187 | Returns:
188 | dict: Metadata associated with this edge
189 |
190 | """
191 | ...
192 |
193 | def get_node_successors(
194 | self, u: Hashable, include_metadata: bool = False
195 | ) -> Collection:
196 | return self.get_node_neighbors(u, include_metadata)
197 |
198 | def get_node_neighbors(
199 | self, u: Hashable, include_metadata: bool = False
200 | ) -> Collection:
201 | """
202 | Get a generator of all downstream nodes from this node.
203 |
204 | Arguments:
205 | u (Hashable): The source node ID
206 |
207 | Returns:
208 | Generator
209 |
210 | """
211 | ...
212 |
213 | def get_node_predecessors(
214 | self, u: Hashable, include_metadata: bool = False
215 | ) -> Collection:
216 | """
217 | Get a generator of all upstream nodes from this node.
218 |
219 | Arguments:
220 | u (Hashable): The source node ID
221 |
222 | Returns:
223 | Generator
224 |
225 | """
226 | ...
227 |
228 | def get_node_count(self) -> int:
229 | """
230 | Get an integer count of the number of nodes in this graph.
231 |
232 | Arguments:
233 | None
234 |
235 | Returns:
236 | int: The count of nodes
237 |
238 | """
239 | return len([i for i in self.all_nodes_as_iterable()])
240 |
241 | def get_edge_count(self) -> int:
242 | """
243 | Get an integer count of the number of edges in this graph.
244 |
245 | Arguments:
246 | None
247 |
248 | Returns:
249 | int: The count of edges
250 |
251 | """
252 | return len([i for i in self.all_edges_as_iterable()])
253 |
254 | def degree(self, u: Hashable) -> int:
255 | """
256 | Get the degree of a node.
257 |
258 | Arguments:
259 | u (Hashable): The node ID
260 |
261 | Returns:
262 | int: The degree of the node
263 |
264 | """
265 | if self.is_directed():
266 | # For directed graphs, degree = in_degree + out_degree
267 | return self.in_degree(u) + self.out_degree(u)
268 | else:
269 | # For undirected graphs, count all neighbors
270 | return len([i for i in self.get_node_neighbors(u)])
271 |
272 | def degrees(self, nbunch=None) -> Collection:
273 | return {
274 | node: self.degree(node) for node in (nbunch or self.all_nodes_as_iterable())
275 | }
276 |
277 | def in_degree(self, u: Hashable) -> int:
278 | """
279 | Get the in-degree of a node.
280 |
281 | Arguments:
282 | u (Hashable): The node ID
283 |
284 | Returns:
285 | int: The in-degree of the node
286 |
287 | """
288 | return len(list(self.get_node_predecessors(u)))
289 |
290 | def in_degrees(self, nbunch=None) -> Collection:
291 | nbunch = nbunch or self.all_nodes_as_iterable()
292 | if isinstance(nbunch, (list, tuple)):
293 | return {node: self.in_degree(node) for node in nbunch}
294 | else:
295 | return self.in_degree(nbunch)
296 |
297 | def out_degree(self, u: Hashable) -> int:
298 | """
299 | Get the out-degree of a node.
300 |
301 | Arguments:
302 | u (Hashable): The node ID
303 |
304 | Returns:
305 | int: The out-degree of the node
306 |
307 | """
308 | return len(list(self.get_node_successors(u)))
309 |
310 | def out_degrees(self, nbunch=None) -> Collection:
311 | nbunch = nbunch or self.all_nodes_as_iterable()
312 | if isinstance(nbunch, (list, tuple)):
313 | return {node: self.out_degree(node) for node in nbunch}
314 | else:
315 | return self.out_degree(nbunch)
316 |
317 |
318 | class CachedBackend(Backend):
319 | """
320 | A proxy Backend that serves as a cache for any other grand.Backend.
321 |
322 | """
323 |
324 | def __init__(self, backend: Backend): ...
325 |
326 |
327 | class InMemoryCachedBackend(CachedBackend):
328 | """
329 | A proxy Backend that serves as a cache for any other grand.Backend.
330 |
331 | Wraps each call to the Backend with an LRU cache.
332 |
333 | """
334 |
335 | _cache_types = {
336 | "LRUCache": cachetools.func.lru_cache,
337 | "TTLCache": cachetools.func.ttl_cache,
338 | "LFUCache": cachetools.func.lfu_cache,
339 | }
340 |
341 | _default_uncacheable_methods = [
342 | "add_node",
343 | "add_nodes_from",
344 | "add_edge",
345 | "add_edges_from",
346 | "ingest_from_edgelist_dataframe",
347 | "remove_node",
348 | ]
349 |
350 | _default_write_methods = [
351 | "add_node",
352 | "add_nodes_from",
353 | "add_edge",
354 | "add_edges_from",
355 | "ingest_from_edgelist_dataframe",
356 | "remove_node",
357 | ]
358 |
359 | def __init__(
360 | self,
361 | backend: Backend,
362 | dirty_cache_on_write: bool = True,
363 | cache_type: str = "TTLCache",
364 | uncacheable_methods: list = None,
365 | write_methods: list = None,
366 | **cache_kwargs,
367 | ):
368 | """
369 | Initialize a new in-memory cache, using the cachetools library.
370 |
371 | Arguments:
372 | backend (grand.Backend): The backend to cache
373 | dirty_cache_on_write (bool): Whether to clear the cache on writes
374 | cache_type (str: "TTLCache"): The cache type to use. One of
375 | ["LRUCache", "TTLCache"]
376 | **cache_kwargs: Additional arguments to pass to the cache
377 |
378 |
379 | """
380 | self.backend = backend
381 | self._dirty_cache_on_write = dirty_cache_on_write
382 | self._uncacheable_methods = (
383 | uncacheable_methods or self._default_uncacheable_methods
384 | )
385 | self._write_methods = write_methods or self._default_write_methods
386 | if cache_type not in self._cache_types:
387 | raise ValueError(
388 | f"Unknown cache type: {cache_type}. "
389 | f"Valid types are: {self._cache_types.keys()}"
390 | )
391 | self._cache_factory = lambda: self._cache_types[cache_type](**cache_kwargs)
392 |
393 | self._method_lookup = {}
394 |
395 | def _dirty_cache_decorator(method_: Callable):
396 | def dirty_cache_dec_wrapper(*args, **kwargs):
397 | self.clear_cache()
398 | return method_(*args, **kwargs)
399 |
400 | return dirty_cache_dec_wrapper
401 |
402 | method_list = [
403 | attribute
404 | for attribute in dir(self.backend)
405 | if callable(getattr(self.backend, attribute))
406 | and not attribute.startswith("_")
407 | ]
408 | for method in method_list:
409 | if method in self._uncacheable_methods:
410 | setattr(self, method, getattr(self.backend, method))
411 | else:
412 | wrapped = self._wrapped(method)
413 | self._method_lookup[method] = wrapped
414 | setattr(self, method, wrapped)
415 |
416 | if self._dirty_cache_on_write and method in self._write_methods:
417 | setattr(
418 | self, method, _dirty_cache_decorator(getattr(self.backend, method))
419 | )
420 |
421 | def _wrapped(self, method: str) -> Callable:
422 | c = self._cache_factory()(getattr(self.backend, method))
423 | return c
424 |
425 | def clear_cache(self):
426 | """
427 | Clear the cache.
428 |
429 | """
430 | for _, method in self._method_lookup.items():
431 | method.cache_clear()
432 |
433 | def cache_info(self):
434 | return {
435 | method_name: method.cache_info()
436 | for method_name, method in self._method_lookup.items()
437 | }
438 |
--------------------------------------------------------------------------------
/grand/backends/metadatastore.py:
--------------------------------------------------------------------------------
1 | from typing import Hashable
2 |
3 |
4 | class MetadataStore:
5 | def add_node(self, node_name: Hashable, metadata: dict) -> Hashable:
6 | raise NotImplementedError()
7 |
8 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict) -> Hashable:
9 | raise NotImplementedError()
10 |
11 | def get_node(self, node_name: Hashable) -> dict:
12 | raise NotImplementedError()
13 |
14 | def get_edge(self, u: Hashable, v: Hashable) -> dict:
15 | raise NotImplementedError()
16 |
17 |
18 | class DictMetadataStore(MetadataStore):
19 | def __init__(self):
20 | self.ndata = {}
21 | self.edata = {}
22 |
23 | def add_node(self, node_name: Hashable, metadata: dict) -> Hashable:
24 | if metadata is None:
25 | metadata = {}
26 | # Add or update metadata
27 | if node_name in self.ndata:
28 | self.ndata[node_name].update(metadata)
29 | else:
30 | self.ndata[node_name] = metadata
31 |
32 | def add_edge(self, u: Hashable, v: Hashable, metadata: dict) -> Hashable:
33 | if metadata is None:
34 | metadata = {}
35 | # Add or update metadata
36 | if (u, v) in self.edata:
37 | self.edata[(u, v)].update(metadata)
38 | else:
39 | self.edata[(u, v)] = metadata
40 |
41 | def get_node(self, node_name: Hashable) -> dict:
42 | return self.ndata[node_name]
43 |
44 | def get_edge(self, u: Hashable, v: Hashable) -> dict:
45 | return self.edata[(u, v)]
46 |
47 |
48 | class NodeNameManager:
49 | def __init__(self):
50 | self.node_names_by_id = {}
51 | self.node_ids_by_name = {}
52 |
53 | def add_node(self, name: Hashable, _id: Hashable):
54 | self.node_names_by_id[_id] = name
55 | self.node_ids_by_name[name] = _id
56 |
57 | def get_name(self, _id: Hashable) -> Hashable:
58 | return self.node_names_by_id[_id]
59 |
60 | def get_id(self, name: Hashable) -> Hashable:
61 | return self.node_ids_by_name[name]
62 |
63 | def __contains__(self, name: Hashable) -> bool:
64 | return name in self.node_ids_by_name
65 |
--------------------------------------------------------------------------------
/grand/backends/test_backends.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import os
3 | import pandas as pd
4 |
5 | import networkx as nx
6 |
7 | from . import NetworkXBackend, DataFrameBackend
8 |
9 | try:
10 | from ._dynamodb import DynamoDBBackend
11 |
12 | _CAN_IMPORT_DYNAMODB = True
13 | except ImportError:
14 | _CAN_IMPORT_DYNAMODB = False
15 |
16 | try:
17 | from ._igraph import IGraphBackend
18 |
19 | _CAN_IMPORT_IGRAPH = True
20 | except ImportError:
21 | _CAN_IMPORT_IGRAPH = False
22 |
23 | try:
24 | from ._networkit import NetworkitBackend
25 |
26 | _CAN_IMPORT_NETWORKIT = True
27 | except ImportError:
28 | _CAN_IMPORT_NETWORKIT = False
29 |
30 | try:
31 | from ._sqlbackend import SQLBackend
32 |
33 | _CAN_IMPORT_SQL = True
34 | except ImportError:
35 | _CAN_IMPORT_SQL = False
36 |
37 | from .. import Graph
38 |
39 |
40 | backend_test_params = [
41 | pytest.param(
42 | (NetworkXBackend, {}),
43 | marks=pytest.mark.skipif(
44 | os.environ.get("TEST_NETWORKXBACKEND", default="1") != "1",
45 | reason="NetworkX Backend skipped because $TEST_NETWORKXBACKEND != 1.",
46 | ),
47 | id="NetworkXBackend",
48 | ),
49 | ]
50 | backend_test_params = [
51 | pytest.param(
52 | (DataFrameBackend, {}),
53 | marks=pytest.mark.skipif(
54 | os.environ.get("TEST_DATAFRAMEBACKEND", default="1") != "1",
55 | reason="DataFrameBackend skipped because $TEST_DATAFRAMEBACKEND != 1.",
56 | ),
57 | id="DataFrameBackend",
58 | ),
59 | ]
60 |
61 | if _CAN_IMPORT_DYNAMODB:
62 | backend_test_params.append(
63 | pytest.param(
64 | (DynamoDBBackend, {}),
65 | marks=pytest.mark.skipif(
66 | os.environ.get("TEST_DYNAMODB", default="1") != "1",
67 | reason="DynamoDB Backend skipped because $TEST_DYNAMODB != 0 or boto3 is not installed",
68 | ),
69 | id="DynamoDBBackend",
70 | ),
71 | )
72 |
73 | if _CAN_IMPORT_SQL:
74 | backend_test_params.append(
75 | pytest.param(
76 | (SQLBackend, {"db_url": "sqlite:///:memory:"}),
77 | marks=pytest.mark.skipif(
78 | os.environ.get("TEST_SQLBACKEND", default="1") != "1",
79 | reason="SQL Backend skipped because $TEST_SQLBACKEND != 1 or sqlalchemy is not installed.",
80 | ),
81 | id="SQLBackend",
82 | ),
83 | )
84 | if _CAN_IMPORT_IGRAPH:
85 | backend_test_params.append(
86 | pytest.param(
87 | (IGraphBackend, {}),
88 | marks=pytest.mark.skipif(
89 | os.environ.get("TEST_IGRAPHBACKEND", default="1") != "1",
90 | reason="IGraph Backend skipped because $TEST_IGRAPHBACKEND != 1 or igraph is not installed.",
91 | ),
92 | id="IGraphBackend",
93 | ),
94 | )
95 | if _CAN_IMPORT_NETWORKIT:
96 | backend_test_params.append(
97 | pytest.param(
98 | (NetworkitBackend, {}),
99 | marks=pytest.mark.skipif(
100 | os.environ.get("TEST_NETWORKIT", default="1") != "1",
101 | reason="Networkit Backend skipped because $TEST_NETWORKIT != 1 or networkit is not installed.",
102 | ),
103 | id="NetworkitBackend",
104 | ),
105 | )
106 |
107 | if os.environ.get("TEST_NETWORKITBACKEND") == "1":
108 | from ._networkit import NetworkitBackend
109 |
110 | backend_test_params.append(
111 | pytest.param(
112 | (NetworkitBackend, {}),
113 | marks=pytest.mark.skipif(
114 | os.environ.get("TEST_NETWORKITBACKEND") != "1",
115 | reason="Networkit Backend skipped because $TEST_NETWORKITBACKEND != 1.",
116 | ),
117 | id="NetworkitBackend",
118 | ),
119 | )
120 |
121 | if os.environ.get("TEST_IGRAPHBACKEND") == "1":
122 | from ._igraph import IGraphBackend
123 |
124 | backend_test_params.append(
125 | pytest.param(
126 | (IGraphBackend, {}),
127 | marks=pytest.mark.skipif(
128 | os.environ.get("TEST_IGRAPHBACKEND") != "1",
129 | reason="Networkit Backend skipped because $TEST_IGRAPHBACKEND != 1.",
130 | ),
131 | id="IGraphBackend",
132 | ),
133 | )
134 |
135 |
136 | # @pytest.mark.parametrize("backend", backend_test_params)
137 | class TestBackendPersistence:
138 | def test_sqlite_persistence(self):
139 | if not _CAN_IMPORT_SQL:
140 | return
141 |
142 | dbpath = "grand_peristence_test_temp.db"
143 | url = "sqlite:///" + dbpath
144 |
145 | # arrange
146 | backend = SQLBackend(db_url=url, directed=True)
147 | node0 = backend.add_node("A", {"foo": "bar"})
148 | backend.commit()
149 | backend.close()
150 | # act
151 | backend = SQLBackend(db_url=url, directed=True)
152 | nodes = list(backend.all_nodes_as_iterable())
153 | # assert
154 | assert node0 in nodes
155 |
156 | # test remove_node
157 | backend = SQLBackend(db_url=url, directed=True)
158 | node1, node2 = backend.add_node("A", {}), backend.add_node("B", {})
159 | backend.add_edge(node1, node2, {})
160 | assert backend.has_node(node1)
161 | assert backend.has_edge(node1, node2)
162 | backend.remove_node(node1)
163 | assert not backend.has_node(node1)
164 | assert not backend.has_edge(node1, node2)
165 | with pytest.raises(KeyError):
166 | assert not backend.get_node_by_id(node1)
167 |
168 | # cleanup
169 | os.remove(dbpath)
170 |
171 |
172 | @pytest.mark.parametrize("backend", backend_test_params)
173 | class TestBackend:
174 | def test_can_create(self, backend):
175 | backend, kwargs = backend
176 | backend(**kwargs)
177 |
178 | def test_can_create_directed_and_undirected_backends(self, backend):
179 | backend, kwargs = backend
180 | b = backend(directed=True, **kwargs)
181 | assert b.is_directed() == True
182 |
183 | b = backend(directed=False, **kwargs)
184 | assert b.is_directed() == False
185 |
186 | def test_can_add_node(self, backend):
187 | backend, kwargs = backend
188 | G = Graph(backend=backend(**kwargs))
189 | nxG = nx.Graph()
190 | G.nx.add_node("A", k="v")
191 | nxG.add_node("A", k="v")
192 | assert len(G.nx.nodes()) == len(nxG.nodes())
193 | G.nx.add_node("B", k="v")
194 | nxG.add_node("B", k="v")
195 | assert len(G.nx.nodes()) == len(nxG.nodes())
196 |
197 | def test_can_update_node(self, backend):
198 | backend, kwargs = backend
199 | G = Graph(backend=backend(**kwargs))
200 | G.nx.add_node("A", k="v", z=3)
201 | G.nx.add_node("A", k="v2", x=4)
202 | assert G.nx.nodes["A"]["k"] == "v2"
203 | assert G.nx.nodes["A"]["x"] == 4
204 | assert G.nx.nodes["A"]["z"] == 3
205 |
206 | def test_can_add_edge(self, backend):
207 | backend, kwargs = backend
208 | G = Graph(backend=backend(**kwargs))
209 | nxG = nx.Graph()
210 | G.nx.add_edge("A", "B")
211 | nxG.add_edge("A", "B")
212 | assert len(G.nx.edges()) == len(nxG.edges())
213 | G.nx.add_edge("A", "B")
214 | nxG.add_edge("A", "B")
215 | assert len(G.nx.edges()) == len(nxG.edges())
216 |
217 | def test_can_update_edge(self, backend):
218 | backend, kwargs = backend
219 | G = Graph(backend=backend(**kwargs))
220 | G.nx.add_edge("A", "B", k="v", z=3)
221 | G.nx.add_edge("A", "B", k="v2", x=4)
222 | assert G.nx.get_edge_data("A", "B")["k"] == "v2"
223 | assert G.nx.get_edge_data("A", "B")["x"] == 4
224 | assert G.nx.get_edge_data("A", "B")["z"] == 3
225 | assert len(G.nx.nodes()) == 2
226 |
227 | def test_can_get_node(self, backend):
228 | backend, kwargs = backend
229 | G = Graph(backend=backend(**kwargs))
230 | nxG = nx.Graph()
231 | md = dict(k="B")
232 | G.nx.add_node("A", **md)
233 | nxG.add_node("A", **md)
234 | assert G.nx.nodes["A"] == nxG.nodes["A"]
235 |
236 | def test_can_get_edge(self, backend):
237 | backend, kwargs = backend
238 | G = Graph(backend=backend(**kwargs))
239 | nxG = nx.Graph()
240 | md = {"k": "B"}
241 | G.nx.add_edge("A", "B", **md)
242 | nxG.add_edge("A", "B", **md)
243 | assert G.nx.get_edge_data("A", "B") == nxG.get_edge_data("A", "B")
244 |
245 | def test_can_get_neighbors(self, backend):
246 | backend, kwargs = backend
247 | G = Graph(backend=backend(**kwargs))
248 | nxG = nx.Graph()
249 | G.nx.add_edge("A", "B")
250 | nxG.add_edge("A", "B")
251 | assert sorted([i for i in G.nx.neighbors("A")]) == sorted(
252 | [i for i in nxG.neighbors("A")]
253 | )
254 | assert sorted([i for i in G.nx.neighbors("B")]) == sorted(
255 | [i for i in nxG.neighbors("B")]
256 | )
257 | G.nx.add_edge("A", "C")
258 | nxG.add_edge("A", "C")
259 | assert sorted([i for i in G.nx.neighbors("A")]) == sorted(
260 | [i for i in nxG.neighbors("A")]
261 | )
262 | assert sorted([i for i in G.nx.neighbors("B")]) == sorted(
263 | [i for i in nxG.neighbors("B")]
264 | )
265 | assert sorted([i for i in G.nx.neighbors("C")]) == sorted(
266 | [i for i in nxG.neighbors("C")]
267 | )
268 |
269 | def test_undirected_adj(self, backend):
270 | backend, kwargs = backend
271 | G = Graph(backend=backend(**kwargs))
272 | nxG = nx.Graph()
273 | assert G.nx._adj == nxG._adj
274 | G.nx.add_edge("A", "B")
275 | nxG.add_edge("A", "B")
276 | assert G.nx._adj == nxG._adj
277 |
278 | def test_directed_adj(self, backend):
279 | backend, kwargs = backend
280 | G = Graph(backend=backend(directed=True, **kwargs))
281 | nxG = nx.DiGraph()
282 | assert G.nx._adj == nxG._adj
283 | G.nx.add_edge("A", "B")
284 | nxG.add_edge("A", "B")
285 | assert G.nx._adj == nxG._adj
286 |
287 | def test_can_traverse_undirected_graph(self, backend):
288 | backend, kwargs = backend
289 | G = Graph(backend=backend(**kwargs))
290 | nxG = nx.Graph()
291 | md = dict(k="B")
292 | G.nx.add_edge("A", "B", **md)
293 | nxG.add_edge("A", "B", **md)
294 | assert dict(nx.bfs_successors(G.nx, "A")) == dict(nx.bfs_successors(nxG, "A"))
295 | G.nx.add_edge("B", "C", **md)
296 | nxG.add_edge("B", "C", **md)
297 | assert dict(nx.bfs_successors(G.nx, "A")) == dict(nx.bfs_successors(nxG, "A"))
298 | G.nx.add_edge("B", "D", **md)
299 | nxG.add_edge("B", "D", **md)
300 | assert dict(nx.bfs_successors(G.nx, "A")) == dict(nx.bfs_successors(nxG, "A"))
301 | assert dict(nx.bfs_successors(G.nx, "C")) == dict(nx.bfs_successors(nxG, "C"))
302 |
303 | def test_can_traverse_directed_graph(self, backend):
304 | backend, kwargs = backend
305 | G = Graph(backend=backend(directed=True, **kwargs))
306 | nxG = nx.DiGraph()
307 | md = dict(k="B")
308 | G.nx.add_edge("A", "B", **md)
309 | nxG.add_edge("A", "B", **md)
310 | assert dict(nx.bfs_successors(G.nx, "A")) == dict(nx.bfs_successors(nxG, "A"))
311 | G.nx.add_edge("B", "C", **md)
312 | nxG.add_edge("B", "C", **md)
313 | assert dict(nx.bfs_successors(G.nx, "A")) == dict(nx.bfs_successors(nxG, "A"))
314 | G.nx.add_edge("B", "D", **md)
315 | nxG.add_edge("B", "D", **md)
316 | assert dict(nx.bfs_successors(G.nx, "A")) == dict(nx.bfs_successors(nxG, "A"))
317 | assert dict(nx.bfs_successors(G.nx, "C")) == dict(nx.bfs_successors(nxG, "C"))
318 |
319 | def test_subgraph_isomorphism_undirected(self, backend):
320 | backend, kwargs = backend
321 | G = Graph(backend=backend(directed=False, **kwargs))
322 | nxG = nx.Graph()
323 |
324 | G.nx.add_edge("A", "B")
325 | nxG.add_edge("A", "B")
326 | G.nx.add_edge("B", "C")
327 | nxG.add_edge("B", "C")
328 | G.nx.add_edge("C", "A")
329 | nxG.add_edge("C", "A")
330 |
331 | from networkx.algorithms.isomorphism import GraphMatcher
332 |
333 | assert len(
334 | [i for i in GraphMatcher(G.nx, G.nx).subgraph_monomorphisms_iter()]
335 | ) == len([i for i in GraphMatcher(nxG, nxG).subgraph_monomorphisms_iter()])
336 |
337 | def test_subgraph_isomorphism_directed(self, backend):
338 | backend, kwargs = backend
339 | G = Graph(backend=backend(directed=True, **kwargs))
340 | nxG = nx.DiGraph()
341 |
342 | G.nx.add_edge("A", "B")
343 | nxG.add_edge("A", "B")
344 | G.nx.add_edge("B", "C")
345 | nxG.add_edge("B", "C")
346 | G.nx.add_edge("C", "A")
347 | nxG.add_edge("C", "A")
348 |
349 | from networkx.algorithms.isomorphism import DiGraphMatcher
350 |
351 | assert len(
352 | [i for i in DiGraphMatcher(G.nx, G.nx).subgraph_monomorphisms_iter()]
353 | ) == len([i for i in DiGraphMatcher(nxG, nxG).subgraph_monomorphisms_iter()])
354 |
355 | def test_can_get_edge_metadata(self, backend):
356 | backend, kwargs = backend
357 | G = Graph(backend=backend(**kwargs))
358 | G.nx.add_edge("foo", "bar", baz=True)
359 | assert list(G.nx.edges(data=True)) == [("foo", "bar", {"baz": True})]
360 |
361 | def test_can_get_edges(self, backend):
362 | backend, kwargs = backend
363 | G = Graph(backend=backend(**kwargs))
364 | G.nx.add_edge("foo", "bar", baz=True)
365 | assert list(G.backend.all_edges_as_iterable()) == [("foo", "bar")]
366 |
367 | def test_edge_dne_raises(self, backend):
368 | backend, kwargs = backend
369 | G = Graph(backend=backend(**kwargs))
370 | G.nx.add_edge("foo", "bar", baz=True)
371 |
372 | assert G.nx.has_edge("foo", "crab") == False
373 | assert G.nx.has_edge("foo", "bar") == True
374 | # assert G.nx.edges[("foo", "bar")] != None
375 | # with pytest.raises(Exception):
376 | # G.nx.edges[("foo", "crab")]
377 |
378 | def test_reverse_edges_in_undirected(self, backend):
379 | backend, kwargs = backend
380 | G = Graph(backend=backend(directed=False, **kwargs))
381 | G.nx.add_edge("foo", "bar", baz=True)
382 |
383 | assert G.nx.has_edge("foo", "bar") == True
384 | assert G.nx.has_edge("bar", "foo") == True
385 |
386 | def test_undirected_degree(self, backend):
387 | backend, kwargs = backend
388 | G = Graph(backend=backend(directed=False, **kwargs))
389 | G.nx.add_edge("foo", "bar", baz=True)
390 | assert G.nx.degree("foo") == 1
391 | assert G.nx.degree("bar") == 1
392 |
393 | def test_directed_degree(self, backend):
394 | backend, kwargs = backend
395 | G = Graph(backend=backend(directed=True, **kwargs))
396 | G.nx.add_edge("foo", "bar", baz=True)
397 | assert G.nx.degree("foo") == 1
398 | assert G.nx.degree("bar") == 1
399 | assert G.nx.in_degree("bar") == 1
400 | assert G.nx.out_degree("bar") == 0
401 | assert G.nx.in_degree("foo") == 0
402 | assert G.nx.out_degree("foo") == 1
403 |
404 | def test_undirected_degree_multiple(self, backend):
405 | backend, kwargs = backend
406 | G = Graph(backend=backend(directed=False, **kwargs))
407 | G.nx.add_edge("foo", "bar", baz=True)
408 | G.nx.add_edge("foo", "baz", baz=True)
409 | assert G.nx.degree("foo") == 2
410 | assert G.nx.degree("bar") == 1
411 | assert G.nx.degree("baz") == 1
412 |
413 | def test_directed_degree_multiple(self, backend):
414 | backend, kwargs = backend
415 | G = Graph(backend=backend(directed=True, **kwargs))
416 | G.nx.add_edge("foo", "bar", baz=True)
417 | G.nx.add_edge("foo", "baz", baz=True)
418 | assert G.nx.degree("foo") == 2
419 | assert G.nx.degree("bar") == 1
420 | assert G.nx.degree("baz") == 1
421 | assert G.nx.out_degree("foo") == 2
422 | assert G.nx.out_degree("bar") == 0
423 | assert G.nx.out_degree("baz") == 0
424 | assert G.nx.in_degree("foo") == 0
425 | assert G.nx.in_degree("bar") == 1
426 | assert G.nx.in_degree("baz") == 1
427 |
428 | def test_node_count(self, backend):
429 | backend, kwargs = backend
430 | G = Graph(backend=backend(**kwargs))
431 | G.nx.add_node("foo", bar=True)
432 | G.nx.add_node("bar", foo=True)
433 | assert len(G.nx) == 2
434 |
435 |
436 | @pytest.mark.benchmark
437 | @pytest.mark.parametrize("backend", backend_test_params)
438 | def test_node_addition_performance(backend):
439 | backend, kwargs = backend
440 | G = Graph(backend=backend(directed=True, **kwargs))
441 | for i in range(1000):
442 | G.nx.add_node(i)
443 | assert len(G.nx) == 1000
444 |
445 |
446 | @pytest.mark.benchmark
447 | @pytest.mark.parametrize("backend", backend_test_params)
448 | def test_get_density_performance(backend):
449 | backend, kwargs = backend
450 | G = Graph(backend=backend(directed=True, **kwargs))
451 | for i in range(1000):
452 | G.nx.add_node(i)
453 | for i in range(1000 - 1):
454 | G.nx.add_edge(i, i + 1)
455 | assert nx.density(G.nx) <= 0.005
456 |
457 |
458 | class TestDataFrameBackend:
459 | def test_can_create_empty(self):
460 | b = DataFrameBackend()
461 | assert b.get_edge_count() == 0
462 | assert b.get_node_count() == 0
463 |
464 | b.add_edge("A", "B", {})
465 | assert b.get_edge_count() == 1
466 | assert b.get_node_count() == 2
467 |
468 | def test_can_create_from_int_dataframes(self):
469 | # Create an edges DataFrame
470 | edges = pd.DataFrame(
471 | {
472 | "source": [0, 1, 2, 3, 4],
473 | "target": [1, 2, 3, 4, 0],
474 | "weight": [1, 2, 3, 4, 5],
475 | }
476 | )
477 |
478 | nodes = pd.DataFrame(
479 | {
480 | "name": [0, 1, 2, 3, 4],
481 | "value": [1, 2, 3, 4, 5],
482 | }
483 | )
484 |
485 | b = DataFrameBackend(edge_df=edges, node_df=nodes)
486 | assert b.get_edge_count() == 5
487 | assert b.get_node_count() == 5
488 |
--------------------------------------------------------------------------------
/grand/backends/test_cached_backend.py:
--------------------------------------------------------------------------------
1 | import time
2 | from .backend import InMemoryCachedBackend
3 | from ._networkx import NetworkXBackend
4 | from ._sqlbackend import SQLBackend
5 |
6 |
7 | def test_can_create_cached_backend():
8 | InMemoryCachedBackend(NetworkXBackend(), maxsize=1024, ttl=20)
9 |
10 |
11 | def test_can_add_node():
12 | vanilla = NetworkXBackend()
13 | cached = InMemoryCachedBackend(vanilla, maxsize=1024, ttl=20)
14 | assert vanilla.get_node_count() == 0
15 | assert cached.get_node_count() == 0
16 | cached.add_node("a", {})
17 | assert vanilla.get_node_count() == 1
18 | assert cached.get_node_count() == 1
19 |
20 |
21 | def test_added_node_ignored_when_no_dirty_on_write():
22 | vanilla = NetworkXBackend()
23 | cached = InMemoryCachedBackend(
24 | vanilla, dirty_cache_on_write=False, maxsize=1024, ttl=20
25 | )
26 | assert vanilla.get_node_count() == 0
27 | assert cached.get_node_count() == 0
28 | cached.add_node("a", {})
29 | assert vanilla.get_node_count() == 1
30 | assert cached.get_node_count() == 0
31 |
32 |
33 | def test_can_add_nodes():
34 | cached = InMemoryCachedBackend(NetworkXBackend(), maxsize=1024, ttl=20)
35 | assert cached.get_node_count() == 0
36 | cached.add_node("a", {})
37 | assert cached.get_node_count() == 1
38 | cached.add_node("a", {})
39 | assert cached.get_node_count() == 1
40 | cached.add_node("b", {})
41 | assert cached.get_node_count() == 2
42 |
43 |
44 | def test_cache_is_faster_than_no_cache():
45 | cached = InMemoryCachedBackend(SQLBackend(), maxsize=1024, ttl=20)
46 | for i in range(1000):
47 | cached.add_node(i, {})
48 |
49 | tic = time.time()
50 | node_count = cached.get_node_count()
51 | toc = time.time()
52 |
53 | tic2 = time.time()
54 | node_count2 = cached.get_node_count()
55 | toc2 = time.time()
56 |
57 | # Dirty the cache:
58 | cached.add_node(1000, {})
59 |
60 | tic3 = time.time()
61 | node_count3 = cached.get_node_count()
62 | toc3 = time.time()
63 |
64 | assert node_count == node_count2 == (node_count3 - 1)
65 | assert (toc - tic) > (toc2 - tic2)
66 | assert (toc3 - tic3) > (toc2 - tic2)
67 |
68 |
69 | def test_cache_info():
70 | cached = InMemoryCachedBackend(SQLBackend(), maxsize=1024, ttl=20)
71 | cached.add_node("foo", {})
72 | assert cached.cache_info()["get_node_count"].misses == 0
73 | assert cached.cache_info()["get_node_count"].hits == 0
74 |
75 | cached.get_node_count()
76 |
77 | assert cached.cache_info()["get_node_count"].misses == 1
78 | assert cached.cache_info()["get_node_count"].hits == 0
79 |
80 | cached.get_node_count()
81 |
82 | assert cached.cache_info()["get_node_count"].misses == 1
83 | assert cached.cache_info()["get_node_count"].hits == 1
84 |
--------------------------------------------------------------------------------
/grand/backends/test_metadatastore.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .metadatastore import DictMetadataStore, NodeNameManager
3 |
4 |
5 | def test_can_create_dict_metadata_store():
6 | store = DictMetadataStore()
7 | assert store is not None
8 |
9 |
10 | def test_can_add_to_dict_metadata_store():
11 | store = DictMetadataStore()
12 | store.add_node("a", {"a": 1})
13 | assert store.get_node("a") == {"a": 1}
14 |
15 |
16 | def test_can_add_edge():
17 | store = DictMetadataStore()
18 | store.add_edge("a", "b", {"b": 2})
19 | assert store.get_edge("a", "b") == {"b": 2}
20 |
21 |
22 | def test_can_update_edge():
23 | store = DictMetadataStore()
24 | store.add_edge("a", "b", {"b": 2})
25 | store.add_edge("a", "b", {"x": "x"})
26 | store.add_edge("a", "b", {"z": "z"})
27 | assert store.get_edge("a", "b")["b"] == 2
28 | assert store.get_edge("a", "b")["x"] == "x"
29 | assert store.get_edge("a", "b")["z"] == "z"
30 |
31 |
32 | def test_cannot_get_invalid_node():
33 | store = DictMetadataStore()
34 | with pytest.raises(KeyError):
35 | store.get_node("a")
36 |
37 |
38 | def test_can_create_dict_name_manager():
39 | store = NodeNameManager()
40 | assert store is not None
41 |
42 |
43 | def test_can_add_name_manager():
44 | store = NodeNameManager()
45 | store.add_node("a", 1)
46 | assert "a" in store
47 | assert 1 not in store
48 | assert store.get_name(1) == "a"
49 |
50 |
51 | def test_can_reverse_lookup_node_name_manager():
52 | store = NodeNameManager()
53 | store.add_node("a", 1)
54 | assert "a" in store
55 | assert 1 not in store
56 | assert store.get_id("a") == 1
57 |
--------------------------------------------------------------------------------
/grand/dialects/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Hashable, Generator, List, Tuple, Union
2 | from typing import TYPE_CHECKING
3 |
4 | if TYPE_CHECKING:
5 | from .. import Graph
6 |
7 | import pandas as pd
8 |
9 | import networkx as nx
10 | from networkx.classes.reportviews import NodeView
11 | from networkx.classes.coreviews import AdjacencyView, AtlasView
12 |
13 |
14 | class _GrandAdjacencyView(AdjacencyView):
15 | __slots__ = ("_parent", "_pred_or_succ") # Still uses AtlasView slots names _atlas
16 |
17 | def __init__(self, parent_nx_dialect: "NetworkXDialect", pred_or_succ: str):
18 | self._parent = parent_nx_dialect.parent
19 | self._pred_or_succ = pred_or_succ
20 |
21 | def __getitem__(self, name):
22 | if self._pred_or_succ == "pred":
23 | return {
24 | neighbor: metadata
25 | for neighbor, metadata in self._parent.backend.get_node_predecessors(
26 | name, include_metadata=True
27 | ).items()
28 | }
29 | elif self._pred_or_succ == "succ":
30 | return {
31 | neighbor: metadata
32 | for neighbor, metadata in self._parent.backend.get_node_successors(
33 | name, include_metadata=True
34 | ).items()
35 | }
36 |
37 | def __len__(self):
38 | return self._parent.backend.get_node_count()
39 |
40 | def __iter__(self):
41 | return iter(self._parent.backend.all_nodes_as_iterable(include_metadata=False))
42 |
43 | def copy(self):
44 | raise NotImplementedError()
45 |
46 | def __str__(self):
47 | return "_GrandAdjacencyView"
48 |
49 | def __repr__(self):
50 | return "_GrandAdjacencyView"
51 |
52 |
53 | class _GrandNodeAtlasView(AtlasView):
54 | def __init__(self, parent):
55 | self.parent = parent.parent
56 |
57 | def __getitem__(self, key):
58 | return self.parent.backend.get_node_by_id(key)
59 |
60 | def __len__(self):
61 | return self.parent.backend.get_node_count()
62 |
63 | def __iter__(self):
64 | return iter(self.parent.backend.all_nodes_as_iterable(include_metadata=False))
65 |
66 | def copy(self):
67 | return {
68 | n: metadata
69 | for n, metadata in self.parent.backend.all_nodes_as_iterable(
70 | include_metadata=True
71 | )
72 | }
73 |
74 | def __str__(self):
75 | return "_GrandNodeAtlasView"
76 |
77 | def __repr__(self):
78 | return "_GrandNodeAtlasView"
79 |
80 |
81 | class NetworkXDialect(nx.Graph):
82 | """
83 | A NetworkXDialect provides a networkx-like interface for graph manipulation
84 |
85 | """
86 |
87 | def __init__(self, parent: "Graph"):
88 | """
89 | Create a new dialect to query a backend with NetworkX syntax.
90 |
91 | Arguments:
92 | parent (Graph): The parent Graph object
93 |
94 | Returns:
95 | None
96 |
97 | """
98 | self.parent = parent
99 |
100 | def add_node(self, name: Hashable, **kwargs):
101 | return self.parent.backend.add_node(name, kwargs)
102 |
103 | def add_nodes_from(self, nodes_for_adding, **attr):
104 | return self.parent.backend.add_nodes_from(nodes_for_adding, **attr)
105 |
106 | def add_edge(self, u: Hashable, v: Hashable, **kwargs):
107 | return self.parent.backend.add_edge(u, v, kwargs)
108 |
109 | def add_edges_from(self, ebunch_to_add, **attr):
110 | return self.parent.backend.add_edges_from(ebunch_to_add, **attr)
111 |
112 | def remove_node(self, name: Hashable):
113 | if hasattr(self.parent.backend, "remove_node"):
114 | return self.parent.backend.remove_node(name)
115 | raise NotImplementedError
116 |
117 | def remove_edge(self, u: Hashable, v: Hashable):
118 | raise NotImplementedError
119 |
120 | def neighbors(self, u: Hashable) -> Generator:
121 | return self.parent.backend.get_node_neighbors(u)
122 |
123 | def predecessors(self, u: Hashable) -> Generator:
124 | return self.parent.backend.get_node_predecessors(u)
125 |
126 | def successors(self, u: Hashable) -> Generator:
127 | return self.parent.backend.get_node_neighbors(u)
128 |
129 | @property
130 | def _node(self):
131 | return _GrandNodeAtlasView(self)
132 |
133 | @property
134 | def adj(self):
135 | """
136 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
137 | """
138 | return _GrandAdjacencyView(self, "succ")
139 |
140 | @property
141 | def _adj(self):
142 | """
143 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
144 | """
145 | return _GrandAdjacencyView(self, "succ")
146 |
147 | @property
148 | def succ(self):
149 | return _GrandAdjacencyView(self, "succ")
150 |
151 | @property
152 | def _succ(self):
153 | return _GrandAdjacencyView(self, "succ")
154 |
155 | @property
156 | def pred(self):
157 | """
158 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
159 | """
160 | return _GrandAdjacencyView(self, "pred")
161 |
162 | @property
163 | def _pred(self):
164 | """
165 | https://github.com/networkx/networkx/blob/master/networkx/classes/digraph.py#L323
166 | """
167 | return _GrandAdjacencyView(self, "pred")
168 |
169 | @property
170 | def graph(self):
171 | return {}
172 |
173 | def in_degree(self, nbunch=None):
174 | return self.parent.backend.in_degrees(nbunch)
175 |
176 | def out_degree(self, nbunch=None):
177 | return self.parent.backend.out_degrees(nbunch)
178 |
179 | def degree(self, nbunch=None):
180 | if self.parent.backend.is_directed():
181 | # For directed graphs, degree = in_degree + out_degree
182 | if nbunch is None:
183 | # Return DegreeView-like object for all nodes
184 | from networkx.classes.reportviews import DegreeView
185 |
186 | combined_degrees = {}
187 | for node in self.parent.backend.all_nodes_as_iterable():
188 | in_deg = self.parent.backend.in_degree(node)
189 | out_deg = self.parent.backend.out_degree(node)
190 | combined_degrees[node] = in_deg + out_deg
191 | return DegreeView(combined_degrees)
192 | elif hasattr(nbunch, "__iter__") and not isinstance(nbunch, str):
193 | # nbunch is a list/iterable of nodes
194 | from networkx.classes.reportviews import DegreeView
195 |
196 | result = {}
197 | for node in nbunch:
198 | in_deg = self.parent.backend.in_degree(node)
199 | out_deg = self.parent.backend.out_degree(node)
200 | result[node] = in_deg + out_deg
201 | return DegreeView(result)
202 | else:
203 | # nbunch is a single node
204 | in_deg = self.parent.backend.in_degree(nbunch)
205 | out_deg = self.parent.backend.out_degree(nbunch)
206 | return in_deg + out_deg
207 | else:
208 | # For undirected graphs, use the backend's degree method directly
209 | if nbunch is None:
210 | # Return DegreeView for all nodes
211 | from networkx.classes.reportviews import DegreeView
212 |
213 | degrees_dict = self.parent.backend.degrees(nbunch)
214 | return DegreeView(degrees_dict)
215 | elif hasattr(nbunch, "__iter__") and not isinstance(nbunch, str):
216 | # nbunch is a list/iterable of nodes
217 | from networkx.classes.reportviews import DegreeView
218 |
219 | degrees_dict = self.parent.backend.degrees(nbunch)
220 | return DegreeView(degrees_dict)
221 | else:
222 | # nbunch is a single node
223 | return self.parent.backend.degree(nbunch)
224 |
225 | def is_directed(self):
226 | return self.parent.backend.is_directed()
227 |
228 | def __len__(self):
229 | return self.parent.backend.get_node_count()
230 |
231 | def number_of_nodes(self):
232 | return self.parent.backend.get_node_count()
233 |
234 | def number_of_edges(self, u=None, v=None):
235 | if u is None and v is None:
236 | return self.parent.backend.get_edge_count()
237 | # Get the number of edges between u and v. because we don't support
238 | # multigraphs, this is 1 if there is an edge, 0 otherwise.
239 | return 1 if self.parent.backend.has_edge(u, v) else 0
240 |
241 |
242 | class IGraphDialect(nx.Graph):
243 | """
244 | An IGraphDialect provides a python-igraph-like interface
245 |
246 | """
247 |
248 | def __init__(self, parent: "Graph"):
249 | """
250 | Create a new dialect to query a backend with Python-IGraph syntax.
251 |
252 | Arguments:
253 | parent (Graph): The parent Graph object
254 |
255 | Returns:
256 | None
257 |
258 | """
259 | self.parent = parent
260 |
261 | def add_vertices(self, num_verts: int):
262 | old_max = len(self.vs)
263 | for new_v_index in range(num_verts):
264 | self.parent.backend.add_node(new_v_index + old_max, {})
265 |
266 | @property
267 | def vs(self):
268 | return [
269 | i for i in self.parent.backend.all_nodes_as_iterable(include_metadata=True)
270 | ]
271 |
272 | @property
273 | def es(self):
274 | return [
275 | i for i in self.parent.backend.all_edges_as_iterable(include_metadata=True)
276 | ]
277 |
278 | def add_edges(self, edgelist: List[Tuple[Hashable, Hashable]]):
279 | for u, v in edgelist:
280 | self.parent.backend.add_edge(u, v, {})
281 |
282 | def get_edgelist(self):
283 | return self.parent.backend.all_edges_as_iterable(include_metadata=False)
284 |
285 |
286 | class NetworkitDialect:
287 | """
288 | A Networkit-like API for interacting with a Grand graph.
289 |
290 | For more details on the original API, see here:
291 | https://networkit.github.io/dev-docs/python_api/graph.html
292 |
293 | """
294 |
295 | def __init__(self, parent: "Graph") -> None:
296 | self.parent = parent
297 |
298 | def addNode(self):
299 | new_id = self.parent.backend.get_node_count()
300 | self.parent.backend.add_node(new_id, {})
301 | return new_id
302 |
303 | def addEdge(self, u: Hashable, v: Hashable) -> None:
304 | self.parent.backend.add_edge(u, v, {})
305 |
306 | def nodes(self):
307 | return [i for i in self.iterNodes()]
308 |
309 | def iterNodes(self):
310 | return self.parent.backend.all_nodes_as_iterable()
311 |
312 | def edges(self):
313 | return [i for i in self.iterEdges()]
314 |
315 | def iterEdges(self):
316 | return self.parent.backend.all_edges_as_iterable()
317 |
318 | def hasEdge(self, u, v) -> bool:
319 | return self.parent.backend.get_edge_by_id(u, v) is not None
320 |
321 | def addNodes(self, numberOfNewNodes: int) -> int:
322 | for _ in range(numberOfNewNodes):
323 | r = self.addNode()
324 | return r
325 |
326 | def hasNode(self, u) -> bool:
327 | return self.parent.backend.has_node(u)
328 |
329 | def degree(self, v):
330 | return self.parent.backend.degree(v)
331 |
332 | def degreeIn(self, v):
333 | return self.parent.backend.in_degree(v)
334 |
335 | def degreeOut(self, v):
336 | return self.parent.backend.out_degree(v)
337 |
338 | def density(self):
339 | # TODO: implement backend#degree?
340 | E = self.parent.backend.get_edge_count()
341 | V = self.parent.backend.get_node_count()
342 |
343 | if self.parent.backend.is_directed():
344 | return E / (V * (V - 1))
345 | else:
346 | return 2 * E / (V * (V - 1))
347 |
348 | def numberOfNodes(self) -> int:
349 | return self.parent.backend.get_node_count()
350 |
351 | def numberOfEdges(self) -> int:
352 | return self.parent.backend.get_edge_count()
353 |
354 | def removeEdge(self, u, v) -> None:
355 | raise NotImplementedError
356 | return self.parent.backend.remove_edge(u, v)
357 |
358 | def removeNode(self, u: Hashable) -> None:
359 | if hasattr(self.parent.backend, "remove_node"):
360 | return self.parent.backend.remove_node(u)
361 | raise NotImplementedError
362 |
363 | def append(self, G):
364 | raise NotImplementedError
365 |
366 | def copyNodes(self):
367 | raise NotImplementedError
368 |
369 | def BFSEdgesFrom(self, start: Union[int, List[int]]):
370 | raise NotImplementedError
371 |
--------------------------------------------------------------------------------
/grand/dialects/test_dialect.py:
--------------------------------------------------------------------------------
1 | import io
2 | import unittest
3 |
4 | from .. import Graph
5 | from ..backends import NetworkXBackend
6 | from . import (
7 | NetworkXDialect,
8 | IGraphDialect,
9 | NetworkitDialect,
10 | _GrandAdjacencyView,
11 | _GrandNodeAtlasView,
12 | )
13 | import networkx as nx
14 |
15 |
16 | class TestIGraphDialect(unittest.TestCase):
17 | def test_igraph_verts(self):
18 | G = Graph()
19 | self.assertEqual(G.igraph.vs, [])
20 | G.nx.add_node("1")
21 | self.assertEqual(G.igraph.vs, [("1", {})])
22 |
23 | def test_add_verts(self):
24 | G = Graph()
25 | G.igraph.add_vertices(1)
26 | self.assertEqual(G.igraph.vs, [(0, {})])
27 | G.igraph.add_vertices(1)
28 | self.assertEqual(G.igraph.vs, [(0, {}), (1, {})])
29 | G.igraph.add_vertices(10)
30 | self.assertEqual(len(G.igraph.vs), 12)
31 |
32 | def test_igraph_edges(self):
33 | G = Graph()
34 | G.igraph.add_vertices(2)
35 | G.igraph.add_edges([(0, 1)])
36 | self.assertEqual(G.igraph.es, [(0, 1, {})])
37 |
38 |
39 | class TestNetworkXHelpers(unittest.TestCase):
40 | def test_nx_adj_length(self):
41 | G = Graph()
42 | assert len(G.nx.adj) == 0
43 | G.nx.add_node("1")
44 | G.nx.add_edge("1", "2")
45 | assert len(G.nx.adj["1"]) == 1
46 |
47 |
48 | class TestNetworkXDialect(unittest.TestCase):
49 | def test_nx_pred(self):
50 | G = Graph(directed=True)
51 | G.nx.add_edge("1", "2")
52 | G.nx.add_edge("1", "3")
53 | H = nx.DiGraph()
54 | H.add_edge("1", "2")
55 | H.add_edge("1", "3")
56 | self.assertEqual(G.nx.pred, H.pred)
57 |
58 | def test_nx_directed(self):
59 | G = Graph(directed=True)
60 | self.assertTrue(G.nx.is_directed())
61 |
62 | G = Graph(directed=False)
63 | self.assertFalse(G.nx.is_directed())
64 |
65 | def test_in_degree(self):
66 | G = Graph(directed=True)
67 | G.nx.add_edge("1", "2")
68 | G.nx.add_edge("1", "3")
69 | H = nx.DiGraph()
70 | H.add_edge("1", "2")
71 | H.add_edge("1", "3")
72 | self.assertEqual(dict(G.nx.in_degree()), dict(H.in_degree()))
73 |
74 | def test_out_degree(self):
75 | G = Graph(directed=True)
76 | G.nx.add_edge("1", "2")
77 | G.nx.add_edge("1", "3")
78 | H = nx.DiGraph()
79 | H.add_edge("1", "2")
80 | H.add_edge("1", "3")
81 | self.assertEqual(dict(G.nx.out_degree()), dict(H.out_degree()))
82 |
83 | def test_nx_edges(self):
84 | G = Graph(directed=True).nx
85 | H = nx.DiGraph()
86 | G.add_edge("1", "2")
87 | G.add_edge("1", "3")
88 | H.add_edge("1", "2")
89 | H.add_edge("1", "3")
90 | self.assertEqual(dict(G.edges), dict(H.edges))
91 | self.assertEqual(dict(G.edges()), dict(H.edges()))
92 | self.assertEqual(list(G.edges["1", "2"]), list(H.edges["1", "2"]))
93 |
94 | def test_degree_undirected(self):
95 | G1 = Graph(backend=NetworkXBackend(directed=False))
96 | G2 = nx.Graph()
97 |
98 | G1.nx.add_node(0)
99 | G1.nx.add_node(1)
100 | G1.nx.add_edge(0, 1)
101 |
102 | G2.add_node(0)
103 | G2.add_node(1)
104 | G2.add_edge(0, 1)
105 |
106 | assert G1.nx.degree(0) == G2.degree(0), f"{G1.nx.degree(0)} != {G2.degree(0)}"
107 | assert G1.nx.degree(1) == G2.degree(1), f"{G1.nx.degree(1)} != {G2.degree(1)}"
108 |
109 | def test_degree_directed(self):
110 | G1 = Graph(backend=NetworkXBackend(directed=True))
111 | G2 = nx.DiGraph()
112 |
113 | G1.nx.add_node(0)
114 | G1.nx.add_node(1)
115 | G1.nx.add_edge(0, 1)
116 |
117 | G2.add_node(0)
118 | G2.add_node(1)
119 | G2.add_edge(0, 1)
120 |
121 | assert G1.nx.degree(0) == G2.degree(0), f"{G1.nx.degree(0)} != {G2.degree(0)}"
122 | assert G1.nx.degree(1) == G2.degree(1), f"{G1.nx.degree(1)} != {G2.degree(1)}"
123 |
124 | def test_nx_export(self):
125 | gg = Graph()
126 | f = io.BytesIO()
127 | nx.write_graphml(gg.nx, f)
128 |
129 |
130 | class TestNetworkitDialect(unittest.TestCase):
131 | def test_add_verts(self):
132 | G = Graph()
133 | u = G.networkit.addNode()
134 | assert G.networkit.hasNode(u)
135 | assert not G.networkit.hasNode("X")
136 |
137 | def test_add_edges(self):
138 | G = Graph()
139 | u = G.networkit.addNode()
140 | v = G.networkit.addNode()
141 | assert len(G.networkit.edges()) == 0
142 | assert G.networkit.numberOfEdges() == 0
143 | G.networkit.addEdge(u, v)
144 | assert G.networkit.hasEdge(u, v)
145 | assert len(G.networkit.edges()) == 1
146 | assert G.networkit.numberOfEdges() == 1
147 | assert G.networkit.numberOfNodes() == 2
148 |
149 | def test_nodes(self):
150 | G = Graph()
151 | G.networkit.addNode()
152 | G.networkit.addNode()
153 | self.assertEqual(len(G.networkit.nodes()), 2)
154 | assert G.networkit.numberOfNodes() == 2
155 |
156 | def test_undirected_degree(self):
157 | G = Graph(directed=False)
158 | assert G.networkit.numberOfNodes() == 0
159 | assert G.networkit.numberOfEdges() == 0
160 | u = G.networkit.addNode()
161 | v = G.networkit.addNode()
162 | assert G.networkit.numberOfNodes() == 2
163 | assert G.networkit.numberOfEdges() == 0
164 | assert G.networkit.degree(u) == 0
165 | assert G.networkit.degree(v) == 0
166 | G.networkit.addEdge(u, v)
167 | assert G.networkit.degree(u) == 1
168 | assert G.networkit.degree(v) == 1
169 | assert G.networkit.numberOfEdges() == 1
170 |
171 | def test_directed_degree(self):
172 | G = Graph(directed=True)
173 | assert G.networkit.numberOfNodes() == 0
174 | assert G.networkit.numberOfEdges() == 0
175 | u = G.networkit.addNode()
176 | v = G.networkit.addNode()
177 | assert G.networkit.numberOfNodes() == 2
178 | assert G.networkit.numberOfEdges() == 0
179 | assert G.networkit.degreeIn(u) == 0
180 | assert G.networkit.degreeOut(u) == 0
181 | assert G.networkit.degreeIn(v) == 0
182 | assert G.networkit.degreeOut(v) == 0
183 | G.networkit.addEdge(u, v)
184 | assert G.networkit.degreeIn(u) == 0
185 | assert G.networkit.degreeOut(u) == 1
186 | assert G.networkit.degreeIn(v) == 1
187 | assert G.networkit.degreeOut(v) == 0
188 | # Test total degree for directed graphs
189 | assert G.networkit.degree(u) == 1 # in_degree + out_degree = 0 + 1 = 1
190 | assert G.networkit.degree(v) == 1 # in_degree + out_degree = 1 + 0 = 1
191 | assert G.networkit.numberOfEdges() == 1
192 |
193 | def test_undirected_density(self):
194 | G = Graph(directed=False)
195 | u = G.networkit.addNode()
196 | v = G.networkit.addNode()
197 | G.networkit.addEdge(u, v)
198 | assert G.networkit.density() == 1
199 |
200 | def test_directed_density(self):
201 | G = Graph(directed=True)
202 | u = G.networkit.addNode()
203 | v = G.networkit.addNode()
204 | G.networkit.addEdge(u, v)
205 | assert G.networkit.density() == 0.5
206 | G.networkit.addEdge(v, u)
207 | assert G.networkit.density() == 1
208 |
--------------------------------------------------------------------------------
/grand/test_graph.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from . import Graph, DiGraph
4 |
5 |
6 | class TestGraph(unittest.TestCase):
7 | def test_can_create(self):
8 | Graph()
9 |
10 | def test_can_use_nx_backend(self):
11 | Graph().nx
12 |
13 | def test_can_create_directed(self):
14 | assert Graph(directed=True).nx.is_directed() is True
15 | assert Graph(directed=False).nx.is_directed() is False
16 | assert DiGraph().nx.is_directed() is True
17 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "grand-graph"
3 | version = "0.7.0"
4 | description = "Graph database wrapper for non-graph datastores"
5 | authors = [{ name = "Jordan Matelsky", email = "opensource@matelsky.com" }]
6 | requires-python = ">=3.10"
7 | dependencies = [
8 | "cachetools>=5.5.2",
9 | "networkx>=2.4",
10 | "numpy>=1.26.4",
11 | "pandas>=2.2.3",
12 | ]
13 |
14 | [build-system]
15 | requires = ["hatchling"]
16 | build-backend = "hatchling.build"
17 |
18 | [tool.hatch.version]
19 | path = "grand/__init__.py"
20 |
21 | [tool.hatch.build.targets.sdist]
22 | include = ["/grand"]
23 |
24 | [tool.hatch.build.targets.wheel]
25 | packages = ["grand"]
26 |
27 | [tool.uv]
28 | dev-dependencies = [
29 | "pytest>=8.3.5",
30 | "ruff>=0.11.5",
31 | "pytest-codspeed>=3.2.0",
32 | "pytest-cov>=6.1.1",
33 | ]
34 |
35 | [project.optional-dependencies]
36 | dynamodb = ["boto3"]
37 | igraph = ["igraph"]
38 | networkit = ["cmake", "cython", "networkit", "numpy<2.0.0"]
39 | sql = ["SQLAlchemy>=1.3"]
40 |
--------------------------------------------------------------------------------