cloudmap 1.3.0__tar.gz → 1.3.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 (80) hide show
  1. {cloudmap-1.3.0 → cloudmap-1.3.1}/PKG-INFO +1 -1
  2. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/__init__.py +1 -1
  3. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/ask/__init__.py +4 -4
  4. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/graph.py +14 -3
  5. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_ask.py +32 -5
  6. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_cli_exports.py +3 -1
  7. {cloudmap-1.3.0 → cloudmap-1.3.1}/.github/workflows/ci.yml +0 -0
  8. {cloudmap-1.3.0 → cloudmap-1.3.1}/.github/workflows/publish.yml +0 -0
  9. {cloudmap-1.3.0 → cloudmap-1.3.1}/.gitignore +0 -0
  10. {cloudmap-1.3.0 → cloudmap-1.3.1}/ARCHITECTURE.md +0 -0
  11. {cloudmap-1.3.0 → cloudmap-1.3.1}/CONTRIBUTING.md +0 -0
  12. {cloudmap-1.3.0 → cloudmap-1.3.1}/FORMAT.md +0 -0
  13. {cloudmap-1.3.0 → cloudmap-1.3.1}/LICENSE +0 -0
  14. {cloudmap-1.3.0 → cloudmap-1.3.1}/PLAN.md +0 -0
  15. {cloudmap-1.3.0 → cloudmap-1.3.1}/README.md +0 -0
  16. {cloudmap-1.3.0 → cloudmap-1.3.1}/SECURITY.md +0 -0
  17. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/__main__.py +0 -0
  18. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/adapters/__init__.py +0 -0
  19. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/ask/intent.py +0 -0
  20. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/ask/narration.py +0 -0
  21. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/ask/queries.py +0 -0
  22. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/cli.py +0 -0
  23. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/extract/__init__.py +0 -0
  24. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/extract/extractors.py +0 -0
  25. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/extract/llm.py +0 -0
  26. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/ingest/__init__.py +0 -0
  27. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/ingest/azure.py +0 -0
  28. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/ingest/fixture.py +0 -0
  29. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/interactive.py +0 -0
  30. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/local_model.py +0 -0
  31. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/model.py +0 -0
  32. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/render/__init__.py +0 -0
  33. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/render/azure_icons.py +0 -0
  34. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/render/csv_export.py +0 -0
  35. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/render/drawio.py +0 -0
  36. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/render/html.py +0 -0
  37. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/render/json_out.py +0 -0
  38. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/render/mermaid.py +0 -0
  39. {cloudmap-1.3.0 → cloudmap-1.3.1}/cloudmap/scrub.py +0 -0
  40. {cloudmap-1.3.0 → cloudmap-1.3.1}/docs/social-preview.html +0 -0
  41. {cloudmap-1.3.0 → cloudmap-1.3.1}/docs/social-preview.png +0 -0
  42. {cloudmap-1.3.0 → cloudmap-1.3.1}/estate-viewer.png +0 -0
  43. {cloudmap-1.3.0 → cloudmap-1.3.1}/fixtures/acme_orders.json +0 -0
  44. {cloudmap-1.3.0 → cloudmap-1.3.1}/fixtures/contoso.json +0 -0
  45. {cloudmap-1.3.0 → cloudmap-1.3.1}/fixtures/estate.json +0 -0
  46. {cloudmap-1.3.0 → cloudmap-1.3.1}/pyproject.toml +0 -0
  47. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/01_input_complex_random.json +0 -0
  48. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/04_scrubbed_output.json +0 -0
  49. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/06_trace_output.json +0 -0
  50. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/07_trace_output.html +0 -0
  51. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/08_trace_output.csv +0 -0
  52. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/09_trace_output.drawio +0 -0
  53. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/10_input_enterprise_architecture.json +0 -0
  54. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/12_enterprise_trace.html +0 -0
  55. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/13_enterprise_trace.json +0 -0
  56. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/14_enterprise_scrubbed.json +0 -0
  57. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/complex_mock_demo/README.md +0 -0
  58. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/data/azure_resource_types.txt +0 -0
  59. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_adapters.py +0 -0
  60. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_arg_rows.py +0 -0
  61. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_azure.py +0 -0
  62. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_common_types.py +0 -0
  63. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_containerapps.py +0 -0
  64. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_containment_vs_association.py +0 -0
  65. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_drawio_xml.py +0 -0
  66. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_enrich.py +0 -0
  67. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_estate.py +0 -0
  68. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_fixtures_safe.py +0 -0
  69. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_golden_orders.py +0 -0
  70. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_graph.py +0 -0
  71. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_html.py +0 -0
  72. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_ingest_paging.py +0 -0
  73. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_interactive_wizard.py +0 -0
  74. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_llm.py +0 -0
  75. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_local_model.py +0 -0
  76. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_render_text.py +0 -0
  77. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_scrub.py +0 -0
  78. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_trust.py +0 -0
  79. {cloudmap-1.3.0 → cloudmap-1.3.1}/tests/test_type_agnostic.py +0 -0
  80. {cloudmap-1.3.0 → cloudmap-1.3.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudmap
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: Trace the blast radius of an Azure resource: one name in, a verified dependency graph out.
5
5
  Project-URL: Homepage, https://github.com/KatsaounisThanasis/cloudmap
6
6
  Project-URL: Repository, https://github.com/KatsaounisThanasis/cloudmap
@@ -1,3 +1,3 @@
1
1
  """cloudmap - trace an Azure resource's full dependency graph and export it."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.3.1"
@@ -67,10 +67,10 @@ def _no_subject(graph, default):
67
67
  return default
68
68
  groups = sorted({n.name for n in graph.nodes.values()
69
69
  if str(n.id).startswith(HIGH_LEVEL_PREFIXES)})
70
- return ("This map is the high-level view: resources are grouped into one box per "
71
- "type, so instance names do not exist in it. Ask about a group instead "
72
- f"({', '.join(groups[:6])}{', ...' if len(groups) > 6 else ''}), or re-run "
73
- "the trace with --level detail to keep instance names.")
70
+ return ("This map is the high-level view: types with several instances are "
71
+ "grouped into one box, so that name is not on it. Ask about a group "
72
+ f"instead ({', '.join(groups[:6])}{', ...' if len(groups) > 6 else ''}), "
73
+ "or re-run the trace with --level detail to keep every instance name.")
74
74
 
75
75
 
76
76
  def _error(message, query=None):
@@ -101,14 +101,25 @@ def collapse_high_level(graph, seed_id):
101
101
  if gkey == seed_id:
102
102
  new_nodes[gkey] = rep
103
103
  else:
104
- label = rep.type.split("/", 1)[-1] if rep.external else friendly_type(rep.type)
105
- if len(members) > 1:
106
- label += f" ×{len(members)}"
104
+ role = rep.type.split("/", 1)[-1] if rep.external else friendly_type(rep.type)
105
+ # Grouping exists for readability, not anonymity: when a "group" has
106
+ # exactly one member, hiding its name buys nothing and costs the one
107
+ # thing the reader needs - WHICH vault, WHICH workspace this is.
108
+ if len(members) == 1:
109
+ label = rep.name or role
110
+ else:
111
+ label = f"{role} ×{len(members)}"
107
112
  # an external group keeps its members' reasons: "why is this unverified"
108
113
  # must not be lost to grouping either
109
114
  note = ""
110
115
  for m in members:
111
116
  note = _merge_semis(note, graph.nodes[m].note)
117
+ if len(members) > 1:
118
+ # the instance names survive collapsing - on hover in the HTML
119
+ # viewer, in the drawio tooltip and in `ask` answers
120
+ shown = sorted(graph.nodes[m].name for m in members)[:8]
121
+ more = "" if len(members) <= 8 else f", +{len(members) - 8} more"
122
+ note = _merge_semis(note, "instances: " + ", ".join(shown) + more)
112
123
  new_nodes[gkey] = Node(id=gkey, name=label, type=rep.type,
113
124
  external=rep.external, note=note)
114
125
  distances[gkey] = dist
@@ -343,18 +343,45 @@ def test_warnings_stay_quiet_on_a_clean_map():
343
343
  assert warnings(graph) == []
344
344
 
345
345
 
346
- def test_high_level_map_explains_itself_instead_of_stonewalling():
347
- # The default trace writes the collapsed view, so this is the error people
348
- # actually hit: the vault IS on their diagram, inside a "Key Vault" box, and
349
- # "name a resource from this map" sends them hunting for a typo.
346
+ def test_a_singleton_group_keeps_its_name_so_asking_about_it_just_works():
347
+ # Grouping is for readability, not anonymity: with one vault in the map,
348
+ # the box carries its real name and the question answers directly.
350
349
  graph, seed = _real()
351
350
  collapsed = collapse_high_level(blast_radius(graph, seed), seed)
352
351
  res = answer(collapsed, "what breaks if I touch kv-orders-dev")
353
352
 
353
+ assert not res.get("error")
354
+ assert res["subject_name"] == "kv-orders-dev"
355
+
356
+
357
+ def test_high_level_map_explains_itself_instead_of_stonewalling():
358
+ # With SEVERAL vaults the box really is anonymous ("Key Vault ×2"), so this
359
+ # is the error people actually hit: the vault IS on their diagram, inside
360
+ # the group box, and "name a resource" sends them hunting for a typo.
361
+ S = "/subscriptions/s/resourcegroups/rg/providers"
362
+ rows = [
363
+ {"id": f"{S}/microsoft.web/sites/web", "name": "web", "type": "microsoft.web/sites",
364
+ "properties": {"siteConfig": {"appSettings": [
365
+ {"name": "A", "value": "https://kv-orders-dev.vault.azure.net/"},
366
+ {"name": "B", "value": "https://kv-payments-dev.vault.azure.net/"}]}}},
367
+ {"id": f"{S}/microsoft.keyvault/vaults/kv-orders-dev", "name": "kv-orders-dev",
368
+ "type": "microsoft.keyvault/vaults",
369
+ "properties": {"vaultUri": "https://kv-orders-dev.vault.azure.net/"}},
370
+ {"id": f"{S}/microsoft.keyvault/vaults/kv-payments-dev", "name": "kv-payments-dev",
371
+ "type": "microsoft.keyvault/vaults",
372
+ "properties": {"vaultUri": "https://kv-payments-dev.vault.azure.net/"}},
373
+ ]
374
+ graph = build_graph(rows)
375
+ seed = find_seeds(graph, "web")[0]
376
+ collapsed = collapse_high_level(blast_radius(graph, seed), seed)
377
+ res = answer(collapsed, "what breaks if I touch kv-orders-dev")
378
+
354
379
  assert res["error"]
355
380
  assert "high-level view" in res["error"]
356
381
  assert "--level detail" in res["error"]
357
- assert "Key Vault" in res["error"] # names a group they can actually ask about
382
+ assert "Key Vault ×2" in res["error"] # names a group they can actually ask about
383
+ grouped = next(n for n in collapsed.nodes.values() if "×2" in n.name)
384
+ assert "kv-orders-dev" in grouped.note # the names survive in the note
358
385
 
359
386
 
360
387
  def test_high_level_map_warns_that_counts_are_per_type():
@@ -142,7 +142,9 @@ def test_a_live_trace_seeded_by_arm_id_renders_valid_xml(tmp_path, monkeypatch):
142
142
  labels = {o.get("label") for o in ET.parse(tmp_path / "map.drawio").getroot().iter("object")}
143
143
 
144
144
  assert "web (sites)" in labels
145
- assert "Key Vault (vaults)" in labels # default view groups by type
145
+ # the default view groups by type, but a group of ONE keeps its name -
146
+ # a box saying just "Key Vault" answers the wrong question (which one?)
147
+ assert "kv-1 (vaults)" in labels
146
148
 
147
149
 
148
150
  def _capture_stubs(monkeypatch, rows, truncated=False):
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
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
File without changes
File without changes
File without changes
File without changes
File without changes