machinevision-toolbox-python 2.0.0__tar.gz → 2.0.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.
Files changed (114) hide show
  1. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/PKG-INFO +1 -1
  2. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/pyproject.toml +1 -1
  3. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageBlobs.py +199 -25
  4. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/Sources.py +19 -3
  5. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/.gitignore +0 -0
  6. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/CHANGELOG.md +0 -0
  7. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/CODEAUTOLINK_FORK_PLAN.md +0 -0
  8. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/LICENSE +0 -0
  9. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/MIGRATION.md +0 -0
  10. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/Makefile +0 -0
  11. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/NOTES +0 -0
  12. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/OPENCV_FUNCTIONS-original.md +0 -0
  13. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/OPENCV_FUNCTIONS.md +0 -0
  14. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/README.md +0 -0
  15. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/audit_typing.py +0 -0
  16. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/bags/RenderOption_2026-03-29-12-09-23.json +0 -0
  17. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/deprecation_warnings.csv +0 -0
  18. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/README.md +0 -0
  19. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/blobs1.py +0 -0
  20. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/blobs2.py +0 -0
  21. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/bunny.py +0 -0
  22. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/bus.jpg +0 -0
  23. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/color.py +0 -0
  24. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/corners.py +0 -0
  25. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/matching.py +0 -0
  26. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/monalisa.py +0 -0
  27. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/morphdemo.py +0 -0
  28. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/randomdots.py +0 -0
  29. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/ransac_line.py +0 -0
  30. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/ros_stream.py +0 -0
  31. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/showzip.py +0 -0
  32. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/street_scene.jpg +0 -0
  33. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/torch-yolo.py +0 -0
  34. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/torch_seg.py +0 -0
  35. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/examples/yolo26n.pt +0 -0
  36. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/findimages.py +0 -0
  37. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/flowers.jpg +0 -0
  38. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/fmtparser.py +0 -0
  39. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/ilabel.c +0 -0
  40. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/ilabel.py-unopt +0 -0
  41. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/inspect_bag.py +0 -0
  42. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/machinevision-toolbox-python.pdf +0 -0
  43. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/machinevision-toolbox-python.svg +0 -0
  44. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/machinevisiontoolbox.pdf +0 -0
  45. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/phone.py +0 -0
  46. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/plot_call_inventory.md +0 -0
  47. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/readbag.py +0 -0
  48. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/BagOfWords.py +0 -0
  49. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/BundleAdjust.py +0 -0
  50. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/Camera.py +0 -0
  51. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageColor.py +0 -0
  52. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageConstants.py +0 -0
  53. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageCore.docstring +0 -0
  54. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageCore.py +0 -0
  55. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageFiducials.py +0 -0
  56. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageIO.py +0 -0
  57. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageLineFeatures.py +0 -0
  58. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageMorph.py +0 -0
  59. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageMultiview.py +0 -0
  60. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImagePointFeatures.py +0 -0
  61. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageProcessing.py +0 -0
  62. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageRegionFeatures.py +0 -0
  63. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageReshape.py +0 -0
  64. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageSpatial.py +0 -0
  65. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageTensor.py +0 -0
  66. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/ImageWholeFeatures.py +0 -0
  67. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/Kernel.py +0 -0
  68. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/PointCloud.py +0 -0
  69. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/VisualServo.py +0 -0
  70. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/__init__.py +0 -0
  71. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/_image_typing.py +0 -0
  72. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/aruco0.pdf +0 -0
  73. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/aruco50.pdf +0 -0
  74. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/__init__.py +0 -0
  75. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/color.py +0 -0
  76. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/data.py +0 -0
  77. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/findpeaks.py +0 -0
  78. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/graphics.py +0 -0
  79. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/imageio.py +0 -0
  80. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/meshgrid.py +0 -0
  81. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/shapes.py +0 -0
  82. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/types.py +0 -0
  83. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/base/version.py +0 -0
  84. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/bin/_bintools.py +0 -0
  85. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/bin/bagtool.py +0 -0
  86. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/bin/imtool.py +0 -0
  87. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/bin/labeltool.py +0 -0
  88. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/bin/mvtbtool.py +0 -0
  89. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/bin/ocrtool.py +0 -0
  90. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/bin/tagtool.py +0 -0
  91. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/Icons/camera.png +0 -0
  92. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/Icons/estpose_p.png +0 -0
  93. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/Icons/icons.sh +0 -0
  94. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/Icons/imageplane.png +0 -0
  95. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/Icons/visjac_p.png +0 -0
  96. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/__init__.py +0 -0
  97. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/camera.py +0 -0
  98. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/blocks/test_blocks.py +0 -0
  99. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/camera_derivatives.py +0 -0
  100. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/cvfuncs.py +0 -0
  101. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/decorators.py +0 -0
  102. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/docbugs.py +0 -0
  103. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/fiducial.py +0 -0
  104. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/mvtb_types.py +0 -0
  105. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/newcameras.py +0 -0
  106. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/test_skimage.py +0 -0
  107. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/testblobplots.py +0 -0
  108. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/src/machinevisiontoolbox/x.json +0 -0
  109. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/ss +0 -0
  110. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/sunday.py +0 -0
  111. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/symbolic/README.md +0 -0
  112. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/symbolic/bundle-adjust.py +0 -0
  113. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/x.json +0 -0
  114. {machinevision_toolbox_python-2.0.0 → machinevision_toolbox_python-2.0.1}/xx.mp4 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machinevision-toolbox-python
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Object-oriented machine vision for Python.
5
5
  Project-URL: Homepage, https://github.com/petercorke/machinevision-toolbox-python
