smallgraphlib 0.6.2__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/PKG-INFO +2 -1
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/pyproject.toml +9 -3
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/core.py +2 -0
- smallgraphlib-0.7.1/smallgraphlib/huffman.py +150 -0
- smallgraphlib-0.7.1/smallgraphlib/latex_export.py +108 -0
- smallgraphlib-0.7.1/smallgraphlib/py.typed +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/tikz_export.py +2 -2
- smallgraphlib-0.6.2/setup.py +0 -26
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/README.md +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/__init__.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/automatons.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/basic_graphs.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/custom_types.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/graphs_constructors.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/labeled_graphs.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/string2automaton.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/trees.py +0 -0
- {smallgraphlib-0.6.2 → smallgraphlib-0.7.1}/smallgraphlib/utilities.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: smallgraphlib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Simple library for handling small graphs, including Tikz code generation.
|
|
5
5
|
Home-page: https://github.com/wxgeo/smallgraphlib
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -12,6 +12,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Project-URL: Repository, https://github.com/wxgeo/smallgraphlib
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "smallgraphlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.1"
|
|
4
4
|
description = "Simple library for handling small graphs, including Tikz code generation."
|
|
5
5
|
authors = ["Nicolas Pourcelot <nicolas.pourcelot@gmail.com>"]
|
|
6
6
|
repository = "https://github.com/wxgeo/smallgraphlib"
|
|
@@ -11,7 +11,7 @@ keywords = ["graph", "tikz", "latex"]
|
|
|
11
11
|
[tool.poetry.dependencies]
|
|
12
12
|
python = "^3.10"
|
|
13
13
|
|
|
14
|
-
[tool.poetry.dev
|
|
14
|
+
[tool.poetry.group.dev.dependencies]
|
|
15
15
|
pytest = "^7"
|
|
16
16
|
mypy = "^1.0"
|
|
17
17
|
flake8 = "^6"
|
|
@@ -22,13 +22,19 @@ sphinx-rtd-theme = "^1.0.0"#
|
|
|
22
22
|
myst-parser = "^0.18.0"
|
|
23
23
|
# To test compatibility with sympy.
|
|
24
24
|
sympy = "^1.10.1"
|
|
25
|
+
# Version 7.29+ are buggy !
|
|
26
|
+
python-semantic-release = "7.28.1"
|
|
25
27
|
|
|
26
28
|
[build-system]
|
|
27
29
|
requires = ["poetry-core>=1.0.0"]
|
|
28
30
|
build-backend = "poetry.core.masonry.api"
|
|
29
31
|
|
|
32
|
+
[tool.semantic_release]
|
|
33
|
+
version_variable = "pyproject.toml:version"
|
|
34
|
+
|
|
30
35
|
[tool.mypy]
|
|
31
36
|
implicit_optional = true
|
|
37
|
+
warn_unused_ignores = true
|
|
32
38
|
|
|
33
39
|
[tool.black]
|
|
34
40
|
line-length = 110
|
|
@@ -46,5 +52,5 @@ commands =
|
|
|
46
52
|
poetry run black smallgraphlib tests
|
|
47
53
|
poetry run pytest tests
|
|
48
54
|
poetry run mypy smallgraphlib tests
|
|
49
|
-
poetry run flake8 --max-line-length 110 --ignore=E203,W503,W391
|
|
55
|
+
poetry run flake8 --max-line-length 110 --ignore=E203,W503,W391 --per-file-ignores="tests/test_latex_export.py:E501"
|
|
50
56
|
"""
|
|
@@ -231,6 +231,8 @@ class AbstractGraph(ABC, Generic[Node]):
|
|
|
231
231
|
>>> g.is_isomorphic_to(g2)
|
|
232
232
|
True
|
|
233
233
|
"""
|
|
234
|
+
# Sort nodes before shuffling, to make shuffling deterministic with a given random.seed.
|
|
235
|
+
# nodes = sorted(self.nodes)
|
|
234
236
|
nodes = list(self.nodes)
|
|
235
237
|
random.shuffle(nodes)
|
|
236
238
|
self.rename_nodes(dict((old_name, new_name) for old_name, new_name in zip(self.nodes, nodes)))
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from operator import attrgetter
|
|
3
|
+
|
|
4
|
+
from smallgraphlib.custom_types import Node
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Tree:
|
|
8
|
+
branches: tuple["Tree", ...]
|
|
9
|
+
|
|
10
|
+
def __init__(self, root: Node, *branches: "Tree") -> None:
|
|
11
|
+
self.root = root
|
|
12
|
+
self.branches = branches
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HuffmanTree(Tree):
|
|
16
|
+
branches: tuple["HuffmanTree", ...]
|
|
17
|
+
|
|
18
|
+
def __init__(self, *branches: "HuffmanTree", char: str = None, weight: int = None) -> None:
|
|
19
|
+
if len(branches) == 2:
|
|
20
|
+
if char is not None or weight is not None:
|
|
21
|
+
raise ValueError("Char and weight can't be set, except for leaves.")
|
|
22
|
+
elif len(branches) == 0:
|
|
23
|
+
if char is None or weight is None:
|
|
24
|
+
raise ValueError("Char and weight must be set for leaves.")
|
|
25
|
+
else:
|
|
26
|
+
raise ValueError(f"There must be either 0 or 2 branches, not {len(branches)}.")
|
|
27
|
+
char = min(branch.char for branch in branches) if branches else char
|
|
28
|
+
weight = sum(branch.weight for branch in branches) if branches else weight
|
|
29
|
+
super().__init__((weight, char), *branches)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_text(cls, text: str) -> "HuffmanTree":
|
|
33
|
+
trees = {
|
|
34
|
+
HuffmanTree(char=letter, weight=occurrences) for letter, occurrences in Counter(text).items()
|
|
35
|
+
}
|
|
36
|
+
sort_func = attrgetter("root")
|
|
37
|
+
while len(trees) >= 2:
|
|
38
|
+
# Select the two smallest values
|
|
39
|
+
tree1 = min(trees, key=sort_func)
|
|
40
|
+
trees.remove(tree1)
|
|
41
|
+
tree2 = min(trees, key=sort_func)
|
|
42
|
+
trees.remove(tree2)
|
|
43
|
+
trees.add(HuffmanTree(tree1, tree2))
|
|
44
|
+
assert len(trees) == 1
|
|
45
|
+
return trees.pop()
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def weight(self):
|
|
49
|
+
return self.root[0]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def char(self):
|
|
53
|
+
return self.root[1]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_leaf(self) -> bool:
|
|
57
|
+
return len(self.branches) == 0
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def left_branch(self) -> "HuffmanTree":
|
|
61
|
+
return self.branches[0]
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def right_branch(self) -> "HuffmanTree":
|
|
65
|
+
return self.branches[1]
|
|
66
|
+
|
|
67
|
+
def compress(self, text: str) -> bytes:
|
|
68
|
+
compressed = []
|
|
69
|
+
buffer = ""
|
|
70
|
+
for char in text:
|
|
71
|
+
buffer += self.labels[char]
|
|
72
|
+
while len(buffer) >= 8:
|
|
73
|
+
compressed.append(sum(2**i * int(c) for i, c in enumerate(buffer[:8])))
|
|
74
|
+
buffer = buffer[8:]
|
|
75
|
+
# On finit de vider le buffer.
|
|
76
|
+
# Cela revient à ajouter des bits nuls, puisque le nombre de bits final doit
|
|
77
|
+
# être un multiple de 8 (on ne peut renvoyer que des octets complets).
|
|
78
|
+
# Si on voulait créer un logiciel de compression réellement utilisable, il faudrait donc
|
|
79
|
+
# soit avoir un caractère de fin de chaîne, soit préciser le nombre de bits finaux
|
|
80
|
+
# inutiles (entre 0 et 7).
|
|
81
|
+
# On pourrait par exemple décider que par convention les 3 premiers bits servent à encoder le
|
|
82
|
+
# nombre de bits finaux inutiles.
|
|
83
|
+
# Accessoirement, il faudrait aussi que la chaîne de caractères intègre le dictionnaire de compression
|
|
84
|
+
# en début de chaîne, et donc se mettre d'accord sur un format, etc.
|
|
85
|
+
if buffer:
|
|
86
|
+
compressed.append(sum(2**i * int(c) for i, c in enumerate(buffer[:8])))
|
|
87
|
+
return bytes(compressed)
|
|
88
|
+
|
|
89
|
+
def uncompress(self, bits: bytes) -> str:
|
|
90
|
+
position = self
|
|
91
|
+
read: list[str] = []
|
|
92
|
+
for byte in bits:
|
|
93
|
+
for i in range(8):
|
|
94
|
+
digit, byte = byte % 2, byte // 2
|
|
95
|
+
position = position.branches[digit]
|
|
96
|
+
if position.is_leaf:
|
|
97
|
+
read.append(position.char)
|
|
98
|
+
position = self
|
|
99
|
+
return "".join(read)
|
|
100
|
+
|
|
101
|
+
def decode(self, bits: str) -> str:
|
|
102
|
+
position = self
|
|
103
|
+
read: list[str] = []
|
|
104
|
+
for digit in bits:
|
|
105
|
+
position = position.branches[int(digit)]
|
|
106
|
+
if position.is_leaf:
|
|
107
|
+
read.append(position.char)
|
|
108
|
+
position = self
|
|
109
|
+
return "".join(read)
|
|
110
|
+
|
|
111
|
+
def encode(self, text: str) -> str:
|
|
112
|
+
return "".join(self.labels[char] for char in text)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def labels(self) -> dict[str, str]:
|
|
116
|
+
"""Binary labels."""
|
|
117
|
+
if self.is_leaf:
|
|
118
|
+
return {self.char: ""}
|
|
119
|
+
return {
|
|
120
|
+
key: str(i) + value
|
|
121
|
+
for i, branch in enumerate(self.branches)
|
|
122
|
+
for key, value in branch.labels.items()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def __repr__(self):
|
|
126
|
+
if self.is_leaf:
|
|
127
|
+
return f"HuffmanTree(char={self.char}, weight={self.weight})"
|
|
128
|
+
return f"HuffmanTree({self.left_branch!r}, {self.right_branch!r})"
|
|
129
|
+
|
|
130
|
+
def __str__(self):
|
|
131
|
+
lines = []
|
|
132
|
+
shift = len(str(self.weight)) + 1
|
|
133
|
+
for n, line in enumerate(str(self.left_branch).split("\n")):
|
|
134
|
+
if n == 0:
|
|
135
|
+
# lines.append("\u252C\u2500\u2500" + line)
|
|
136
|
+
lines.append(f"{self.weight}\u2500\u2500" + line)
|
|
137
|
+
else:
|
|
138
|
+
lines.append("\u2502" + shift * " " + line)
|
|
139
|
+
for n, line in enumerate(str(self.right_branch).split("\n")):
|
|
140
|
+
if n == 0:
|
|
141
|
+
lines.append("\u2514" + shift * "\u2500" + line)
|
|
142
|
+
else:
|
|
143
|
+
lines.append((shift + 1) * " " + line)
|
|
144
|
+
return "\n".join(lines)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def encode(text: str) -> str:
|
|
148
|
+
"""Return a string of `0` and `1` representing the bits of a text encoded using Huffman algorithm."""
|
|
149
|
+
tree = HuffmanTree.from_text(text)
|
|
150
|
+
return tree.encode(text)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
4
|
+
from smallgraphlib.core import AbstractGraph
|
|
5
|
+
from smallgraphlib.custom_types import Node
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def latex_Dijkstra(graph: AbstractGraph[Node], start: Node, end: Node = None) -> str:
|
|
9
|
+
"""Generate the LaTeX code of a table corresponding to Dijkstra algorithm's steps.
|
|
10
|
+
|
|
11
|
+
If `end` is `None`, only stop when the shorter path to all other nodes have been found.
|
|
12
|
+
"""
|
|
13
|
+
nodes = graph.nodes
|
|
14
|
+
num_cols = len(nodes) + 2
|
|
15
|
+
lines: list[str] = [
|
|
16
|
+
rf"\begin{{tabular}}{{|*{num_cols}{{c |}}}}\cline{{2-{num_cols}}}",
|
|
17
|
+
r"\multicolumn{1}{c|}{} & "
|
|
18
|
+
+ " & ".join(sorted(f"${node}$" for node in nodes))
|
|
19
|
+
+ r" & Selected\\\hline",
|
|
20
|
+
]
|
|
21
|
+
# Nodes which have been already visited, but still not archived:
|
|
22
|
+
# visited: dict[Node, tuple[float, list[Node]]] = {start: (0, [start])}
|
|
23
|
+
# format: {node: [distance from start, [previous node, alternative previous node, ...]]}
|
|
24
|
+
|
|
25
|
+
previous_nodes: dict[Node, set[Node]] = {node: set() for node in graph.nodes}
|
|
26
|
+
distance_from_start: dict[Node, float] = {node: math.inf for node in graph.nodes}
|
|
27
|
+
being_processed: set[Node] = {start}
|
|
28
|
+
completed: set[Node] = set()
|
|
29
|
+
distance_from_start[start] = 0
|
|
30
|
+
|
|
31
|
+
# Nodes which will not change anymore (shorter path from start has been found):
|
|
32
|
+
# archived: dict[Node, tuple[float, list[Node]]] = {}
|
|
33
|
+
|
|
34
|
+
def cell_content(node: Node) -> str:
|
|
35
|
+
"""Used to print the node in the table."""
|
|
36
|
+
dist = distance_from_start[node]
|
|
37
|
+
previous = previous_nodes[node]
|
|
38
|
+
if dist == math.inf:
|
|
39
|
+
return r"$+\infty$"
|
|
40
|
+
if node in completed:
|
|
41
|
+
return r"\cellcolor{lightgray}"
|
|
42
|
+
printing = str(dist)
|
|
43
|
+
if node != start:
|
|
44
|
+
printing += f" $({','.join(sorted(str(node) for node in previous))})$"
|
|
45
|
+
if node == current:
|
|
46
|
+
printing = rf"\cellcolor{{blue!20}}\textbf{{{printing}}}"
|
|
47
|
+
return printing
|
|
48
|
+
|
|
49
|
+
current: Node
|
|
50
|
+
first_cell = r"\text{start}"
|
|
51
|
+
while being_processed and end != (
|
|
52
|
+
current := min(being_processed, key=(lambda n: distance_from_start[n]))
|
|
53
|
+
):
|
|
54
|
+
lines.append(
|
|
55
|
+
f"${first_cell}$ & "
|
|
56
|
+
+ " & ".join(cell_content(node) for node in nodes)
|
|
57
|
+
+ rf" & {current} {cell_content(current)}\\\hline"
|
|
58
|
+
)
|
|
59
|
+
first_cell = str(current)
|
|
60
|
+
being_processed.remove(current)
|
|
61
|
+
completed.add(current)
|
|
62
|
+
# We update the distances
|
|
63
|
+
for neighbor in graph.successors(current):
|
|
64
|
+
if neighbor not in completed:
|
|
65
|
+
being_processed.add(neighbor)
|
|
66
|
+
# best distance found until now between neighbor and start
|
|
67
|
+
current_distance = distance_from_start[neighbor]
|
|
68
|
+
# new distance found using current node:
|
|
69
|
+
new_distance = distance_from_start[current] + graph.weight(current, neighbor)
|
|
70
|
+
# replace with new distance only if better
|
|
71
|
+
if new_distance < current_distance:
|
|
72
|
+
distance_from_start[neighbor] = new_distance
|
|
73
|
+
previous_nodes[neighbor] = {current}
|
|
74
|
+
elif new_distance == current_distance:
|
|
75
|
+
previous_nodes[neighbor].add(current)
|
|
76
|
+
|
|
77
|
+
lines.append("\\end{tabular}")
|
|
78
|
+
lines.append("")
|
|
79
|
+
|
|
80
|
+
def shortest_paths() -> str:
|
|
81
|
+
paths_in_construction = [[target]]
|
|
82
|
+
completed_paths = []
|
|
83
|
+
while paths_in_construction:
|
|
84
|
+
partial_path = paths_in_construction.pop()
|
|
85
|
+
assert len(partial_path) > 0
|
|
86
|
+
for previous in previous_nodes[partial_path[0]]:
|
|
87
|
+
if previous == start:
|
|
88
|
+
completed_paths.append([previous] + partial_path)
|
|
89
|
+
else:
|
|
90
|
+
paths_in_construction.append([previous] + partial_path)
|
|
91
|
+
|
|
92
|
+
def path_to_str(path: Iterable[Node]):
|
|
93
|
+
return "-".join(str(node) for node in path)
|
|
94
|
+
|
|
95
|
+
return ",".join(path_to_str(path) for path in completed_paths)
|
|
96
|
+
|
|
97
|
+
targets = list(nodes) if end is None else [end]
|
|
98
|
+
|
|
99
|
+
for target in targets:
|
|
100
|
+
if target != start:
|
|
101
|
+
distance = distance_from_start[target]
|
|
102
|
+
lines.append(
|
|
103
|
+
f"Shorter(s) path(s) from ${start}$ to ${target}$: "
|
|
104
|
+
f"${shortest_paths()}$ (length: {distance})."
|
|
105
|
+
)
|
|
106
|
+
lines.append("")
|
|
107
|
+
|
|
108
|
+
return "\n" + "\n".join(lines)
|
|
File without changes
|
|
@@ -212,7 +212,7 @@ class TikzPrinter(Generic[Node]):
|
|
|
212
212
|
r"\usepackage{tikz}",
|
|
213
213
|
r"\usetikzlibrary{arrows.meta}",
|
|
214
214
|
r"\usepackage[outline]{contour}",
|
|
215
|
-
r"\contourlength{0.
|
|
215
|
+
r"\contourlength{0.15em}",
|
|
216
216
|
]
|
|
217
217
|
|
|
218
218
|
def tikz_code(self, graph, *, shuffle_nodes=False, options="") -> str:
|
|
@@ -226,7 +226,7 @@ class TikzPrinter(Generic[Node]):
|
|
|
226
226
|
For labeled graphs, it is recommended to load `contour` package too::
|
|
227
227
|
|
|
228
228
|
\usepackage[outline]{contour}
|
|
229
|
-
\contourlength{0.
|
|
229
|
+
\contourlength{0.15em}
|
|
230
230
|
|
|
231
231
|
"""
|
|
232
232
|
self.graph = graph
|
smallgraphlib-0.6.2/setup.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
from setuptools import setup
|
|
3
|
-
|
|
4
|
-
packages = \
|
|
5
|
-
['smallgraphlib']
|
|
6
|
-
|
|
7
|
-
package_data = \
|
|
8
|
-
{'': ['*']}
|
|
9
|
-
|
|
10
|
-
setup_kwargs = {
|
|
11
|
-
'name': 'smallgraphlib',
|
|
12
|
-
'version': '0.6.2',
|
|
13
|
-
'description': 'Simple library for handling small graphs, including Tikz code generation.',
|
|
14
|
-
'long_description': '# Small Graph Lib\n\n## Installing\n\n $ git clone https://github.com/wxgeo/smallgraphlib\n\n $ pip install --user smallgraphlib\n\n## Usage\n\nMain classes are `Graph`, `DirectedGraph`, `WeightedGraph` and `WeightedDirectedGraph`:\n\n >>> from smallgraphlib import DirectedGraph\n >>> g = DirectedGraph(["A", "B", "C"], ("A", "B"), ("B", "A"), ("B", "C"))\n >>> g.is_simple\n True\n >>> g.is_complete\n False\n >>> g.is_directed\n True\n >>> g.adjacency_matrix\n [[0, 1, 0], [1, 0, 1], [0, 0, 0]]\n >>> g.degree\n 3\n >>> g.order\n 3\n >>> g.is_eulerian\n False\n >>> g.is_semi_eulerian\n True\n\nSpecial graphs may be generated using factory functions:\n \n >>> from smallgraphlib import complete_graph, complete_bipartite_graph\n >>> K5 = complete_graph(5)\n >>> len(K5.greedy_coloring)\n 5\n >>> K33 = complete_bipartite_graph(3, 3)\n >>> K33.degree\n 6\n >>> K33.diameter\n 2\n \nIf the graph is not too complex, Tikz code may be generated:\n\n >>> g.as_tikz()\n ...\n\n## Development\n\n1. Get last version:\n \n $ git clone https://github.com/wxgeo/smallgraphlib\n\n2. Install Poetry.\n \n Poetry is a tool for dependency management and packaging in Python.\n\n Installation instructions are here:\n https://python-poetry.org/docs/#installation\n\n3. Install developments tools:\n \n $ poetry install\n\n4. Optionally, update development tools:\n \n $ poetry update\n\n5. Optionally, install library in editable mode:\n\n $ pip install -e smallgraphlib\n\n6. Make changes, add tests.\n \n7. Launch tests:\n\n $ tox\n\n8. Everything\'s OK ? Commit. :)',
|
|
15
|
-
'author': 'Nicolas Pourcelot',
|
|
16
|
-
'author_email': 'nicolas.pourcelot@gmail.com',
|
|
17
|
-
'maintainer': 'None',
|
|
18
|
-
'maintainer_email': 'None',
|
|
19
|
-
'url': 'https://github.com/wxgeo/smallgraphlib',
|
|
20
|
-
'packages': packages,
|
|
21
|
-
'package_data': package_data,
|
|
22
|
-
'python_requires': '>=3.10,<4.0',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
setup(**setup_kwargs)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|