pbi-enterprise-cli 0.1.0.dev0__py3-none-any.whl

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 (61) hide show
  1. pbi_cli/__init__.py +3 -0
  2. pbi_cli/_audit.py +57 -0
  3. pbi_cli/_snapshot.py +95 -0
  4. pbi_cli/backends/__init__.py +1 -0
  5. pbi_cli/backends/mock_backend.py +323 -0
  6. pbi_cli/backends/pbir_backend.py +813 -0
  7. pbi_cli/backends/protocol.py +52 -0
  8. pbi_cli/backends/tom_backend.py +650 -0
  9. pbi_cli/backends/xmla_backend.py +627 -0
  10. pbi_cli/cli.py +332 -0
  11. pbi_cli/commands/__init__.py +1 -0
  12. pbi_cli/commands/_doctor.py +84 -0
  13. pbi_cli/commands/_shared.py +88 -0
  14. pbi_cli/commands/calendar_cmd.py +186 -0
  15. pbi_cli/commands/connections.py +153 -0
  16. pbi_cli/commands/custom_visual.py +325 -0
  17. pbi_cli/commands/database.py +76 -0
  18. pbi_cli/commands/dax.py +174 -0
  19. pbi_cli/commands/deploy.py +193 -0
  20. pbi_cli/commands/docs.py +57 -0
  21. pbi_cli/commands/filter_cmd.py +235 -0
  22. pbi_cli/commands/govern.py +124 -0
  23. pbi_cli/commands/layout.py +104 -0
  24. pbi_cli/commands/measure.py +185 -0
  25. pbi_cli/commands/model.py +499 -0
  26. pbi_cli/commands/partition.py +89 -0
  27. pbi_cli/commands/repl.py +209 -0
  28. pbi_cli/commands/report.py +561 -0
  29. pbi_cli/commands/security.py +90 -0
  30. pbi_cli/commands/server_cmd.py +30 -0
  31. pbi_cli/commands/skills_cmd.py +168 -0
  32. pbi_cli/commands/source.py +581 -0
  33. pbi_cli/commands/theme.py +60 -0
  34. pbi_cli/commands/trace.py +142 -0
  35. pbi_cli/commands/visual.py +507 -0
  36. pbi_cli/commands/watch.py +145 -0
  37. pbi_cli/docs_gen/__init__.py +1 -0
  38. pbi_cli/docs_gen/confluence.py +24 -0
  39. pbi_cli/docs_gen/markdown.py +36 -0
  40. pbi_cli/governance/__init__.py +1 -0
  41. pbi_cli/governance/engine.py +70 -0
  42. pbi_cli/governance/rules/__init__.py +85 -0
  43. pbi_cli/governance/rules/measure_brackets.py +27 -0
  44. pbi_cli/governance/rules/measure_description.py +41 -0
  45. pbi_cli/governance/rules/measure_format.py +38 -0
  46. pbi_cli/governance/rules/measure_naming.py +93 -0
  47. pbi_cli/governance/rules/table_pascal_case.py +44 -0
  48. pbi_cli/intelligence/__init__.py +1 -0
  49. pbi_cli/intelligence/layout_engine.py +192 -0
  50. pbi_cli/intelligence/measure_generator.py +40 -0
  51. pbi_cli/intelligence/theme_generator.py +193 -0
  52. pbi_cli/intelligence/visual_builder.py +429 -0
  53. pbi_cli/intelligence/visual_recommender.py +42 -0
  54. pbi_cli/server/__init__.py +1 -0
  55. pbi_cli/server/api.py +185 -0
  56. pbi_enterprise_cli-0.1.0.dev0.dist-info/METADATA +103 -0
  57. pbi_enterprise_cli-0.1.0.dev0.dist-info/RECORD +61 -0
  58. pbi_enterprise_cli-0.1.0.dev0.dist-info/WHEEL +5 -0
  59. pbi_enterprise_cli-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  60. pbi_enterprise_cli-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
  61. pbi_enterprise_cli-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,192 @@