6
6
  Project-URL: Bug Tracker, https://github.com/pypa/sampleproject/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "machinevision-toolbox-python"
3
- version = "2.0.0"
3
+ version = "2.0.1"
4
4
  authors = [{ name = "Peter Corke", email = "rvc@petercorke.com" }]
5
5
  description = "Object-oriented machine vision for Python."
6
6
  readme = "README.md"
@@ -3,6 +3,8 @@ Detection and description of 2D blob (connected region) features in images.
3
3
  """
4
4
 
5
5
  import copy
6
+ import io
7
+ import json
6
8
  import subprocess
7
9
  import sys
8
10
  import tempfile
@@ -1194,7 +1196,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
1194
1196
  >>> blobs[2].level
1195
1197
  >>> blobs.level
1196
1198
 
1197
- :seealso: :meth:`color` :meth:`parent` :meth:`children` :meth:`dotfile`
1199
+ :seealso: :meth:`color` :meth:`parent` :meth:`children` :meth:`dotfile` :meth:`graph`
1198
1200
  """
1199
1201
  return [b.level for b in self.data]
1200
1202
 
@@ -1247,7 +1249,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
1247
1249
 
1248
1250
  A parent of -1 is the image background.
1249
1251
 
1250
- :seealso: :meth:`id` :meth:`children` :meth:`level` :meth:`dotfile`
1252
+ :seealso: :meth:`id` :meth:`children` :meth:`level` :meth:`dotfile` :meth:`graph`
1251
1253
  """
1252
1254
  return [b.parent.id if b.parent is not None else -1 for b in self.data]
1253
1255
 
@@ -1272,7 +1274,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
1272
1274
  >>> blobs[6].id
1273
1275
 
1274
1276
 
1275
- :seealso: :meth:`parent` :meth:`children` :meth:`level` :meth:`dotfile`
1277
+ :seealso: :meth:`parent` :meth:`children` :meth:`level` :meth:`dotfile` :meth:`graph`
1276
1278
  """
1277
1279
  return [b.id for b in self.data]
1278
1280
 
@@ -1293,7 +1295,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
1293
1295
  >>> blobs = im.blobs()
1294
1296
  >>> blobs[5].children
1295
1297
 
1296
- :seealso: :meth:`parent` :meth:`level` :meth:`dotfile`
1298
+ :seealso: :meth:`parent` :meth:`level` :meth:`dotfile` :meth:`graph`
1297
1299
  """
1298
1300
  return [[c.id for c in b.children] for b in self.data]
1299
1301
 
@@ -2380,6 +2382,180 @@ class Blobs(UserList): # lgtm[py/missing-equals]
2380
2382
 
2381
2383
  return image.__class__(labels)
2382
2384
 
2385
+ def graph(
2386
+ self,
2387
+ format: str = "dot",
2388
+ filename: str | io.TextIOBase | None = None,
2389
+ direction: str | None = None,
2390
+ ) -> str:
2391
+ """
2392
+ Render blob hierarchy graph text.
2393
+
2394
+ :param format: graph output format: ``"dot"``, ``"mermaid"``,
2395
+ ``"mermaid_fenced"``, ``"graphml"`` or ``"elk"``
2396
+ :type format: str, optional
2397
+ :param filename: destination path or open text stream, defaults to None
2398
+ :type filename: str | file handle | None, optional
2399
+ :param direction: graph direction for DOT output, defaults to None
2400
+ :type direction: str, optional
2401
+ :return: rendered graph text
2402
+ :rtype: str
2403
+
2404
+ Creates graph text representing the blob hierarchy. If ``filename`` is
2405
+ provided, the rendered text is also written to it.
2406
+
2407
+ Mermaid outputs:
2408
+
2409
+ - ``"mermaid"`` returns plain Mermaid text.
2410
+ - ``"mermaid_fenced"`` returns Markdown fenced text suitable for
2411
+ direct display via ``IPython.display.Markdown``.
2412
+
2413
+ The blob hierarchy is represented as a directed graph where each node is
2414
+ a blob id and each edge points from parent blob to child blob.
2415
+
2416
+ :seealso: :meth:`dotfile` :meth:`parent` :meth:`children` :meth:`level`
2417
+ """
2418
+
2419
+ graph_format = format.lower().replace("-", "_")
2420
+
2421
+ def _parent_id(blob: Any) -> int:
2422
+ parent = blob.parent
2423
+ if parent is None:
2424
+ return -1
2425
+ if isinstance(parent, int):
2426
+ return parent
2427
+ return parent.id
2428
+
2429
+ hierarchy_edges = [(_parent_id(blob), blob.id) for blob in self]
2430
+ node_ids_set = {blob.id for blob in self}
2431
+ if any(parent_id == -1 for parent_id, _ in hierarchy_edges):
2432
+ node_ids_set.add(-1)
2433
+
2434
+ if graph_format == "dot":
2435
+ lines = ["digraph {"]
2436
+ if direction is not None:
2437
+ lines.append(f"rankdir = {direction}")
2438
+
2439
+ for node_id in sorted(node_ids_set):
2440
+ lines.append(f' "{node_id}"')
2441
+
2442
+ for parent_id, child_id in hierarchy_edges:
2443
+ lines.append(f' "{parent_id}" -> "{child_id}"')
2444
+
2445
+ lines.append("}")
2446
+ text = "\n".join(lines) + "\n"
2447
+
2448
+ elif graph_format in ("mermaid", "mermaid_fenced"):
2449
+ lines = ["flowchart TB"]
2450
+ node_ids: dict[Any, str] = {}
2451
+ sorted_nodes = sorted(node_ids_set)
2452
+
2453
+ for i, node in enumerate(sorted_nodes):
2454
+ node_alias = f"b{i}"
2455
+ node_ids[node] = node_alias
2456
+ lines.append(f' {node_alias}["{node}"]')
2457
+
2458
+ for parent_id, child_id in hierarchy_edges:
2459
+ lines.append(f" {node_ids[parent_id]} --> {node_ids[child_id]}")
2460
+
2461
+ mermaid_text = "\n".join(lines) + "\n"
2462
+ if graph_format == "mermaid_fenced":
2463
+ text = f"```mermaid\n{mermaid_text}```\n"
2464
+ else:
2465
+ text = mermaid_text
2466
+
2467
+ elif graph_format == "graphml":
2468
+
2469
+ def _xml_escape(value: str) -> str:
2470
+ return (
2471
+ value.replace("&", "&")
2472
+ .replace("<", "&lt;")
2473
+ .replace(">", "&gt;")
2474
+ .replace('"', "&quot;")
2475
+ .replace("'", "&apos;")
2476
+ )
2477
+
2478
+ lines = [
2479
+ '<?xml version="1.0" encoding="UTF-8"?>',
2480
+ '<graphml xmlns="http://graphml.graphdrawing.org/xmlns"',
2481
+ ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
2482
+ ' xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns',
2483
+ ' http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">',
2484
+ ' <key id="node_label" for="node" attr.name="label" attr.type="string"/>',
2485
+ ' <graph id="G" edgedefault="directed">',
2486
+ ]
2487
+
2488
+ for node in sorted(node_ids_set):
2489
+ node_id = f"n{node}"
2490
+ lines.extend(
2491
+ [
2492
+ f' <node id="{node_id}">',
2493
+ f' <data key="node_label">{_xml_escape(str(node))}</data>',
2494
+ " </node>",
2495
+ ]
2496
+ )
2497
+
2498
+ edge_id = 0
2499
+ for parent_id, child_id in hierarchy_edges:
2500
+ lines.append(
2501
+ f' <edge id="e{edge_id}" source="n{parent_id}" target="n{child_id}"/>'
2502
+ )
2503
+ edge_id += 1
2504
+
2505
+ lines.extend([" </graph>", "</graphml>"])
2506
+ text = "\n".join(lines) + "\n"
2507
+
2508
+ elif graph_format == "elk":
2509
+ children = []
2510
+ edges = []
2511
+
2512
+ for node in sorted(node_ids_set):
2513
+ children.append(
2514
+ {
2515
+ "id": f"n{node}",
2516
+ "labels": [{"text": str(node)}],
2517
+ "width": 60,
2518
+ "height": 40,
2519
+ }
2520
+ )
2521
+
2522
+ edge_id = 0
2523
+ for parent_id, child_id in hierarchy_edges:
2524
+ edges.append(
2525
+ {
2526
+ "id": f"e{edge_id}",
2527
+ "sources": [f"n{parent_id}"],
2528
+ "targets": [f"n{child_id}"],
2529
+ }
2530
+ )
2531
+ edge_id += 1
2532
+
2533
+ elk_graph = {
2534
+ "id": "root",
2535
+ "layoutOptions": {
2536
+ "org.eclipse.elk.algorithm": "layered",
2537
+ "org.eclipse.elk.direction": "DOWN",
2538
+ },
2539
+ "children": children,
2540
+ "edges": edges,
2541
+ }
2542
+ text = json.dumps(elk_graph, indent=2) + "\n"
2543
+
2544
+ else:
2545
+ raise ValueError(
2546
+ "unsupported graph format "
2547
+ f"'{format}', expected 'dot', 'mermaid', 'mermaid_fenced', 'graphml' or 'elk'"
2548
+ )
2549
+
2550
+ if filename is not None:
2551
+ if isinstance(filename, str):
2552
+ with open(filename, "w") as file:
2553
+ file.write(text)
2554
+ else:
2555
+ filename.write(text)
2556
+
2557
+ return text
2558
+
2383
2559
  def dotfile(
2384
2560
  self, filename: Any = None, direction: str | None = None, show: bool = False
2385
2561
  ) -> None:
@@ -2400,50 +2576,48 @@ class Blobs(UserList): # lgtm[py/missing-equals]
2400
2576
  .. note:: If ``filename`` is a file object then the file will *not*
2401
2577
  be closed after the GraphViz model is written.
2402
2578
 
2403
- :seealso: :meth:`child` :meth:`parent` :meth:`level`
2579
+ :seealso: :meth:`graph` :meth:`child` :meth:`parent` :meth:`level`
2404
2580
  """