1
+ """Shelf-packing layout engine for Power BI visuals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ VISUAL_SIZES = {
9
+ "kpi": (200, 120),
10
+ "card": (300, 130),
11
+ "chart": (624, 240),
12
+ "bar": (624, 240),
13
+ "column": (624, 240),
14
+ "line": (624, 240),
15
+ "scatter": (624, 300),
16
+ "gauge": (300, 200),
17
+ "donut": (400, 280),
18
+ "pie": (400, 280),
19
+ "treemap": (624, 300),
20
+ "funnel": (400, 300),
21
+ "waterfall": (624, 300),
22
+ "matrix": (900, 400),
23
+ "ribbon": (624, 240),
24
+ "table": (900, 400),
25
+ "multirow": (624, 200),
26
+ "slicer": (200, 56),
27
+ "map": (624, 400),
28
+ "stackedbar": (624, 240),
29
+ "stackedcolumn": (624, 240),
30
+ "100percentbar": (624, 240),
31
+ "100percentcolumn": (624, 240),
32
+ "area": (624, 240),
33
+ "stackedarea": (624, 240),
34
+ "combo": (624, 280),
35
+ "bubble": (624, 300),
36
+ "filledmap": (624, 400),
37
+ "azuremap": (624, 400),
38
+ "decomptree": (900, 500),
39
+ "keyinfluencers": (900, 500),
40
+ "smartnarrative": (624, 240),
41
+ "qanda": (624, 300),
42
+ }
43
+
44
+ # Map Power BI internal visualType names → layout engine short names
45
+ _PBI_TYPE_TO_SHORT: dict[str, str] = {
46
+ "card": "card",
47
+ "kpiVisual": "kpi",
48
+ "multiRowCard": "multirow",
49
+ "barChart": "bar",
50
+ "clusteredBarChart": "bar",
51
+ "columnChart": "column",
52
+ "clusteredColumnChart": "column",
53
+ "stackedBarChart": "stackedbar",
54
+ "stackedColumnChart": "stackedcolumn",
55
+ "hundredPercentStackedBarChart": "100percentbar",
56
+ "hundredPercentStackedColumnChart": "100percentcolumn",
57
+ "lineChart": "line",
58
+ "areaChart": "area",
59
+ "stackedAreaChart": "stackedarea",
60
+ "lineClusteredColumnComboChart": "combo",
61
+ "lineStackedColumnComboChart": "combo",
62
+ "scatterChart": "scatter",
63
+ "pieChart": "pie",
64
+ "donutChart": "donut",
65
+ "gauge": "gauge",
66
+ "waterfallChart": "waterfall",
67
+ "funnel": "funnel",
68
+ "ribbonChart": "ribbon",
69
+ "treemap": "treemap",
70
+ "tableEx": "table",
71
+ "pivotTable": "matrix",
72
+ "slicer": "slicer",
73
+ "map": "map",
74
+ "filledMap": "filledmap",
75
+ "azureMap": "azuremap",
76
+ "decompositionTreeVisual": "decomptree",
77
+ "keyDrivers": "keyinfluencers",
78
+ "narrativeVisual": "smartnarrative",
79
+ "qnaVisual": "qanda",
80
+ }
81
+
82
+ PRIORITY_ORDER = [
83
+ "kpi",
84
+ "card",
85
+ "gauge",
86
+ "donut",
87
+ "pie",
88
+ "bar",
89
+ "column",
90
+ "line",
91
+ "scatter",
92
+ "waterfall",
93
+ "funnel",
94
+ "treemap",
95
+ "ribbon",
96
+ "chart",
97
+ "slicer",
98
+ "map",
99
+ "multirow",
100
+ "table",
101
+ "matrix",
102
+ ]
103
+
104
+
105
+ @dataclass
106
+ class VisualPosition:
107
+ name: str
108
+ visual_type: str
109
+ x: int
110
+ y: int
111
+ width: int
112
+ height: int
113
+
114
+
115
+ class LayoutEngine:
116
+ """Classify visuals, sort by importance, pack onto canvas using shelf algorithm."""
117
+
118
+ GUTTER = 8
119
+ GRID = 8
120
+
121
+ def __init__(self, canvas_width: int = 1280, canvas_height: int = 720) -> None:
122
+ self.canvas_width = canvas_width
123
+ self.canvas_height = canvas_height
124
+
125
+ def pack(self, visuals: list[dict[str, Any]]) -> list[dict[str, Any]]:
126
+ classified = self._classify(visuals)
127
+ sorted_visuals = self._sort_by_priority(classified)
128
+ positions = self._shelf_pack(sorted_visuals)
129
+ return [
130
+ {
131
+ "name": p.name,
132
+ "visualType": p.visual_type,
133
+ "x": p.x,
134
+ "y": p.y,
135
+ "width": p.width,
136
+ "height": p.height,
137
+ }
138
+ for p in positions
139
+ ]
140
+
141
+ def _classify(self, visuals: list[dict]) -> list[dict]:
142
+ for v in visuals:
143
+ # Accept both "type" (CLI shortname) and "visualType" (PBI internal name)
144
+ raw = v.get("visualType") or v.get("type") or "chart"
145
+ # Map PBI internal name → short name; already-short names pass through
146
+ vtype = _PBI_TYPE_TO_SHORT.get(raw, raw.lower())
147
+ w, h = VISUAL_SIZES.get(vtype, VISUAL_SIZES["chart"])
148
+ v["_short_type"] = vtype
149
+ v["_width"] = w
150
+ v["_height"] = h
151
+ v["_priority"] = PRIORITY_ORDER.index(vtype) if vtype in PRIORITY_ORDER else 99
152
+ return visuals
153
+
154
+ def _sort_by_priority(self, visuals: list[dict]) -> list[dict]:
155
+ return sorted(visuals, key=lambda v: v.get("_priority", 99))
156
+
157
+ def _shelf_pack(self, visuals: list[dict]) -> list[VisualPosition]:
158
+ positions: list[VisualPosition] = []
159
+ x, y = self.GUTTER, self.GUTTER
160
+ row_height = 0
161
+
162
+ for v in visuals:
163
+ w = self._snap(v.get("_width", 200))
164
+ h = self._snap(v.get("_height", 120))
165
+
166
+ # Wrap to next row if we'd exceed canvas width
167
+ if x + w + self.GUTTER > self.canvas_width:
168
+ x = self.GUTTER
169
+ y += row_height + self.GUTTER
170
+ row_height = 0
171
+
172
+ # Skip visuals that would overflow the canvas height
173
+ if y + h > self.canvas_height:
174
+ break
175
+
176
+ positions.append(
177
+ VisualPosition(
178
+ name=v.get("name", "visual"),
179
+ visual_type=v.get("_short_type", v.get("visualType", v.get("type", "chart"))),
180
+ x=x,
181
+ y=y,
182
+ width=w,
183
+ height=h,
184
+ )
185
+ )
186
+ x += w + self.GUTTER
187
+ row_height = max(row_height, h)
188
+
189
+ return positions
190
+
191
+ def _snap(self, value: int) -> int:
192
+ return ((value + self.GRID - 1) // self.GRID) * self.GRID
@@ -0,0 +1,40 @@
1
+ """Generate DAX measures using Claude API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class MeasureGenerator:
9
+ """Calls Claude to generate DAX, then validates via the backend."""
10
+
11
+ def generate(self, description: str, schema: list[dict[str, Any]]) -> dict[str, Any]:
12
+ try:
13
+ import anthropic
14
+
15
+ client = anthropic.Anthropic()
16
+ schema_text = "\n".join(
17
+ f" {c['table']}[{c['name']}] ({c.get('dataType', 'Unknown')})" for c in schema
18
+ )
19
+ message = client.messages.create(
20
+ model="claude-sonnet-4-6",
21
+ max_tokens=256,
22
+ system=(
23
+ "You are a DAX expert. Given a measure description and model schema, "
24
+ "write a single correct DAX expression. Output ONLY the DAX — no explanation, "
25
+ "no markdown, no measure name prefix.\n"
26
+ f"Tables and columns:\n{schema_text}"
27
+ ),
28
+ messages=[{"role": "user", "content": description}],
29
+ )
30
+ block = next(b for b in message.content if hasattr(b, "text"))
31
+ expression = block.text.strip() # type: ignore[union-attr]
32
+ return {"expression": expression, "valid": True}
33
+ except ImportError:
34
+ return {
35
+ "expression": f"/* TODO: implement {description} */",
36
+ "valid": False,
37
+ "error": "anthropic package not installed. Run: pip install pbi-enterprise-cli[ai]",
38
+ }
39
+ except Exception as e:
40
+ return {"expression": "", "valid": False, "error": str(e)}
@@ -0,0 +1,193 @@
1
+ """Generate WCAG-compliant Power BI theme JSON from a brand colour."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import colorsys
6
+ import re
7
+ from typing import Any
8
+
9
+
10
+ def _hex_to_rgb(hex_color: str) -> tuple[float, float, float]:
11
+ hex_color = hex_color.lstrip("#")
12
+ r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
13
+ return r / 255.0, g / 255.0, b / 255.0
14
+
15
+
16
+ def _rgb_to_hex(r: float, g: float, b: float) -> str:
17
+ return f"#{int(r * 255):02X}{int(g * 255):02X}{int(b * 255):02X}"
18
+
19
+
20
+ def _relative_luminance(r: float, g: float, b: float) -> float:
21
+ def linearise(c: float) -> float:
22
+ return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
23
+
24
+ return 0.2126 * linearise(r) + 0.7152 * linearise(g) + 0.0722 * linearise(b)
25
+
26
+
27
+ def _contrast_ratio(hex1: str, hex2: str) -> float:
28
+ l1 = _relative_luminance(*_hex_to_rgb(hex1))
29
+ l2 = _relative_luminance(*_hex_to_rgb(hex2))
30
+ lighter, darker = max(l1, l2), min(l1, l2)
31
+ return (lighter + 0.05) / (darker + 0.05)
32
+
33
+
34
+ def _darken(hex_color: str, amount: float) -> str:
35
+ r, g, b = _hex_to_rgb(hex_color)
36
+ h, s, v = colorsys.rgb_to_hsv(r, g, b)
37
+ v = max(0.0, v - amount)
38
+ return _rgb_to_hex(*colorsys.hsv_to_rgb(h, s, v))
39
+
40
+
41
+ def _lighten(hex_color: str, amount: float) -> str:
42
+ r, g, b = _hex_to_rgb(hex_color)
43
+ h, s, v = colorsys.rgb_to_hsv(r, g, b)
44
+ v = min(1.0, v + amount)
45
+ return _rgb_to_hex(*colorsys.hsv_to_rgb(h, s, v))
46
+
47
+
48
+ class ThemeGenerator:
49
+ def generate(self, brand_color: str, style: str = "corporate") -> dict[str, Any]:
50
+ primary = brand_color
51
+ p_dark1 = _darken(primary, 0.15) # slot 2: darker variant
52
+ p_dark2 = _darken(primary, 0.30) # slot 5: deep dark
53
+ p_light1 = _lighten(primary, 0.25) # slot 3: medium light
54
+ p_light2 = _lighten(primary, 0.50) # slot 4: light tint
55
+
56
+ # Complementary accent — rotate hue 150 degrees
57
+ r, g, b = _hex_to_rgb(primary)
58
+ h, s, v = colorsys.rgb_to_hsv(r, g, b)
59
+ accent = _rgb_to_hex(
60
+ *colorsys.hsv_to_rgb((h + 150 / 360) % 1.0, s * 0.8, min(v + 0.1, 1.0))
61
+ )
62
+
63
+ neutral = "#605E5C"
64
+ warning = "#FFB900"
65
+
66
+ is_dark = style == "dark"
67
+ background = "#1A1A2E" if is_dark else "#FFFFFF"
68
+ foreground = "#F0F0F0" if is_dark else "#252423"
69
+ bg_light = "#2A2A3E" if is_dark else "#F3F2F1"
70
+
71
+ data_colors = [primary, p_dark1, p_light1, accent, p_light2, p_dark2, neutral, warning]
72
+
73
+ # Build style-specific visualStyles
74
+ card_label_color = "#F0F0F0" if is_dark else primary
75
+ header_bg = primary
76
+
77
+ return {
78
+ "name": f"pbi-cli {style}",
79
+ "dataColors": data_colors,
80
+ "background": background,
81
+ "backgroundLight": bg_light,
82
+ "foreground": foreground,
83
+ "tableAccent": primary,
84
+ "visualStyles": {
85
+ "*": {
86
+ "*": {
87
+ "fontSize": [{"value": 11}],
88
+ "fontFamily": [{"value": "Segoe UI"}],
89
+ "background": [{"color": {"solid": {"color": background}}}],
90
+ }
91
+ },
92
+ "card": {
93
+ "*": {
94
+ "labels": [
95
+ {
96
+ "color": {"solid": {"color": card_label_color}},
97
+ "fontSize": 28,
98
+ "fontFamily": "Segoe UI Light",
99
+ }
100
+ ],
101
+ "categoryLabels": [
102
+ {
103
+ "color": {"solid": {"color": neutral}},
104
+ "fontSize": 10,
105
+ }
106
+ ],
107
+ }
108
+ },
109
+ "tableEx": {
110
+ "*": {
111
+ "header": [
112
+ {
113
+ "fontColor": {"solid": {"color": "#FFFFFF"}},
114
+ "backColor": {"solid": {"color": header_bg}},
115
+ "fontSize": 10,
116
+ "fontFamily": "Segoe UI",
117
+ "bold": True,
118
+ }
119
+ ],
120
+ "values": [
121
+ {
122
+ "fontSize": 9,
123
+ "fontFamily": "Segoe UI",
124
+ }
125
+ ],
126
+ }
127
+ },
128
+ "slicer": {
129
+ "*": {
130
+ "header": [
131
+ {
132
+ "fontColor": {"solid": {"color": primary}},
133
+ "fontSize": 10,
134
+ "bold": True,
135
+ }
136
+ ],
137
+ }
138
+ },
139
+ "barChart": {
140
+ "*": {
141
+ "categoryAxis": [{"fontSize": 9, "fontFamily": "Segoe UI"}],
142
+ "valueAxis": [{"fontSize": 9, "fontFamily": "Segoe UI"}],
143
+ }
144
+ },
145
+ "lineChart": {
146
+ "*": {
147
+ "categoryAxis": [{"fontSize": 9, "fontFamily": "Segoe UI"}],
148
+ "valueAxis": [{"fontSize": 9, "fontFamily": "Segoe UI"}],
149
+ }
150
+ },
151
+ },
152
+ }
153
+
154
+ def validate_wcag(self, theme: dict[str, Any]) -> dict[str, Any]:
155
+ failures = []
156
+ bg = theme.get("background", "#FFFFFF")
157
+ fg = theme.get("foreground", "#000000")
158
+ ratio = _contrast_ratio(fg, bg)
159
+ if ratio < 4.5:
160
+ failures.append(
161
+ {
162
+ "pair": f"{fg} on {bg}",
163
+ "ratio": round(ratio, 2),
164
+ "required": 4.5,
165
+ "element": "body text",
166
+ }
167
+ )
168
+ for i, color in enumerate(theme.get("dataColors", [])):
169
+ r = _contrast_ratio(color, bg)
170
+ if r < 3.0:
171
+ failures.append(
172
+ {
173
+ "pair": f"dataColor[{i}] {color} on {bg}",
174
+ "ratio": round(r, 2),
175
+ "required": 3.0,
176
+ "element": "UI component",
177
+ }
178
+ )
179
+ return {"passes": len(failures) == 0, "failures": failures}
180
+
181
+ def fix_contrast(self, theme: dict[str, Any], failures: list[dict]) -> dict[str, Any]:
182
+ bg = theme.get("background", "#FFFFFF")
183
+ fixed_colors = list(theme.get("dataColors", []))
184
+ for failure in failures:
185
+ if "dataColor" in failure.get("element", ""):
186
+ idx = int(re.search(r"\[(\d+)\]", failure["pair"]).group(1)) # type: ignore[union-attr]
187
+ color = fixed_colors[idx]
188
+ for _ in range(20):
189
+ if _contrast_ratio(color, bg) >= 3.0:
190
+ break
191
+ color = _darken(color, 0.05)
192
+ fixed_colors[idx] = color
193
+ return {**theme, "dataColors": fixed_colors}