2405
2581
 
2406
2582
  if show:
2407
2583
  # create the temporary dotfile
2408
2584
  filename = tempfile.TemporaryFile(mode="w")
2585
+ dot_text = self.graph(format="dot", direction=direction)
2586
+
2409
2587
  if filename is None:
2410
2588
  f = sys.stdout
2589
+ f.write(dot_text)
2411
2590
  elif isinstance(filename, str):
2412
- f = open(filename, "w")
2591
+ with open(filename, "w") as f:
2592
+ f.write(dot_text)
2413
2593
  else:
2414
2594
  f = filename
2415
-
2416
- print("digraph {", file=f)
2417
-
2418
- if direction is not None:
2419
- print(f"rankdir = {direction}", file=f)
2420
-
2421
- # add the nodes including name and position
2422
- for id, blob in enumerate(self):
2423
- print(' "{:d}"'.format(id), file=f)
2424
- print(
2425
- ' "{:d}" -> "{:d}"'.format(blob.parent if blob.parent else -1, id),
2426
- file=f,
2427
- )
2428
-
2429
- print("}", file=f)
2595
+ f.write(dot_text)
2430
2596
 
2431
2597
  if show:
2432
2598
  # rewind the dot file, create PDF file in the filesystem, run dot
2433
- f.seek(0)
2599
+ if filename is None:
2600
+ dot_stream = tempfile.TemporaryFile(mode="w+")
2601
+ dot_stream.write(dot_text)
2602
+ dot_stream.seek(0)
2603
+ elif isinstance(filename, str):
2604
+ dot_stream = open(filename, "r")
2605
+ else:
2606
+ f.seek(0)
2607
+ dot_stream = f
2608
+
2434
2609
  pdffile = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False)
2435
2610
  subprocess.run(
2436
2611
  ["dot", "-Tpdf"],
2437
- stdin=cast(Any, f),
2612
+ stdin=cast(Any, dot_stream),
2438
2613
  stdout=pdffile,
2439
2614
  check=True,
2440
2615
  )
2441
2616
 
2442
2617
  # open the PDF file in browser (hopefully portable), then cleanup
2443
2618
  webbrowser.open(f"file://{pdffile.name}")
2444
- else:
2445
2619
  if filename is None or isinstance(filename, str):
2446
- f.close() # noqa
2620
+ dot_stream.close()
2447
2621
 
2448
2622
 
2449
2623
  class ImageBlobsMixin:
@@ -631,6 +631,9 @@ class VideoCamera(ImageSource):
631
631
  self.rgb = rgb
632
632
  self.i = 0
633
633
 
634
+ def __len__(self) -> int:
635
+ raise TypeError("VideoCamera is a live stream and has no defined length")
636
+
634
637
  def __iter__(self) -> VideoCamera:
635
638
  self.i = 0
636
639
  self.cap.release()
@@ -1749,6 +1752,9 @@ class WebCam(ImageSource):
1749
1752
  self.args = kwargs
1750
1753
  self.cap = None
1751
1754
 
1755
+ def __len__(self) -> int:
1756
+ raise TypeError("WebCam is a live stream and has no defined length")
1757
+
1752
1758
  def __iter__(self) -> WebCam:
1753
1759
  if self.cap is not None:
1754
1760
  self.cap.release()
@@ -1851,7 +1857,7 @@ class EarthView(ImageSource):
1851
1857
 
1852
1858
  .. note::
1853
1859
  - If the key is not passed in, a value is sought from the
1854
- environment variable ``GOOGLE_KEY``.
1860
+ environment variable ``GOOGLE_API_KEY``.
1855
1861
  - Uses the `Google Maps Static API <https://developers.google.com/maps/documentation/maps-static/start>`_
1856
1862
 
1857
1863
  :references:
@@ -1878,7 +1884,7 @@ class EarthView(ImageSource):
1878
1884
  ) -> None:
1879
1885
 
1880
1886
  if key is None:
1881
- self.key = os.getenv("GOOGLE_KEY")
1887
+ self.key = os.getenv("GOOGLE_API_KEY")
1882
1888
  else:
1883
1889
  self.key = key
1884
1890
 
@@ -1888,6 +1894,16 @@ class EarthView(ImageSource):
1888
1894
  self.shape = shape
1889
1895
  self.args = kwargs
1890
1896
 
1897
+ def __len__(self) -> int:
1898
+ raise TypeError(
1899
+ "EarthView is an on-demand image service and has no defined length"
1900
+ )
1901
+
1902
+ def __iter__(self) -> Iterator:
1903
+ raise TypeError(
1904
+ "EarthView is not iterable; use grab() to retrieve individual images"
1905
+ )
1906
+
1891
1907
  def grab(
1892
1908
  self,
1893
1909
  lat: float,
@@ -1946,7 +1962,7 @@ class EarthView(ImageSource):
1946
1962
  # https://developers.google.com/maps/documentation/maps-static/start#URL_Parameters
1947
1963
 
1948
1964
  # now read the map
1949
- url = f"https://maps.googleapis.com/maps/api/staticmap?center={lat},{lon}&zoom={zoom}&size={shape[0]}x{shape[1]}&scale={scale}&format=png&maptype={type}&key={self.key}&sensor=false"
1965
+ url = f"https://maps.googleapis.com/maps/api/staticmap?center={lat},{lon}&zoom={zoom}&size={shape[0]}x{shape[1]}&scale={scale}&format=png&maptype={type}&key={self.key}"
1950
1966
 
1951
1967
  opturl = []
1952
1968